用 JavaScript 實現鏈表操做 - 04 Insert Nth Node

TL;DR

插入第 N 個節點。系列目錄見 前言和目錄javascript

需求

實現一個 insertNth() 方法,在鏈表的第 N 個索引處插入一個新節點。java

insertNth() 能夠當作是 01 Push & Build List 中的 push() 函數的更通用版本。給定一個鏈表,一個範圍在 0..length 內的索引號,和一個數據,這個函數會生成一個新的節點並插入到指定的索引位置,並始終返回鏈表的頭。node

insertNth(1 -> 2 -> 3 -> null, 0, 7) === 7 -> 1 -> 2 -> 3 -> null)
insertNth(1 -> 2 -> 3 -> null, 1, 7) === 1 -> 7 -> 2 -> 3 -> null)
insertNth(1 -> 2 -> 3 -> null, 3, 7) === 1 -> 2 -> 3 -> 7 -> null)

若是索引號超出了鏈表的長度,函數應該拋出異常。git

實現這個函數容許使用第一個 kata 中的 push 方法。github

遞歸版本

讓咱們先回憶一下 push 函數的用處,指定一個鏈表的頭和一個數據,push 會生成一個新節點並添加到鏈表的頭部,並返回新鏈表的頭。好比:segmentfault

push(null, 23) === 23 -> null
push(1 -> 2 -> null, 23) === 23 -> 1 -> 2 -> null

如今看看 insertNth ,假設函數方法簽名是 insertNth(head, index, data) ,那麼有兩種狀況:函數

若是 index === 0 ,則等同於調用 push 。實現爲 push(head, data)測試

若是 index !== 0 ,咱們能夠把下一個節點當成子鏈表傳入 insertNth ,並讓 index 減一。insertNth 的返回值必定是個鏈表,咱們把它賦值給 head.next 就行。這就是一個遞歸過程。若是此次遞歸的 insertNth 完不成任務,它會繼續遞歸到下一個節點,直到 index === 0 的最簡單狀況,或 head 爲空拋出異常(索引過大)。ui

完整代碼實現爲:code

function insertNth(head, index, data) {
  if (index === 0) return push(head, data)
  if (!head) throw 'invalid argument'
  head.next = insertNth(head.next, index - 1, data)
  return head
}

循環版本

若是能理解遞歸版本的 head.next = insertNth(...) ,那麼循環版本也不難實現。不一樣的是,在循環中咱們遍歷到 index 的前一個節點,而後用 push 方法生成新節點,並賦值給前一個節點的 next 屬性造成一個完整的鏈表。

完整代碼實現以下:

function insertNthV2(head, index, data) {
  if (index === 0) return push(head, data)

  for (let node = head, idx = 0; node; node = node.next, idx++) {
    if (idx + 1 === index) {
      node.next = push(node.next, data)
      return head
    }
  }

  throw 'invalid argument'
}

這裏有一個邊界狀況要注意。由於 insertNth 要求返回新鏈表的頭。根據 index 是否爲 0 ,這個新鏈表的頭多是生成的新節點,也可能就是老鏈表的頭 。這點若是寫進 for 循環就不可避免有 if/else 的返回值判斷。因此咱們把 index === 0 的狀況單獨拿出來放在函數頂部。這個邊界狀況並不是沒法歸入循環中,咱們下面介紹的一個技巧就與此有關。

循環版本 - dummy node

在以前的幾個 kata 裏,咱們提到循環能夠更好的容納邊界狀況,由於一些條件判斷都能寫到 for 的頭部中去。但這個例子的邊界狀況是返回值不一樣:

  1. 若是 index === 0 ,返回新節點 。

  2. 若是 index !== 0 ,返回 head 。新節點會被插入 head 以後的某個節點鏈條中。

如何解決這個問題呢,咱們能夠在 head 前面再加入一個節點(數據任意,通常賦值 null)。這個節點稱爲 dummy 節點。這樣一來,無論新節點插入到哪裏,dummy.next 均可以引用到修改後的鏈表。

代碼實現以下,注意 return 的不一樣。

function insertNthV3(head, index, data) {
  const dummy = push(head, null)

  for (let node = dummy, idx = 0; node; node = node.next, idx++) {
    if (idx === index) {
      node.next = push(node.next, data)
      return dummy.next
    }
  }

  throw 'invalid argument'
}

dummy 節點是不少鏈表操做的經常使用技巧,雖然在這個 kata 中使用 dummy 節點的代碼量並無變少,但這個技巧在後續的一些複雜 kata 中會很是有用。

參考資料

Codewars Kata
GitHub 的代碼實現
GitHub 的測試

相關文章
相關標籤/搜索