JavaScript資料結構之連結串列--設計

snowLu發表於2019-03-21

上一篇文章中介紹了幾種常見連結串列的含義,今天介紹下如何寫出正確的連結串列程式碼。


如何表示連結串列

我們一般設計的連結串列有兩個類。Node 類用來表示節點,LinkedList 類提供了一些輔助方法,比如說結點的增刪改查,以及顯示列表元素等方法。 接下來看看如何用 js 程式碼表示一個連結串列。

程式碼演示:

{
  var Node = function(data) {
    this.data = data;
    this.next = null;
  };
  var node1 = new Node(1);
  var node2 = new Node(2);
  var node3 = new Node(3);

  node1.next = node2;
  node2.next = node3;
  console.log(node1.data);
  console.log(node1.next.data);
  console.log(node1.next.next.data);
}
複製程式碼

Node 類包含兩個屬性:data 用來儲存節點上的資料,next 用來儲存指向下一個節點的連結。

{
    var LList = function() {
        this.head = new Node('head');
        this.find = find;
        this.insert = insert;
        this.remove = remove;
        this.display = display;
    }
}
複製程式碼

LList 類提供了對連結串列進行操作的方法。該類中使用一個 Node 類來儲存連結串列中的頭結點。當有新元素插入時,next 就會指向新的元素。

怎麼樣,簡單吧,你已經學會連結串列了

JavaScript資料結構之連結串列--設計

但,這只是基本的連結串列表示法,水還很深。

注意事項

JavaScript資料結構之連結串列--設計

把上一篇文章中的單連結串列圖搬過來,方便參考

JavaScript資料結構之連結串列--設計

重點一:理解指標或引用的含義

這裡的指標或者引用,他們的意思都是一樣的,都是儲存所指物件的記憶體地址。

將某個變數賦值給指標,實際上就是將某個變數的地址賦值給指標,或者反過來說,指標中儲存了這個變數的記憶體地址,指向了這個變數,通過指標就能找到這個變數。

看下面的虛擬碼表示什麼意思:

p -> next = q;
複製程式碼

這行程式碼就是說 p 結點中的 next 指標儲存了 q 結點的記憶體地址。

再看下面的程式碼表示什麼:

p -> next = p -> next -> next;
複製程式碼

這行程式碼表示,p 結點的 next 指標儲存了 p 結點的下下一個結點的記憶體地址。

現在應該能有所體會指標或者引用的概念了吧。

JavaScript資料結構之連結串列--設計

重點二:警惕指標丟失和記憶體洩露

在寫連結串列程式碼的時候,尤其是我們的指標,會不斷的改變,指來指去的。所以在寫的時候,一定注意不要弄丟了指標。

如下圖中單連結串列的插入操作

JavaScript資料結構之連結串列--設計

我們希望在結點 B 和相鄰的結點 C 之間插入 D 結點。假設當前指標 p 指向 B 結點。如果你將程式碼實現成下面這個樣子,就會發生指標丟失和記憶體洩露。

// 虛擬碼
p -> next = d; // 將 p 的 next 指標指向 D 結點;
d -> next = p -> next // 將 D 的結點的 next 指標指向 C? 結點。
複製程式碼

我們來分析下:p -> next 指標在完成第一步操作後,已經不再指向結點 C 了,而是指向新增加的結點 D。第二行的程式碼相當於將 D 賦值給 d->next,自己指向自己。因此,整個連結串列也就被截斷了。

所以我們新增結點時,一定要注意操作的順序,要先將結點 D 的 next 指標指向結點 C,再把結點 B 的指標指向 D,這樣才不會丟失指標。對於剛才的那段程式碼,你知道怎麼修改才是正確的了吧。

重點三:重點留意邊界條件處理

首先來回顧下剛才所說的單連結串列的插入操作。如果在 p 結點後面增加一個新的結點,只需要關注以下兩步即可。

new_node -> next = p -> next;
p -> next = new_node;
複製程式碼

但是,當我們向一個空連結串列中插入第一個結點時,就需要特殊處理了。當連結串列為空時,也就是連結串列的head為空,那直接賦值即可,如下:

if(head == null) {
    head = new_node;
}
複製程式碼

看一段完整的新增節點程式碼:

// 新增一個新結點  tail:表示尾結點
  append(data) {
    const node = new Node(data);
    if (!this.head) {
      this.head = node;
      this.tail = this.head;
    } else {
      this.tail.next = node;
      this.tail = node;
    }
  }
複製程式碼

如果頭結點不存在的話,頭結點等於尾結點。如果頭結點存在的話,利用尾結點來擴充連結串列的資料,別忘了再移動 tail 成為尾結點。

再來看單連結串列結點的刪除操作。如果在p結點後刪除一個結點,只需要關注一步即可:

JavaScript資料結構之連結串列--設計

p -> next = p -> next -> next;
複製程式碼

但是,當連結串列中只剩一個結點head時,也需要特殊處理才可以,如下:

if(head -> next == null){
    head = null;
}
複製程式碼

刪除的程式碼邏輯請檢視 github-連結串列-remove

我提供的方法比較繁瑣,當閱讀了《資料結構與演算法JavaScript描述》的時候,發現書上寫的方法特別簡潔。在此分享一下。

以下是此書中的刪除結點程式碼:

{
/*
首先根據要刪除的元素,檢查每一個結點的下一個結點中是否儲存著要刪除的資料。
如果找到,則返回該結點,即前一個結點。
*/
  function findPvevious(item) {
    var currNode = this.head;
    while(!(currNode.next == null) && (currNode.next.element != item)) {
      currNode = currNode.next;
    }
    return currNode;
  }
 // 然後找到前一個結點後,利用上文提到的單連結串列刪除操作進行刪除。
  function remove(item) {
    var prevNode = this.findPvevious(item);
    if(!(prevNode.next == null)) {
      prevNode.next = prevNode.next.next;
    }
  }
}
複製程式碼

所以寫連結串列程式碼時,要經常注意邊界條件是否考慮到了:

  • 如果連結串列為空時,程式碼是否能正常工作?
  • 如果連結串列只有一個結點時,程式碼是否能正常工作?
  • 如果在處理頭結點和尾結點時,程式碼是否能正常工作?

重點四:要學會畫圖輔助思考

比如一些單連結串列的增刪改查等,指標總是不斷的改變。這個時候如果大腦思考不過來的話,可以簡單的畫個示意圖輔助一下。比如說單連結串列的增加結點操作,可以畫出增加前後的連結串列變化。

JavaScript資料結構之連結串列--設計
看圖寫程式碼,是不是會比較清楚指標接下來的指向呢。

程式碼演示

請前往 github 檢視 連結串列常見的方法

參考書籍

《資料結構與演算法JavaScript描述》

有你才完美

連結串列這種資料結構,確實比較容易懂,但是想寫出好相關的操作程式碼,確實不易,指標或者引用也是我的薄弱環節,指來指去便記不清該怎麼指啦!動物的關節對於動物來講非常的重要,指標感覺就是連結串列的關節,連結串列像一輛火車,每個車廂的連線全靠車廂間的連線軸。

傳送門

  1. JavaScript資料結構之棧
  2. JavaScript資料結構之佇列
  3. JavaScript 資料結構之隊棧互搏
  4. JavaScript資料結構之連結串列--介紹
  5. JavaScript 演算法之複雜度分析
  6. JavaScript 演算法之最好、最壞時間複雜度分析

相關文章