用 JavaScript 實現鏈表操做 - 05 Sorted Insert

TL;DR

把節點插入一個已排序的鏈表。系列目錄見 前言和目錄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

  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 的測試

相關文章
相關標籤/搜索