JavaScript 鏈表

原文連接javascript

前言

在以前的章節中,咱們討論瞭如何使用數組來實現列表隊列等數據結構。本章節,咱們討論另外一種列表:鏈表。咱們將會認識到爲何有時候,鏈表會優於數組,還會實現一個基於對象的鏈表,而且附上一些實戰內容。java

背景

數組並不老是組織數據的最佳數據結構,緣由以下。在不少編程語言中,數組的長度是固定的,因此當數組已被數據填滿時,再要加入新的元素就會很是困難。在數組中,添加和刪除元素也很麻煩,由於須要將數組中的其餘元素向前或向後平移,以反映數組剛剛進行了添加或刪除操做。然而,JavaScript 的數組並不存在上述問題,由於使用 split() 方法不須要再訪問數組中的其餘元素了。git

JavaScript 中數組的主要問題是,它們被實現成了對象,與其餘語言(好比 C++ 和 Java)的數組相比,效率很低。github

若是你發現數組在實際使用時很慢,就能夠考慮使用鏈表來替代它。除了對數據的隨機訪問,鏈表幾乎能夠用在任何可使用一維數組的狀況中。若是須要隨機訪問,數組仍然是更好的選擇。編程

定義

鏈表是一種物理存儲單元上非連續、非順序的存儲結構,數據元素的邏輯順序是經過鏈表中的指針連接次序實現的集合。以下圖所示:數組

default

數組元素靠它們的位置進行引用,鏈表元素則是靠相互之間的關係進行引用。然而要標識出鏈表的起始節點卻有點麻煩,許多鏈表的實現都在鏈表最前面有一個特殊節點,叫作頭節點。以下圖所示:數據結構

default

鏈表中插入一個節點的效率很高。向鏈表中插入一個節點,須要修改它前面的節點(前驅),使其指向新加入的節點,而新加入的節點則指向原來前驅指向的節點。下圖演示瞭如何在 Tue 節點後加入 Fri 節點。app

default

從鏈表中刪除一個元素也很簡單。將待刪除元素的前驅節點指向待刪除元素的後繼節點,同時將待刪除元素指向 null,元素就刪除成功了。下圖演示了從鏈表中刪除「Fri」節點的過程。編程語言

default

鏈表節點(Node)類實現

完整代碼地址,Node類包含兩個屬性:函數

  • el 用來保存節點上的數據
  • next 用來保存指向下一個節點的連接

1. 構造函數

function Node (el) {
    this.el = el;
    this.next = null;
}
複製代碼

鏈表(Link)類實現

完整代碼地址,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('找不到要刪除的節點');
    }
}
複製代碼

鏈表(Link)類測試

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
複製代碼

上面介紹的鏈表咱們稱做:單向鏈表(單鏈表),其特色是鏈表的連接方向是單向的,對鏈表的訪問要經過順序讀取從頭部開始。下面咱們介紹另外一種鏈表:雙向鏈表(雙鏈表)

雙向鏈表

雙向鏈表也叫雙鏈表,是鏈表的一種,它的每一個數據結點中都有兩個指針,分別指向直接後繼和直接前驅。因此,從雙向鏈表中的任意一個結點開始,均可以很方便地訪問它的前驅結點和後繼結點。以下圖所示:

default

雙向鏈表節點(DNode)類實現

完整代碼地址,相比於單向鏈表節點(Node)類,咱們只需新增一個 prev 屬性,指向以前一個鏈表節點的引用便可。

function DNode (el) {
    this.el = el;
    this.prev = null;
    this.next = null;
}
複製代碼

雙向鏈表(DLink)類實現

完整代碼地址,相比於單鏈表,雙鏈表的操做會複雜一點。

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('找不到要刪除對應的節點');
    }
}
複製代碼

雙向鏈表(DLink)類測試

// 實例化
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,以下圖所示:

循環鏈表

循環鏈表(CLink)實現

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('找不到要刪除的節點');
    }
}
複製代碼

循環鏈表(CLink)類測試

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
複製代碼
相關文章
相關標籤/搜索