本文基於leetcode的探索鏈表卡片所編寫。遺憾的是, 個人卡片的進度爲80%, 依然沒有所有完成。我在探索卡片的時候, 免不了谷歌搜索。而且有一些題目, 個人解答可能不是最優解。敬請見諒。算法
鏈表屬於比較簡單的數據結構, 在這裏我在過多的贅述。值的注意的是, 本文都是基於單鏈表的, 雙鏈表的設計我尚未實現。數據結構
關於鏈表的算法題目, 我本身總結了如下幾種套路, 或者說常見的手段this
原題的地址, 我在原題的基礎使用了TypeScript模擬實現了鏈表。
鏈表須要擁有如下幾種方法:spa
// 定義鏈表節點類以及鏈表類的接口 interface LinkedListNodeInterface { val: number; next: LinkedListNode; } interface LinkedListInterface { head: LinkedListNode; length: number; get(index: number): number; addAtHead(val: number): void; addAtTail(val: number): void; addAtIndex(index: number, val: number): void; deleteAtIndex(index: number): void } class LinkedListNode implements LinkedListNodeInterface { constructor ( public val: number = null, public next: LinkedListNode = null ) {} } class LinkedList implements LinkedListInterface { constructor ( public head: LinkedListNode = null, public length: number = 0 ) {} /** * 經過while循環鏈表, 同時在循環的過程當中使用計數器計數, 既能夠實現 */ public get(index: number): number { if (index >= 0 && index < this.length) { let num: number = 0 let currentNode: LinkedListNode = this.head while (index !== num) { num += 1 currentNode = currentNode.next } return currentNode.val } return -1 } /** * 將新節點的next屬性指向當前的head, 將head指針指向新節點 */ public addAtHead (val: number): void { let newNode: LinkedListNode = new LinkedListNode(val, this.head) this.head = newNode this.length += 1 } /** * 將鏈表尾部的節點的next屬性指向新生成的節點, 獲取鏈表尾部的節點須要遍歷鏈表 */ public addAtTail(val: number): void { let newNode: LinkedListNode = new LinkedListNode(val, null) let currentNode: LinkedListNode = this.head if (!this.head) { this.head = newNode } else { while (currentNode && currentNode.next) { currentNode = currentNode.next } currentNode.next = newNode } this.length += 1 } /** * 這裏須要須要運用技巧, 遍歷鏈表的同時, 同時保留當前的節點和當前節點的前一個節點的指針 */ public addAtIndex(index: number, val: number): void { if (index >= 0 && index <= this.length) { let newNode: LinkedListNode = null if (index === 0) { // 若是index爲0, 插入頭部須要與其餘位置區別對待 this.addAtHead(val) } else { let pointer: number = 1 let prevNode: LinkedListNode = this.head let currentNode: LinkedListNode = this.head.next while (pointer !== index && currentNode) { prevNode = currentNode currentNode = currentNode.next pointer += 1 } // 中間插入 newNode = new LinkedListNode(val, currentNode) prevNode.next = newNode this.length += 1 } } } public deleteAtIndex(index: number): void { if (index >= 0 && index < this.length && this.length > 0) { if (index === 0) { this.head = this.head.next } else { let pointer: number = 1 let prevNode: LinkedListNode = this.head let currentNode: LinkedListNode = this.head.next // 值得注意的是這裏的判斷條件使用的是currentNode.next // 這意味着currentNode最遠到達當前鏈表的尾部的節點,而非null // 這是由於prevNode.next = prevNode.next.next, 咱們不能取null的next屬性 while (pointer !== index && currentNode.next) { prevNode = currentNode currentNode = currentNode.next pointer += 1 } prevNode.next = prevNode.next.next } this.length -= 1 } } }
原題地址, 將環形鏈表想象成一個跑道, 運動員的速度是肥宅的兩倍, 那麼通過一段時間後, 運動員必然會超過肥宅一圈。這個時候, 運動員和肥宅必然會相遇。快慢指針的思想就是源於此。
/** * 判斷鏈表是否成環 */ function hasCycle (head: LinkedListNode): boolean { // 快指針 let flst = head // 慢指針 let slow = head while (flst && flst.next && flst.next.next) { flst = flst.next.next slow = flst.next if (flst === slow) { return true } } return false }
原題地址, 在環形鏈表的基礎上, 咱們須要獲取環形鏈表的入口。一樣使用快慢指針實現。可是值的注意的是。鏈表可能只有 部分成環, 這意味着。快慢指針相遇的點, 可能並非環的入口。
慢節點的運動距離爲, a + b - c設計
快節點的運動距離爲, 2b + a - c指針
快節點的運動距離是慢節點的兩倍。能夠得出這個公式 2(a + b - c) = 2b + a - c, 化簡 2a - 2c = a - c, 能夠得出 a = c。code
相遇的點距離入口的距離, 等於起點距離入口距離遞歸
function hasCycleEntrance (head: LinkedListNode): LinkedListNode | Boolean { // 快指針 let flst = head // 慢指針 let slow = head while (flst && flst.next && flst.next.next) { flst = flst.next.next slow = flst.next // 快指針移動到入口,而且速度變爲1 if (flst === slow) { // 變道起點 flst = head // a 和 c距離是一致的 // 每一次移動一個next,必然會在入口出相遇 while (flst !== slow) { flst = flst.next slow = slow.next } return flst } } return false }
原題地址, 相交鏈表的解題思路依然是使用快慢指針。思路見下圖, 將a鏈的tail連接到b鏈head, 若是a與b鏈相交, 那麼就會成環。套用上一題的思路就能夠獲取到a與b的交點。
function hasCross (headA: LinkedListNode, headB: LinkedListNode): LinkedListNode { if (headA && headB) { // 自身相等的狀況下 if (headA === headB) { return headA } // a鏈的tail連上b鏈的head let lastA: LinkedListNode = headA let lastB: LinkedListNode = headB while (lastA && lastA.next) { lastA = lastA.next } lastA.next = headB let fast: LinkedListNode = headA let slow: LinkedListNode = headA while (fast && fast.next && fast.next.next) { slow = slow.next fast = fast.next.next if (slow === fast) { fast = headA while (slow !== fast) { slow = slow.next fast = fast.next if (slow === fast) { lastA.next = null return slow } } lastA.next = null return fast } } lastA.next = null return null } }
原題地址, 這裏我使用的是比較笨的辦法, 先計算鏈表的長度, 獲取正序的時n的大小。而後按照刪除鏈表中某一個節點的方法進行刪除便可。須要區分刪除的是不是第一個。
原題地址, 常見的反轉鏈表的方式就是使用遞歸或者迭代的方式。反轉鏈表的過程, 若是拆解開來, 能夠分爲下面幾步。從拆解的過程能夠看出, 反轉鏈表的過程就是依次將head的後面的節點, 放到鏈表的頭部。
1 -> 2 -> 3 -> 4 -> null索引
2 -> 1 -> 3 -> 4 -> null接口
3 -> 2 -> 1 -> 4 -> null
4 -> 3 -> 2 -> 1 -> null
const reverseList = function(head: LinkedListNode): LinkedListNode { let newHead: LinkedListNode = head let current: LinkedListNode = head // current的指針將會向後移動 function reverse () { let a = current.next let b = current.next.next a.next = head current.next = b head = a } while (current && current.next) { reverse() } return head };
原題地址。我使用的也是笨辦法。因爲鏈表頭部特殊性,刪除頭部時須要進行遞歸(由於在第一次刪除頭部的節點後, 新的頭部也有多是知足刪除條件的節點)。對於其餘位置的節點使用常規辦法便可。
function removeElements (head: LinkedListNode, val: number): LinkedListNode { /** * 刪除鏈表的頭部 */ function deleteHead () { head = head.next if (head && head.val === val) { deleteHead() } } if (head) { if (head.val === val) { // 遞歸刪除頭部的節點 deleteHead() } if (head && head.next) { let prevNode = head let currentNode = head.next while (currentNode) { // 刪除鏈表中間符合條件的節點 if (currentNode.val === val) { prevNode.next = currentNode.next currentNode = currentNode.next } else { prevNode = prevNode.next currentNode = currentNode.next } } } } return head }
原題地址, 對於這道題目咱們就須要運用上以前提到的兩種套路(同時保留頭部的指針以及當前的節點的指針和虛假的頭部)
function oddEvenList (head: LinkedListNode): LinkedListNode { let oddHead: LinkedListNode = new LinkedListNode(0) let evenHead: LinkedListNode = new LinkedListNode(0) let oddTail: LinkedListNode = oddHead let evenTail: LinkedListNode = evenHead let index: number = 1 while (head) { // 連接不一樣奇偶兩條鏈 // 這裏默認開頭是1,因此index從1開始 if (index % 2 !== 0) { oddTail.next = head oddTail = oddTail.next } else { evenTail.next = head evenTail = evenTail.next } head = head.next index += 1 } // 偶數鏈的結尾是null,由於是尾部 evenTail.next = null // evenHead.next忽略到假頭 oddTail.next = evenHead.next // oddHead.next忽略到假頭 return oddHead.next }
原題地址, 何爲所謂的迴文鏈表, 1 -> 2 -> 1 或者 1 -> 1 亦或則 1 -> 2 -> 2 -> 1 能夠被稱爲迴文鏈表。迴文鏈表若是長度爲奇數, 那麼除去中間點, 兩頭的鏈表應當是在反轉後應當是相同的。若是是偶數個, 鏈表的一半通過反轉應該等於前半部分。固然有兩種狀況須要特殊考慮, 好比鏈表爲 1 或者 1 -> 1 的狀況下。在排除了這兩種特點狀況後, 能夠經過快慢指針獲取鏈表的中點(fast的速度是slow的兩倍)。反轉中點以後的鏈表後, 而後從頭部開始和中點開始對比每個節點的val。
function isPalindrome (head: LinkedListNode): boolean { if (!head) { return true } // 經過快慢指針獲取中點 let fast: LinkedListNode = head let slow: LinkedListNode = head // 鏈表中點 let middle = null // 循環結束後慢節點就是鏈表的中點 while (fast && fast.next && fast.next.next) { fast = fast.next.next slow = slow.next } // 一個和兩個的狀況 if (fast === slow) { if (!fast.next) { return true } else if ( fast.val === fast.next.val ) { return true } else { return false } } // middle保持對中點的引用 // slow日後移動 middle = slow // 反轉中點之後的鏈表 function reverse () { let a = slow.next let b = slow.next.next a.next = middle slow.next = b middle = a } while (slow && slow.next) { reverse() } // 從頭部和中點開始對比 while (head && middle) { if (head.val !== middle.val) { return false } head = head.next middle = middle.next } return true }
原題地址, 對於建立一個新的鏈表使用的思路就是建立一個虛假的頭部, 這道題目的解答也是如此。以及同時保留頭部的指針以及尾部的指針, 不管是添加節點仍是返回鏈表都會很是方便。
function mergeTwoLists (l1: LinkedListNode, l2: LinkedListNode): LinkedListNode { // 頭部的引用 let newHead: LinkedListNode = new LinkedListNode(0) // 尾部的引用 let newTail: LinkedListNode = newHead while (l1 && l2) { if (l1.val < l2.val) { newTail.next = l1 l1 = l1.next } else { newTail.next = l2 l2 = l2.next } // 始終指向尾部 newTail = newTail.next } if (!l1) { newTail.next = l2 } if (!l2) { newTail.next = l1 } // 忽略虛假的頭部 return newHead.next }
原題地址。生成虛假的頭部後, 兩個鏈表兩兩相加, 注意 進位以及 保留位便可。若是val不存在, 取0。
(2 -> 4 -> 3) + (5 -> 6 -> 7 -> 8)
0343
__
9108
function addTwoNumbers (l1: LinkedListNode, l2: LinkedListNode): LinkedListNode { let newHead: LinkedListNode = new LinkedListNode(0) let newTail: LinkedListNode = newHead // carry是進位,8 + 8 = 16 ,進位爲1 let carry: number = 0 while (l1 || l2) { let a: number = l1 ? l1.val : 0 let b: number = l2 ? l2.val : 0 // val是保留的位 let val: number = (a + b + carry) % 10 carry = Math.floor((a + b + carry) / 10) let newNode = new LinkedListNode(val) newTail.next = newNode newTail = newTail.next if (l1) { l1 = l1.next } if (l2) { l2 = l2.next } } if (carry !== 0) { // 注意最後一位的進位 let newNode: LinkedListNode = new LinkedListNode(carry) newTail.next = newNode newTail = newTail.next } return newHead.next }
原題地址, 經過觀察可知, 所謂的旋轉就是依次將鏈表尾部的節點移動到鏈表的頭部, 同時能夠發現若是旋轉的次數等於鏈表的長度。鏈表是沒有發生改變的。因此經過提早計算出鏈表的長度, 能夠減小旋轉的次數。
輸入: 0-> 1-> 2 -> NULL
向右旋轉 1 步: 2 -> 0 -> 1 -> NULL
向右旋轉 2 步: 1 -> 2 -> 0 -> NULL
向右旋轉 3 步: 0 -> 1 -> 2 -> NULL
向右旋轉 4 步: 2 -> 0 -> 1 -> NULL
var rotateRight = function(head, k) { if (!head || !head.next) { return head } let length = 0 let c = head // 計算出鏈表的長度 while (c) { length += 1 c = c.next } // 將鏈表的尾部移動到鏈表的頭部 // 鏈表尾部的前一個next指向null function rotate () { let a = head let b = head.next while (b && b.next) { a = b b = b.next } b.next = head head = b a.next = null } // 避免沒有必要的選裝 let newK = k % length let index = 1 while (index <= newK) { rotate() index += 1 } return head };