把一個鏈表居中切分紅兩個,系列目錄見 前言和目錄 。javascript
實現函數 frontBackSplit()
把鏈表居中切分紅兩個子鏈表 -- 一個前半部分,另外一個後半部分。若是節點數爲奇數,則多餘的節點應該歸類到前半部分中。例子以下,注意 front
和 back
是做爲空鏈表被函數修改的,因此這個函數不須要返回值。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
代碼以下: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
把數組轉回鏈表。由於涉及到屢次遍歷,這並非一個高效的方案,並且還須要一個數組處理臨時數據。數組
代碼以下: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
方法選擇性地把節點數據加入 front
或 back
兩個鏈表中去。這個解法不依賴中間數據(數組)。函數
append
方法有個值得注意的地方。通常狀況下把數據插入鏈表的末尾的空間複雜度是 O(n) ,爲了不這種狀況 append
方法爲鏈表加了一個 tail
屬性並讓它指向尾節點,讓空間複雜度變成 O(1) 。測試
代碼以下:
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 }
思路在開篇已經有解釋,當快指針遍歷到鏈表末尾,慢指針正好走到鏈表中部。但如何修改 front
和 back
兩個鏈表仍是有點技巧的。
對於 front
鏈表,慢指針每次遍歷的數據就是它須要的,因此每次遍歷時把慢指針的數據 append
到 front
鏈表中就行(第 9 行)。
對於 back
鏈表,它所需的數據就是慢指針停下的位置到末尾。咱們不用複製整個鏈表數據到 back
,只用複製第一個節點的 data
和 next
便可。這種 複製頭結點,共用剩餘節點 的技巧常常出如今一些 Immutable Data 的操做中,以省去沒必要要的複製。這個技巧其實也能夠用到上一個解法裏。