一、定義
1.1 概念
前面我們學習了數組這種數據結構。數組(或者也可以稱爲列表)是一種非常簡單的存儲數據序列的數據結構。在這一節,我們要學習如何實現和使用鏈表這種動態的數據結構,這意味着我們可以從中任意添加或移除項,它會按需進行擴容。
要存儲多個元素,數組(或列表)可能是最常用的數據結構,它提供了一個便利的**[]**語法來訪問它的元素。然而,這種數據結構有一個缺點:(在大多數強類型語言中)數組的大小是固定的,需要預先分配,從數組的起點或中間插入或移除項的成本很高,因爲需要移動元素。 (注意:在JavaScript中數組的大小隨時可變,不需要預先定義長度)
鏈表存儲有序的元素集合,但不同於數組,鏈表中的元素在內存中並不是連續放置的。每個元素由一個存儲元素本身的節點和一個指向下一個元素的引用(也稱指針或鏈接)組成。
相對於傳統的數組,鏈表的一個好處在於,添加或刪除元素的時候不需要移動其他元素。然而,鏈表需要使用指針,因此實現鏈表時需要額外注意。數組的另一個細節是可以直接訪問任何位置的元素,而想要訪問鏈表中間的一個元素,需要從起點(表頭)開始迭代列表直到找到所需的元素。
火車可以當做生活中一個典型的鏈表的例子。一列火車是由一系列車廂組成的。每節車廂都相互連接。你很容易分離一節車廂,改變它的位置,添加或移除它。
1.2 分類
鏈表最常用的有三類:
- 單向鏈表
- 雙向鏈表
- 循環鏈表
二、鏈表的實現
2.1 單向鏈表
創建單向鏈表類:
// SinglyLinkedList function SinglyLinkedList () { function Node (element) { this.element = element; this.next = null; } var length = 0; var head = null; 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 () {}; } 複製代碼
SinglyLinkedList需要一個輔助類Node。Node類表示要加入鏈表的項。它包含一個element屬性,即要添加到鏈表的值,以及一個next屬性,即指向鏈表中下一個節點項的指針。
鏈表裏面有一些聲明的輔助方法:
- append(element):向鏈表尾部添加新項
- insert(position, element):向鏈表的特定位置插入一個新的項
- removeAt(position):從鏈表的特定位置移除一項
- remove(element):從鏈表中移除一項
- indexOf(element):返回元素在鏈表中的索引。如果鏈表中沒有該元素則返回-1
- isEmpty():如果鏈表中不包含任何元素,返回true,如果鏈表長度大於0,返回false
- size():返回鏈表包含的元素個數,與數組的length屬性類似
- getHead():返回鏈表的第一個元素
- toString():由於鏈表使用了Node類,就需要重寫繼承自JavaScript對象默認的toString()方法,讓其只輸出元素的值
- print():打印鏈表的所有元素
下面我們來一一實現這些輔助方法:
// 向鏈表尾部添加一個新的項 this.append = function (element) { var node = new Node(element); var currentNode = head; // 判斷是否爲空鏈表 if (currentNode === null) { // 空鏈表 head = node; } else { // 從head開始一直找到最後一個node while (currentNode.next) { // 後面還有node currentNode = currentNode.next; } currentNode.next = node; } length++; }; // 向鏈表特定位置插入一個新的項 this.insert = function (position, element) { if (position < 0 && position > length) { // 越界 return false; } else { var node = new Node(element); var index = 0; var currentNode = head; var previousNode; if (position === 0) { node.next = currentNode; head = node; } else { while (index < position) { index++; previousNode = currentNode; currentNode = currentNode.next; } previousNode.next = node; node.next = currentNode; } length++; return true; } }; // 從鏈表的特定位置移除一項 this.removeAt = function (position) { if (position < 0 && position >= length || length === 0) { // 越界 return false; } else { var currentNode = head; var index = 0; var previousNode; if (position === 0) { head = currentNode.next; } else { while (index < position) { index++; previousNode = currentNode; currentNode = currentNode.next; } previousNode.next = currentNode.next; } length--; return true; } }; // 從鏈表的特定位置移除一項 this.removeAt = function (position) { if (position < 0 && position >= length || length === 0) { // 越界 return false; } else { var currentNode = head; var index = 0; var previousNode; if (position === 0) { head = currentNode.next; } else { while (index < position) { index++; previousNode = currentNode; currentNode = currentNode.next; } previousNode.next = currentNode.next; } length--; return true; } }; // 從鏈表中移除指定項 this.remove = function (element) { var index = this.indexOf(element); return this.removeAt(index); }; // 返回元素在鏈表的索引,如果鏈表中沒有該元素則返回-1 this.indexOf = function (element) { var currentNode = head; var index = 0; while (currentNode) { if (currentNode.element === element) { return index; } index++; currentNode = currentNode.next; } return -1; }; // 如果鏈表中不包含任何元素,返回true,如果鏈表長度大於0,返回false this.isEmpty = function () { return length == 0; }; // 返回鏈表包含的元素個數,與數組的length屬性類似 this.size = function () { return length; }; // 獲取鏈表頭部元素 this.getHead = function () { return head.element; }; // 由於鏈表使用了Node類,就需要重寫繼承自JavaScript對象默認的toString()方法,讓其只輸出元素的值 this.toString = function () { var currentNode = head; var string = ''; while (currentNode) { string += ',' + currentNode.element; currentNode = currentNode.next; } return string.slice(1); }; this.print = function () { console.log(this.toString()); }; 複製代碼
創建單向鏈表實例進行測試:
// 創建單向鏈表實例 var singlyLinked = new SinglyLinkedList(); console.log(singlyLinked.removeAt(0)); // true console.log(singlyLinked.isEmpty()); // true singlyLinked.append('Tom'); singlyLinked.append('Peter'); singlyLinked.append('Paul'); singlyLinked.print(); // "Tom,Peter,Paul" singlyLinked.insert(0, 'Susan'); singlyLinked.print(); // "Susan,Tom,Peter,Paul" singlyLinked.insert(1, 'Jack'); singlyLinked.print(); // "Susan,Jack,Tom,Peter,Paul" console.log(singlyLinked.getHead()); // "Susan" console.log(singlyLinked.isEmpty()); // false console.log(singlyLinked.indexOf('Peter')); // 3 console.log(singlyLinked.indexOf('Cris')); // -1 singlyLinked.remove('Tom'); singlyLinked.removeAt(2); singlyLinked.print(); // "Susan,Jack,Paul" 複製代碼
2.2 雙向鏈表
雙向鏈表和普通鏈表的區別在於,在普通鏈表中,一個節點只有鏈向下一個節點的鏈接,而在雙向鏈表中,鏈接是雙向的:一個鏈向下一個元素,另一個鏈向前一個元素。
創建雙向鏈表類:
// 創建雙向鏈表DoublyLinkedList類 function DoublyLinkedList () { function Node (element) { this.element = element; this.next = null; this.prev = null; // 新增 } var length = 0; var head = null; var tail = null; // 新增 } 複製代碼
可以看到,雙向鏈表在Node類裏有prev屬性(一個新指針),在DoublyLinkedList類裏也有用來保存對列表最後一項的引用的tail屬性。
雙向鏈表提供了兩種迭代列表的方法:從頭到尾,或者從尾到頭。我們可以訪問一個特定節點的下一個或前一個元素。
在單向鏈表中,如果迭代鏈表時錯過了要找的元素,就需要回到鏈表起點,重新開始迭代。在雙向鏈表中,可以從任一節點,向前或向後迭代,這是雙向鏈表的一個優點。
實現雙向鏈表的輔助方法:
// 向鏈表尾部添加一個新的項 this.append = function (element) { var node = new Node(element); var currentNode = tail; // 判斷是否爲空鏈表 if (currentNode === null) { // 空鏈表 head = node; tail = node; } else { currentNode.next = node; node.prev = currentNode; tail = node; } length++; }; // 向鏈表特定位置插入一個新的項 this.insert = function (position, element) { if (position < 0 && position > length) { // 越界 return false; } else { var node = new Node(element); var index = 0; var currentNode = head; var previousNode; if (position === 0) { if (!head) { head = node; tail = node; } else { node.next = currentNode; currentNode.prev = node; head = node; } } else if (position === length) { this.append(element); } else { while (index < position) { index++; previousNode = currentNode; currentNode = currentNode.next; } previousNode.next = node; node.next = currentNode; node.prev = previousNode; currentNode.prev = node; } length++; return true; } }; // 從鏈表的特定位置移除一項 this.removeAt = function (position) { if (position < 0 && position >= length || length === 0) { // 越界 return false; } else { var currentNode = head; var index = 0; var previousNode; if (position === 0) { // 移除第一項 if (length === 1) { head = null; tail = null; } else { head = currentNode.next; head.prev = null; } } else if (position === length - 1) { // 移除最後一項 if (length === 1) { head = null; tail = null; } else { currentNode = tail; tail = currentNode.prev; tail.next = null; } } else { while (index < position) { index++; previousNode = currentNode; currentNode = currentNode.next; } previousNode.next = currentNode.next; previousNode = currentNode.next.prev; } length--; return true; } }; // 從鏈表中移除指定項 this.remove = function (element) { var index = this.indexOf(element); return this.removeAt(index); }; // 返回元素在鏈表的索引,如果鏈表中沒有該元素則返回-1 this.indexOf = function (element) { var currentNode = head; var index = 0; while (currentNode) { if (currentNode.element === element) { return index; } index++; currentNode = currentNode.next; } return -1; }; // 如果鏈表中不包含任何元素,返回true,如果鏈表長度大於0,返回false this.isEmpty = function () { return length == 0; }; // 返回鏈表包含的元素個數,與數組的length屬性類似 this.size = function () { return length; }; // 獲取鏈表頭部元素 this.getHead = function () { return head.element; }; // 由於鏈表使用了Node類,就需要重寫繼承自JavaScript對象默認的toString()方法,讓其只輸出元素的值 this.toString = function () { var currentNode = head; var string = ''; while (currentNode) { string += ',' + currentNode.element; currentNode = currentNode.next; } return string.slice(1); }; this.print = function () { console.log(this.toString()); }; 複製代碼
創建雙向鏈表實例進行測試:
// 創建雙向鏈表 var doublyLinked = new DoublyLinkedList(); console.log(doublyLinked.isEmpty()); // true doublyLinked.append('Tom'); doublyLinked.append('Peter'); doublyLinked.append('Paul'); doublyLinked.print(); // "Tom,Peter,Paul" doublyLinked.insert(0, 'Susan'); doublyLinked.print(); // "Susan,Tom,Peter,Paul" doublyLinked.insert(1, 'Jack'); doublyLinked.print(); // "Susan,Jack,Tom,Peter,Paul" console.log(doublyLinked.getHead()); // "Susan" console.log(doublyLinked.isEmpty()); // false console.log(doublyLinked.indexOf('Peter')); // 3 console.log(doublyLinked.indexOf('Cris')); // -1 doublyLinked.remove('Tom'); doublyLinked.removeAt(2); doublyLinked.print(); // "Susan,Jack,Paul" 複製代碼
2.3 循環鏈表
循環鏈表可以像單向鏈表一樣只有單向引用,也可以像雙向鏈表一樣有雙向引用。循環鏈表和普通鏈表之間唯一的區別在於,最後一個元素指向下一個元素的指針(next)不是引用null,而是指向第一個元素(head)。這裏就不進行代碼實現了,大家可以結合上面的單向鏈表和雙向鏈表自己實現一個循環鏈表。
三、結束
完整代碼可以到我的github倉庫查看,如果對你有幫助的話歡迎點一個Star~~