用 JavaScript 實現鏈表操做 - 12 Front Back Split

TL;DR

把一個鏈表居中切分紅兩個,系列目錄見 前言和目錄javascript

需求

實現函數 frontBackSplit() 把鏈表居中切分紅兩個子鏈表 -- 一個前半部分,另外一個後半部分。若是節點數爲奇數,則多餘的節點應該歸類到前半部分中。例子以下,注意 frontback 是做爲空鏈表被函數修改的,因此這個函數不須要返回值。java

var source = 1 -> 3 -> 7 -> 8 -> 11 -> 12 -> 14 -> null
var front = new Node()
var back = new Node()
frontBackSplit(source, front, back)
front === 1 -> 3 -> 7 -> 8 -> null
back === 11 -> 12 -> 14 -> null

若是函數的任何一個參數爲 null 或者原鏈表長度小於 2 ,應該拋出異常。node

提示:一個簡單的作法是計算鏈表的長度,而後除以 2 得出前半部分的長度,最後分割鏈表。另外一個方法是利用雙指針。一個 「慢」 指針每次遍歷一個節點,同時一個 」快「 指針每次遍歷兩個節點。當快指針遍歷到末尾時,慢指針正好遍歷到鏈表的中段。git

這個 kata 主要考驗的是指針操做,因此解法用不上遞歸。github

解法 1 -- 根據長度分割

代碼以下:segmentfault

function frontBackSplit(source, front, back) {
  if (!front || !back || !source || !source.next) throw new Error('invalid arguments')

  const array = []
  for (let node = source; node; node = node.next) array.push(node.data)

  const splitIdx = Math.round(array.length / 2)
  const frontData = array.slice(0, splitIdx)
  const backData = array.slice(splitIdx)

  appendData(front, frontData)
  appendData(back, backData)
}

function appendData(list, array) {
  let node = list
  for (const data of array) {
    if (node.data !== null) {
      node.next = new Node(data)
      node = node.next
    } else {
      node.data = data
    }
  }
}

解法思路是把鏈表變成數組,這樣方便計算長度,也方便用 slice 方法分割數組。最後用 appendData 把數組轉回鏈表。由於涉及到屢次遍歷,這並非一個高效的方案,並且還須要一個數組處理臨時數據。數組

解法 2 -- 根據長度分割改進版

代碼以下:app

function frontBackSplitV2(source, front, back) {
  if (!front || !back || !source || !source.next) throw new Error('invalid arguments')

  let len = 0
  for (let node = source; node; node = node.next) len++
  const backIdx = Math.round(len / 2)

  for (let node = source, idx = 0; node; node = node.next, idx++) {
    append(idx < backIdx ? front : back, node.data)
  }
}

// Note that it uses the "tail" property to track the tail of the list.
function append(list, data) {
  if (list.data === null) {
    list.data = data
    list.tail = list
  } else {
    list.tail.next = new Node(data)
    list.tail = list.tail.next
  }
}

這個解法經過遍歷鏈表來獲取總長度並算出中間節點的索引,算出長度後再遍歷一次鏈表,而後用 append 方法選擇性地把節點數據加入 frontback 兩個鏈表中去。這個解法不依賴中間數據(數組)。函數

append 方法有個值得注意的地方。通常狀況下把數據插入鏈表的末尾的空間複雜度是 O(n) ,爲了不這種狀況 append 方法爲鏈表加了一個 tail 屬性並讓它指向尾節點,讓空間複雜度變成 O(1) 。測試

解法 3 -- 雙指針

代碼以下:

function frontBackSplitV3(source, front, back) {
  if (!front || !back || !source || !source.next) throw new Error('invalid arguments')

  let slow = source
  let fast = source

  while (fast) {
    // use append to copy nodes to "front" list because we don't want to mutate the source list.
    append(front, slow.data)
    slow = slow.next
    fast = fast.next && fast.next.next
  }

  // "back" list just need to copy one node and point to the rest.
  back.data = slow.data
  back.next = slow.next
}

思路在開篇已經有解釋,當快指針遍歷到鏈表末尾,慢指針正好走到鏈表中部。但如何修改 frontback 兩個鏈表仍是有點技巧的。

對於 front 鏈表,慢指針每次遍歷的數據就是它須要的,因此每次遍歷時把慢指針的數據 appendfront 鏈表中就行(第 9 行)。

對於 back 鏈表,它所需的數據就是慢指針停下的位置到末尾。咱們不用複製整個鏈表數據到 back ,只用複製第一個節點的 datanext 便可。這種 複製頭結點,共用剩餘節點 的技巧常常出如今一些 Immutable Data 的操做中,以省去沒必要要的複製。這個技巧其實也能夠用到上一個解法裏。

參考資料

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

相關文章
相關標籤/搜索