国产精品电影_久久视频免费_欧美日韩国产激情_成年人视频免费在线播放_日本久久亚洲电影_久久都是精品_66av99_九色精品美女在线_蜜臀a∨国产成人精品_冲田杏梨av在线_欧美精品在线一区二区三区_麻豆mv在线看

前端進階:從零實現單向 & 雙向鏈表

開發 前端
鏈表是一種線性表數據結構,由一系列結點(鏈表中每一個元素稱為結點)組成,結點可以在運行時動態生成。每個結點包括兩個部分:一個是存儲數據元素的數據域,另一個是存儲下一個結點地址的指針域。

[[397775]]

前言

前端工程師對于算法和數據結構這塊的知識的掌握程度,是進階高級工程師的非常重要的標志之一,為了總結一下數據結構和算法方面的知識,筆者今天繼續把鏈表這一塊的知識補上,也作為自己知識體系的一個梳理,筆者早在去年就寫過一篇關于使用javascript實現二叉樹和二叉搜索樹的文章,如果感興趣或者想進階高級的朋友們可以參考學習一下: JavaScript 中的二叉樹以及二叉搜索樹的實現及應用.

你將收獲

  • 鏈表的概念和應用
  • 原生javascript實現一條單向鏈表
  • 原生javascript實現一條個雙單向鏈表
  • 鏈表和數組的對比及優缺點

正文

1. 鏈表的概念和應用

鏈表是一種線性表數據結構,由一系列結點(鏈表中每一個元素稱為結點)組成,結點可以在運行時動態生成。每個結點包括兩個部分:一個是存儲數據元素的數據域,另一個是存儲下一個結點地址的指針域。

以上概念用圖表示為以下結構:

鏈表是非連續的,所以說從底層存儲結構上看,它不需要一整塊連續的存儲空間,而是通過“指針”將一組零散的數據單元串聯起來成為一個整體。鏈表也有幾種不同的類型:單向鏈表,雙向鏈表,循環鏈表。上圖就是一種單向鏈表。由其定義不難發現雙向鏈表無非就是每個節點加上了前后節點的指針引用,如下圖所示:

那什么是循環鏈表呢?循環鏈表本質上是一種特殊的單向鏈表,唯一的區別就在于它的尾結點指向了鏈表的頭結點,這樣首尾相連,形成了一個環,所以叫做循環鏈表。如下圖所示:

當然我們還可以擴展出雙向循環鏈表,這里就不一一舉例了。總之鏈表結構在計算機底層語言中應用的比較多,當我們在用高級語言做編程時可能不會察覺,比如我們用javascript敲js的時候,其實我們在深入了解鏈表之后我們就會發現鏈表有很多應用場景,比如LRU 緩存淘汰,最近消息推送等。

舉個更接地氣的,當我們在用PS畫圖時軟件提供了一個動作面板,可以記錄用戶之前的操作記錄,并批量執行動作,或者當我們在使用編輯器時的回退撤銷功能等,用鏈表結構來存儲狀態信息還是比較方便的。

最近比較火的react hooks API,其結構也是一個鏈表型的數據結構,所以學習鏈表還是非常有幫助的。讀到這里可能還是有點懵,接下來我們先用js實現一個鏈表,這樣有助于理解鏈表的本質,后面筆者會總結一下鏈表和數組的對比以及優劣勢,方便大家對鏈表有一個更加直觀的認識。

2.原生javascript實現一條單向鏈表

在上面一節介紹的鏈表結構中大家可能對鏈表有了初步的認識,因為javascript中沒有鏈表的數據結構,為了模擬鏈表結構,我們可以通過js面向對象的方式實現一個鏈表結構及其API,具體設計如下:

有了以上需求點之后,這個鏈表才是基本可用的鏈表,那么我們一步步來實現它吧。

2.1 定義鏈表結構

為了實現鏈表以及鏈表的操作,首先我們需要先定義鏈表的基本結構,第一步就是定義節點的數據結構。我們知道一個節點會有自己的值以及指向下一個節點的引用,所以可以這樣定義節點:

  1. let Node = function(el) { 
  2.       this.el = el; 
  3.       this.next = null
  4.  } 

接下來我們定義一下鏈表的基本骨架:

  1. // 單向鏈表, 每一個元素都有一個存儲元素自身的節點和一個指向下一個元素引用的節點組成 
  2. function linkedList() { 
  3.   let Node = function(el) { 
  4.       this.el = el; 
  5.       this.next = null
  6.   } 
  7.   let length = 0 
  8.   let head = null  // 用來存儲第一個元素的引用 
  9.  
  10.   // 尾部添加元素 
  11.   this.append = (el) => {}; 
  12.   //插入元素 
  13.   this.insert = (pos, el) => {}; 
  14.   // 移除指定位置的元素 
  15.   this.removeAt = (pos) => {}; 
  16.   // 移除指定節點 
  17.   this.remove = (el) => {}; 
  18.   // 查詢節點所在位置 
  19.   this.indexOf = (el) => {}; 
  20.   // 判斷鏈表是否為空 
  21.   this.isEmpty = () => {}; 
  22.   // 返回鏈表長度 
  23.   this.size = () => {}; 
  24.   // 將鏈表轉化為數組返回 
  25.   this.toArray = () => {}; 

由以上代碼我們可以知道鏈表的初始長度為0,頭部元素為null,接下來我們實現添加節點的功能。

2.2 實現添加節點

追加節點的時候首先需要知道頭部節點是否存在,如果不存在直接賦值,存在的話則從頭部開始遍歷,直到找到下一個節點為空的節點,再賦值,并將鏈表長度+1,代碼如下:

  1. // 尾部添加元素 
  2. this.append = (el) => { 
  3.     let node = new Node(el), 
  4.         current
  5.     if(!head) { 
  6.       head = node 
  7.     }else { 
  8.       current = head; 
  9.       while(current.next) { 
  10.         current = current.next
  11.       } 
  12.       current.next = node; 
  13.     } 
  14.     length++ 
  15. }; 

2.3 實現插入節點

實現插入節點邏輯首先我們要考慮邊界條件,如果插入的位置在頭部或者比尾部位置還大,我們就沒必要從頭遍歷一遍處理了,這樣可以提高性能,所以我們可以這樣處理:

  1. //插入元素 
  2. this.insert = (pos, el) => { 
  3.     if(pos >=0 && pos <= length) { 
  4.       let node = new Node(el), 
  5.           previousNode = null
  6.           current = head, 
  7.           curIdx = 0; 
  8.       if(pos === 0) { 
  9.         node.next = current
  10.         head = node; 
  11.       }else { 
  12.         while(curIdx++ < pos) { 
  13.           previousNode = current
  14.           current = current.next
  15.         } 
  16.         node.next = current
  17.         previousNode.next = node; 
  18.         length++; 
  19.         return true 
  20.       } 
  21.     }else { 
  22.       return false 
  23.     } 
  24. }; 

2.4 根據節點的值查詢節點位置

根據節點的值查詢節點位置實現起來比較簡單,我們只要從頭開始遍歷,然后找到對應的值之后記錄一下索引即可:

  1. // 查詢節點所在位置 
  2. this.indexOf = (el) => { 
  3.     let idx = -1, 
  4.         curIdx = -1, 
  5.         current = head; 
  6.     while(current) { 
  7.       idx++ 
  8.       if(current.el === el) { 
  9.         curIdx = idx 
  10.         break; 
  11.       } 
  12.       current = current.next
  13.     } 
  14.     return curIdx 
  15. }; 

這里我們之所以要用idx和curIdx兩個變量來處理,是因為如果用戶傳入的值不在鏈表里,那么idx的值就會有問題,所以用curIdx來保證準確性。

2.5 移除指定位置的節點

移除指定位置的節點也需要判斷一下邊界條件,可插入節點類似,但要注意移除之后一定要將鏈表長度-1,代碼如下:

  1. // 移除指定位置的元素 
  2. this.removeAt = (pos) => { 
  3.     // 檢測邊界條件 
  4.     if(pos >=0 && pos < length) { 
  5.       let previousNode = null
  6.                current = head, 
  7.                curIdx = 0; 
  8.       if(pos === 0) { 
  9.         // 如果pos為第一個元素 
  10.         head = current.next 
  11.       }else { 
  12.         while(curIdx++ < pos) { 
  13.           previousNode = current
  14.           current = current.next
  15.         } 
  16.         previousNode.next = current.next
  17.       } 
  18.       length --; 
  19.       return current.el 
  20.     }else { 
  21.       return null 
  22.     } 
  23. }; 

2.6 移除指定節點

移除指定節點實現非常簡單,我們只需要利用之前實現好的查找節點先找到節點的位置,然后再用實現過的removeAt即可,代碼如下:

  1. // 移除指定節點 
  2. this.remove = (el) => { 
  3.   let idx = this.indexOf(el); 
  4.   this.removeAt(idx); 
  5. }; 

2.7 獲取節點長度

這里比較簡單,直接上代碼:

  1. // 返回鏈表長度 
  2. this.size = () => { 
  3.   return length 
  4. }; 

2.8 判斷鏈表是否為空

判斷鏈表是否為空我們只需要判斷長度是否為零即可:

  1. // 返回鏈表長度 
  2. this.size = () => { 
  3.   return length 
  4. }; 

2.9 打印節點

打印節點實現方式有很多,大家可以按照自己喜歡的格式打印,這里筆者直接將其打印為數組格式輸出,代碼如下:

  1. // 將鏈表轉化為數組返回 
  2. this.toArray = () => { 
  3.     let current = head, 
  4.         results = []; 
  5.     while(current) { 
  6.       results.push(current.el); 
  7.       current = current.next
  8.     } 
  9.     return results 
  10. };  

這樣,我們的單向鏈表就實現了,那么我們可以這么使用:

  1. let link = new linkedList() 
  2. // 添加節點 
  3. link.append(1) 
  4. link.append(2) 
  5. // 查找節點 
  6. link.indexOf(2) 
  7. // ... 

3.原生javascript實現一條個雙單向鏈表

有了單向鏈表的實現基礎,實現雙向鏈表也很簡單了,我們無非要關注的是雙向鏈表的節點創建,這里筆者實現一個例子供大家參考:

  1. let Node = function(el) { 
  2.       this.el = el; 
  3.       this.previous = null
  4.       this.next = null
  5.  } 
  6. let length = 0 
  7. let head = null  // 用來存儲頭部元素的引用 
  8. let tail = null  // 用來存儲尾部元素的引用 

由代碼可知我們在節點中會有上一個節點的引用以及下一個節點的引用,同時這里筆者添加了頭部節點和尾部節點方便大家操作。大家可以根據自己的需求實現雙向鏈表的功能,這里筆者提供一份自己實現的代碼,可以參考交流一下:

  1. // 雙向鏈表, 每一個元素都有一個存儲元素自身的節點和指向上一個元素引用以及下一個元素引用的節點組成 
  2. function doubleLinkedList() { 
  3.   let Node = function(el) { 
  4.       this.el = el; 
  5.       this.previous = null
  6.       this.next = null
  7.   } 
  8.   let length = 0 
  9.   let head = null  // 用來存儲頭部元素的引用 
  10.   let tail = null  // 用來存儲尾部元素的引用 
  11.  
  12.   // 尾部添加元素 
  13.   this.append = (el) => { 
  14.     let node = new Node(el) 
  15.     if(!head) { 
  16.       head = node 
  17.     }else { 
  18.       tail.next = node; 
  19.       node.previous = tail; 
  20.     } 
  21.     tail = node; 
  22.     length++ 
  23.   }; 
  24.   // 插入元素 
  25.   this.insert = (pos, el) => { 
  26.     if(pos >=0 && pos < length) { 
  27.       let node = new Node(el); 
  28.       if(pos === length - 1) { 
  29.         // 在尾部插入 
  30.         node.previous = tail.previous; 
  31.         node.next = tail; 
  32.         tail.previous = node; 
  33.         length++; 
  34.         return true 
  35.       } 
  36.       let current = head, 
  37.           i = 0; 
  38.       while(i < pos) { 
  39.         current = current.next
  40.         i++ 
  41.       } 
  42.       node.next = current
  43.       node.previous = current.previous; 
  44.       current.previous.next = node; 
  45.       current.previous = node; 
  46.       length ++; 
  47.       return true     
  48.     }else { 
  49.       throw new RangeError(`插入范圍有誤`) 
  50.     } 
  51.   }; 
  52.   // 移除指定位置的元素 
  53.   this.removeAt = (pos) => { 
  54.     // 檢測邊界條件 
  55.     if(pos < 0 || pos >= length) { 
  56.       throw new RangeError(`刪除范圍有誤`) 
  57.     }else { 
  58.       if(length) { 
  59.         if(pos === length - 1) { 
  60.           // 如果刪除節點位置為尾節點,直接刪除,節省查找時間 
  61.           let previous = tail.previous; 
  62.           previous.next = null
  63.           length --; 
  64.           return tail.el 
  65.         }else { 
  66.           let current = head, 
  67.               previous = null
  68.               next = null
  69.               i = 0; 
  70.           while(i < pos) { 
  71.             current = current.next 
  72.             i++ 
  73.           } 
  74.           previous = current.previous; 
  75.           next = current.next
  76.           previous.next = next
  77.           length --; 
  78.           return current.el 
  79.         } 
  80.       }else { 
  81.         return null 
  82.       } 
  83.     } 
  84.   }; 
  85.   // 移除指定節點 
  86.   this.remove = (el) => { 
  87.     let idx = this.indexOf(el); 
  88.     this.removeAt(idx); 
  89.   }; 
  90.   // 查詢指定位置的鏈表元素 
  91.   this.get = (index) => { 
  92.     if(index < 0 || index >= length) { 
  93.       return undefined 
  94.     }else { 
  95.       if(length) { 
  96.         if(index === length - 1) { 
  97.           return tail.el 
  98.         } 
  99.         let current = head, 
  100.             i = 0; 
  101.         while(i < index) { 
  102.           current = current.next 
  103.           i++ 
  104.         } 
  105.         return current.el 
  106.       }else { 
  107.         return undefined 
  108.       } 
  109.     } 
  110.   } 
  111.   // 查詢節點所在位置 
  112.   this.indexOf = (el) => { 
  113.     let idx = -1, 
  114.         current = head, 
  115.         curIdx = -1; 
  116.     while(current) { 
  117.       idx++ 
  118.       if(current.el === el) { 
  119.         curIdx = idx; 
  120.         break; 
  121.       } 
  122.       current = current.next
  123.     } 
  124.     return curIdx 
  125.   }; 
  126.   // 判斷鏈表是否為空 
  127.   this.isEmpty = () => { 
  128.     return length === 0 
  129.   }; 
  130.   // 返回鏈表長度 
  131.   this.size = () => { 
  132.     return length 
  133.   }; 
  134.   // 將鏈表轉化為數組返回 
  135.   this.toArray = () => { 
  136.     let current = head, 
  137.         results = []; 
  138.     while(current) { 
  139.       results.push(current.el); 
  140.       current = current.next
  141.     } 
  142.     return results 
  143.   }; 

4.鏈表和數組的對比及優缺點

實現完鏈表之后我們會對鏈表有更深入的認知,接下來我們進一步分析鏈表的優缺點。筆者將從3個維度來帶大家分析鏈表的性能情況:

  • 插入刪除性能
  • 查詢性能
  • 內存占用

我們先看看插入和刪除的過程:

由上圖可以發現,鏈表的插入、刪除數據效率非常高,只需要考慮相鄰結點的指針變化,因為不需要移動其他節點,時間復雜度是 O(1)。

再來看看查詢過程:

我們對鏈表進行每一次查詢時,都需要從鏈表的頭部開始找起,一步步遍歷到目標節點,這個過程效率是非常低的,時間復雜度是 O(n)。這方面我們使用數組的話效率會更高一點。

我們再看看內存占用。鏈表的內存消耗比較大,因為每個結點除了要存儲數據本身,還要儲存前后結點的地址。但是好處是可以動態分配內存。

另一方面,對于數組來說,也存在一些缺點,比如數組必須占用整塊、連續的內存空間,如果聲明的數組數據量過大,可能會導致“內存不足”。其次就是數組一旦需要擴容,會重新申請連續的內存空間,并且需要把上一次的數組數據全部copy到新的內存空間中。

綜上所述,當我們的數據存在頻繁的插入刪除操作時,我們可以采用鏈表結構來存儲我們的數據,如果涉及到頻繁查找的操作,我們可以采用數組來處理。實際工作中很多底層框架的封裝都是采用組合模式進行設計,一般純粹采用某種數據結構的比較少,所以具體還是要根據所處環境進行適當的方案設計。

 

責任編輯:姜華 來源: 趣談前端
相關推薦

2024-11-04 06:00:00

redis雙向鏈表

2010-02-06 09:46:46

C++單向鏈表

2024-11-22 15:00:00

開源Redis鏈表

2013-12-18 13:30:19

Linux運維Linux學習Linux入門

2025-04-18 00:00:00

MCPSSEHTTP

2021-08-15 22:52:30

前端H5拼圖

2020-09-24 11:46:03

Promise

2011-02-28 18:19:40

無線

2015-10-12 16:37:39

前端編碼雙向編譯

2013-07-01 15:06:04

2021-02-19 23:07:02

Vue綁定組件

2022-12-26 00:51:33

雙向鏈表二叉搜索樹

2023-11-07 14:30:28

Python開發

2024-08-28 08:09:13

contextmetrics類型

2023-01-07 08:09:41

零代碼Dooring組件

2020-05-20 22:37:42

HTTPSSSL雙向驗證

2022-02-13 23:00:48

前端微前端qiankun

2025-07-11 09:10:00

AI框架訓練

2024-05-31 08:53:56

2021-12-07 06:55:17

二叉搜索樹鏈表
點贊
收藏

51CTO技術棧公眾號

jizz久久精品永久免费| 国产精品毛片一区二区三区| 亚洲成精国产精品女| 日韩亚洲欧美精品| 九九99久久精品在免费线bt| 欧美日韩国产综合一区二区| 91最新在线观看| 久久成人在线| 国产精品网站视频| 色婷婷成人网| 91精品国产美女浴室洗澡无遮挡| 久久精品影视大全| 午夜亚洲福利在线老司机| 午夜精品理论片| 日本不卡网站| 日韩欧美国产骚| 黄色国产小视频| 奇米一区二区三区av| 国产精品激情av在线播放| 97精品国产综合久久久动漫日韩| 欧美精品aⅴ在线视频| 国产色a在线观看| 99国产麻豆精品| 日韩精品久久久| 亚洲乱码免费伦视频| 欧美风情在线观看| 色在线免费观看| 91精品免费观看| 在线看片免费人成视久网| 久久久99精品免费观看不卡| 伊人久久大香线蕉成人综合网| 一本精品一区二区三区| 欧美综合在线观看| 欧美一区二区三区婷婷| 日韩电影网在线| 男人的天堂在线视频免费观看 | 久久久999免费视频| 亚洲综合日本| 亚洲tv在线观看| 国产精品亚洲片在线播放| 精品中文字幕在线| 日韩五码电影| 欧美成人午夜激情视频| 欧美综合社区国产| 久久综合伊人77777蜜臀| 福利一区在线| 另类色图亚洲色图| 中文字幕一区二区三区四区久久| 久久综合久久八八| 操欧美女人视频| 欧美亚洲第一区| 国产欧美高清视频在线| 国产精品久久久久久av下载红粉| 国产中文字幕一区二区三区| 国产欧美日韩亚洲精品| 亚洲电影在线一区二区三区| 亚洲va欧美va国产综合剧情| 伊人成人在线| 亚洲精品乱码久久久久久蜜桃91 | 日韩精彩视频在线观看| 精品久久久久久综合日本| 欧美日韩精品| 国产午夜精品在线| 日韩精品欧美精品| 欧美激情精品久久久久久大尺度 | 91精品国产综合久久香蕉的用户体验| 天天躁日日躁狠狠躁欧美| 91av在线播放视频| 国产精品欧美在线观看| 亚洲专区在线视频| 日韩在线观看一区二区| 国产激情在线看| 日本一区二区三区在线不卡| 国产一级视频| 色婷婷久久久综合中文字幕| 麻豆视频在线观看免费网站| 亚洲女人天堂视频| 国产另类在线| 99在线高清视频在线播放| 久久久国产精品一区二区中文| 特级黄色录像片| 国产精品毛片久久久久久| 中文字幕大看焦在线看| 欧美精品久久天天躁| 亚洲精华液一区二区三区| 九九热r在线视频精品| 97精品97| 亚洲小视频在线播放| 中文字幕一区二区三区在线观看| 日韩有码电影| 精品亚洲男同gayvideo网站| 中文字幕亚洲在线观看| 成人看片视频| 国产福利一区在线| 麻豆电影传媒二区| 欧美精品一区二区三区蜜臀| 日本一区二区三区视频在线看| 成人伊人精品色xxxx视频| 韩国精品久久久| 波多野结衣在线中文| 日韩欧美一区二区免费| 久久国产精品免费精品3p| 免费试看一区| 国产精品久久久久婷婷二区次| jizz视频在线观看| 欧美成人性生活| 在线亚洲自拍| 欧美色图色综合| 91成人国产精品| 精品视频成人| 久久综合色一本| 国产精品久久久久久久久图文区| 国产黄色在线观看| 午夜精品福利电影| 麻豆精品一区二区av白丝在线| 91麻豆福利| 日韩精品欧美国产精品忘忧草| 欧美在线电影| 激情深爱综合网| 欧美在线你懂得| 开心激情综合| 日韩激情视频一区二区| 五月激情综合网| 999精品嫩草久久久久久99| 久久av二区| 一区二区三区鲁丝不卡| 欧美二三四区| 国产精品视频免费一区二区三区| 国产视频在线观看一区二区三区| 亚洲小说区图片| 亚洲伊人久久综合| 中文字幕在线不卡视频| 超碰超碰人人人人精品| 狠狠色综合色区| 性久久久久久久| 欧美激情99| 国产精品wwwww| 亚洲欧美另类在线观看| 99av国产精品欲麻豆| 理论片在线观看理伦片| 欧美高清videos高潮hd| 国产精品一级二级三级| 色女人在线视频| 好吊色欧美一区二区三区视频| 亚洲精品成人精品456| 91蜜桃臀久久一区二区| 国产极品在线视频| 亚洲免费视频网站| 蜜臀av在线播放一区二区三区| 伊人在线视频| 亚洲a∨日韩av高清在线观看| 亚洲激情网站免费观看| 国产 日韩 欧美| 国产精品8888| 欧美卡1卡2卡| 久久久久久久久久久免费| 欧美1o一11sex性hdhd| 成a人片在线观看| 99精品热视频只有精品10| 国产成人小视频| 国产精品素人一区二区| 成人免费视频免费观看| 日欧美一区二区| 亚洲免费大片在线观看| 精品成人乱色一区二区| 欧美一区二区三区的| 亚洲精品电影网在线观看| 一本大道综合伊人精品热热 | 成人黄色在线免费| 视频一区视频二区视频三区视频四区国产 | 在线激情av| 国产人成网在线播放va免费| 国产在视频一区二区三区吞精| 色婷婷综合久久久中字幕精品久久| 美女色狠狠久久| www一区二区三区| 久久亚州av| 93在线视频精品免费观看| 成人精品视频一区二区三区尤物| 久久看人人摘| 国产麻豆精品久久一二三| 国产调教视频一区| 久久99视频免费| 美媛馆国产精品一区二区| 先锋在线亚洲| 人妖欧美1区| 三级久久三级久久久| 久久亚洲影院| 久久夜色精品一区| 日韩精品在线免费观看| 全球成人中文在线| 青青视频免费在线| 男人av在线| 中文字幕免费一区二区三区| 国产精品福利在线播放| 欧美性猛交xxxx黑人交| 午夜精品久久17c| 色姑娘综合网| jzzjzzjzz亚洲成熟少妇| 国产不卡精品|