別說鏈表不重要(一):單鏈表的實現原理+JavaScript實現實戰+常見操作一篇搞定

鏈表的原理及Js基本實現

虛心接受批評和指正,互相成就,共勉!

從這篇文章你會收穫什麼?

  • 瞭解單鏈表和雙鏈表的基本結構;
  • 在單鏈表或雙鏈表中實現遍歷、插入和刪除;
  • 在鏈表中使用雙指針技巧(快慢指針技巧);
  • 解決一些經典問題,例如反轉鏈表;

目錄

鏈表的由來

一、鏈表的由來

我們接觸最多的數據存儲結構應該是數組了,在實際場景中它的出現頻率極高,但是它並不能適於用所有情況。這也是的鏈表

原因如下:

  • 在很多編程語言中,數組的長度是固定的,所以當數組已被數據填滿時,再要加入新的元素就會非常困難。
  • 在數組中的添加刪除元素很麻煩,因爲需要將數組中的其他元素向前或向後平移。
  • JavaScript中數組的主要問題是,它們被實現成了對象,與其他語言(比如 C++ 和 Java) 的數組相比,效率較低。

爲了解決上述問題
如果你發現數組在實際使用時很慢,就可以考慮使用鏈表來替代它。除了對數據的隨機訪問,鏈表幾乎可以用在任何可以使用一維數組的情況中,如果需要頻繁的刪除和添加操作,就主動考慮一下鏈表吧~

1.1 特點

優點

  • 鏈表結構可以充分利用計算機內存空間,實現靈活的內存動態管理。
  • 增加數據和刪除數據很容易。
  • 鏈表中的每個元素實際上是一個單獨的對象,而所有對象都通過每個元素中的引用字段鏈接在一起。

缺點?

  • 訪問時間是線性的(而且難以管道化),更快的訪問,如隨機訪問,是不可行的。與鏈表相比,數組具有更好的緩存位置。
  • 失去了數組隨機讀取的優點,同時鏈表由於增加了結點的指針域,空間開銷比較大

鏈表有很多種不同的類型:單向鏈表,雙向鏈表以及循環鏈表。鏈表可以在多種編程語言中實現。下面出現的代碼都是用Js實現的,如果不對的地方,歡迎大佬們指正,我們共勉。

二、單鏈表

單鏈表中的每個結點不僅包含值,還包含鏈接到下一個結點的引用字段。通過這種方式,單鏈表將所有結點按順序組織起來。、

下面是一個單鏈表的例子:

當你得到了head節點,就得到了整個列表。

我們創建單一節點(Node)的操作應該是這樣的:

var Node = function(ele){
    this.ele = ele; // 保存當前節點的值
    this.next = null; // 保存指向下一個節點的地址
}

2.1 添加節點

就像給繩子打結一樣,添加節點,就是在兩個繩結之間,再打一個新結。

如果我們想在給定的結點 prev 之後添加新值,我們應該:

  1. 創建要插入的Node——cur
    在這裏插入圖片描述
  2. 將cur節點的next鏈接到next節點(pre的下一個節點)
    在這裏插入圖片描述
  3. 將pre的next鏈接到cur節點
    在這裏插入圖片描述

在開頭添加結點

衆所周知,我們使用頭結點(head)來代表整個列表。

因此,在列表開頭添加新節點時更新頭結點 head 至關重要。

  1. 初始化一個新結點 cur
  2. 將新結點curnext鏈接到我們的原始頭結點 head.next節點
  3. 將head節點的next鏈接到cur即可。

在末尾添加節點

  1. 創建新節點cur
  2. 將鏈表的末尾節點的next鏈接到cur即可

2.2 刪除節點

如果我們要刪除指定的節點cur,該這麼做:

  1. 找到cur的上一個節點prev,及其下一個節點prev.next(要刪除的節點)
  2. prev.next鏈接讓 `prev.next.next,即跳過刪除節點。

注意:我們必須從頭節點遍歷至指定節點,刪除節點的平均時間複雜度是O(N)

刪除末尾節點

  1. 找到next節點鏈接爲null的節點,以及它的前節點prev
  2. prev.next 鏈接 null 即可

三、設計鏈表

以LeetCode的中的基礎題爲例,我們嘗試用代換實現前文提過的思路。707.設計鏈表

題目

設計鏈表的實現。您可以選擇使用單鏈表或雙鏈表。單鏈表中的節點應該具有兩個屬性:val 和 next。val 是當前節點的值,next 是指向下一個節點的指針/引用。如果要使用雙向鏈表,則還需要一個屬性 prev 以指示鏈表中的上一個節點。假設鏈表中的所有節點都是 0-index 的。

在鏈表類中實現這些功能:

  • get(index):獲取鏈表中第 index 個節點的值。如果索引無效,則返回-1。
  • addAtHead(val):在鏈表的第一個元素之前添加一個值爲 val 的節點。插入後,新節點將成爲鏈表的第一個節點。
  • addAtTail(val):將值爲 val 的節點追加到鏈表的最後一個元素。
  • addAtIndex(index,val):在鏈表中的第 index 個節點之前添加值爲 val 的節點。如果 index 等於鏈表的長度,則該節點將附加到鏈表的末尾。如果 index 大於鏈表長度,則不會插入節點。如果index小於0,則在頭部插入節點。
  • deleteAtIndex(index):如果索引 index 有效,則刪除鏈表中的第 index 個節點。

示例:

MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2);   //鏈表變爲1-> 2-> 3
linkedList.get(1);            //返回2
linkedList.deleteAtIndex(1);  //現在鏈表是1-> 3
linkedList.get(1);            //返回3

Js版代碼實現

// 創建節點類
var Node = function(ele){
    this.ele = elel; // 保存當前節點的值
    this.next = null; // 保存的下一個節點的地址
}

/**
 * 鏈表構造函數
 */
var MyLinkedList = function() {
    this.head = new Node('head')
};

/**
 * 查詢 index 位節點
 * @param {index} index
 * @return {Node} 返回節點 || null
 */
MyLinkedList.prototype.find = function (index) {
    var i = 0 // 記錄下標
    var current = this.head //新節點->head節點
    while(current.next){
        if(i == index){
            return current.next
        }
        i++
        current = current.next
    }
    return null
};

/**
 * 查詢 index 位節點的值
 * @param {number} index
 * @return {number}
 */
MyLinkedList.prototype.get = function(index) {
    var _this = this
    var res = _this.find(index) // 利用find查找
    return res ? res.ele : -1
};

/**
 * 插入到鏈表首位
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtHead = function(val) {
    var current = this.head
    var node = new Node(val)
    node.next = current.next
    current.next = node
};

/**
 * 插入到鏈表首末位
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtTail = function(val) {
    var current = this.head
    while(current.next){
        current = current.next
    }
    var node = new Node(val)
    current.next = node
};

/**
 * 在 index 位置插入值爲 val 的節點
 * @param {number} index
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtIndex = function(index, val) {
    if(index < 0){
        this.addAtHead(val) // 下標小於鏈表0,則插入頭部
        return;
    }
    var current = this.head
    var i = 0
    while(current.next){
        if(i == index){
            var node = new Node(val)
            node.next = current.next
            current.next = node
            return;
        }
        i++
        current = current.next
    }

    this.addAtTail(val) // 下標超過鏈表長度,則插入末尾
    return;
};

/**
 * Delete the index-th node in the linked list, if the index is valid.
 * @param {number} index
 * @return {void}
 */

MyLinkedList.prototype.deleteAtIndex = function(index) {
    if(index < 0) return;
    var i = 0
    var current = this.head
    while(current.next){
        if(i == index){
            current.next = current.next.next
            return;
        }
        i++
        current = current.next
    }
};

爲了方便操作,我們主動創建了一個節點爲頭節點,實際存儲過程中是完全不需要的。

四、鏈表的基本使用場景

  • 對線性表的長度或者規模難以估計;
  • 頻繁做插入刪除操作;
  • 構建動態性比較強的線性表

鏈表的基本操作

1.創建節點

// Node類 
function Node(element) {
    this.element = element;
    this.next = null;
}

2.創建鏈表

function LList() {
    console.log('---創建---')
    this.head = new Node("head");
    this.find = find; //查找指定節點
    this.insert = insert; //插入節點
    this.remove = remove; //刪除節點
    this.display = display; //打印鏈表
    this.findNextEle = findNextEle; //查找存儲指定節點的節點
}

3.查找目標節點

function find(item) {
    var currNode = this.head; //創建一個節點,並將頭節點指向它
    while (currNode.element != item) {
        currNode = currNode.next; // 如果currNode的值不是我們找的值,向下查找
    }
    return currNode;
}

4.添加操作

function insert(newElement, item) {
    var newNode = new Node(newElement); // 創建要插入的節點
    var current = this.find(item); //找到要插入的節點
    newNode.next = current.next; //要添加位置節點的next -> 查找到節點的next
    current.next = newNode; // 被緹娜家位置的節點
}

5.查找存儲目標節點的節點

function findNextEle(item) {
    var currNode = this.head
    while (!(currNode.next == null) && (currNode.next.element != item)) {
        currNode = currNode.next
    }
    return currNode
}

6.刪除操作

function remove(item) {
    console.log('---刪除---',item)
    var preNode = this.findNextEle(item) //找到存儲着《待刪除節點》的節點
    if (!(preNode.next == null)) {
        preNode.next = preNode.next.next // 存儲着 《待刪除節點》.next -> .next.next
    }
}

7.打印操作

function display() {
    var currNode = this.head
    while (!(currNode.next == null)) {
        console.log(currNode.next.element,currNode.next.next)
        currNode = currNode.next
    }
}

測試數據

var testCities = new LList()
testCities.insert('北京', 'head')
testCities.insert('上海', '北京')
testCities.insert('廣州', '上海')
testCities.insert('深圳', '廣州')
testCities.display()
testCities.remove('廣州');
testCities.display()

總結

  • 鏈表的特點,優缺點。
  • 鏈表的插入、刪除思想
  • 如何實現一個鏈表類,並且支持必要的操作
  • 鏈表得實際場景的應用

關於我

  • 19年畢業的前端開發
  • 沉迷Js、熱衷開源(菜鳥一個)
  • 郵箱:[email protected]
瞧一瞧(求Star!!!)

下一篇我們會總結一下雙指針在鏈表中的作用,未完待續…

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章