把节点插入一个已排序的链表。系列目录见 前言和目录 。javascript
写一个 sortedInsert()
函数,把一个节点插入一个已排序的链表中,链表为升序排列。这个函数接受两个参数:一个链表的头节点和一个数据,而且始终返回新链表的头节点。java
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)
。node
当 head
为空,即空链表,直接返回新节点:git
if (!head) return new Node(data, null)
当 head
的值大于或等于 data
时,新节点也应该插入头部:github
if (head.data >= data) return new Node(data, head)
若是以上两点都不知足,data
就应该插入后续的节点了,这种 “把数据插入某链表” 的逻辑刚好符合 sortedInsert
的定义,由于这个函数始终返回修改后的链表,咱们能够新链表赋值给 head.next
完成连接:算法
head.next = sortedInsert(head.next, data) return head
整合起来代码以下,很是简单而且有表达力:segmentfault
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 } }
这段代码比较复杂,主要有几个边界状况处理:code
函数须要始终返回新链表的头,但插入的节点可能在链表头部或者其余地方,因此返回值须要判断是返回新节点仍是 head
。
由于插入节点的操做须要链接先后两个节点,循环体要维护 prevNode
和 node
两个变量,这也间接致使 for
的写法会比较麻烦,因此才用 while
。
咱们能够用 上一个 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(...)
的问题。但能不能继续简化一下每次循环中维护两个节点变量的问题呢?
为何要在循环中维护两个变量 prevNode
和 node
?这是由于新节点要插入两个节点之间,而咱们每次循环的当前节点是 node
,单链表中的节点没办法引用到上一个节点,因此才须要维护一个 prevNode
。
若是在每次循环中检查的主体是 node.next
呢?这个问题就解决了。换言之,咱们检查的是数据是否适合插入到 node
和 node.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 上,若是对你有帮助请帮我点个赞!