原文連接javascript
在以前的章節中,咱們討論瞭如何使用數組來實現列表、棧和隊列等數據結構。本章節,咱們討論另外一種列表:鏈表。咱們將會認識到爲何有時候,鏈表會優於數組,還會實現一個基於對象的鏈表,而且附上一些實戰內容。java
數組並不老是組織數據的最佳數據結構,緣由以下。在不少編程語言中,數組的長度是固定的,因此當數組已被數據填滿時,再要加入新的元素就會很是困難。在數組中,添加和刪除元素也很麻煩,由於須要將數組中的其餘元素向前或向後平移,以反映數組剛剛進行了添加或刪除操做。然而,JavaScript 的數組並不存在上述問題,由於使用 split() 方法不須要再訪問數組中的其餘元素了。git
JavaScript 中數組的主要問題是,它們被實現成了對象,與其餘語言(好比 C++ 和 Java)的數組相比,效率很低。github
若是你發現數組在實際使用時很慢,就能夠考慮使用鏈表來替代它。除了對數據的隨機訪問,鏈表幾乎能夠用在任何可使用一維數組的狀況中。若是須要隨機訪問,數組仍然是更好的選擇。編程
鏈表是一種物理存儲單元上非連續、非順序的存儲結構,數據元素的邏輯順序是經過鏈表中的指針連接次序實現的集合。以下圖所示:數組
數組元素靠它們的位置進行引用,鏈表元素則是靠相互之間的關係進行引用。然而要標識出鏈表的起始節點卻有點麻煩,許多鏈表的實現都在鏈表最前面有一個特殊節點,叫作頭節點。以下圖所示:數據結構
鏈表中插入一個節點的效率很高。向鏈表中插入一個節點,須要修改它前面的節點(前驅),使其指向新加入的節點,而新加入的節點則指向原來前驅指向的節點。下圖演示瞭如何在 Tue 節點後加入 Fri 節點。app
從鏈表中刪除一個元素也很簡單。將待刪除元素的前驅節點指向待刪除元素的後繼節點,同時將待刪除元素指向 null,元素就刪除成功了。下圖演示了從鏈表中刪除「Fri」節點的過程。編程語言
完整代碼地址,Node類包含兩個屬性:函數
el
用來保存節點上的數據next
用來保存指向下一個節點的連接1. 構造函數
function Node (el) {
this.el = el;
this.next = null;
}
複製代碼
完整代碼地址,Link類提供瞭如下的方法:
insert
插入新節點remove
刪除節點display
顯示鏈表元素的方法1. 構造函數
function Link () {
this.head = new Node('head');
}
複製代碼
2. find:按節點的值查找節點
Link.prototype.find = function (el) {
var currNode = this.head;
while (currNode && currNode.el != el) {
currNode = currNode.next;
}
return currNode;
}
複製代碼
find
方法展現瞭如何在鏈表上進行移動。首先,建立一個新節點,並將鏈表的頭節點賦給這個新建立的節點。而後在鏈表上進行循環,若是當前節點的 el
屬性和咱們要找的信息不符,就從當前節點移動到下一個節點。若是查找成功,該方法返回包含該數據的節點;不然,返回 null
。
3. insert:插入一個節點
Link.prototype.insert = function (newEl, oldEl) {
var newNode = new Node(newEl);
var findNode = this.find(oldEl);
if (findNode) {
newNode.next = findNode.next;
findNode.next = newNode;
} else {
throw new Error('找不到給定插入的節點');
}
}
複製代碼
find
方法一旦找到給定的節點,就能夠將新節點插入鏈表了。
4. display:展現鏈表節點元素
// 展現鏈表中的元素
Link.prototype.display = function () {
var currNode = this.head.next;
while (currNode) {
console.log(currNode.el);
currNode = currNode.next;
}
}
複製代碼
5. findPrev:尋找給定節點的前一個節點
Link.prototype.findPrev = function (el) {
var currNode = this.head;
while (currNode.next && currNode.next.el !== el) {
currNode = currNode.next;
}
return currNode;
}
複製代碼
6. remove:刪除給定的節點
Link.prototype.remove = function (el) {
var prevNode = this.findPrev (el);
if (prevNode.next != null) {
prevNode.next = prevNode.next.next;
} else {
throw new Error('找不到要刪除的節點');
}
}
複製代碼
var link = new Link();
link.append(1);
link.append(3);
link.display();
console.log('------------');
link.insert(2, 1);
link.display();
console.log('------------');
link.remove(1);
link.display();
複製代碼
運行結果:
1
3
------------
1
2
3
------------
2
3
複製代碼
上面介紹的鏈表咱們稱做:單向鏈表(單鏈表),其特色是鏈表的連接方向是單向的,對鏈表的訪問要經過順序讀取從頭部開始。下面咱們介紹另外一種鏈表:雙向鏈表(雙鏈表)。
雙向鏈表也叫雙鏈表,是鏈表的一種,它的每一個數據結點中都有兩個指針,分別指向直接後繼和直接前驅。因此,從雙向鏈表中的任意一個結點開始,均可以很方便地訪問它的前驅結點和後繼結點。以下圖所示:
完整代碼地址,相比於單向鏈表節點(Node)類,咱們只需新增一個 prev
屬性,指向以前一個鏈表節點的引用便可。
function DNode (el) {
this.el = el;
this.prev = null;
this.next = null;
}
複製代碼
完整代碼地址,相比於單鏈表,雙鏈表的操做會複雜一點。
1. 構造函數
function DLink () {
this.head = new DNode('head');
}
複製代碼
2. append:向鏈表結尾添加一個節點
CLink.prototype.append = function (el) {
var currNode = this.head;
while (currNode.next != null) {
currNode = currNode.next;
}
var newNode = new Node(el);
newNode.next = currNode.next;
currNode.next = newNode;
}
複製代碼
3. find:查找給定的節點
DLink.prototype.find = function (el) {
var currNode = this.head;
while (currNode && currNode.el != el) {
currNode = currNode.next;
}
return currNode;
}
複製代碼
4. insert:插入一個節點
DLink.prototype.insert = function (newEl, oldEl) {
var newNode = new DNode(newEl);
var currNode = this.find(oldEl);
if (currNode) {
newNode.next = currNode.next;
newNode.prev = currNode;
currNode.next = newNode;
} else {
throw new Error('未找到指定要插入節點位置對應的值!')
}
}
複製代碼
5. display:順序展現鏈表節點
DLink.prototype.display = function () {
var currNode = this.head.next;
while (currNode) {
console.log(currNode.el);
currNode = currNode.next;
}
}
複製代碼
6. findLast:查找最後一個節點
DLink.prototype.findLast = function () {
var currNode = this.head;
while (currNode.next != null) {
currNode = currNode.next;
}
return currNode;
}
複製代碼
7. dispReverse:逆序展現鏈表元素
DLink.prototype.dispReverse = function () {
var currNode = this.head;
currNode = this.findLast();
while (currNode.prev != null) {
console(currNode.el);
currNode = currNode.prev;
}
}
複製代碼
8. remove:刪除節點
DLink.prototype.remove = function (el) {
var currNode = this.find(el);
if (currNode && currNode.next != null) {
currNode.prev.next = currNode.next;
currNode.next.prev = currNode.prev;
currNode.next = null;
currNode.previous = null;
} else {
throw new Error('找不到要刪除對應的節點');
}
}
複製代碼
// 實例化
var doubleLink = new DLink();
doubleLink.append(1);
doubleLink.append(2);
doubleLink.append(4);
doubleLink.display();
console.log('-------------------------');
doubleLink.dispReverse();
console.log('-------------------------');
doubleLink.insert(3, 2);
doubleLink.display();
doubleLink.remove(1);
console.log('-------------------------');
doubleLink.display();
複製代碼
運行結果:
1
2
4
-------------------------
4
2
1
-------------------------
1
2
3
4
-------------------------
2
3
4
複製代碼
循環鏈表和單向鏈表類似,節點類型都是同樣的。惟一的區別是,在建立循環鏈表時,讓其頭節點的 next 屬性指向它自己,即:this.head.next = this.head
,並保證鏈表中最後一個節點的 next
屬性,始終指向 head
,以下圖所示:
1. 構造函數
完整代碼地址,這裏,咱們沿用單鏈表中的節點(Node)類,作爲循環鏈表的節點類。不一樣的是,咱們在 CLink
構造函數階段,就要把 this.head
賦值給 this.head.next
。
function CLink () {
this.head = new Node('head');
this.head.next = this.head;
}
複製代碼
2. append: 向鏈表節點增長一個元素
DLink.prototype.append = function (el) {
var currNode = this.head;
while (currNode.next != null && currNode.next != this.head) {
currNode = currNode.next;
}
var newNode = new Node(el);
newNode.next = currNode.next;
newNode.prev = currNode;
currNode.next = newNode;
}
複製代碼
3. find:根據節點的值查找鏈表節點
CLink.prototype.find = function (el) {
var currNode = this.head;
while (currNode && currNode.el != el) {
currNode = currNode.next;
}
return currNode;
}
複製代碼
4. insert:插入一個節點
CLink.prototype.insert = function (newEl, oldEl) {
var newNode = new Node(newEl);
var currNode = this.find(oldEl);
if (currNode) {
newNode.next = currNode.next;
currNode.next = newNode;
} else {
throw new Error('未找到指定要插入節點位置對應的值!');
}
}
複製代碼
5. display:展現鏈表元素節點
CLink.prototype.display = function () {
var currNode = this.head.next;
while (currNode && currNode != this.head) {
console.log(currNode.el);
currNode = currNode.next;
}
}
複製代碼
6. 根據給定值尋找前一個節點
CLink.prototype.findPrev = function (el) {
var currNode = this.head;
while (currNode.next && currNode.next.el !== el) {
currNode = currNode.next;
}
return currNode;
}
複製代碼
7. 刪除給定值對應的節點
CLink.prototype.remove = function (el) {
var prevNode = this.findPrev(el);
if (prevNode.next != null) {
prevNode.next = prevNode.next.next;
prevNode.next.next = null;
} else {
throw new Error('找不到要刪除的節點');
}
}
複製代碼
var circleLink = new CLink ();
circleLink.append(1);
circleLink.append(2);
circleLink.append(4);
circleLink.display();
console.log('---------------------------');
circleLink.insert(3, 2);
circleLink.display();
console.log('---------------------------');
circleLink.remove(4);
circleLink.display();
複製代碼
運行結果:
1
2
4
---------------------------
1
2
3
4
---------------------------
1
2
3
複製代碼
題目: 傳說在公元 1 世紀的猶太戰爭中,猶太曆史學家弗拉維奧·約瑟夫斯和他的 40 個同胞被羅馬士兵包圍。猶太士兵決定寧肯自殺也不作俘虜,因而商量出了一個自殺方案。他們圍成一個圈,從一我的開始,數到第三我的時將第三我的殺死,而後再數,直到殺光全部人。約瑟夫和另一我的決定不參加這個瘋狂的遊戲,他們快速地計算出了兩個位置,站在那裏得以倖存。寫一段程序將 n 我的圍成一圈,而且第 m 我的會被殺掉,計算一圈人中哪兩我的最後會存活。使用循環鏈表解決該問題。
題解:
function survival (n, m) {
if (n <= 2) {
return;
}
var clink = new CLink();
for (var i = 1; i <= n; i++) {
clink.append(i);
}
var p = clink.head,
count = 0;
while (n > 2) {
p = p.next;
if (p === clink.head) {
continue;
}
count++;
if (count === m) {
clink.remove(p.el);
count = 0;
n--;
}
}
console.log('倖存者:');
clink.display();
}
複製代碼
測試:
survival(10, 3);
複製代碼
運行結果:
倖存者:
4
10
複製代碼
解析:
首先,咱們把 n
我的按1-n的序號排好,而後按照每第 m
我的死亡來循環遍歷循環鏈表,直到剩下兩我的爲止。
1 2 3 4 5 6 7 8 9 10
x x x
x x
x x
x
複製代碼