JS版資料結構第三篇(連結串列)

走音發表於2019-04-28
連結串列分為單連結串列,雙連結串列,以及環形連結串列,我將分三個模組分別介紹,並有相應的題目對應講解。

單連結串列

定義

還是按照老規矩先看一下百度百科對單連結串列的定義

JS版資料結構第三篇(連結串列)

根據以上文字我們可以得出

  • 單連結串列是一種鏈式的資料結構
  • 它由一系列結點組成
  • 每個結點包含兩個部分:
  1. 結點元素的資料
  2. 指向下一個位置的指標(next)

JS版資料結構第三篇(連結串列)

頭指標head和終端結點

  • 單連結串列中每個結點的儲存地址是存放在其前趨結點next域(即它前一個節點的next指標)中
  • 而開始結點無前趨,故應設頭指標head指向開始結點。
  • 連結串列由頭指標唯一確定,單連結串列可以用頭指標的名字來命名。
  • 終端結點無後繼,故終端結點的指標域為空,即NULL。

JS版資料結構第三篇(連結串列)

想象一下,每個結點都儲存著指向下一個結點的指標,這樣只要給出頭指標head,

你就可以根據它獲取到連結串列中的任何一個元素了,將結點元素'連線'在一起就稱之為連結串列,

是不是還蠻形象的?

接下來我們可以嘗試自己設計一個單連結串列

例題-設計連結串列

LeetCode第707題    原題地址

設計單連結串列的實現。單連結串列中的節點應該具有兩個屬性:valnextval 是當前節點的值,next 是指向下一個節點的指標/引用。假設連結串列中的所有節點都是 0-index 的。

在連結串列類中實現這些功能:

  • get(index):獲取連結串列中第 index 個節點的值。如果索引無效,則返回-1
  • addAtHead(val):在連結串列的第一個元素之前新增一個值為 val 的節點。插入後,新節點將成為連結串列的第一個節點。
  • addAtTail(val):將值為 val 的節點追加到連結串列的最後一個元素。
  • addAtIndex(index,val):在連結串列中的第 index 個節點之前新增值為 val 的節點。如果 index 等於連結串列的長度,則該節點將附加到連結串列的末尾。如果 index 大於連結串列長度,則不會插入節點。
  • deleteAtIndex(index):如果索引 index 有效,則刪除連結串列中的第 index 個節點。

示例:

MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2);   //連結串列變為1-> 2-> 3
linkedList.get(1);            //返回2
linkedList.deleteAtIndex(1);  //現在連結串列是1-> 3
linkedList.get(1);            //返回3複製程式碼

題目要求我們設計一個基本的列表並實現增刪改查等基礎功能,在對單連結串列有一個基本的瞭解後應該很容易讀懂題目,我們直接開始解題

首先我們需要建立一個連結串列類(建構函式),這個也就是我們要設計的連結串列,它包含了:

  • _length 用於表示連結串列中的節點數量

  • head 分配一個節點作為連結串列的頭

  • get(index) 根據索引查詢節點對應的值
  • addAtHead(value) 向連結串列中第一個元素前新增一個節點

  • addAtTail(value) 向連結串列中最後元素後新增一個節點

  • addAtIndex(index,val) 在連結串列中的第 index 個節點之前新增值為 val 的節點

  • deleteAtIndex(index) 刪除指定位置的節點

// 連結串列類
class MyLinkedList{    
    constructor(){
        // 頭指標初始為空
        this.head = null
        // 長度初始為0
        this._length = 0
    }
    get (index) {

    }    
    addAtHead (val) {

    }
    addAtTail (val) {

    }    
    addAtIndex (index, val) {

    }    
    deleteAtIndex (index) {

    }}複製程式碼

然後我們需要一個節點類(建構函式),用來儲存每個節點的value以及next屬性

  • data 儲存資料

  • next 指向連結串列中下一個節點的指標

// 節點類
class Node(value) {
    // 例項化時賦值
    this.val = value
    // next指標初始為null
    this.next = null
}複製程式碼

兩個建構函式已經寫好了,接下來我們開始實現以下它的功能

  • 方法1:  addAtHead(val) 

在連結串列的第一個元素之前新增一個值為 val 的節點,我們需要

  1. 用一個變數oldHead儲存當前連結串列中的頭指標head
  2. 以val值例項化一個新的node節點,並將其作為新的頭指標head
  3. 將新的頭指標的next指向oldHead
程式碼如下:

addAtHead(val){
    let oldHead = this.head
    this.head = new Node(val)
    this.head.next = oldHead
    this._length++
}複製程式碼

  • 方法2:addAtTail(val)
向連結串列中最後元素後新增一個節點,我們需要
  1. 以val值例項化新的node節點
  2. 根據頭指標head遍歷到連結串列中最後一個元素
  3. 將最後一個元素的next指標指向新的node節點
程式碼如下:

addAtTail (val) {
    let node = new Node(val)
    // 用一個變數儲存連結串列最後一個節點 初始值為頭指標
    let last = this.head
    // 遍歷找到最後一個節點
    while(last && last.next){
        last = last.next
    }
    // 如果非空連結串列
    if(last){
        // 將最後一個節點指向插入的節點
        last.next = node
    }else{
        // 若為空連結串列 將頭指標指向新插入節點
        this.head = node
    }
    this._length++
}複製程式碼

  • 方法3:get(index)
根據索引查詢連結串列中的節點,我們需要:
  1. 宣告一個變數currentIndex記錄遍歷的次數,初始為0
  2. 根據頭指標head的next指標遍歷連結串列,每查詢一次next,將currentIndex加1
  3. 直到currentIndex與要查詢的index相等
程式碼如下:

get(index){
    // 記錄遍歷次數 初始為0
    let currentIndex = 0
    // 記錄遍歷到的節點 初始為head
    let cur = thi.head
    // 根據題意, 索引無效則返回 -1
    if(index < 0 || this._length < index + 1){
        return -1
    }
    // 直到查詢到索引為index的元素為止
    while (cur){
        if(currentIndex = index){
            return cur.val
        }
    }
    cur = cur.next
    currentIndex++
}複製程式碼

  • 方法4:addAtIndex(index,val)
從索引位置為index的前端插入值為val的元素,我們需要:
  1. 以val值例項化一個新node結點
  2. 找到要插入元素的前一位元素,改變它的next指標,指向新node結點
  3. 將新插入的node結點的next指標指向原位置的結點
但是程式碼中我們要考慮到特殊的一些情況(在頭部或尾部插入,連結串列為空等情況)

程式碼如下:

addAtIndex(index, val){
    //  若索引在0位之前 直接呼叫addAtHead方法
    if(index <= 0){
        return this.addAtHead(val)
    }
    // 如果index值大於0
    if(index >= 0 && index <= this._length){
        // 以val值例項化新結點
        let node = new Node(val)
        // 記錄當前遍歷的結點,初始為head    
        let cur = this.head
        // 記錄遍歷次數
        let currentIndex = 0
        // 記錄當前遍歷結點的next指標
        let next
        while(cur){
            if(currentIndex === index - 1){
                // 此時cur為要插入結點的前方結點
                // 用next儲存插入元素前方結點的原next指標
                next = cur.next
                cur.next = node
                node.next = next
                this._length++
                break
             }
             cur = cur.next
             currentIndex++
        }   
    }
}複製程式碼

  • 方法5:deleteAtIndex(index)
接下來實現我們的最後一個方法,根據索引刪除元素,我們需要:
  1. 通過head指標遍歷到index位置的元素
  2. 用變數將根據索引查詢到的要刪除的元素下一位記錄下來
  3. 將要刪除元素的前一位的next指標指向要刪除元素的下一位
程式碼如下:

deleteIndex(index){
    // 若索引有效
    if(index >=0 && index <= this._length -1){
        let cur = this.head
        // 若刪除索引為0的元素,只需將head指標向下移動一位,不需要考慮刪除元素之前結點的指標
        if(index === 0){
            this.head = cur.next
            this._length--
        }else{
            // 若刪除索引非0
            let currentIndex = 0
            let next
            while(cur){
                if(currentIndex === index-1){
                    // 此時cur為要刪除元素的前一個結點
                    next = cur.next.next
                    // 先將刪除元素清空 避免記憶體洩漏
                    cur.next = null
                    cur.next = next
                    this._length--
                }
                cur = cur.next
                currentIndex++
            }
        }
    }
}複製程式碼

這樣我們就實現了一個完整的實現增刪改查功能的單連結串列了,完整程式碼如下:

class Node {
  constructor (val) {
    this.value = val
    this.next = null
  }
}
class MyLinkedList {
  constructor () {
    this._length = 0
    this.head = null
  }
  addAtHead (val) {
    let originHead = this.head
    this.head = new Node(val)
    this.head.next = originHead
    this._length++
  }
  addAtTail (val) {
    let last = this.head
    let node = new Node(val)
    while (last && last.next) {
      last = last.next
    }
    if (last) {
      last.next = node
    } else {
      this.head = node
    }
    this._length++
  }
  get (index) {
    let currentIndex = 0
    let cur = this.head
    if (index < 0 || this._length < index + 1) {
      return -1
    }
    while (cur) {
      if (currentIndex === index) {
        return cur.value
      }
      cur = cur.next
      currentIndex++
    }
  }
  // 特定位置插入
  addAtIndex(index, val){    //  若索引在0位之前 直接呼叫addAtHead方法
    if(index <= 0){
        return this.addAtHead(val)
    }
    // 如果index值大於0
    if(index >= 0 && index <= this._length){
        // 以val值例項化新結點
        let node = new Node(val)
        // 記錄當前遍歷的結點,初始為head    
        let cur = this.head
        // 記錄遍歷次數
        let currentIndex = 0
        // 記錄當前遍歷結點的next指標
        let next
        while(cur){
            if(currentIndex === index - 1){
                // 此時cur為要插入結點的前方結點
                // 用next儲存插入元素前方結點的原next指標
                next = cur.next
                cur.next = node
                node.next = next
                this._length++
                break
             }
             cur = cur.next
             currentIndex++
        }   
    }
}  // 按索引刪除
  deleteAtIndex (index) {
    if (index >= 0 && index <= this._length - 1) {
      let cur = this.head
      // 如果刪第0位
      if (index === 0) {
        this.head = cur.next
        this._length--
      } else {
        let currentIndex = 0
        let next
        while (cur) {
          if (currentIndex === index - 1) {
          // 此時cur是被刪除元素的前一位
            next = cur.next.next
            // 先將刪除元素清空 避免記憶體洩漏
            cur.next = null
            cur.next = next
            this._length--
          }
          cur = cur.next
          currentIndex++
        }
      }
    }
  }
}
複製程式碼

迴圈連結串列

在學習雙向連結串列之前,我們先來看一下迴圈連結串列

定義

百度百科是這樣定義的

JS版資料結構第三篇(連結串列)

其實相對於單連結串列,只是連結串列的最後一位元素的next指向了頭指標

JS版資料結構第三篇(連結串列)

可以說是再簡單不過了。

例題-環形連結串列

LeetCode-141題 原題地址

給定一個連結串列,判斷連結串列中是否有環。

為了表示給定連結串列中的環,我們使用整數 pos 來表示連結串列尾連線到連結串列中的位置(索引從 0 開始)。 如果 pos-1,則在該連結串列中沒有環。

示例1:
輸入:head = [3,2,0,-4], pos = 1
輸出:true
解釋:連結串列中有一個環,其尾部連線到第二個節點。

複製程式碼

示例2:

輸入:head = [1,2], pos = 0                                                                                            輸出:true                                                                                                                      解釋:連結串列中有一個環,其尾部連線到第一個節點。                                                                                                                                                                     JS版資料結構第三篇(連結串列)

注意:這個題目裡面的環形連結串列和我們定義中的迴圈有所不同,迴圈連結串列的定義是連結串列的最後一位元素一定指向頭指標,而這裡題目中的是尾指標可能指向連結串列中的任何一個非尾指標。

那我們如何判斷是否有環呢?

我們正常的思路一定是從頭指標head開始遍歷這個連結串列,一直到它長度的最後一位,如果它的next指標一直不為Null,那麼它就是有環的,這個時候問題來了,由於這個連結串列並不是我們自己設計的,他將連結串列傳進來時是已經處理好的狀態,他並沒有宣告length屬性,如果這樣遍歷的話他並沒有截止條件,會造成一個死迴圈,這裡也困擾了我很久。

於是我看了一下這道題的評論,發現了一種很巧妙的'快慢指標'的解法。

我們先來看一下下面這個動畫

JS版資料結構第三篇(連結串列)

我們先分別宣告兩個快慢指標,

初始位置都指向head,控制快指標每次移動兩位,慢指標每次移動一位。

在上邊的這個動畫中快慢指標同時移動三次的時候fast指標指向了值為2的結點,,slow指標指向了。大家想象一下,當移動第四次的時候,他們會同時指向值為7的位置(原諒我動畫只錄到第三次)。   

那麼是不是可以認為只要有環,兩個指標就會有指向同一個結點的情況呢?

根據這樣的思路,我們可以完成以下程式碼:

var hasCycle = (head) => {
    let fast = head
    let slow = head
    while( fast != null && fast.next != null){
        slow = slow.next
        fast = fast.next.next
        if( slow === fast){
            return true
        }
    }
    return false
}複製程式碼


雙向連結串列

定義

JS版資料結構第三篇(連結串列)

其實說白了雙向連結串列就是在單連結串列的基礎上每個結點多了一個指向上一個結點的指標,瞭解完了單連結串列和環形連結串列,雙向連結串列應該是不難理解了。

JS版資料結構第三篇(連結串列)JS版資料結構第三篇(連結串列)

實現雙向連結串列

雙項鍊表與單連結串列的結構類似,這裡就不過多闡述了,在這裡給出實現一個基本雙向連結串列的原始碼供大家參考。

function Node(value) {
    this.data = value;
    this.previous = null;
    this.next = null;
}
 
function DoublyList() {
    this._length = 0;
    this.head = null;
    this.tail = null;
}
 
DoublyList.prototype.add = function(value) {
    var node = new Node(value);
 
    if (this._length) {
        this.tail.next = node;
        node.previous = this.tail;
        this.tail = node;
    } else {
        this.head = node;
        this.tail = node;
    }
 
    this._length++;
 
    return node;
};
 
DoublyList.prototype.searchNodeAt = function(position) {
    var currentNode = this.head,
        length = this._length,
        count = 1,
        message = {failure: 'Failure: non-existent node in this list.'};
 
    // 1st use-case: an invalid position
    if (length === 0 || position < 1 || position > length) {
        throw new Error(message.failure);
    }
 
    // 2nd use-case: a valid position
    while (count < position) {
        currentNode = currentNode.next;
        count++;
    }
 
    return currentNode;
};
 
DoublyList.prototype.remove = function(position) {
    var currentNode = this.head,
        length = this._length,
        count = 1,
        message = {failure: 'Failure: non-existent node in this list.'},
        beforeNodeToDelete = null,
        nodeToDelete = null,
        deletedNode = null;
 
    // 1st use-case: an invalid position
    if (length === 0 || position < 1 || position > length) {
        throw new Error(message.failure);
    }
 
    // 2nd use-case: the first node is removed
    if (position === 1) {
        this.head = currentNode.next;
 
        // 2nd use-case: there is a second node
        if (!this.head) {
            this.head.previous = null;
        // 2nd use-case: there is no second node
        } else {
            this.tail = null;
        }
 
    // 3rd use-case: the last node is removed
    } else if (position === this._length) {
        this.tail = this.tail.previous;
        this.tail.next = null;
    // 4th use-case: a middle node is removed
    } else {
        while (count < position) {
            currentNode = currentNode.next;
            count++;
        }
 
        beforeNodeToDelete = currentNode.previous;
        nodeToDelete = currentNode;
        afterNodeToDelete = currentNode.next;
 
        beforeNodeToDelete.next = afterNodeToDelete;
        afterNodeToDelete.previous = beforeNodeToDelete;
        deletedNode = nodeToDelete;
        nodeToDelete = null;
    }
 
    this._length--;
 
    return message.success;
};複製程式碼

參考

瘋狂的技術宅- JavaScript資料結構:單向連結串列與雙向連結串列     原文連結

百度百科

LeetCode原題

總結

今天我們用js分別實現了單連結串列,迴圈連結串列,雙連結串列的實現以及相關的LeetCode真題,相信以後在面試中再次碰到有關連結串列這種資料結構的題目你一定會遊刃有餘。

下一篇我們將介紹矩陣。

上一篇 
JS版資料結構第二篇(佇列) 


相關文章