上一篇文章中介紹了幾種常見連結串列的含義,今天介紹下如何寫出正確的連結串列程式碼。
如何表示連結串列
我們一般設計的連結串列有兩個類。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
就會指向新的元素。
怎麼樣,簡單吧,你已經學會連結串列了
但,這只是基本的連結串列表示法,水還很深。
注意事項
把上一篇文章中的單連結串列圖搬過來,方便參考
重點一:理解指標或引用的含義
這裡的指標或者引用,他們的意思都是一樣的,都是儲存所指物件的記憶體地址。
將某個變數賦值給指標,實際上就是將某個變數的地址賦值給指標,或者反過來說,指標中儲存了這個變數的記憶體地址,指向了這個變數,通過指標就能找到這個變數。
看下面的虛擬碼表示什麼意思:
p -> next = q;
複製程式碼
這行程式碼就是說 p 結點中的 next 指標儲存了 q 結點的記憶體地址。
再看下面的程式碼表示什麼:
p -> next = p -> next -> next;
複製程式碼
這行程式碼表示,p 結點的 next 指標儲存了 p 結點的下下一個結點的記憶體地址。
現在應該能有所體會指標或者引用的概念了吧。
重點二:警惕指標丟失和記憶體洩露
在寫連結串列程式碼的時候,尤其是我們的指標,會不斷的改變,指來指去的。所以在寫的時候,一定注意不要弄丟了指標。
如下圖中單連結串列的插入操作
我們希望在結點 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結點後刪除一個結點,只需要關注一步即可:
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;
}
}
}
複製程式碼
所以寫連結串列程式碼時,要經常注意邊界條件是否考慮到了:
- 如果連結串列為空時,程式碼是否能正常工作?
- 如果連結串列只有一個結點時,程式碼是否能正常工作?
- 如果在處理頭結點和尾結點時,程式碼是否能正常工作?
重點四:要學會畫圖輔助思考
比如一些單連結串列的增刪改查等,指標總是不斷的改變。這個時候如果大腦思考不過來的話,可以簡單的畫個示意圖輔助一下。比如說單連結串列的增加結點操作,可以畫出增加前後的連結串列變化。
看圖寫程式碼,是不是會比較清楚指標接下來的指向呢。程式碼演示
請前往 github 檢視 連結串列常見的方法
參考書籍
《資料結構與演算法JavaScript描述》
有你才完美
連結串列這種資料結構,確實比較容易懂,但是想寫出好相關的操作程式碼,確實不易,指標或者引用也是我的薄弱環節,指來指去便記不清該怎麼指啦!動物的關節對於動物來講非常的重要,指標感覺就是連結串列的關節,連結串列像一輛火車,每個車廂的連線全靠車廂間的連線軸。