用 JavaScript 完成链表操纵 - 05 Sorted Insert

TL;DR

把节点插进去一个已排序的链表。系列目次见 前言和目次

需求

写一个 sortedInsert() 函数,把一个节点插进去一个已排序的链表中,链表为升序分列。这个函数接收两个参数:一个链表的头节点和一个数据,而且一直返回新链表的头节点。

sortedInsert(1 -> 2 -> 3 -> null, 4) === 1 -> 2 -> 3 -> 4 -> null)
sortedInsert(1 -> 7 -> 8 -> null, 5) === 1 -> 5 -> 7 -> 8 -> null)
sortedInsert(3 -> 5 -> 9 -> null, 7) === 3 -> 5 -> 7 -> 9 -> null)

递归版本

我们能够从简朴的状况推演递归的算法。下面假定函数签名为 sortedInsert(head, data)

head 为空,即空链表,直接返回新节点:

if (!head) return new Node(data, null)

head 的值大于或即是 data 时,新节点也应当插进去头部:

if (head.data >= data) return new Node(data, head)

假如以上两点都不满足,data 就应当插进去后续的节点了,这类 “把数据插进去某链表” 的逻辑恰好相符 sortedInsert 的定义,由于这个函数一直返回修改后的链表,我们能够新链表赋值给 head.next 完成链接:

head.next = sortedInsert(head.next, data)
return head

整合起来代码以下,异常简朴而且有表达力:

function sortedInsert(head, data) {
  if (!head || data <= head.data) return new Node(data, head)

  head.next = sortedInsert(head.next, data)
  return head
}

轮回版本

轮回逻辑是如许:从头至尾搜检每一个节点,对第 n 个节点,假如数据小于或即是节点的值,则新建一个节点插进去节点 n 和节点 n-1 之间。假如数据大于节点的值,则对下个节点做一样的推断,直到完毕。

先上代码:

function sortedInsertV2(head, data) {
  let node = head
  let prevNode

  while (true) {
    if (!node || data <= node.data) {
      let newNode = new Node(data, node)
      if (prevNode) {
        prevNode.next = newNode
        return head
      } else {
        return newNode
      }
    }

    prevNode = node
    node = node.next
  }
}

这段代码比较复杂,主要有几个边境状况处置惩罚:

  1. 函数须要一直返回新链表的头,但插进去的节点可能在链表头部或许其他地方,所以返回值须要推断是返回新节点照样 head

  2. 由于插进去节点的操纵须要衔接前后两个节点,轮回体要保护 prevNodenode 两个变量,这也间接致使 for 的写法会比较贫苦,所以才用 while

轮回版本 – dummy node

我们能够用 上一个 kata 中提到的 dummy node 来处理链表轮回中头结点的 if/else 推断,从而简化一下代码:

function sortedInsertV3(head, data) {
  const dummy = new Node(null, head)
  let prevNode = dummy
  let node = dummy.next

  while (true) {
    if (!node || node.data > data) {
      prevNode.next = new Node(data, node)
      return dummy.next
    }

    prevNode = node
    node = node.next
  }
}

这段代码简化了初版轮回中返回 head 照样 new Node(...) 的题目。但能不能继承简化一下每次轮回中保护两个节点变量的题目呢?

轮回版本 – dummy node & check next node

为何要在轮回中保护两个变量 prevNodenode ?这是由于新节点要插进去两个节点之间,而我们每次轮回的当前节点是 node ,单链表中的节点没办法援用到上一个节点,所以才须要保护一个 prevNode

假如在每次轮回中搜检的主体是 node.next 呢?这个题目就处理了。换言之,我们搜检的是数据是不是合适插进去到 nodenode.next 之间。这类做法的唯一题目是第一次轮回,我们须要 node.next 指向头结点,那 node 自身又是什么? dummy node 恰好处理了这个题目。这块有点绕,不懂的话能够细致想一想。这是链表的一个经常运用技能。

简化后的代码以下,顺带一提,由于能够少保护一个变量,while 能够简化成 for 了:

function sortedInsertV4(head, data) {
  const dummy = new Node(null, head)

  for (let node = dummy; node; node = node.next) {
    const nextNode = node.next
    if (!nextNode || nextNode.data >= data) {
      node.next = new Node(data, nextNode)
      return dummy.next
    }
  }
}

总结

这个 kata 是递归简朴轮回贫苦的一个例子,有比较才会明白递归的文雅的地方。别的合理运用 dummy node 能够简化不少轮回的代码。算法相干的代码和测试我都放在 GitHub 上,假如对你有协助请帮我点个赞!

参考资料

Codewars Kata
GitHub 的代码完成
GitHub 的测试

    原文作者:darkbaby123
    原文地址: https://segmentfault.com/a/1190000007912308
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞