大家一定聽說過資料結構與演算法,演算法就暫且先不討論了,因為我知之甚少。但是資料結構這塊我略懂一些,從這篇文章起我會陸續講解一下JavaScirpt
中的資料結構,比如:陣列、佇列、棧、集合、字典和雜湊表等等。
本次我要和大家嘮叨的是 連結串列 ------------------------●'◡'●
1、連結串列資料結構
首先想和大家說的是,像連結串列呀、字典呀等詞,不能一看見它就害怕,感覺它有多麼高深,這些詞只是一個概念而已,當你瞭解了背後的原理或者定義,你會覺得,呵呵,just so so
。
先來點理論吧,必要的概念還是得看看的。
1-1、什麼是連結串列
連結串列儲存有序的元素集合,每個元素由一個儲存元素本身的節點和一個指向下一個元素的引用(也稱指標或連結)組成。連結串列不同於陣列,連結串列中的元素在記憶體中並不是連續放置的。
一圖勝千言,還是上圖吧
舉個栗子:火車就很像連結串列。一列火車是由一系列車廂組成的。每節車廂都相互連線。你很容易分離一節車廂,改變它的位置,新增或移除它,每節車廂都是列表的元素,車廂間的連線就是指標
1-2、連結串列優缺點
優點:新增或移除元素的時候不需要移動其他元素。
缺點:訪問連結串列的一個元素,需要從起點(表頭)開始迭代列表直到找到所需的元素
如果不理解的話,那就等讀完本文再回過頭來看看就懂了。
2、建立連結串列
理解了連結串列是什麼之後,現在開始實現一個連結串列資料結構。我定義一個類用來描述連結串列,這個類的名稱是LinkedList
,以下程式碼是這個類的骨架。
ps:什麼是骨架,就是這個類應該有的屬性或者方法,先不關注方法的具體實現,只關注有什麼。
function LinkedList() {
let Node = function(element){ // {1}
this.element = element;
this.next = null;
};
let length = 0; // {2}
let head = null; // {3}
this.append = function(element){};
this.insert = function(position, element){};
this.removeAt = function(position){};
this.remove = function(element){};
this.indexOf = function(element){};
this.isEmpty = function() {};
this.size = function() {};
this.getHead = function(){};
this.toString = function(){};
this.print = function(){};
}
複製程式碼
因為每一個節點需要儲存自身的值和一個指向列表中下一個節點的指標,所以我們定義了一個建構函式Node
(行{1})
LinkedList
類也有儲存列表項的數量的 length 屬性(內部/私有變數)(行{2})。
另外,我們還需要儲存第一個節點的引用,把這個引用儲存在head
變數中(行{3})。(如果你不想叫它head
,可以改成其他的,你隨意)
接下來,我們定義了LinkedList
類的例項方法,你看看,還挺多的,其實都不難。
我們在實現這些方法前,先看看他們的職責:
方法名 | 職責 |
---|---|
append(element) | 向列表尾部新增一個新的項 |
insert(position, element) | 向列表的特定位置插入一個新的項 |
remove(element) | 從列表中移除一項 |
indexOf(element) | 返回元素在列表中的索引。如果列表中沒有該元素則返回-1 |
removeAt(position) | 從列表的特定位置移除一項 |
isEmpty() | 如果連結串列中不包含任何元素,返回 true,如果連結串列長度大於0則返回 false |
size() | 返回連結串列包含的元素個數 |
toString() | 由於列表項使用了 Node 類,就需要重寫繼承自 JavaScript 物件預設的 toString 方法,讓其只輸出元素的值 |
2-1、向連結串列尾部追加元素
向 LinkedList
物件尾部新增一個元素時,可能有兩種場景:列表為空,新增的是第一個元素,或者列表不為空,向其追加元素。
下面是我們實現的 append 方法:
this.append = function(element){
let node = new Node(element),
current;
if (head === null){ //列表中第一個節點
head = node;
} else {
current = head; //{4}
//迴圈列表,直到找到最後一項
while(current.next){
current = current.next;
}
//找到最後一項,將其 next 賦為 node,建立連結
current.next = node; //{5}
}
length++; //更新列表的長度 //{6}
};
複製程式碼
解釋一下上述程式碼:
根據傳入的值,用Node
建構函式建立一個節點
如果head
元素為null
,意味著連結串列為空,我們只需要將head
指向該元素
如果head
元素不為null
,我們需要找到最後一個節點,但是我們只有第一個元素的引用,也就是head
,所以,我們來個while
迴圈找到最後一個節點,你可能注意到了current
這個變數。我詳細的說明以下這個東西是幹嗎的吧。current
變數就像一個指標,隨著迴圈的進行,它指向的元素一直在變,迴圈結束時,它指向了連結串列中的最後一個元素,這樣我們就拿到了連結串列中最後一個元素,也就是current
。
你看,我這樣說,你理解了這個current
變數了嗎?
你必須理解,因為這是程式設計中最常用的一種方法。
拿到了連結串列種的最後一項,我們就很容易的將元素插入到連結串列尾部了,只需要將current
(別忘了,此時的current
元素代表的是連結串列最後一個元素)的next
屬性置為待插入的元素(也就是node
)
以上步驟,我們來張圖說明一下吧。
最後,別忘了,更新一下連結串列的長度。
2-2、從連結串列中移除元素
有新增就有刪除,此處刪除節點有兩個方法,分別為remove
removeAt
,我們先來看看removeAt
移除元素的程式碼,remove
方法非常簡單我放到了後面講解。
this.removeAt = function(position){
//檢查越界值
if (position > -1 && position < length){
let current = head, //current代表當前項,它是隨著迴圈的進行而不斷變化的
previous, // current代表當前項,那麼previous代表前一項
index = 0; // 用index來控制當前迴圈
//移除第一項
if (position === 0){ // 刪除頭部元素
head = current.next; //日常玩指標
} else {
while (index++ < position){ // index小於給定的查詢位置的話,就繼續迴圈
previous = current; // 改變前一項的引用
current = current.next; // 改變當前項的引用
}
//將 previous 與 current 的下一項鍊接起來:跳過 current,從而移除它
previous.next = current.next;
}
length--; // 更改連結串列的長度
return current.element; //返回移除元素的值
} else {
return null; // {11}
}
};
複製程式碼
首先我們先說說怎麼就叫新增元素了,怎麼就叫刪除元素了。其實,就是一個玩指標的過程。
連結串列的大部分操作都是在玩指標,讓這個元素的指標指向誰,讓那個元素的指標指向誰。啥叫指標?就是next
屬性,obj.next=a; obj.next=b
;你看,我一直在改變obj
的指標,是不是so easy
。
我先說一下刪除元素的總體思路,帶著這個思路去看上面的程式碼。
移除元素:
比如將元素A從連結串列中移除,A元素在連結串列中的位置為5.
A元素的上一個元素為B,A元素的下一個元素為C。
我們只需要幹一件事就完成了移除元素A,那就是:將元素B的指標指向元素C。
如果我說的讓你的思路亂了,那就想想火車吧,我要移除一節車廂,只需要將這節車廂的前一個車廂連結到這節車廂的後一個車廂就可以了。
來吧,叨叨半天了,看看圖吧,可能一看圖,就懂了。
可能會有人問,那A元素步還在嗎,的確,它還在,不過,如果對它的引用計數為0,那麼垃圾回收機制會將它收回。
2-3、在任意位置插入元素
我們一開始學習了,如何在連結串列尾部新增一個元素,現在該實現一下在任意位置插入元素的方法了insert
看程式碼之前,我建議你先思考一下,如何做,就叫插入元素,沒思路的話就想想火車。火車要加一節車廂,該怎麼做。
。。。。。。。。。
。。。。。。。。。
。。。。。。。。。
。。。。。。。。。
。。。。。。。。。
上程式碼嘍:
this.insert = function(position, element){
//檢查越界值
if (position >= 0 && position <= length){
let node = new Node(element),
current = head,
previous,
index = 0;
if (position === 0){ //在第一個位置新增
node.next = current;
head = node;
} else {
while (index++ < position){
previous = current;
current = current.next;
}
node.next = current;
previous.next = node;
}
length++; //更新列表的長度
return true;
} else {
return false;
}
};
複製程式碼
這段程式碼我希望你能讀懂,我幾乎沒有加註釋。
讀不懂就想想火車,想想怎麼改變指標。
讀不懂就看看下面的圖
2-4、實現其他方法
在這一節中,我們將會實現 toString
、indexOf
、isEmpty
和 size
等其他方法。這些方法沒有多大難度。
toString方法
toString
方法會把 LinkedList
物件轉換成一個字串.
this.toString = function(){
let current = head,
string = '';
while (current) {
string +=current.element +(current.next ? 'n' : '');
current = current.next;
}
return string;
};
複製程式碼
indexOf 方法
indexOf
方法接收一個元素的值,如果在列表中找到它,就返回元素的位置,否則返回-1
。
this.indexOf = function(element){
let current = head,
index = -1;
 
while (current) {
if (element === current.element) {
return index;
}
index++;
current = current.next;
}
return -1;
};
複製程式碼
實現了這個方法,我們就可以實現 remove
等其他的方法:
this.remove = function(element){
let index = this.indexOf(element);
return this.removeAt(index);
};
複製程式碼
isEmpty、size 和 getHead 方法
this.isEmpty = function() {
return length === 0;
};
複製程式碼
如果列表中沒有元素,isEmpty
方法就返回 true
,否則返回 false
。
this.size = function() {
return length;
};
複製程式碼
這個size()
方法,我就不多說了。
this.getHead = function(){
return head;
};
複製程式碼
到這裡,一個簡單的連結串列就算是完成了。怎麼樣,沒有想象的那麼可怕吧。 我們接下來介紹以下連結串列的其他型別。
3、雙向連結串列
連結串列有多種不同的型別,雙向連結串列和普通連結串列的區別在於,在連結串列中,一個節點只有鏈向下一個節點的連結,而在雙向連結串列中,連結是雙向的:一個鏈向下一個元素,另一個鏈向前一個元素。
來張圖看看吧
從圖中,我們發現幾點不一樣的地方:
- 每個節點有兩個指標,一個指向當前節點的前一個節點,另一個指向當前節點的後一個節點
LinkedList
類增加了一個tail
變數,用來指向連結串列尾部元素
知道了這些區別,讓我們愉快的寫一下雙向連結串列這個類吧,我叫它DoublyLinkedList
,你隨意。
function DoublyLinkedList() {
let Node = function(element){
this.element = element;
this.next = null;
this.prev = null; //新增的
};
let length = 0;
let head = null;
let tail = null; //新增的
//這裡是方法
}
複製程式碼
雙向連結串列提供了兩種迭代列表的方法:從頭到尾,或者反過來。我們也可以訪問一個特定節點的下一個或前一個元素。在單向連結串列中,如果迭代列表時錯過了要找的元素,就需要回到列表起點,重新開始迭代。這是雙向連結串列的一個優點。
3-1、在任意位置插入新元素
向雙向連結串列中插入一個新項跟(單向)連結串列非常類似。區別在於,連結串列只要控制一個 next
指標,而雙向連結串列則要同時控制 next
和 prev
(previous
,前一個)這兩個指標。
我先把程式碼端出來:
this.insert = function(position, element){
//檢查越界值
if (position >= 0 && position <= length){
let node = new Node(element),
current = head,
previous,
index = 0;
if (position === 0){ //在第一個位置新增
if (!head){ //新增的
head = node;
tail = node;
} else {
node.next = current;
current.prev = node; //新增的
head = node;
}
} else if (position === length) { //最後一項 //新增的
current = tail; // {3}
current.next = node;
node.prev = current;
tail = node;
} else {
while (index++ < position){
previous = current;
current = current.next;
}
node.next = current;
previous.next = node;
current.prev = node; //新增的
node.prev = previous; //新增的
}
length++; //更新列表的長度
return true;
} else {
return false;
}
};
複製程式碼
在連結串列插入一個新元素的步驟:
- 迭代列表,直到到達要找的位置。我們將在
current
和previous
元素之間插入新元素。 - 首先,
node.next
將指向current
,而previous.next
將指向node
,這樣就不會丟失節點之間的連結。 - 然後需要處理所有的連結:
current.prev
將指向node
,而node.prev
將指向 previous。
用圖來描述一下這個過程:
3-2、在任意位置移除元素
從雙向連結串列中移除元素跟連結串列非常類似。唯一的區別就是還需要設定前一個位置的指標
從列表中間移除一個元素的步驟:
- 首先需要迭代列表,直到到達要找的位置。
current
變數所引用的就是要移除的元素。 - 要移除它,我們可以通過更新
previous.next
和current.next.prev
的引用,在列表中跳過它。 previous.next
將指向current.next
,而current.next.prev
將指向previous
還是用圖來描述一下這個過程:
接下來就是程式碼實現了,我再三思考,決定,這部分程式碼由看客老爺實現吧,真好也能檢測以下自己有沒有掌握。
4、迴圈連結串列
前面介紹的兩種連結串列的最後一個元素的next
指標是指向null
的,這樣一來,只要我們遍歷連結串列,當到達尾部節點時,迴圈就結束了。
迴圈連結串列,顧名思義,連結串列沒有終止。可以像連結串列一樣只有單向引用,也可以像雙向連結串列一樣有雙向引用。
單向迴圈列表,最後一個元素指向下一個元素的指標(tail.next)不是引用 null,而是指向第一個元素(head)。
來張圖
雙向迴圈連結串列有指向 head 元素的 tail.next,和指向 tail 元素的 head.prev。
再來張圖
5、總結
總結一下吧
- 連結串列的優點是在不移動其他元素的情況下,就可以新增和移除元素
- 連結串列的缺點是每次操作都必須從連結串列第一個節點開始迭代,不想陣列那樣,通過下標就可以獲取到值。
- 連結串列操作的是指標,無論新增還是刪除節點。火車就是最好的例子。
好了,今天就到這裡了。
文章中如果有錯誤或者有不懂的地方,歡迎給我留言。