JS數據結構與算法_鏈表

上一篇:JS數據結構與算法_棧&隊列
下一篇:JS數據結構與算法_集合&字典javascript

寫在前面

說明:JS數據結構與算法 系列文章的代碼和示例都可在此找到

上一篇博客發佈之後,僅幾天的時間居然成爲了我寫博客以來點贊數最多的一篇博客。歡喜之餘,不禁得思考背後的緣由,前端er離數據結構與算法太遙遠了,論壇裏也少有人去專門爲數據結構與算法撰文,才使得這看似平平的文章收穫如此。不過,這樣也更加堅決了我繼續學習數據結構與算法的決心(雖然只是入門級的)前端

1、鏈表數據結構

相較於以前學習的 棧/隊列 只關心 棧頂/首尾 的模式,鏈表更加像是數組。鏈表和數組都是用於存儲有序元素的集合,但有幾點大不相同java

  1. 鏈表不一樣於數組,鏈表中的元素在內存中並非連續放置的
  2. 鏈表添加或移除元素不須要移動其餘元素
  3. 數組能夠直接訪問任何一個位置的元素,鏈表必須從表頭開始迭代到指定位置訪問

下面是單鏈表的基本結構node

clipboard.png

  • 長度爲3的單鏈表
  • 每一個元素由一個存儲元素自己data的節點和一個指向下一個元素的引用next(也稱指針或連接)組成
  • 尾節點的引用next指向爲null

類比:尋寶遊戲,你有一條線索,這條線索是指向尋找下一條線索的地點的指針。你順着這條連接去下一個地點,獲得另外一條指向再下一處的線索。獲得列表中間的線索的惟一辦法,就是從起點(第一條線索)順着列表尋找git

2、鏈表的實現

鏈表的實現不像以前介紹的棧和隊列通常依賴於數組(至少咱們目前是這樣實現的),它必須本身構建類並組織邏輯實現。咱們先建立一個Nodegithub

// 節點基類
class Node {
  constructor(data) {
    this.data = data;
    this.next = null;
  }
}

通常單鏈表有如下幾種方法:面試

  • append 在鏈表尾部添加一個元素
  • insert 在指定位置插入元素
  • removeAt 在指定位置刪除元素
  • getNode 獲取指定位置的元素
  • print 打印整個鏈表
  • indexOf 查找鏈表中是否有某個元素,有則返回索引,沒有則返回-1
  • getHead 獲取鏈表頭部
  • getTail 獲取鏈表尾部(有些並未實現尾部)
  • size 返回鏈表包含的元素個數
  • clear 清空鏈表
// 初始化鏈表
class LinkedList {
  constructor() {
    this._head = null;
    this._tail = null;
    this._length = 0;
  }
  // 方法...
}

下面咱們來實現幾個重要的方法算法

2.1 append方法

在鏈表尾部添加一個新的元素可分爲兩種狀況:segmentfault

  1. 原鏈表中無元素,添加元素後,headtail均指向新元素
  2. 原鏈表中有元素,更新tail元素(以下)

clipboard.png

代碼實現數組

// 在鏈表尾端添加元素
append(data) {
  const newNode = new Node(data);
  if (this._length === 0) {
    this._head = newNode;
    this._tail = newNode;
  } else {
    this._tail.next = newNode;
    this._tail = newNode;
  }

  this._length += 1;
  return true;
}

2.2 print方法

爲方便驗證,咱們先實現print方法。方法雖簡單,這裏卻涉及到鏈表遍歷精髓

// 打印鏈表
print() {
  let ret = [];
  // 遍歷需從鏈表頭部開始
  let currNode = this._head;
  // 單鏈表最終指向null,做爲結束標誌
  while (currNode) {
    ret.push(currNode.data);
    // 輪詢至下一節點
    currNode = currNode.next;
  }
  console.log(ret.join(' --> '));
}

// 驗證
const link = new LinkedList();
link.append(1);
link.append(2);
link.append(3);

link.print(); // 1 --> 2 --> 3

2.3 getNode方法

獲取指定索引位置的節點,依次遍歷鏈表,直到指定位置返回

// 獲取指定位置元素
getNode(index) {
  let currNode = this._head;
  let currIndex = 0;
  while (currIndex < index) {
    currIndex += 1;
    currNode = currNode.next;
  }
  return currNode;
}

// 驗證【銜接上面的鏈表實例】
console.log(link.getNode(0));
// Node { data: 1, next: Node { data: 2, next: Node { data: 3, next: null } } }
console.log(link.getNode(3)); // null

2.4 insert方法

插入元素,須要考慮三種狀況

  1. 插入尾部,至關於append
  2. 插入首部,替代_head並指向原有頭部元素
  3. 中間,須要斷開原有連接並從新組合(以下)

clipboard.png

代碼實現

// 在鏈表指定位置插入元素
insert(index, data) {
  // 不知足條件的索引值
  if (index < 0 || index > this._length) return false;
  // 插入尾部
  if (index === this._length) return this.append(data);

  const insertNode = new Node(data);
  if (index === 0) {
    // 插入首部
    insertNode.next = this._head;
    this._head = insertNode;
  } else {
    // 找到原有位置節點
    const prevTargetNode = this.getNode(index - 1);
    const targetNode = prevTargetNode.next;
    // 重塑節點鏈接
    prevTargetNode.next = insertNode;
    insertNode.next = targetNode;
  }

  this._length += 1;
  return true;
}

// 驗證
link.insert(0, 0);
link.insert(4, 4);
link.insert(5, 5);
link.print(); // 0 --> 1 --> 2 --> 3 --> 4 --> 5

2.5 removeAt方法

在指定位置刪除元素同添加元素相似

  1. 首部:從新定義_head
  2. 其餘:找到目標位置的先後元素,重塑鏈接,若是目標位置爲尾部,則從新定義_tail

clipboard.png

代碼實現

// 在鏈表指定位置移除元素
removeAt(index) {
  if (index < 0 || index >= this._length) return false;

  if (index === 0) {
    this._head = this._head.next;
  } else {
    const prevNode = this.getNode(index - 1);
    const delNode = prevNode.next;
    const nextNode = delNode.next;
    // 若移除爲最後一個元素
    if (!nextNode) this._tail = prevNode;
    prevNode.next = nextNode;
  }

  this._length -= 1;
  return true;
}

// 驗證
link.removeAt(3);
link.print(); // 0 --> 1 --> 2 --> 4 --> 5

2.6 其它方法

完整的鏈表代碼,可點此獲取

// 判斷數據是否存在於鏈表內,存在返回index,不然返回-1
indexOf(data) {
  let currNode = this._head;
  let index = 0;
  while (currNode) {
    if (currNode.data === data) return index;
    index += 1;
    currNode = currNode.next;
  }

  return -1;
}

getHead() {
  return this._head;
}

getTail() {
  return this._tail;
}

size() {
  return this._length;
}

isEmpty() {
  return !this._length;
}

clear() {
  this._head = null;
  this._tail = null;
  this._length = 0;
}

3、鏈表的應用

3.1 基於鏈表實現的StackQueue

基於鏈表實現棧

class Stack {
  constructor() {
    this._link = new LinkedList();
  }
  push(item) {
    this._link.append(item);
  }
  pop() {
    const tailIndex = this._link - 1;
    return this._link.removeAt(tailIndex);
  }
  peek() {
    if (this._link.size() === 0) return undefined;
    return this._link.getTail().data;
  }
  size() {
    return this._link.size();
  }
  isEmpty() {
    return this._link.isEmpty();
  }
  clear() {
    this._link.clear()
  }
}

基於鏈表實現隊列

class Queue {
  constructor() {
    this._link = new LinkedList();
  }
  enqueue(item) {
    this._link.append(item);
  }
  dequeue() {
    return this._link.removeAt(0);
  }
  head() {
    if (this._link.size() === 0) return undefined;
    return this._link.getHead().data;
  }
  tail() {
    if (this._link.size() === 0) return undefined;
    return this._link.getTail().data;
  }
  size() {
    return this._link.size();
  }
  isEmpty() {
    return this._link.isEmpty();
  }
  clear() {
    this._link.clear()
  }
}

3.2 鏈表翻轉【面試常考】

(1)迭代法

迭代法的核心就是currNode.next = prevNode,而後從頭部一次向後輪詢

clipboard.png

代碼實現

reverse() {
  if (!this._head) return false;

  let prevNode = null;
  let currNode = this._head;
  while (currNode) {
    // 記錄下一節點並重塑鏈接
    const nextNode = currNode.next;
    currNode.next = prevNode;
    // 輪詢至下一節點
    prevNode = currNode;
    currNode = nextNode;
  }

  // 交換首尾
  let temp = this._tail;
  this._tail = this._head;
  this._head = temp;

  return true;
}

(2)遞歸法

遞歸的本質就是執行到當前位置時,本身並不去解決,而是等下一階段執行。直到遞歸終止條件,而後再依次向上執行

function _reverseByRecusive(node) {
  if (!node) return null;
  if (!node.next) return node; // 遞歸終止條件

  var reversedHead = _reverseByRecusive(node.next);
  node.next.next = node;
  node.next = null;
  
  return reversedHead;
};

_reverseByRecusive(this._head);

3.3 鏈表逆向輸出

利用遞歸,反向輸出

function _reversePrint(node){
  if(!node) return;// 遞歸終止條件
  _reversePrint(node.next);
  console.log(node.data);
};

4、雙向鏈表和循環鏈表

4.1 雙向鏈表

雙向鏈表和普通鏈表的區別在於,在鏈表中,一個節點只有鏈向下一個節點的連接,而在雙向鏈表中,連接是雙向的:一個鏈向下一個元素,另外一個鏈向前一個元素,以下圖

clipboard.png

正是由於這種變化,使得鏈表相鄰節點之間不只只有單向關係,能夠經過prev來訪問當前節點的上一節點。相應的,雙向循環鏈表的基類也有變化

class Node {
  constructor(data) {
    this.data = data;
    this.next = null;
    this.prev = null;
  }
}

繼承單向鏈表後,最終的雙向循環鏈表DoublyLinkedList以下【prev對應的更改成NEW

class DoublyLinkedList extends LinkedList {
  constructor() {
    super();
  }

  append(data) {
    const newNode = new DoublyNode(data);

    if (this._length === 0) {
      this._head = newNode;
      this._tail = newNode;
    } else {
      newNode.prev = this._tail; // NEW
      this._tail.next = newNode;
      this._tail = newNode;
    }

    this._length += 1;
    return true;
  }

  insert(index, data) {
    if (index < 0 || index > this._length) return false;
    if (index === this._length) return this.append(data);

    const insertNode = new DoublyNode(data);
    if (index === 0) {
      insertNode.prev = null; // NEW
      this._head.prev = insertNode; // NEW
      insertNode.next = this._head;
      this._head = insertNode;
    } else {
      // 找到原有位置節點
      const prevTargetNode = this.getNode(index - 1);
      const targetNode = prevTargetNode.next;
      // 重塑節點鏈接
      prevTargetNode.next = insertNode;
      insertNode.next = targetNode;
      insertNode.prev = prevTargetNode; // NEW
      targetNode.prev = insertNode; // NEW
    }

    this._length += 1;
    return true;
  }

  removeAt(index) {
    if (index < 0 || index > this._length) return false;

    let delNode;

    if (index === 0) {
      delNode = this._head;
      this._head = this._head.next;
      this._head.prev = null; // NEW
    } else {
      const prevNode = this.getNode(index - 1);
      delNode = prevNode.next;
      const nextNode = delNode.next;
      // 若移除爲最後一個元素
      if (!nextNode) {
        this._tail = prevNode;
        this._tail.next = null; // NEW
      } else {
        prevNode.next = nextNode; // NEW
        nextNode.prev = prevNode; // NEW
      }
    }

    this._length -= 1;
    return delNode.data;
  }
}

4.2 循環鏈表

循環鏈表能夠像鏈表同樣只有單向引用,也能夠像雙向鏈表同樣有雙向引用。循環鏈表和鏈 表之間惟一的區別在於,單向循環鏈表最後一個節點指向下一個節點的指針 tail.next不是引用 null, 而是指向第一個節點 head,雙向循環鏈表的第一個節點指向上一節點的指針 head.prev不是引用 null,而是指向最後一個節點 tail

clipboard.png

總結

鏈表的實現較於棧和隊列的實現複雜許多,一樣的,鏈表的功能更增強大

咱們能夠經過鏈表實現棧和隊列,一樣也能夠經過鏈表來實現棧和隊列的問題

鏈表更像是數組同樣的基礎數據結構,同時也避免了數組操做中刪除或插入元素對其餘元素的影響

上一篇:JS數據結構與算法_棧&隊列
下一篇:JS數據結構與算法_集合&字典

相關文章
相關標籤/搜索