鏈表(Linked List)是不同于數組的另一種數據結構,它的存儲單元(即結點或元素)除了包含任意類型的數據之外,還需要包含指向另一個結點的引用,后文會用術語鏈接表示對結點的引用。
  下面會列出鏈表與數組的具體不同:
  (1)數組需要一塊連續的內存空間來存儲;而鏈表則恰恰相反,通過指針將零散的內存串聯在一起。
  (2)數組在插入和刪除時,會做數據搬移,其時間復雜度會 O(n);而鏈表只需考慮相鄰結點的指針變化,因此時間復雜度是 O(1)。
  (3)當隨機訪問第 K 個元素時,數據可根據首地址和索引計算出對應的內存地址,其時間復雜度為 O(1);而鏈表則需要讓指針依次遍歷鏈接的結點,因此時間復雜度是 O(n)。
  本系列中面試例題來源于[LeetCode](https://leetcode-cn.com/problemset/all/)、《[劍指Offer](https://book.douban.com/subject/27008702/)》等渠道。像下面這樣以“面試題”為前綴的題目,其解法大都來源于《劍指Offer》一書。
  面試題5[替換空格](https://leetcode-cn.com/problems/ti-huan-kong-ge-lcof/)。合并數組,從后往前合并,減少數字移動次數。
## 一、鏈表結構
  鏈表包含三種最常見的鏈表結構:單鏈表、雙向鏈表和循環鏈表。
**1)單鏈表**
  單鏈表的結點結構如下所示,其中next是后繼指針,可鏈接下一個結點。
~~~
class Node {
constructor(key=null) {
this.next = null;
this.key = key;
}
}
~~~
  而單鏈表又可以細分為有頭結點的單鏈表和無頭結點的單鏈表,其中頭結點不存儲任何數據,如下圖1所示。
:-: 
圖 1
  下面以有頭結點的[單鏈表為例](https://codepen.io/strick/pen/GRoYevm),演示單鏈表的插入、遍歷和刪除。
~~~
class List {
constructor() {
this.header = new Node(); //頭結點
}
add(node) {
//插入
if (!this.header.next) {
this.header.next = node;
return;
}
let current = this.header;
while (current.next != null) {
current = current.next;
}
current.next = node;
}
traverse() {
//遍歷
let current = this.header.next;
while (current) {
console.log(current.key);
current = current.next;
}
}
del(node) {
//刪除
let current = this.header.next, //當前結點
prev = this.header; //前驅結點
while (current != node) {
current = current.next;
prev = prev.next;
}
if (current) {
prev.next = current.next;
current.next = null;
}
}
}
~~~
  盡管刪除操作的時間復雜度是 O(1),但遍歷查找是主要的耗時點,復雜度為 O(n)。因為在刪除時需要知道前驅結點,而單鏈表不能直接讀取,只能從頭開始遍歷。
  面試題6[從尾到頭打印鏈表](https://leetcode-cn.com/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof/)。每經過一個結點,就放到棧中。當遍歷完后,從棧頂輸出。
  面試題18[刪除鏈表的結點](https://leetcode-cn.com/problems/shan-chu-lian-biao-de-jie-dian-lcof/)。將結點 j 覆蓋結點 i,結點 i 的next指針指向 j 的下一個結點,這樣能避免獲取結點 i 的前置結點。
  面試題52[兩個鏈表的第一個公共結點](https://leetcode-cn.com/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof/)。分別把兩個鏈接的結點放入兩個棧中,尾結點就是兩個棧的頂部,如果相同就接著比較下一個棧頂,直至找到最后一個相同結點。
**2)雙向鏈表**
  雙向鏈表顧名思義包含兩個方向的指針:前驅和后繼,結點結構如下所示。
~~~
class Node {
constructor(key = null) {
this.prev = null;
this.key = key;
this.next = null;
}
}
~~~
  雙向鏈表比單鏈表要占更多的內存空間,依托用空間換時間的設計思想,雙向鏈表要比單鏈表更加的高效。
  例如之前的刪除,由于已經保存了前驅結點,也就避免了多余的遍歷([如下所示](https://codepen.io/strick/pen/ExPdMLx))。當希望在某個結點之前插入結點,雙向鏈表的優勢也很明顯。
~~~
class List {
add(node) {
//插入
if (!this.header.next) {
this.header.next = node;
node.prev = this.header;
return;
}
let current = this.header;
while (current.next != null) {
current = current.next;
}
current.next = node;
node.prev = current;
}
del(node) {
//刪除
let current = this.header.next; //當前結點
while (current != node) {
current = current.next;
}
if (current) {
current.prev.next = current.next;
current.next = null;
}
}
}
~~~
**3)循環鏈表**
  循環鏈表是一種特殊的單鏈表,它的尾結點的后繼結點是頭結點,適合處理具有環形結構的問題,例如約瑟夫環。
  面試題62[圓圈中最后剩下的數字](https://leetcode-cn.com/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/)。用環形鏈表模擬圓圈,每刪除一個數字需要 m 步運算,共有 n 個數字,時間復雜度O(mn)。
## 二、經典例題
**1)單鏈表逆序**
  從鏈表的第二個結點開始,把遍歷到的結點插入到頭結點的后面,直至結束,例如head→1→2→3變為 head→3→2→1。
  采用遞歸的方式完成單鏈表的逆序,[如下所示](https://codepen.io/strick/pen/WNrammY)。例題:LeetCode的[206\. 反轉鏈表](https://leetcode-cn.com/problems/reverse-linked-list/)。
~~~
class List {
reverse() {
//逆序
this.recursive(this.header.next);
}
recursive(node) {
if (!node) return;
const current = node,
next = current.next;
if (!next) {
//頭結點指向逆序后鏈表的第一個結點
this.header.next = current;
return;
}
this.recursive(next);
/************************************
* 移動結點 1->2->3,1->2<-3
* 例如Node(2).next.next就是Node(3).next
* 巧妙的將Node(3).next鏈接為Node(2)
************************************/
current.next.next = current;
current.next = null;
}
}
~~~
**2)鏈表中環的檢測**
  第一種思路是緩存每個經過的結點,每到一個新結點,就判斷當前序列中是否存在,如果存在,就說明訪問過了。
  第二種思路是使用兩個指針,快指針每次前移兩步,慢指針每次前移一步,當兩個指針指向相同結點時,就證明有環,否則就沒有環,[如下所示](https://codepen.io/strick/pen/VweENWQ)。例題:LeetCode的[141\. 環形鏈表](https://leetcode-cn.com/problems/linked-list-cycle/)。
~~~
class List {
isLoop() {
//檢測環
let fast = this.header.next,
slow = this.header.next;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) return true;
}
return false;
}
}
~~~
**3)合并兩個有序鏈表**
  用兩個指針遍歷兩個鏈表,如果head1指向的數據小于head2的,則將head1指向的結點歸入合并后的鏈表中,否則用head2的,[如下所示](https://codepen.io/strick/pen/bGEmJMP)。例題:LeetCode的[21\. 合并兩個有序鏈表](https://leetcode-cn.com/problems/merge-two-sorted-lists/)。
~~~
function merge(head1, head2) {
let cur1 = head1.next,
cur2 = head2.next,
cur = null, //合并后的尾結點
head = null; //合并后的頭結點
//合并后鏈表的頭結點為第一個結點元素最小的那個鏈表的頭結點
if (cur1.key > cur2.key) {
head = head2;
cur = cur2;
cur2 = cur2.next;
} else {
head = head1;
cur = cur1;
cur1 = cur1.next;
}
//每次找鏈表剩余結點的最小值對應的結點連接到合并后鏈表的尾部
while (cur1 && cur2) {
if (cur1.key > cur2.key) {
cur.next = cur2;
cur = cur2;
cur2 = cur2.next;
} else {
cur.next = cur1;
cur = cur1;
cur1 = cur1.next;
}
}
//當遍歷完一個鏈表后把另外一個鏈表剩余的結點鏈接到合并后的鏈表后面
if (cur1 != null) cur.next = cur1;
if (cur2 != null) cur.next = cur2;
return head;
}
~~~
**4)找出鏈表倒數第 n 個結點**
  使用兩個指針,快指針比慢指針先前移 n 步,然后兩個指針同時移動。當快指針到底后,慢指針的位置就是所要找的結點,[如下所示](https://codepen.io/strick/pen/xxZmxWv)。例題:LeetCode的[劍指 Offer 22. 鏈表中倒數第k個節點](https://leetcode-cn.com/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/)。
~~~
class List {
findLast(n) {
//刪除鏈表倒數第 n 個結點
let slow = null,
fast = null;
slow = fast = this.header.next;
let i = 0;
//前移 n 步
while (i < n && fast) {
fast = fast.next;
i++;
}
while (fast) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
~~~
**5)求鏈表的中間結點**
  使用兩個指針一起遍歷鏈表。慢指針每次走一步,快指針每次走兩步。那么當快指針到達鏈表的末尾時,慢指針必然處于中間位置,[如下所示](https://codepen.io/strick/pen/GRoPRPm)。例題:LeetCode的[876\. 鏈表的中間結點](https://leetcode-cn.com/problems/middle-of-the-linked-list/)。
~~~
class List {
middle() {
//求鏈表的中間結點
let slow = this.header.next,
fast = this.header.next;
while (slow && fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
}
~~~
*****
> 原文出處:
[博客園-數據結構和算法躬行記](https://www.cnblogs.com/strick/category/1809992.html)
已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎閱讀。

推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、擴展運算符和剩余參數
- 3、解構
- 4、模板字面量
- 5、對象字面量的擴展
- 6、Symbol
- 7、代碼模塊化
- 8、數字
- 9、字符串
- 10、正則表達式
- 11、對象
- 12、數組
- 13、類型化數組
- 14、函數
- 15、箭頭函數和尾調用優化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、類
- 21、類的繼承
- 22、Promise
- 23、Promise的靜態方法和應用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基礎實踐
- 3、WebRTC視頻通話
- 4、Web音視頻基礎
- CSS進階
- 1、CSS基礎拾遺
- 2、偽類和偽元素
- 3、CSS屬性拾遺
- 4、浮動形狀
- 5、漸變
- 6、濾鏡
- 7、合成
- 8、裁剪和遮罩
- 9、網格布局
- 10、CSS方法論
- 11、管理后臺響應式改造
- React
- 1、函數式編程
- 2、JSX
- 3、組件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表單
- 8、樣式
- 9、組件通信
- 10、高階組件
- 11、Redux基礎
- 12、Redux中間件
- 13、React Router
- 14、測試框架
- 15、React Hooks
- 16、React源碼分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基礎
- 4、webpack進階
- 5、Git
- 6、Fiddler
- 7、自制腳手架
- 8、VSCode插件研發
- 9、WebView中的頁面調試方法
- Vue.js
- 1、數據綁定
- 2、指令
- 3、樣式和表單
- 4、組件
- 5、組件通信
- 6、內容分發
- 7、渲染函數和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、數據類型
- 2、接口
- 3、類
- 4、泛型
- 5、類型兼容性
- 6、高級類型
- 7、命名空間
- 8、裝飾器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系統和網絡
- 3、命令行工具
- 4、自建前端監控系統
- 5、定時任務的調試
- 6、自制短鏈系統
- 7、定時任務的進化史
- 8、通用接口
- 9、微前端實踐
- 10、接口日志查詢
- 11、E2E測試
- 12、BFF
- 13、MySQL歸檔
- 14、壓力測試
- 15、活動規則引擎
- 16、活動配置化
- 17、UmiJS版本升級
- 18、半吊子的可視化搭建系統
- 19、KOA源碼分析(上)
- 20、KOA源碼分析(下)
- 21、花10分鐘入門Node.js
- 22、Node環境升級日志
- 23、Worker threads
- 24、低代碼
- 25、Web自動化測試
- 26、接口攔截和頁面回放實驗
- 27、接口管理
- 28、Cypress自動化測試實踐
- 29、基于Electron的開播助手
- Node.js精進
- 1、模塊化
- 2、異步編程
- 3、流
- 4、事件觸發器
- 5、HTTP
- 6、文件
- 7、日志
- 8、錯誤處理
- 9、性能監控(上)
- 10、性能監控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 監控系統
- 1、SDK
- 2、存儲和分析
- 3、性能監控
- 4、內存泄漏
- 5、小程序
- 6、較長的白屏時間
- 7、頁面奔潰
- 8、shin-monitor源碼分析
- 前端性能精進
- 1、優化方法論之測量
- 2、優化方法論之分析
- 3、瀏覽器之圖像
- 4、瀏覽器之呈現
- 5、瀏覽器之JavaScript
- 6、網絡
- 7、構建
- 前端體驗優化
- 1、概述
- 2、基建
- 3、后端
- 4、數據
- 5、后臺
- Web優化
- 1、CSS優化
- 2、JavaScript優化
- 3、圖像和網絡
- 4、用戶體驗和工具
- 5、網站優化
- 6、優化閉環實踐
- 數據結構與算法
- 1、鏈表
- 2、棧、隊列、散列表和位運算
- 3、二叉樹
- 4、二分查找
- 5、回溯算法
- 6、貪心算法
- 7、分治算法
- 8、動態規劃
- 程序員之路
- 大學
- 2011年
- 2012年
- 2013年
- 2014年
- 項目反思
- 前端基礎學習分享
- 2015年
- 再一次項目反思
- 然并卵
- PC網站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端學習之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 2024年
- 日志
- 2020