前端也需要了解的資料結構-連結串列

鮑康霖發表於2018-11-24

前言

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

單向連結串列

前端也需要了解的資料結構-連結串列

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

前端也需要了解的資料結構-連結串列

程式表示

  • 表示一個節點node
function ListNode(key){ // 節點就單純是一個資料結構,不需要其他方法
this.key = key; // 傳入的key
this.next = null; // 初始化時,下一個節點指向null
}
複製程式碼
  • 表示單向連結串列
class LinkedList { // 使用class,可以新增其他方法
constructor(){
this.head = null; // 初始化時,頭指標指向null
}
}
複製程式碼

向空連結串列中插入元素

  1. 建立一個空連結串列,HEAD指標指向NULL

前端也需要了解的資料結構-連結串列

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

前端也需要了解的資料結構-連結串列

const node = new ListNode(1)
複製程式碼
  1. 將HEAD指標指向節點
list.head = node;
複製程式碼
  1. 當前連結串列結構
LinkedList {
head: ListNode {
key: 1,
next: null
}
}
複製程式碼
  1. 再插入一個元素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

前端也需要了解的資料結構-連結串列

  1. 找到節點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.next ) {  
        fast = fast.next.next;  
        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)
複製程式碼

結語

個人能力有限,如有錯誤,請指出.

如果能給您的程式碼之路增加點幫助,點個贊吧

相關文章