數據結構中的鏈表仍是很重要的,因此這章節把劍指offer 和 LeetCode 中的相關題目作一個彙總,分享給你們🤭前端
說真的,有時候,想要表達清楚本身的想法有點小困難,奈何又是個文筆不是很好的粗漢子,有些概念上問題,仍是引用別處的解釋比較好,因此還望你們諒解。node
對於時間複雜度和空間複雜度,不太瞭解的話,能夠看看下面這篇文章git
如何理解算法時間複雜度的表示法,例如 O(n²)、O(n)、O(1)、O(nlogn) 等?github
碼字不易,對你有所幫助,點贊個支持一下面試
鏈表題目將收入GitHub中,思路和代碼都有,有興趣的小夥伴能夠來玩👇算法
數據結構-鏈表數組
一種常見的基礎數據結構,也是一種線性表,可是並不會按線性表的順序存儲數據,而是在每個節點裏存到下一個節點的指針(Pointer)。瀏覽器
鏈表在插入的時候能夠達到 O(1) 的複雜度,比另外一種線性表 —— 順序錶快得多,可是查找一個節點或者訪問特定編號的節點則須要 O(n)的時間,而順序表相應的時間複雜度分別是 O(log n) 和 O(1)。數據結構
優缺點:
❝使用鏈表結構能夠克服數組鏈表須要預先知道數據大小的缺點,鏈表結構能夠充分利用計算機內存空間,實現靈活的內存動態管理。可是鏈表失去了數組隨機讀取的優勢,同時鏈表因爲增長告終點的指針域,空間開銷比較大。
❞
「鏈表容許插入和移除表上任意位置上的節點,可是不容許隨機存取。」
鏈表有不少種不一樣的類型:
鏈表一般能夠衍生出循環鏈表,靜態鏈表,雙鏈表等。對於鏈表使用,須要注意「頭結點」的使用。
class ListNode {
constructor(val) { this.val = val; this.next = null; } } //單鏈表插入、刪除、查找 class LinkedList { constructor(val) { val = val === undefined ? 'head' : val; this.head = new ListNode(val) } // 找val值節點,沒有找到返回-1 findByVal(val) { let current = this.head while (current !== null && current.val !== val) { current = current.next } return current ? current : -1 } // 插入節點,在值爲val後面插入 insert(newVal, val) { let current = this.findByVal(val) if (current === -1) return false let newNode = new ListNode(newVal) newNode.next = current.next current.next = newNode } // 獲取值爲nodeVal的前一個節點,找不到爲-1,參數是val // 適用於鏈表中無重複節點 findNodePreByVal(nodeVal) { let current = this.head; while (current.next !== null && current.next.val !== nodeVal) current = current.next return current !== null ? current : -1 } // 根據index查找當前節點, 參數爲index // 能夠做爲比較鏈表是否有重複節點 findByIndex(index) { let current = this.head, pos = 1 while (current.next !== null && pos !== index) { current = current.next pos++ } return (current && pos === index) ? current : -1 } // 刪除某一個節點,刪除失敗放回false remove(nodeVal) { if(nodeVal === 'head') return false let needRemoveNode = this.findByVal(nodeVal) if (needRemoveNode === -1) return false let preveNode = this.findNodePreByVal(nodeVal) preveNode.next = needRemoveNode.next } //遍歷節點 disPlay() { let res = new Array() let current = this.head while (current !== null) { res.push(current.val) current = current.next } return res } // 在鏈表末尾插入一個新的節點 push(nodeVal) { let current = this.head let node = new ListNode(nodeVal) while (current.next !== null) current = current.next current.next = node } // 在頭部插入 frontPush(nodeVal) { let newNode = new ListNode(nodeVal) this.insert(nodeVal,'head') } } 複製代碼
固然了,可能還有一些其餘的方法我是沒有想到的,剩下的能夠自行去完成
「鏈表類的使用」
let demo = new LinkedList() // LinkedList {head: ListNode}
// console.log((demo.disPlay())) demo.push('1232') demo.insert(123, 'head'); demo.push('last value') demo.frontPush('start') demo.remove('head') // demo.remove('last value') // console.log(demo.remove('head')) // demo.push('2132') // demo.insert('不存在的值', '插入失敗') //return -1 console.log(demo.findByIndex(1)) console.log((demo.disPlay())) 複製代碼
上面的代碼片斷是測試用到,測試過了,基本上沒有上面大問題,固然了,有些細枝末節的地方仍是得注意的,好比findByIndex
這個函數中pos = 0
仍是 pos = 1
問題,取決於本身,還有的話,remove
函數到底能不能刪除'head'頭節點,這都是沒有準確的標準的,這個能夠根據本身狀況而定,
必定記住,不是惟一標準,你認爲能夠刪除'head'的話,也沒有問題。
「雙向鏈表」
雙鏈表以相似的方式工做,但還有一個引用字段
,稱爲「prev」
字段。有了這個額外的字段,您就可以知道當前結點的前一個結點。
讓咱們看一個例子:
綠色箭頭表示咱們的「prev」字段是如何工做的。
「結構相似👇」
class doubleLinkNode {
constructor (val) { this.val = val this.prev = null this.next = null } } 複製代碼
與單連接列表相似,咱們將使用頭結點
來表示整個列表。
對於插入和刪除,相比較單鏈表而言,會稍微複雜一些,由於咱們還須要處理「prev」字段。
「添加操做-雙鏈表」
舉個例子吧,固然了,最好的形式就是畫圖來解決。
讓咱們在現有結點 6 以後添加一個新結點 9:
第一步:連接 cur
(結點 9)與 prev
(結點 6)和 next
(結點 15)
第二步:用 cur
(結點 9)從新連接 prev
(結點 6)和 next
(結點 15)
「因此說,作鏈表題,畫圖最重要了,畫完圖,代碼也就出來了」
留下來一個問題,若是咱們想在開頭
或結尾
插入一個新結點怎麼辦?
「刪除操做-雙鏈表」
舉個例子吧👇
咱們的目標是從雙鏈表中刪除結點 6
所以,咱們將它的前一個結點 23 和下一個結點 15 連接起來:
結點 6 如今不在咱們的雙鏈表中
留個問題:若是咱們要刪除第一個結點
或最後一個結點
怎麼辦?
畫圖🤭
代碼就不寫了,網上不少均可以代碼,能夠看看人家怎麼寫的
讓咱們簡要回顧一下單鏈表和雙鏈表的表現。
它們在不少操做中是類似的
在 O(1) 時間內刪除第一個結點
。
在 O(1) 時間內在給定結點以後或列表開頭添加一個新結點
。
隨機訪問數據
。
可是刪除給定結點(包括最後一個結點)時略有不一樣。
O(N)
時間來找出前一結點。
O(1)
時間內刪除給定結點。
對比一下鏈表與其餘數據結構(數組,隊列,棧)之間時間複雜度
的比較:
通過此次比較,咱們不可貴出結論:
❝若是你須要常常添加或刪除結點,鏈表多是一個不錯的選擇。
若是你須要常常按索引訪問元素,數組多是比鏈表更好的選擇。
❞
接下來也就是本文的重點,從理論到實際出發,看看有哪些題型吧👇
接下來的題型梳理是按照我的刷題順序的,難易程度,也會作個劃分,能夠參考一下。
主要作題網站👇
題目描述:將兩個升序鏈表合併爲一個新的 「升序」 鏈表並返回。新鏈表是經過拼接給定的兩個鏈表的全部節點組成的。
❝連接:[力扣]合併兩個有序鏈表
❞
「示例:」
輸入:1->2->4, 1->3->4 輸出:1->1->2->3->4->4 複製代碼
非遞歸思路:
模擬題+鏈表
思路固然簡單,重要的是模擬過程,在算法程度上,這種題目能夠較爲模擬題,模擬你思考的過程,每次比較兩個l1.val 與l2.val的大小,取小的值,同時更新小的值指向下一個節點
主要注意的就是循環終止的條件:當二者其中有一個爲空時,即指向null
最後須要判斷兩個鏈表哪一個非空,在將非空的鏈表與tmp哨兵節點鏈接就好。
var mergeTwoLists = function (l1, l2) {
let newNode = new ListNode('start'), // 作題套路,頭節點 tmp = newNode; // tmp做爲哨兵節點 while (l1 && l2) { // 循環結束的條件就是二者都要爲非null if(l1.val >= l2.val) { tmp.next = l2 l2 = l2.next }else{ tmp.next = l1 l1 = l1.next } tmp = tmp.next // 哨兵節點更新指向下一個節點 } // 最後須要判斷哪一個鏈表還存在非null tmp.next = l1 == null ? l2 : l1; return newNode.next; }; 複製代碼
遞歸思路: 「遞歸解法要注意遞歸主題裏每次返回值較小得節點,這樣才能保證咱們最後獲得得是鏈表得最小開頭」
一開始的作法就是模擬+鏈表,可是看見討論區中有遞歸寫法,絕對仍是好好看一遍。一題多解仍是很重要的,這也在某種程度上發散了思惟,仍是提倡多解。
題目描述:實現一種算法,找出單向鏈表中倒數第 k 個節點。返回該節點的值。
❝ ❞
雙指針寫法👇
搞倆個先後指針,先讓後指針走k,接着兩個指針就相差k步,最後遍歷後指針,當後指針爲null時,前指針就是答案,由於一開始他們兩就是相差k距離
題目描述:反轉一個單鏈表。
❝ ❞
「示例:」
輸入: 1->2->3->4->5->NULL 輸出: 5->4->3->2->1->NULL 複製代碼
思路:迭代 三個指針 prev curr next 前指針 當前指針 下一個指針
小技巧:一開始把哨兵節點設置爲null,curr設置爲head
一直迭代下取,知道curr當前節點爲尾節點
var reverseList = function (head) {
if(!head) return null let prev = null, curr = head while( curr != null) { let next = curr.next; curr.next = prev prev = curr curr = next } return prev }; 複製代碼
遞歸寫法
以前講過思路了,咱們之間看代碼吧
var reverseList = function(head) {
let reverse = (prev,curr) => { if(!curr)return prev; let next = curr.next; curr.next = prev; return reverse(curr,next); } return reverse(null,head); }; 複製代碼
題目描述:反轉從位置 m 到 n 的鏈表。請使用一趟掃描完成反轉。
「說明:」 1 ≤ m ≤ n ≤ 鏈表長度。
❝ ❞
「示例:」
輸入: 1->2->3->4->5->NULL, m = 2, n = 4 輸出: 1->4->3->2->5->NULL 複製代碼
跟上一題差很少,換湯不換藥,因此咱們仍是能夠用迭代的作法來完成。
須要記錄兩個節點,tail和front節點
兩個節點做用就是爲了最後區間反轉後,好從新鏈接成一個新的鏈表。
var reverseBetween = function (head, m, n) {
let count = n-m, newNode = new ListNode('head'); tmp = newNode; tmp.next = head; // 哨兵節點,這樣子同時也保證了newNode下一個節點就是head for(let i = 0; i < m -1; i++ ){ tmp = tmp.next; } // 此時循環後,tmp保留的就是反轉區間前一個節點,須要用front保留下來 let front, prev, curr,tail; front = tmp; // 保留的是區間首節點 // 同時tail指針的做用是將反轉後的連接到最後節點 prev = tail = tmp.next; // 保留反轉後的隊尾節點 也就是tail curr = prev.next for(let i = 0; i < count; i++ ) { let next = curr.next; curr.next = prev; prev = curr curr = next } // 將本來區間首節點連接到後結點 tail.next = curr // font是區間前面一個節點,須要連接的就是區間反轉的最後一個節點 front.next = prev return newNode.next // 最後返回newNode.next就行,一開始咱們指向了head節點 }; 複製代碼
題目描述:給定一個鏈表,兩兩交換其中相鄰的節點,並返回交換後的鏈表。「你不能只是單純的改變節點內部的值」,而是須要實際的進行節點交換。
❝ ❞
「示例:」
給定 1->2->3->4, 你應該返回 2->1->4->3.
複製代碼
迭代思路,套路,加個tmp哨兵節點就行噠,還不懂的話,畫圖解決一切,實在看不懂的話,看這個圖
var swapPairs = function (head) {
let newNode = new ListNode('start'); newNode.next = head, // 鏈表頭節點套路操做 tmp = newNode; // tmp哨兵節點,這裏要從newNode節點開始,並非從head開始的 while( tmp.next !== null && tmp.next.next !== null) { let start = tmp.next, end = start.next; tmp.next = end start.next = end.next end.next = start tmp = start } return newNode.next // 返回的天然就是指向 鏈表頭節點的next指針 }; 複製代碼
固然了,面試的時候要真的寫,畫圖應該能夠的吧,看着圖來寫,就輕鬆了,講真的,我遞歸寫法✍想不出來,我好蠢🤭
題目描述:給你一個鏈表,每 k 個節點一組進行翻轉,請你返回翻轉後的鏈表。
說明:k 是一個正整數,它的值小於或等於鏈表的長度。若是節點總數不是 k 的整數倍,那麼請將最後剩餘的節點保持原有順序。
❝連接:[K 個一組翻轉鏈表](https://leetcode-cn.com/problems/swap-nodes-in-pairs/)
❞
示例 :
給定這個鏈表:1->2->3->4->5 當 k = 2 時,應當返回: 2->1->4->3->5 當 k = 3 時,應當返回: 3->2->1->4->5 複製代碼
先看題解,leetcode⭐⭐⭐難題的話,不須要去浪費時間本身去思考,能夠看看別人的思路,把別人思路搞明白,最後轉換爲本身的思路很重要。看完真的就頓悟了,就知道該怎麼實現了。
start
指針表明的含義就是
start
記錄的信息是當前分組的起始節點位置的前一個節點。
end
指針表明的含義就是要區間翻轉的後一個節點。
start
指向翻轉後鏈表, 區間
(start,end)
中的最後一個節點, 返回
start
節點。
front.next = cur
在來舉個例子,head=[1,2,3,4,5,6,7,8], k = 3
看不到就本身畫個圖,而後結合代碼多看幾遍吧,難題就要多看着寫幾遍,天然就有感受了。
「關鍵點分析」
var reverseKGroup = (head, k) => {
let reverseList = (start, end) => { let [pre, cur] = [start, start.next], front = cur; // 終止條件就是cur當前節點不能等於end節點 // 翻轉的套路 while( cur !== end) { let next = cur.next cur.next = pre pre = cur cur = next } front.next = end // 新翻轉鏈表須要鏈接,也就是front指向原來區間後一個節點 start.next = pre // 新翻轉的開頭須要鏈接start.next return front // 返回翻轉後須要鏈接鏈表,也就是front指向 } let newNode = new ListNode('start') newNode.next = head; let [start, end] = [newNode,newNode.next], count = 0; while(end !== null ) { count++ if( count % k === 0) { // k個節點翻轉後,又從新開始,返回值就是end節點前面一個 start = reverseList(start, end.next) end = start.next }else{ //不是一個分組就指向下一個節點 end = end.next } } return newNode.next }; 複製代碼
好傢伙,面試的時候,要我寫這個,不讓我畫圖的話,我抽象不出來💢💢
題目描述:合併 k 個排序鏈表,返回合併後的排序鏈表。請分析和描述算法的複雜度。
❝連接:[合併K個排序鏈表](https://leetcode-cn.com/problems/swap-nodes-in-pairs/)
❞
「示例:」
輸入: [ 1->4->5, 1->3->4, 2->6 ] 輸出: 1->1->2->3->4->4->5->6 複製代碼
題目描述:請判斷一個鏈表是否爲迴文鏈表。
❝ ❞
「示例:」
輸入: [ 1->4->5, 1->3->4, 2->6 ] 輸出: 1->1->2->3->4->4->5->6 複製代碼
「示例 1:」
輸入: 1->2
輸出: false 複製代碼
「示例 2:」
輸入: 1->2->2->1
輸出: true 複製代碼
解題思路:
找到鏈表中點,而後將後半部分反轉,就能夠依次比較得出結論了。
關鍵就是怎麼去找中點呢?
「快慢指針」
這個在鏈表中應用太普遍了,思路就是:設置一箇中間指針 mid,在一次遍歷中,head 走兩格,mid 走一格,當 head 取到最後一個值或者跳出時,mid 就指向中間的值。
let mid = head
// 循環條件:只要head存在則最少走一次 while(head !== null && head.next !== null) { head = head.next.next // 指針一次走兩格 mid = mid.next// 中間指針一次走一格 } 複製代碼
遍歷的時候經過迭代來反轉鏈表,mid 以前的 node 都會被反轉。 使用迭代來反轉。
while(head !== null && head.next !== null) {
pre = mid mid = mid.next head = head.next.next pre.next = reversed reversed = pre } 複製代碼
例如:
奇數:1 -> 2 -> 3 -> 2 ->1 遍歷完成後:mid = 3->2->1 reversed = 2->1 複製代碼
偶數:1 -> 2 -> 2 ->1 遍歷完成後:mid = 2->1 reversed = 2->1 複製代碼
完整代碼:
var isPalindrome = function (head) {
if (head === null || head.next === null) return true; let mid = head, pre = null, reversed = null; // reversed翻轉的鏈表 while (head !== null && head.next !== null) { // 常規翻轉的套路 pre = mid mid = mid.next head = head.next.next pre.next = reversed reversed = pre } // 判斷鏈表數是否是奇數,是的話mid日後走一位 if (head) mid = mid.next while (mid) { if (reversed.val !== mid.val) return false reversed = reversed.next mid = mid.next } return true }; 複製代碼
題目描述:給定兩個(單向)鏈表,斷定它們是否相交併返回交點。請注意相交的定義基於節點的引用,而不是基於節點的值。換句話說,若是一個鏈表的第k個節點與另外一個鏈表的第j個節點是同一節點(引用徹底相同),則這兩個鏈表相交。
❝ ❞
示例 1:
輸入: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。 複製代碼
思路:
var getIntersectionNode = function (headA, headB) {
let p1 = headA, p2 = headB; while (p1 != p2) { p1 = p1 === null ? headB : p1.next p2 = p2 === null ? headA : p2.next } return p1 }; 複製代碼
選一部分題目出來,但願對你們算是一個拋磚引玉的過程吧,也算是對自個人總結,接下來還會繼續刷題的,須要繼續跟着我刷題的話,能夠看看下面噢👇
若是你以爲這篇內容對你挺有有幫助的話:
點贊支持下吧,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓 -_-)
歡迎在留言區與我分享你的想法,也歡迎你在留言區記錄你的思考過程。
以爲不錯的話,也能夠看看往期文章:
[誠意滿滿👍]Chrome DevTools調試小技巧,效率➡️🚀🚀🚀
[1.1W字]寫給女朋友的祕籍-瀏覽器工做原理(渲染流程)篇