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
}
}
这段代码比较复杂,主要有几个边境状况处置惩罚:
函数须要一直返回新链表的头,但插进去的节点可能在链表头部或许其他地方,所以返回值须要推断是返回新节点照样
head
。由于插进去节点的操纵须要衔接前后两个节点,轮回体要保护
prevNode
和node
两个变量,这也间接致使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
为何要在轮回中保护两个变量 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 上,假如对你有协助请帮我点个赞!