翻譯:瘋狂的技術宅
英文:https://code.tutsplus.com/art...
說明:本文翻譯自系列文章《Data Structures With JavaScript》,總共爲四篇,原做者是在美國硅谷工做的工程師 Cho S. Kim 。這是本系列的第三篇。javascript
說明:本專欄文章首發於公衆號:jingchengyideng 。java
計算機科學中最多見的兩種數據結構是單鏈表和雙鏈表。node
在我學習這些數據結構的時候,曾經問個人同伴在生活中有沒有相似的概念。我所聽到的例子是購物清單和火車。可是我最終明白了,這些類比是不許確的,購物清單更相似隊列,火車則更像是一個數組。數組
隨着時間的推移,我終於發現了一個可以準確類比單鏈表和雙向鏈表的例子:尋寶遊戲。 若是你對尋寶遊戲和鏈表之間的關係感到好奇,請繼續往下讀。數據結構
在計算機科學中,單鏈表是一種數據結構,保存了一系列連接的節點。 每一個節點中包含數據和一個可指向另外一個節點的指針。ide
單鏈列表的節點很是相似於尋寶遊戲中的步驟。 每一個步驟都包含一條消息(例如「您已到達法國」)和指向下一步驟的指針(例如「訪問這些經緯度座標」)。 當咱們開始對這些單獨的步驟進行排序並造成一系列步驟時,就是在玩一個尋寶遊戲。函數
如今咱們對單鏈表有了一個基本的概念,接下來討論單鏈表的操做工具
由於單鏈表包含節點,這二者的構造函數能夠是兩個獨立的構造函數,因此咱們須要些構造函數:Node
和 SinglyList
學習
data
存儲數據this
next
指向鏈表中下一個節點的指針
_length
用於表示鏈表中的節點數量
head
分配一個節點做爲鏈表的頭
add(value)
向鏈表中添加一個節點
searchNodeAt(position)
找到在列表中指定位置 n 上的節點
remove(position)
刪除指定位置的節點
在實現時,咱們首先定義一個名爲Node
的構造函數,而後定義一個名爲SinglyList
的構造函數。
Node
的每一個實例都應該可以存儲數據而且可以指向另一個節點。 要實現此功能,咱們將分別建立兩個屬性:data
和next
。
function Node(data) { this.data = data; this.next = null; }
下一步咱們定義SinglyList
:
function SinglyList() { this._length = 0; this.head = null; }
SinglyList
的每一個實例有兩個屬性:_length
和head
。前者保存鏈表中的節點數,後者指向鏈表的頭部,鏈表前面的節點。因爲新建立的singlylist
實例不包含任何節點,因此head
的默認值是null
,_length
的默認值是 0
。
咱們須要定義能夠從鏈表中添加、查找和刪除節點的方法。先從添加節點開始。
add(value)
太棒了,如今咱們來實現將節點添加到鏈表的功能。
SinglyList.prototype.add = function(value) { var node = new Node(value), currentNode = this.head; // 1st use-case: an empty list if (!currentNode) { this.head = node; this._length++; return node; } // 2nd use-case: a non-empty list while (currentNode.next) { currentNode = currentNode.next; } currentNode.next = node; this._length++; return node; };
把節點添加到鏈表會涉及不少步驟。先從方法開始。 咱們使用add(value)
的參數來建立一個節點的新實例,該節點被分配給名爲node
的變量。咱們還聲明瞭一個名爲currentNode
的變量,並將其初始化爲鏈表的_head
。 若是鏈表中尚未節點,那麼head
的值爲null
。
實現了這一點以後,咱們將處理兩種狀況。
第一種狀況考慮將節點添加到空的鏈表中,若是head
沒有指向任何節點的話,那麼將該node
指定爲鏈表的頭,同時鏈表的長度加一,並返回node
。
第二種狀況考慮將節點添加到飛空鏈表。咱們進入while
循環,在每次循環中,判斷currentNode.next
是否指向下一個節點。(第一次循環時,CurrentNode
指向鏈表的頭部。)
若是答案是否認的,咱們會把currentnode.next
指向新添加的節點,並返回node
。
若是答案是確定的,就進入while
循環。 在循環體中,咱們將currentNode
從新賦值給currentNode.next
。 重複這個過程,直到currentNode.next
再也不指向任何。換句話說,currentNode
指向鏈表中的最後一個節點。
while
循環結束後,使currentnode.next
指向新添加的節點,同時_length
加1,最後返回node
。
searchNodeAt(position)
如今咱們能夠將節點添加到鏈表中了,可是尚未辦法找到特定位置的節點。下面添加這個功能。建立一個名爲searchNodeAt(position)
的方法,它接受一個名爲 position
的參數。這個參數是個整數,用來表示鏈表中的位置n。
SinglyList.prototype.searchNodeAt = function(position) { var currentNode = this.head, length = this._length, count = 1, message = {failure: 'Failure: non-existent node in this list.'}; // 1st use-case: an invalid position if (length === 0 || position < 1 || position > length) { throw new Error(message.failure); } // 2nd use-case: a valid position while (count < position) { currentNode = currentNode.next; count++; } return currentNode; };
在if
中檢查第一種狀況:參數非法。
若是傳給searchNodeAt(position)的索引是有效的,那麼咱們執行第二種狀況 —— while
循環。 在while
的每次循環中,指向頭的currentNode
被從新指向鏈表中的下一個節點。
這個循環不斷執行,一直到count
等於position
。
remove(position)
最後一個方法是remove(position)
。
SinglyList.prototype.remove = function(position) { var currentNode = this.head, length = this._length, count = 0, message = {failure: 'Failure: non-existent node in this list.'}, beforeNodeToDelete = null, nodeToDelete = null, deletedNode = null; // 1st use-case: an invalid position if (position < 0 || position > length) { throw new Error(message.failure); } // 2nd use-case: the first node is removed if (position === 1) { this.head = currentNode.next; deletedNode = currentNode; currentNode = null; this._length--; return deletedNode; } // 3rd use-case: any other node is removed while (count < position) { beforeNodeToDelete = currentNode; nodeToDelete = currentNode.next; count++; } beforeNodeToDelete.next = nodeToDelete.next; deletedNode = nodeToDelete; nodeToDelete = null; this._length--; return deletedNode; };
咱們要實現的remove(position)
涉及三種狀況:
無效的位置做爲參數傳遞。
第一個位置(鏈表的的`head)做爲參數的傳遞。
一個合法的位置(不是第一個位置)做爲參數的傳遞。
前兩種狀況是最簡單的處理。 關於第一種狀況,若是鏈表爲空或傳入的位置不存在,則會拋出錯誤。
第二種狀況處理鏈表中第一個節點的刪除,這也是頭節點。 若是是這種狀況,就執行下面的邏輯:
頭被從新賦值給currentNode.next
。
deletedNode
指向currentNode
。
currentNode
被從新賦值爲null。
將的鏈表的長度減1。
返回deletedNode
。
第三種狀況是最難理解的。 其複雜性在於咱們要在每一次循環中操做兩個節點的必要性。 在每次循環中,須要處理要刪除的節點和它前面的節點。當循環到要被刪除的位置的節點時,循環終止。
在這一點上,咱們涉及到三個節點:beforeNodeToDelete
, nodeToDelete
, 和 deletedNode
。刪除nodeToDelete
以前,必須先把它的next
的值賦給beforeNodeToDelete
的next
,若是不清楚這一步驟的目的,能夠提醒本身有一個節點負責連接其先後的其餘節點,只須要刪除這個節點,就能夠把鏈表斷開。
接下來,咱們將deletedNode
賦值給nodeToDelete
。 而後咱們將nodeToDelete
的值設置爲null
,將列表的長度減1,最後返回deletedNode
。
如下是單向鏈表的完整實現:
function Node(data) { this.data = data; this.next = null; } function SinglyList() { this._length = 0; this.head = null; } SinglyList.prototype.add = function(value) { var node = new Node(value), currentNode = this.head; // 1st use-case: an empty list if (!currentNode) { this.head = node; this._length++; return node; } // 2nd use-case: a non-empty list while (currentNode.next) { currentNode = currentNode.next; } currentNode.next = node; this._length++; return node; }; SinglyList.prototype.searchNodeAt = function(position) { var currentNode = this.head, length = this._length, count = 1, message = {failure: 'Failure: non-existent node in this list.'}; // 1st use-case: an invalid position if (length === 0 || position < 1 || position > length) { throw new Error(message.failure); } // 2nd use-case: a valid position while (count < position) { currentNode = currentNode.next; count++; } return currentNode; }; SinglyList.prototype.remove = function(position) { var currentNode = this.head, length = this._length, count = 0, message = {failure: 'Failure: non-existent node in this list.'}, beforeNodeToDelete = null, nodeToDelete = null, deletedNode = null; // 1st use-case: an invalid position if (position < 0 || position > length) { throw new Error(message.failure); } // 2nd use-case: the first node is removed if (position === 1) { this.head = currentNode.next; deletedNode = currentNode; currentNode = null; this._length--; return deletedNode; } // 3rd use-case: any other node is removed while (count < position) { beforeNodeToDelete = currentNode; nodeToDelete = currentNode.next; count++; } beforeNodeToDelete.next = nodeToDelete.next; deletedNode = nodeToDelete; nodeToDelete = null; this._length--; return deletedNode; };
咱們已經完整的實現了單鏈表,這真是極好的。如今能夠在一個佔用費連續的空間的鏈表結構中,進行添加、刪除和查找節點的操做了。
然而如今全部的操做都是從鏈表的起始位置開始,並運行到鏈表的結尾。換句話說,它們是單向的。
可能在某些狀況下咱們但願操做是雙向的。若是你考慮了這種可能性,那麼你剛纔就是描述了一個雙向鏈表。
雙向鏈表具備單鏈表的全部功能,並將其擴展爲在鏈表中能夠進行雙向遍歷。 換句話說,咱們可從鏈表中第一個節點遍歷到到最後一個節點;也能夠從最後一個節點遍歷到第一個節點。
在本節中,咱們將重點關注雙向鏈表和單鏈列表之間的差別。
咱們的鏈表將包括兩個構造函數:Node
和DoublyList
。看看他們是怎樣運做的。
data
存儲數據。
next
指向鏈表中下一個節點的指針。
previous
指向鏈表中前一個節點的指針。
_length
保存鏈表中節點的個數
head
指定一個節點做爲鏈表的頭節點
tail
指定一個節點做爲鏈表的尾節點
add(value)
向鏈表中添加一個節點
searchNodeAt(position)
找到在列表中指定位置 n 上的節點
remove(position)
刪除鏈表中指定位置上的節點
如今開始寫代碼!
在實現中,將會建立一個名爲Node
的構造函數:
function Node(value) { this.data = value; this.previous = null; this.next = null; }
想要實現雙向鏈表的雙向遍歷,咱們須要指向鏈表兩個方向的屬性。這些屬性被命名爲previous
和next
。
接下來,咱們須要實現DoublyList
並添加三個屬性:_length
,head
和tail
。
與單鏈表不一樣,雙向鏈表包含對鏈表開頭和結尾節點的引用。 因爲DoublyList
剛被實例化時並不包含任何節點,因此head
和tail
的默認值都被設置爲null
。
function DoublyList() { this._length = 0; this.head = null; this.tail = null; }
接下來咱們討論如下方法:add(value)
, remove(position)
, 和 searchNodeAt(position)
。全部這些方法都用於單鏈表; 然而,它們必須備重寫爲能夠雙向遍歷。
add(value)
DoublyList.prototype.add = function(value) { var node = new Node(value); if (this._length) { this.tail.next = node; node.previous = this.tail; this.tail = node; } else { this.head = node; this.tail = node; } this._length++; return node; };
在這個方法中,存在兩種可能。首先,若是鏈表是空的,則給它的head
和tail
分配節點。其次,若是鏈表中已經存在節點,則查找鏈表的尾部並把心節點分配給tail.next
;一樣,咱們須要配置新的尾部以供進行雙向遍歷。換句話說,咱們須要把tail.previous
設置爲原來的尾部。
searchNodeAt(position)
searchNodeAt(position)
的實現與單鏈表相同。 若是你忘記了如何實現它,請經過下面的代碼回憶:
DoublyList.prototype.searchNodeAt = function(position) { var currentNode = this.head, length = this._length, count = 1, message = {failure: 'Failure: non-existent node in this list.'}; // 1st use-case: an invalid position if (length === 0 || position < 1 || position > length) { throw new Error(message.failure); } // 2nd use-case: a valid position while (count < position) { currentNode = currentNode.next; count++; } return currentNode; };
remove(position)
理解這個方法是最具挑戰性的。我先寫出代碼,而後再解釋它。
DoublyList.prototype.remove = function(position) { var currentNode = this.head, length = this._length, count = 1, message = {failure: 'Failure: non-existent node in this list.'}, beforeNodeToDelete = null, nodeToDelete = null, deletedNode = null; // 1st use-case: an invalid position if (length === 0 || position < 1 || position > length) { throw new Error(message.failure); } // 2nd use-case: the first node is removed if (position === 1) { this.head = currentNode.next; // 2nd use-case: there is a second node if (!this.head) { this.head.previous = null; // 2nd use-case: there is no second node } else { this.tail = null; } // 3rd use-case: the last node is removed } else if (position === this._length) { this.tail = this.tail.previous; this.tail.next = null; // 4th use-case: a middle node is removed } else { while (count < position) { currentNode = currentNode.next; count++; } beforeNodeToDelete = currentNode.previous; nodeToDelete = currentNode; afterNodeToDelete = currentNode.next; beforeNodeToDelete.next = afterNodeToDelete; afterNodeToDelete.previous = beforeNodeToDelete; deletedNode = nodeToDelete; nodeToDelete = null; } this._length--; return message.success; };
remove(position)
處理如下四種狀況:
若是remove(position)
的參數傳遞的位置存在, 將會拋出一個錯誤。
若是remove(position)
的參數傳遞的位置是鏈表的第一個節點(head
),將把head
賦值給deletedNode
,而後把head
從新分配到鏈表中的下一個節點。 此時,咱們必須考慮鏈表中否存在多個節點。 若是答案爲否,頭部將被分配爲null,以後進入if-else
語句的if
部分。 在if
的代碼中,還必須將tail
設置爲null
—— 換句話說,咱們返回到一個空的雙向鏈表的初始狀態。若是刪除列表中的第一個節點,而且鏈表中存在多個節點,那麼咱們輸入if-else
語句的else
部分。 在這種狀況下,咱們必須正確地將head
的previous
屬性設置爲null
—— 在鏈表的頭前面是沒有節點的。
若是remove(position)
的參數傳遞的位置是鏈表的尾部,首先把tail
賦值給deletedNode
,而後tail
被從新賦值爲尾部以前的那個節點,最後新尾部後面沒有其餘節點,須要將其next
值設置爲null
。
這裏發生了不少事情,因此我將重點關注邏輯,而不是每一行代碼。 一旦CurrentNode
指向的節點是將要被remove(position)
刪除的節點時,就退出while
循環。這時咱們把nodeToDelete
以後的節點從新賦值給beforeNodeToDelete.next
。相應的,
把nodeToDelete
以前的節點從新賦值給afterNodeToDelete.previous
。——換句話說,咱們把指向已刪除節點的指針,改成指向正確的節點。最後,把nodeToDelete
賦值爲null
。
最後,把鏈表的長度減1,返回deletedNode
。
function Node(value) { this.data = value; this.previous = null; this.next = null; } function DoublyList() { this._length = 0; this.head = null; this.tail = null; } DoublyList.prototype.add = function(value) { var node = new Node(value); if (this._length) { this.tail.next = node; node.previous = this.tail; this.tail = node; } else { this.head = node; this.tail = node; } this._length++; return node; }; DoublyList.prototype.searchNodeAt = function(position) { var currentNode = this.head, length = this._length, count = 1, message = {failure: 'Failure: non-existent node in this list.'}; // 1st use-case: an invalid position if (length === 0 || position < 1 || position > length) { throw new Error(message.failure); } // 2nd use-case: a valid position while (count < position) { currentNode = currentNode.next; count++; } return currentNode; }; DoublyList.prototype.remove = function(position) { var currentNode = this.head, length = this._length, count = 1, message = {failure: 'Failure: non-existent node in this list.'}, beforeNodeToDelete = null, nodeToDelete = null, deletedNode = null; // 1st use-case: an invalid position if (length === 0 || position < 1 || position > length) { throw new Error(message.failure); } // 2nd use-case: the first node is removed if (position === 1) { this.head = currentNode.next; // 2nd use-case: there is a second node if (!this.head) { this.head.previous = null; // 2nd use-case: there is no second node } else { this.tail = null; } // 3rd use-case: the last node is removed } else if (position === this._length) { this.tail = this.tail.previous; this.tail.next = null; // 4th use-case: a middle node is removed } else { while (count < position) { currentNode = currentNode.next; count++; } beforeNodeToDelete = currentNode.previous; nodeToDelete = currentNode; afterNodeToDelete = currentNode.next; beforeNodeToDelete.next = afterNodeToDelete; afterNodeToDelete.previous = beforeNodeToDelete; deletedNode = nodeToDelete; nodeToDelete = null; } this._length--; return message.success; };
本文中已經介紹了不少信息。 若是其中任何地方看起來使人困惑,就再讀一遍並查看代碼。若是它最終對你有所幫助,我會感到自豪。你剛剛揭開了一個單鏈表和雙向鏈表的祕密,能夠把這些數據結構添加到本身的編碼工具彈藥庫中!
歡迎掃描二維碼關注公衆號,天天推送我翻譯的技術文章。