js數據結構-鏈表

鏈表和數組

你們都用過js中的數組,數組實際上是一種線性表的順序存儲結構,它的特色是用一組地址連續的存儲單元依次存儲數據元素。而它的缺點也正是其特色而形成,好比對數組作刪除或者插入的時候,可能須要移動大量的元素。前端

這裏大體模擬一下數組的插入操做:node

function insert(arr, index, data) {
      for (let i = arr.length; i >index; i--) {
        arr[i] = arr[i - 1];
      }
      arr[index] = data;
    }

從上面的代碼能夠看出數組的插入以及刪除都有可能會是一個O(n)的操做。從而就引出了鏈表這種數據結構,鏈表不要求邏輯上相鄰的元素在物理位置上也相鄰,所以它沒有順序存儲結構所具備的缺點,固然它也失去了數組在一塊連續空間內隨機存取的優勢。數組

單向鏈表

圖片描述

單向鏈表的特色:

  • 用一組任意的內存空間去存儲數據元素(這裏的內存空間能夠是連續的,也能夠是不連續的)
  • 每一個節點(node)都由數據自己和一個指向後續節點的指針組成
  • 整個鏈表的存取必須從頭指針開始,頭指針指向第一個節點
  • 最後一個節點的指針指向空(NULL)

鏈表中的幾個主要操做

  • 建立節點
  • 插入節點
  • 搜索/遍歷節點
  • 刪除節點
  • 合併

初始化節點

  • 指針指向空
  • 存儲數據
class Node {
        constructor(key) {
            this.next = null;
            this.key = key;
        }
    }

初始化單向鏈表

  • 每一個鏈表都有一個頭指針,指向第一個節點,沒節點則指向NULL
class List {
        constructor() {
            this.head = null;
        }
    }

建立節點

static createNode(key) {
        return new createNode(key);
    }

這裏說明一下,這一塊我是向外暴露了一個靜態方法來建立節點,而並不是直接把它封裝進插入操做裏去,由於我感受這樣的邏輯會更加正確一些。 從建立一個鏈表 -> 建立一個節點 -> 將節點插入進鏈表中。可能你會遇到一些文章介紹的方式是直接將一個數據做爲參數去調用insert操做,在insert內部作了一個建立節點。數據結構

插入節點(插入到頭節點以後)

插入操做只須要去調整節點的指針便可,兩種狀況:this

  • head沒有指向任何節點,說明當前插入的節點是第一個spa

    • head指向新節點
    • 新節點的指針指向NULL
  • head有指向的節點指針

    • head指向新的節點
    • 新節點的指針指向本來head所指向的節點
insert(node) {
        // 若是head有指向的節點
        if(this.head){
           node.next = this.head;
        }else {
           node.next = null;
        }
        this.head = node;
    }

搜索節點

  • 從head開始查找
  • 找到節點中的key等於想要查找的key的時候,返回該節點
find(key) {
        let node = this.head;
        while(node !== null && node.key !== key){
            node = node.next;
        }
        return node;
    }

刪除節點

這裏分三種狀況:code

  • 所要刪除的節點恰好是第一個,也就是head指向的節點blog

    • 將head指向所要刪除節點的下一個節點(node.next)
  • 要刪除的節點爲最後一個節點隊列

    • 尋找到所要刪除節點的上一個節點(prevNode)
    • 將prevNode中的指針指向NULL
  • 在列表中間刪除某個節點

    • 尋找到所要刪除節點的上一個節點(prevNode)
    • 將prevNode中的指針指向當前要刪除的這個節點的下一個節點
delete(node) {
        // 第一種狀況
        if(node === this.head){
            this.head = node.next;
            return;
        }
        
        // 查找所要刪除節點的上一個節點
        let prevNode = this.head;
        while (prevNode.next !== node) {
            prevNode = prevNode.next;
        }
        
        // 第二種狀況
        if(node.next === null) {
            prevNode.next = null;
        }
        
        // 第三種狀況
        if(node.next) {
            prevNode.next = node.next;
        }
    }

單向鏈表總體的代碼

class ListNode {
  constructor(key) {
    this.next = null;
    this.key = key;
  }
}

class List {
  constructor() {
    this.head = null;
    this.length = 0;
  }

  static createNode(key) {
    return new ListNode(key);
  }

  // 往頭部插入數據
  insert(node) {
    // 若是head後面有指向的節點
    if (this.head) {
      node.next = this.head;
    } else {
      node.next = null;
    }
    this.head = node;
    this.length++;
  }

  find(key) {
    let node = this.head;
    while (node !== null && node.key !== key) {
      node = node.next;
    }
    return node;
  }

  delete(node) {
    if (this.length === 0) {
      throw 'node is undefined';
    }

    if (node === this.head) {
      this.head = node.next;
      this.length--;
      return;
    }

    let prevNode = this.head;

    while (prevNode.next !== node) {
      prevNode = prevNode.next;
    }

    if (node.next === null) {
      prevNode.next = null;
    }
    if (node.next) {
      prevNode.next = node.next;
    }
    this.length--;
  }
}

雙向鏈表

若是你把上面介紹的單向列表都看明白了,那麼這裏介紹的雙向列表其實差很少。

圖片描述

圖片描述

從上面的圖能夠很清楚的看到雙向鏈表和單向鏈表的區別。雙向鏈表多了一個指向上一個節點的指針。

初始化節點

  • 指向前一個節點的指針
  • 指向後一個節點的指針
  • 節點數據
class ListNode {
        this.prev = null;
        this.next = null;
        this.key = key;
    }

初始化雙向鏈表

  • 頭指針指向NULL
class List {
        constructor(){
            this.head = null;
        }
    }

建立節點

static createNode(key){
        return new ListNode(key);
    }

插入節點((插入到頭節點以後)

  • 看上圖中head後面的第一個節點能夠知道,該節點的prev指向NULL
  • 節點的next指針指向後一個節點, 也就是當前頭指針所指向的那個節點
  • 若是head後有節點,那麼本來head後的節點的prev指向新插入的這個節點(由於是雙向的嘛)
  • 最後將head指向新的節點
insert(node) {
        node.prev = null;
        node.next = this.head;
        if(this.head){
            this.head.prev = node;
        }
        this.head = node;
    }

搜索節點

這裏和單向節點同樣,就直接貼代碼了

search(key) {
    let node = this.head;
    while (node !== null && node.key !== key) {
      node = node.next;
    }
    return node;
  }

刪除節點

和以前單向鏈表同樣,分三種狀況去看:

  • 刪除的是第一個節點

    • head指向所要刪除節點的下一個節點
    • 下一個節點的prev指針指向所要刪除節點的上一個節點
  • 刪除的是中間的某個節點

    • 所要刪除的前一個節點的next指向所要刪除的下一個節點
    • 所要刪除的下一個節點的prev指向所要刪除的前一個節點
  • 刪除的是最後一個節點

    • 要刪除的節點的上一個節點的next指向null(也就是指向刪除節點的next所指的地址)

圖片描述

delete(node) {
        const {prev,next} = node;
        delete node.prev;
        delete node.next;
        if(node === this.head){
            this.head = next;
        }
        if(next){
            next.prev = prev;
        }
        if(prev){
            prev.next = next;
        }
    }

雙向鏈表總體代碼

class ListNode {
  constructor(key) {
    // 指向前一個節點
    this.prev = null;
    // 指向後一個節點
    this.next = null;
    // 節點的數據(或者用於查找的鍵)
    this.key = key;
  }
}

/**
 * 雙向鏈表
 */
class List {
  constructor() {
    this.head = null;
  }

  static createNode(key) {
    return new ListNode(key);
  }

  insert(node) {
    node.prev = null;
    node.next = this.head;
    if (this.head) {
      this.head.prev = node;
    }
    this.head = node;
  }

  search(key) {
    let node = this.head;
    while (node !== null && node.key !== key) {
      node = node.next;
    }
    return node;
  }

  delete(node) {
    const { prev, next } = node;
    delete node.prev;
    delete node.next;

    if (node === this.head) {
      this.head = next;
    }

    if (prev) {
      prev.next = next;
    }
    if (next) {
      next.prev = prev;
    }
  }
}

總結

這裏作一個小總結吧,可能有一部分人讀到這裏還不是特別的明白,個人建議是先好好看懂上面的單向鏈表。 其實只要你明白了鏈表的基礎概念,就是有一個head,而後在有好多的節點(Node),而後用一個指針把他們串起來就行了,至於裏面的插入操做也好,刪除也好,其實都是在調整節點中指針的指向。

後續

後續可能還會是數據結構,多是講二叉堆,也可能回過頭來說一些隊列和棧的思想在程序中的應用。歡迎你們指出文章的錯誤,若是有什麼寫做建議也能夠提出。我會持續的去寫關於前端的一些技術文章,若是你們喜歡的話能夠關注一下哈

相關文章
相關標籤/搜索