「算法與數據結構」鏈表的9個基本操做

前言

數據結構中的鏈表仍是很重要的,因此這章節把劍指offer 和 LeetCode 中的相關題目作一個彙總,分享給你們🤭前端

說真的,有時候,想要表達清楚本身的想法有點小困難,奈何又是個文筆不是很好的粗漢子,有些概念上問題,仍是引用別處的解釋比較好,因此還望你們諒解。node

對於時間複雜度和空間複雜度,不太瞭解的話,能夠看看下面這篇文章git

如何理解算法時間複雜度的表示法,例如 O(n²)、O(n)、O(1)、O(nlogn) 等?github

算法的時間與空間複雜度(一看就懂)web

碼字不易,對你有所幫助,點贊個支持一下面試

鏈表題目將收入GitHub中,思路和代碼都有,有興趣的小夥伴能夠來玩👇算法

數據結構-鏈表數組

鏈表 Linked List

一種常見的基礎數據結構,也是一種線性表,可是並不會按線性表的順序存儲數據,而是在每個節點裏存到下一個節點的指針(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) 時間來找出前一結點。
  • 在雙鏈表中,這會更容易,由於咱們可使用「prev」引用字段獲取前一個結點。所以咱們能夠在 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;  }; 複製代碼

遞歸思路: 遞歸解法要注意遞歸主題裏每次返回值較小得節點,這樣才能保證咱們最後獲得得是鏈表得最小開頭

一開始的作法就是模擬+鏈表,可是看見討論區中有遞歸寫法,絕對仍是好好看一遍。一題多解仍是很重要的,這也在某種程度上發散了思惟,仍是提倡多解。

  • 遞歸出口:任意一個鏈表爲空時,直接return 另一個連接,也就是拼接過程
  • 從兩個鏈表中依次取出節點比較,小的那一個就拎出來做爲下一個鏈表節點

代碼點這裏☑️


返回倒數第k個節點⭐

題目描述:實現一種算法,找出單向鏈表中倒數第 k 個節點。返回該節點的值。

連接:[力扣]返回倒數第k個節點

雙指針寫法👇

搞倆個先後指針,先讓後指針走k,接着兩個指針就相差k步,最後遍歷後指針,當後指針爲null時,前指針就是答案,由於一開始他們兩就是相差k距離

代碼點這裏☑️


反轉鏈表⭐

題目描述:反轉一個單鏈表。

連接:[leetcode]反轉一個鏈表

示例:

輸入: 1->2->3->4->5->NULL
輸出: 5->4->3->2->1->NULL 複製代碼

思路:迭代 三個指針 prev curr next 前指針 當前指針 下一個指針

  • 每次把當前curr指針指向上一個pre
  • 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); };  複製代碼

代碼點這裏☑️


區間反轉⭐⭐

題目描述:反轉從位置 mn 的鏈表。請使用一趟掃描完成反轉。

說明: 1 ≤ mn ≤ 鏈表長度。

連接:[leetcode]反轉鏈表II

示例:

輸入: 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節點   }; 複製代碼

點這裏代碼🤭


兩兩交換鏈表中的節點⭐⭐

題目描述:給定一個鏈表,兩兩交換其中相鄰的節點,並返回交換後的鏈表。你不能只是單純的改變節點內部的值,而是須要實際的進行節點交換。

連接:leetcode兩兩交換鏈表中的節點

示例:

給定 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 的整數倍,那麼請將最後剩餘的節點保持原有順序。

連接:[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⭐⭐⭐難題的話,不須要去浪費時間本身去思考,能夠看看別人的思路,把別人思路搞明白,最後轉換爲本身的思路很重要。看完真的就頓悟了,就知道該怎麼實現了。

  • 由於是k個分組,因此得有一個count計數,記錄節點個數。
  • start指針表明的含義就是 start記錄的信息是當前分組的起始節點位置的前一個節點。
  • end指針表明的含義就是要區間翻轉的後一個節點。
  • 翻轉後, start指向翻轉後鏈表, 區間 (start,end)中的最後一個節點, 返回 start 節點。
  • 此時還須要將翻轉後的分組中最後一個節點指向下一個分組,也就是 front.next = cur
  • 也就是圖中值爲1節點指向end

在來舉個例子,head=[1,2,3,4,5,6,7,8], k = 3

看不到就本身畫個圖,而後結合代碼多看幾遍吧,難題就要多看着寫幾遍,天然就有感受了。

關鍵點分析

  • 創建一個newNode
  • 對鏈表進行k個單位分組,記錄每一組的起始和最後節點位置
  • 對每一組進行相應的翻轉,記得更換位置
  • 返回newNode.next
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 個排序鏈表,返回合併後的排序鏈表。請分析和描述算法的複雜度。

連接:[合併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 複製代碼

[迴文鏈表]⭐

題目描述:請判斷一個鏈表是否爲迴文鏈表。

連接:leetcode-迴文鏈表

示例:

輸入:
[  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個節點是同一節點(引用徹底相同),則這兩個鏈表相交。

連接:[leetcode-鏈表相交]

示例 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。 複製代碼

思路:

  • 設置兩個指針,每條指針走完本身的路後,指向另一個鏈表,那麼兩個節點相等的話,必定是同一個點。
  • 由於兩個指針走的距離是同樣的,並且每次都前進1,距離相等,速度相同,若是相等,必定是同一個點。
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 }; 複製代碼

代碼點這裏🤭


拋磚引玉

選一部分題目出來,但願對你們算是一個拋磚引玉的過程吧,也算是對自個人總結,接下來還會繼續刷題的,須要繼續跟着我刷題的話,能夠看看下面噢👇

GitHub點這裏

❤️ 感謝你們

若是你以爲這篇內容對你挺有有幫助的話:

  1. 點贊支持下吧,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓 -_-)

  2. 歡迎在留言區與我分享你的想法,也歡迎你在留言區記錄你的思考過程。

  3. 以爲不錯的話,也能夠看看往期文章:

    [誠意滿滿👍]Chrome DevTools調試小技巧,效率➡️🚀🚀🚀

    [實用👍]推薦一些很是棒的前端網站

    [乾貨👍]從詳細操做js數組到淺析v8中array.js

    [1.2W字👍]寫給女朋友的祕籍-瀏覽器工做原理(上)篇

    [1.1W字]寫給女朋友的祕籍-瀏覽器工做原理(渲染流程)篇

    [建議👍]再來100道JS輸出題酸爽繼續(共1.8W字+鞏固JS基礎)

    [誠意滿滿✍]帶你填一些JS容易出錯的坑

相關文章
相關標籤/搜索