上一篇:JS數據結構與算法_棧&隊列
下一篇:JS數據結構與算法_集合&字典javascript
說明:JS數據結構與算法 系列文章的代碼和示例都可在此找到
上一篇博客發佈之後,僅幾天的時間居然成爲了我寫博客以來點贊數最多的一篇博客。歡喜之餘,不禁得思考背後的緣由,前端er離數據結構與算法太遙遠了,論壇裏也少有人去專門爲數據結構與算法撰文,才使得這看似平平的文章收穫如此。不過,這樣也更加堅決了我繼續學習數據結構與算法的決心(雖然只是入門級的)前端
相較於以前學習的 棧/隊列 只關心 棧頂/首尾 的模式,鏈表更加像是數組。鏈表和數組都是用於存儲有序元素的集合,但有幾點大不相同java
表頭
開始迭代到指定位置訪問下面是單鏈表的基本結構node
data
的節點和一個指向下一個元素的引用next
(也稱指針或連接)組成next
指向爲null
類比:尋寶遊戲,你有一條線索,這條線索是指向尋找下一條線索的地點的指針。你順着這條連接去下一個地點,獲得另外一條指向再下一處的線索。獲得列表中間的線索的惟一辦法,就是從起點(第一條線索)順着列表尋找git
鏈表的實現不像以前介紹的棧和隊列通常依賴於數組(至少咱們目前是這樣實現的),它必須本身構建類並組織邏輯實現。咱們先建立一個Node
類github
// 節點基類 class Node { constructor(data) { this.data = data; this.next = null; } }
通常單鏈表有如下幾種方法:面試
append
在鏈表尾部添加一個元素insert
在指定位置插入元素removeAt
在指定位置刪除元素getNode
獲取指定位置的元素print
打印整個鏈表indexOf
查找鏈表中是否有某個元素,有則返回索引,沒有則返回-1getHead
獲取鏈表頭部getTail
獲取鏈表尾部(有些並未實現尾部)size
返回鏈表包含的元素個數clear
清空鏈表// 初始化鏈表 class LinkedList { constructor() { this._head = null; this._tail = null; this._length = 0; } // 方法... }
下面咱們來實現幾個重要的方法算法
append
方法在鏈表尾部添加一個新的元素可分爲兩種狀況:segmentfault
head
和tail
均指向新元素tail
元素(以下)代碼實現數組
// 在鏈表尾端添加元素 append(data) { const newNode = new Node(data); if (this._length === 0) { this._head = newNode; this._tail = newNode; } else { this._tail.next = newNode; this._tail = newNode; } this._length += 1; return true; }
print
方法爲方便驗證,咱們先實現print
方法。方法雖簡單,這裏卻涉及到鏈表遍歷精髓
// 打印鏈表 print() { let ret = []; // 遍歷需從鏈表頭部開始 let currNode = this._head; // 單鏈表最終指向null,做爲結束標誌 while (currNode) { ret.push(currNode.data); // 輪詢至下一節點 currNode = currNode.next; } console.log(ret.join(' --> ')); } // 驗證 const link = new LinkedList(); link.append(1); link.append(2); link.append(3); link.print(); // 1 --> 2 --> 3
getNode
方法獲取指定索引位置的節點,依次遍歷鏈表,直到指定位置返回
// 獲取指定位置元素 getNode(index) { let currNode = this._head; let currIndex = 0; while (currIndex < index) { currIndex += 1; currNode = currNode.next; } return currNode; } // 驗證【銜接上面的鏈表實例】 console.log(link.getNode(0)); // Node { data: 1, next: Node { data: 2, next: Node { data: 3, next: null } } } console.log(link.getNode(3)); // null
insert
方法插入元素,須要考慮三種狀況
append
_head
並指向原有頭部元素代碼實現
// 在鏈表指定位置插入元素 insert(index, data) { // 不知足條件的索引值 if (index < 0 || index > this._length) return false; // 插入尾部 if (index === this._length) return this.append(data); const insertNode = new Node(data); if (index === 0) { // 插入首部 insertNode.next = this._head; this._head = insertNode; } else { // 找到原有位置節點 const prevTargetNode = this.getNode(index - 1); const targetNode = prevTargetNode.next; // 重塑節點鏈接 prevTargetNode.next = insertNode; insertNode.next = targetNode; } this._length += 1; return true; } // 驗證 link.insert(0, 0); link.insert(4, 4); link.insert(5, 5); link.print(); // 0 --> 1 --> 2 --> 3 --> 4 --> 5
removeAt
方法在指定位置刪除元素同添加元素相似
_head
_tail
代碼實現
// 在鏈表指定位置移除元素 removeAt(index) { if (index < 0 || index >= this._length) return false; if (index === 0) { this._head = this._head.next; } else { const prevNode = this.getNode(index - 1); const delNode = prevNode.next; const nextNode = delNode.next; // 若移除爲最後一個元素 if (!nextNode) this._tail = prevNode; prevNode.next = nextNode; } this._length -= 1; return true; } // 驗證 link.removeAt(3); link.print(); // 0 --> 1 --> 2 --> 4 --> 5
完整的鏈表代碼,可點此獲取
// 判斷數據是否存在於鏈表內,存在返回index,不然返回-1 indexOf(data) { let currNode = this._head; let index = 0; while (currNode) { if (currNode.data === data) return index; index += 1; currNode = currNode.next; } return -1; } getHead() { return this._head; } getTail() { return this._tail; } size() { return this._length; } isEmpty() { return !this._length; } clear() { this._head = null; this._tail = null; this._length = 0; }
Stack
和Queue
基於鏈表實現棧
class Stack { constructor() { this._link = new LinkedList(); } push(item) { this._link.append(item); } pop() { const tailIndex = this._link - 1; return this._link.removeAt(tailIndex); } peek() { if (this._link.size() === 0) return undefined; return this._link.getTail().data; } size() { return this._link.size(); } isEmpty() { return this._link.isEmpty(); } clear() { this._link.clear() } }
基於鏈表實現隊列
class Queue { constructor() { this._link = new LinkedList(); } enqueue(item) { this._link.append(item); } dequeue() { return this._link.removeAt(0); } head() { if (this._link.size() === 0) return undefined; return this._link.getHead().data; } tail() { if (this._link.size() === 0) return undefined; return this._link.getTail().data; } size() { return this._link.size(); } isEmpty() { return this._link.isEmpty(); } clear() { this._link.clear() } }
(1)迭代法
迭代法的核心就是currNode.next = prevNode
,而後從頭部一次向後輪詢
代碼實現
reverse() { if (!this._head) return false; let prevNode = null; let currNode = this._head; while (currNode) { // 記錄下一節點並重塑鏈接 const nextNode = currNode.next; currNode.next = prevNode; // 輪詢至下一節點 prevNode = currNode; currNode = nextNode; } // 交換首尾 let temp = this._tail; this._tail = this._head; this._head = temp; return true; }
(2)遞歸法
遞歸的本質就是執行到當前位置時,本身並不去解決,而是等下一階段執行。直到遞歸終止條件,而後再依次向上執行
function _reverseByRecusive(node) { if (!node) return null; if (!node.next) return node; // 遞歸終止條件 var reversedHead = _reverseByRecusive(node.next); node.next.next = node; node.next = null; return reversedHead; }; _reverseByRecusive(this._head);
利用遞歸,反向輸出
function _reversePrint(node){ if(!node) return;// 遞歸終止條件 _reversePrint(node.next); console.log(node.data); };
雙向鏈表和普通鏈表的區別在於,在鏈表中,一個節點只有鏈向下一個節點的連接,而在雙向鏈表中,連接是雙向的:一個鏈向下一個元素,另外一個鏈向前一個元素,以下圖
正是由於這種變化,使得鏈表相鄰節點之間不只只有單向關係,能夠經過prev
來訪問當前節點的上一節點。相應的,雙向循環鏈表的基類也有變化
class Node { constructor(data) { this.data = data; this.next = null; this.prev = null; } }
繼承單向鏈表後,最終的雙向循環鏈表DoublyLinkedList
以下【prev
對應的更改成NEW
】
class DoublyLinkedList extends LinkedList { constructor() { super(); } append(data) { const newNode = new DoublyNode(data); if (this._length === 0) { this._head = newNode; this._tail = newNode; } else { newNode.prev = this._tail; // NEW this._tail.next = newNode; this._tail = newNode; } this._length += 1; return true; } insert(index, data) { if (index < 0 || index > this._length) return false; if (index === this._length) return this.append(data); const insertNode = new DoublyNode(data); if (index === 0) { insertNode.prev = null; // NEW this._head.prev = insertNode; // NEW insertNode.next = this._head; this._head = insertNode; } else { // 找到原有位置節點 const prevTargetNode = this.getNode(index - 1); const targetNode = prevTargetNode.next; // 重塑節點鏈接 prevTargetNode.next = insertNode; insertNode.next = targetNode; insertNode.prev = prevTargetNode; // NEW targetNode.prev = insertNode; // NEW } this._length += 1; return true; } removeAt(index) { if (index < 0 || index > this._length) return false; let delNode; if (index === 0) { delNode = this._head; this._head = this._head.next; this._head.prev = null; // NEW } else { const prevNode = this.getNode(index - 1); delNode = prevNode.next; const nextNode = delNode.next; // 若移除爲最後一個元素 if (!nextNode) { this._tail = prevNode; this._tail.next = null; // NEW } else { prevNode.next = nextNode; // NEW nextNode.prev = prevNode; // NEW } } this._length -= 1; return delNode.data; } }
循環鏈表能夠像鏈表同樣只有單向引用,也能夠像雙向鏈表同樣有雙向引用。循環鏈表和鏈 表之間惟一的區別在於,單向循環鏈表最後一個節點指向下一個節點的指針tail.next
不是引用null
, 而是指向第一個節點head
,雙向循環鏈表的第一個節點指向上一節點的指針head.prev
不是引用null
,而是指向最後一個節點tail
鏈表的實現較於棧和隊列的實現複雜許多,一樣的,鏈表的功能更增強大
咱們能夠經過鏈表實現棧和隊列,一樣也能夠經過鏈表來實現棧和隊列的問題
鏈表更像是數組同樣的基礎數據結構,同時也避免了數組操做中刪除或插入元素對其餘元素的影響