把節點插入一個已排序的鏈表。系列目錄見 前言和目錄 。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 上,若是對你有幫助請幫我點個贊!