點擊上方藍色字體輕鬆關注前端
前言
上一文咱們知道了「鏈表 - 反轉鏈表、刪除鏈表的倒數第N個節點、合併兩個有序鏈表和兩數相加」。接下來咱們來學習一下關於鏈表 - 排序鏈表、相交鏈表和奇偶鏈表。node
排序鏈表
在O(n * log(n))時間複雜度和常數級空間複雜度下,對鏈表進行排序。web
示例輸入: 4->2->1->3輸出: 1->2->3->4
輸入: -1->5->3->4->0輸出: -1->0->3->4->5
分析
題目看完就兩個要求:算法
-
時間複雜度:O(n * log(n)) -
空間複雜度:O(1)
這道題的難點在於選擇什麼算法,先考慮時間複雜度,知足條件的只有堆排序、快排和歸併排序。此外,本題的對象是鏈表。當數列以鏈表的形式存儲的時候,歸併排序就不須要額外申請O(n)級別的空間。此時它的空間複雜度是O(1)。而快排和堆排序雖然都是速度很快的排序,但在鏈表中不是很合適。因此這道題優先選擇歸併排序來作。數組
解法1
思路微信
使用歸併排序的方法實現。數據結構
詳解架構
-
先判斷是否只有一個元素,若只有一個元素,直接返回; -
若不僅有一個元素,首先找到鏈表的中間節點; -
而後遞歸的對前半部分鏈表和後半部分鏈表分別進行遞歸排序; -
最後對兩個子鏈表進行歸併操做。
const sortList = function (head) { // 只有一個元素 if (head === null || head.next === null) { return head; } // 快慢雙指針 let slow = head; let fast = head; while (slow.next && fast.next && fast.next.next) { slow = slow.next; fast = fast.next.next; } const middle = slow.next; slow.next = null; // 一分爲二 const left = head; const right = middle; return merge(sortList(left), sortList(right));};const merge = function (left, right) { const tmp = new ListNode(null); let p1 = left; let p2 = right; let p = tmp; while (p1 && p2) { if (p1.val < p2.val) { const s = p1; p1 = p1.next; s.next = null; p.next = s; p = s; } else { const s = p2; p2 = p2.next; s.next = null; p.next = s; p = s; } } if (p1) p.next = p1; if (p2) p.next = p2; return tmp.next;};function ListNode (val) { this.val = val; this.next = null;}
複雜度分析app
-
時間複雜度O(n * log(n))
採用了歸併排序的方法,所以爲O(n * log(n))。框架
-
空間複雜度O(1)
過程當中產生 2 個臨時變量存儲,所以爲O(1)。
解法2
思路
藉助數組實現,方法取巧。
詳解
-
先判斷是否只有一個元素,若只有一個元素,直接返回; -
若不僅有一個元素,首先把鏈表轉爲數組; -
而後把數組排序後重建鏈表,方法取巧。
const sortList = function (head) { // 只有一個元素 if (head === null || head.next === null) { return head; } let cur = head; let index = 0; const arr = []; // 鏈表轉化爲數組 while (cur !== null) { arr[index] = cur.val; cur = cur.next; index += 1; } arr.sort((a, b) => a - b); // 數組升序排序 cur = head; index = 0; // 重建鏈表 while (cur !== null) { cur.val = arr[index]; index += 1; cur = cur.next; } return head;};
複雜度分析
-
時間複雜度O(n * log(n))
採用了歸併排序的方法,所以爲O(n)。
-
空間複雜度O(1)
過程當中產生 2 個臨時變量存儲,所以爲O(1)。
相交鏈表
編寫一個程序,找到兩個單鏈表相交的起始節點。
示例1在節點 c1 開始相交。
輸入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3輸出:Reference of the node with value = 8輸入解釋:相交節點的值爲 8 (注意,若是兩個列表相交則不能爲 0)。從各自的表頭開始算起,鏈表 A 爲 [4,1,8,4,5],鏈表 B 爲 [5,0,1,8,4,5]。在 A 中,相交節點前有 2 個節點;在 B 中,相交節點前有 3 個節點。
示例2
輸入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1輸出:Reference of the node with value = 2輸入解釋:相交節點的值爲 2 (注意,若是兩個列表相交則不能爲 0)。從各自的表頭開始算起,鏈表 A 爲 [0,9,1,2,4],鏈表 B 爲 [3,2,4]。在 A 中,相交節點前有 3 個節點;在 B 中,相交節點前有 1 個節點。
示例3
輸入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2輸出:null輸入解釋:從各自的表頭開始算起,鏈表 A 爲 [2,6,4],鏈表 B 爲 [1,5]。因爲這兩個鏈表不相交,因此 intersectVal 必須爲 0,而 skipA 和 skipB 能夠是任意值。解釋:這兩個鏈表不相交,所以返回 null。
注意:
-
若是兩個鏈表沒有交點,返回 null. -
在返回結果後,兩個鏈表仍須保持原有的結構。 -
可假定整個鏈表結構中沒有循環。 -
程序儘可能知足 時間複雜度,且僅用 內存。
分析
-
兩條鏈表相交,交點開始一定全部的節點值都相等。 -
鏈表相交不單單是鏈表的值相同,而是鏈表的引用都相同,因此只要某個節點開始相等,就會一直相等。因此下圖這種狀況不存在。
解法1: 暴力破解法
思路
若是給的數據結構是雙向鏈表,很容易獲得解法,兩條鏈表從末尾開始遍歷,直到鏈表同一個位置的兩個值不相等便可。既然數據結構定了單向鏈表這種方法就不考慮了。
若是兩條鏈表是同樣的長度也很好獲得解法,節點逐一比較,直到末尾節點值都是相等的就說明是相交點。咱們能夠按照此思路,先將兩條鏈表處理成相同的長度在進行比較。
詳解
-
計算鏈表長度 -
將較長的那條鏈表的長度調整爲較短的那條的長度 -
繼續遍歷找出相交點
const getIntersectionNode = function (headA, headB) { if (headA === null || headB === null) return null;
let pA = headA; let pB = headB;
// 第一步:計算鏈表的長度 let lenA = 0; let lenB = 0; while (pA !== null) { lenA += 1; pA = pA.next; } while (pB !== null) { lenB += 1; pB = pB.next; } let lenDiff = lenA - lenB;
// 第二步:將較長的那條鏈表的長度調整爲較短的那條的長度 // 若鏈表a比較長,須要調整a鏈表 pA = headA; pB = headB; if (lenDiff > 0) { while (lenDiff !== 0) { pA = pA.next; lenDiff -= 1; } } else { // 若鏈表b比較長,須要調整b鏈表 while (lenDiff !== 0) { pB = pB.next; lenDiff += 1; } }
// 第三步:繼續遍歷找出相交點 while (pA !== null) { if (pA === pB) { return pA; } pB = pB.next; pA = pA.next; } return null;};
複雜度分析
-
時間複雜度O(n)
最差狀況會完整遍歷一遍鏈表,時間複雜度與鏈表長度呈線性正相關,所以爲O(n)。
-
空間複雜度O(1)
只開闢了固定個數的變量空間,所以爲O(1)。
解法2: 雙指針法
思路
經過加法的手段消除長度差。將兩鏈表首尾相接造成 ab 和 ba 鏈表,此時咱們構建了兩條長度相同的鏈表,若 a 和 b 相交,則 ab 和 ba 也一定相交。
詳解
-
定義兩個指針 pA 和 pB; -
pA 從鏈表 a 的頭部開始走,走完後再從鏈表 b 的頭部開始走; -
pB 從鏈表 b 的頭部開始走,走完後再從鏈表 a 的頭部開始走; -
若是存在相交結點,則兩個指針必會相遇
const getIntersectionNode = function (headA, headB) { if (headA === null || headB === null) return null;
let pA = headA; let pB = headB; while (pA !== pB) { pA = pA === null ? headB : pA.next; pB = pB === null ? headA : pB.next; } return pA;};
複雜度分析
-
時間複雜度O(n)
最差狀況會完整遍歷一遍鏈表,時間複雜度與鏈表長度呈線性正相關,所以爲O(n)。
-
空間複雜度O(1)
只開闢了固定個數的變量空間,所以爲O(1)。
奇偶鏈表
給定一個單鏈表,把全部的奇數節點和偶數節點分別排在一塊兒。請注意,這裏的奇數節點和偶數節點指的是節點編號的奇偶性,而不是節點的值的奇偶性。請嘗試使用原地算法完成。空間複雜度爲O(1), 時間複雜度爲O(nodes), nodes爲數組長度
-
應當保持奇數節點和偶數節點的相對順序。 -
鏈表的第一個節點視爲奇數節點,第二個節點視爲偶數節點,以此類推。
示例輸入: 1->2->3->4->5->NULL輸出: 1->3->5->2->4->NULL
輸入: 2->1->3->5->6->4->7->NULL輸出: 2->3->6->7->1->5->4->NULL
解法1: 奇偶鏈表分離法
思路
將鏈表中全部元素按照奇數位置、偶數位置劃分爲兩個鏈表:odd 鏈表、event 鏈表,遍歷結束,直接將偶數鏈表掛在奇數鏈表以後。
詳解
-
若是鏈表中節點個數爲 0、一、2 個時,鏈表自身已知足奇偶鏈表,直接返回 head 節點便可; -
定義 odd 變量指向頭節點、even 和 evenHeadPointer 變量指向鏈表的第二個節點,其中 head 即表明奇數鏈表的頭節點、evenHeadPointer 即表明偶數鏈表的頭節點; -
while 循環遍歷鏈表(利用 odd、even 變量遍歷),利用原鏈表中奇數位置節點的子節點應該掛到偶鏈表中、偶數位置節點的子節點應該掛到奇鏈表中交叉遍歷賦值,odd、even 變量永遠指向奇鏈表、偶鏈表最後一個節點; -
奇鏈表最後一個節點 odd 的子節點指向偶鏈表的頭節點 evenHeadPointer; -
返回 head 頭節點便可;
/** * Definition for singly-linked list. * function ListNode(val) { * this.val = val; * this.next = null; * } *//** * @param {ListNode} head * @return {ListNode} */const oddEvenList = function (head) { if (head === null || head.next === null || head.next.next === null) { return head; } let odd = head; let even = head.next; const evenHeadPointer = head.next; while (even != null && even.next != null) { odd.next = even.next; odd = odd.next; even.next = odd.next; even = even.next; } odd.next = evenHeadPointer; return head;};
複雜度分析
-
時間複雜度O(n)
while 循環遍歷一次鏈表,以第一個節點做爲奇數鏈表的頭節點,第二個節點做爲偶數鏈表的頭節點,交叉串聯起奇數、偶數鏈表,所以爲O(n)。
-
空間複雜度O(1)
過程當中產生 3 個臨時變量存儲,所以爲O(1)。
解法2: 數組暫存法
思路
遍歷鏈表並利用數組暫存鏈表節點,而後在數組中對奇數、偶數位置的節點進行串聯;
詳解
-
若是鏈表中節點個數爲 0、一、2 個時,鏈表自身已知足奇偶鏈表,直接返回 head 節點便可; -
定義一個數組暫存鏈表節點; -
while 循環遍歷鏈表(利用 head 變量遍歷),將每個節點 push 到數組中,而且從第三個節點開始,將第三個節點做爲子節點掛到第一個節點上,第四個節點做爲子節點掛到第二個節點上,以此類推; -
遍歷到最後一個節點時,倒數第二個節點的子節點賦值爲 null; -
若是數組長度爲偶數個,則數組的倒數第二個元素是奇鏈表的最後一個節點,若是數組長度爲奇數個,則數組的最後一個元素是奇鏈表的最後一個節點; -
數組的第二個元素是偶鏈表的頭節點; -
串聯奇偶鏈表,直接將奇鏈表的最後一個節點的 next 指向偶鏈表的頭節點; -
返回數組的第一個元素便可;
/** * Definition for singly-linked list. * function ListNode(val) { * this.val = val; * this.next = null; * } *//** * @param {ListNode} head * @return {ListNode} */const oddEvenList = function (head) { // 若是鏈表中元素個數少於2個,直接返回鏈表 if (head === null || head.next === null || head.next.next === null) { return head; } // 爲了防止鏈表節點丟失,利用一個數組暫存鏈表 const linkArr = []; while (head != null) { linkArr.push(head); const len = linkArr.length; // 從第三個節點開始處理next if (len > 2) { linkArr[len - 3].next = linkArr[len - 1]; } head = head.next; if (head === null) { linkArr[len - 2].next = null; } const isOdd = len % 2 !== 0; if (!isOdd) { linkArr[len - 2].next = linkArr[1]; } else { linkArr[len - 1].next = linkArr[1]; } } return linkArr[0];};
複雜度分析
-
時間複雜度O(n)
while 循環遍歷一次鏈表,從第三個節點開始處理 next,所以爲O(n)。
-
空間複雜度O(1)
借用一個跟鏈表等長的數組暫存鏈表元素,所以爲O(1)。
後記
以上就是小編今天給你們分享的內容,喜歡的小夥伴記得收藏
、轉發
,點擊在看
推薦給更多的小夥伴。
大前端實踐者,專一於大前端技術領域,分享前端系統架構,框架實現原理,最新最高效的技術實踐!
關注大前端實踐者,學習前端不迷路,歡迎多多留言交流...
大前端實踐者
本文分享自微信公衆號 - 大前端實踐者(daqianduanJS)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。