前端也需要了解的資料結構-連結串列
最近被小夥伴問到連結串列是什麼,連結串列作為一種常見的資料結構,但是很多前端coder對此並不瞭解,寫下這篇文章,介紹下連結串列的js實現,不瞭解連結串列的同學也可以做個參考
單向連結串列

- 和陣列區別,地址離散。它在記憶體地址中可以離散的分配,由於它是離散的分配,所以他可以省去很多的麻煩,不像陣列由於預留空間不足經常需要拷貝,分配新的記憶體地址
- 總體上還是線性的資料,屬於鏈式的排列

程式表示
· 表示一個節點node ···js function ListNode(key){ // 節點就單純是一個數據結構,不需要其他方法 this.key = key; // 傳入的key this.next = null; // 初始化時,下一個節點指向null }
- 表示單向連結串列 ```js class LinkedList { // 使用class,可以新增其他方法 constructor(){ this.head = null; // 初始化時,頭指標指向null } } 複製程式碼
向空連結串列中插入元素
- 建立一個空連結串列,HEAD指標指向NULL

const list = new LinkedList() 複製程式碼
- 建立一個包含資料1的節點

const node = new ListNode(1) 複製程式碼
- 將HEAD指標指向節點
list.head = node; 複製程式碼
- 當前連結串列結構
LinkedList { head: ListNode { key: 1, next: null } } 複製程式碼
- 再插入一個元素2
- 再建立一個包含資料2的節點

const node2 = new ListNode(2) 複製程式碼
- 將節點2的next指標指向節點1

node2.next = node; 複製程式碼
- 調整HEAD指標指向節點2
list.head = node2; 複製程式碼
- 當前連結串列結構
LinkedList { head: ListNode { key: 2, next: ListNode { key: 1, next: null } } } 複製程式碼
插入的完整程式 (時間複雜度O(1))
當頭指標不為null時,新插入的節點的next指標首先指向頭指標指向的節點,然後將頭指標指向插入的節點
class LinkedList { constructor(){ this.head = null; } insert(node){ if(this.head !== null){ node.next = this.head } this.head = node; } } 複製程式碼
在連結串列中查詢節點(時間複雜度O(n))

class LinkedList { ... find(node){ let p = this.head; // 建立一個遍歷指標 while(p && p !== node){ // 當p為null或者p為node時,停止遍歷 p = p.next; } return p; // 如果node在連結串列中, p = node,否則返回null } } 複製程式碼
已知節點2,刪除節點2

- 找到節點2之前的節點prev 這是一個O(n)的操作
prev.next = node2.next; 複製程式碼
雙向連結串列圖示

雙向連結串列(Double Linked-List)
- 追加(append/push) - O(1)
- 索引 訪問/修改 (A[idx] = ...) - O(n)
- 插入 (insert) - O(1)
- 刪除 (delete/remove) - O(1)
- 合併 (merge/concat) - O(1)
從api上看,連結串列比陣列 在索引上變慢了,但是在插入、刪除、合併上都變快了
雙向連結串列程式
- 表示一個連結串列節點
function ListNode(key){ this.key = key; this.prev = null; this.next= null; } 複製程式碼
- 表示雙向連結串列
class DoubleLinkedList { constructor(){ this.head = null; } } 複製程式碼
雙向連結串列刪除元素2 (時間複雜度O(1))

節點2的前一個節點(節點1)的next指標指向節點2的下一個節點(節點3)
node2.prev.next = node2.next 複製程式碼

節點2的下一個節點(節點2)的prev指標指向節點2的上一個節點(節點1)
node2.next.prev = node2.prev; 複製程式碼

刪除節點2的指標,減少引用計數
delete node2.next; delete node2.prev 複製程式碼
雙向連結串列的插入 - O(1)
insert(node) { if(!(node instanceof ListNode)){ node = new ListNode(node); } if (this.tail === null) { this.tail = node; } if (this.head !== null) { this.head.prev = node; node.next = this.head; } this.head = node; } 複製程式碼
雙向連結串列的合併 - O(m+n)
為了讓合併操作可以在O(1)完成,除了頭指標head外,還需要維護一個尾指標tail。
merge(list) { this.tail.next = list.head; list.head.prev = this.tail; this.tail = list.tail; } 複製程式碼
列印雙向連結串列
print() { let str = ''; let p = this.head while (p !== null) { str += p.key + '<->'; p = p.next; } console.log(str += 'NULL'); } 複製程式碼
- 完整程式碼
class DoubleLinkedList { constructor() { this.head = null; this.tail = null; } print() { let str = ''; let p = this.head while (p !== null) { str += p.key + '<->'; p = p.next; } console.log(str += 'NULL'); } insert(node) { if(!(node instanceof ListNode)){ node = new ListNode(node); } if (this.tail === null) { this.tail = node; } if (this.head !== null) { this.head.prev = node; node.next = this.head; } this.head = node; } merge(list) { this.tail.next = list.head; list.head.prev = this.tail; this.tail = list.tail; } } class ListNode { constructor(key) { this.prev = null this.next = null this.key = key } } const list = new DoubleLinkedList() list.print() // 輸出: NULL for (let i = 0; i < 5; i++) { list.insert(String.fromCharCode('A'.charCodeAt(0) + i)) } list.print() // 輸出: E<->D<->C<->B<->A<->NULL list.insert('X') list.print() // 輸出: X<->E<->D<->C<->B<->A<->NULL const list2 = new DoubleLinkedList() list2.insert('Q') list2.insert('P') list2.insert('O') list2.print() // 輸出 O<->P<->Q<->NULL list2.merge(list) list2.print() // 輸出 O<->P<->Q<->X<->E<->D<->C<->B<->A<->NULL 複製程式碼
擴充套件方法
在連結串列的使用中,經常要用到一些方法,讓我們來實現它吧
- 翻轉單向連結串列
class List { ... reverse(p = this.head){ if(p.next){ reverse(p.next); p.next.next = p; p.next = null }else{ this.head = p; } } } 複製程式碼
- 寫一個函式
center(list)
找到一個連結串列的中間節點。 如果連結串列有基數個節點,那麼返回中心節點。 如果連結串列有偶數個節點,返回中間偏左的節點。
// 解法一: 空間複雜度高 O(n) 時間複雜度O(n) const center = (list)=>{ let p = list.head const arr = [] while(p){ arr.push(p) p = p.next; } return arr.length % 2 ? arr[~~(arr.length/2)] : arr[arr.length/2-1] } // 解法二 時間複雜度O(n) const center = (list)=>{ let p = list.head if(p==null) return null let count = 0 while(p){ count++; p = p.next; } count = count % 2 ? ~~(count/2) : count/2-1 p = list.head; while(count){ count-- p = p.next; } return p } // 解法三 function center(list) { let fast = list.head,// 快指標,每次移動兩個 slow = list.head// 慢指標,每次移動一個 while(fast) { fast = fast.next fast && (fast = fast.next) fast && (fast = fast.next) fast && (slow = slow.next) } return slow } const list = new DoubleLinkedList() console.log(center(list) )// null list.insert(4) list.insert(3) list.insert(2) list.insert(1) // list = 1-2-3-4 const node = center(list) // node.key = 2 console.log(node) list.insert(5) // list = 5-1-2-3-4 const node2 = center(list) // node.key = 2 console.log(node2) 複製程式碼