用 JavaScript 實現鏈表

做者:Roberto Hernandez
譯者:前端小智
來源:dev
點贊再看,養成習慣

本文 GitHub https://github.com/qq44924588... 上已經收錄,更多往期高贊文章的分類,也整理了不少個人文檔,和教程資料。歡迎Star和完善,你們面試能夠參照考點複習,但願咱們一塊兒有點東西。javascript

clipboard.png

什麼是鏈表

單鏈表是表示一系列節點的數據結構,其中每一個節點指向鏈表中的下一個節點。 相反,雙向鏈表具備指向其先後元素的節點。前端

與數組不一樣,鏈表不提供對鏈表表中特定索引訪問。 所以,若是須要鏈表表中的第三個元素,則必須遍歷第一個和第二個節點才能到獲得它。java

鏈表的一個好處是可以在固定的時間內從鏈表的開頭和結尾添加和刪除項。node

這些都是在技術面試中常常被問到的數據結構,因此讓咱們開始吧。git

另外,能夠對鏈表進行排序。 這意味着當每一個節點添加到鏈表中時,它將被放置在相對於其餘節點的適當位置。github

節點

鏈表只是一系列節點,因此讓咱們從 Node 對象開始。面試

clipboard.png

一個節點有兩條信息segmentfault

  • 指向鏈表中下一項的指針或引用(對於單鏈表)
  • 節點的值

對於咱們的節點,咱們只須要建立一個函數,該函數接受一個值,並返回一個具備上面兩個信息的對象:指向下一個節點的指針和該節點的值數組

注意,咱們能夠只聲明 value 而不是 value: value。這是由於變量名稱相同(ES6 語法)

節點鏈表

如今,讓咱們深刻研究 NodeList 類,如下就是節點鏈表樣子。數據結構

clipboard.png

節點鏈表將包含五個方法:

  • push(value): 將值添加到鏈表的末尾
  • pop() :彈出鏈表中的最後一個值
  • get(index):返回給定索引中的項
  • delete(index):從給定索引中刪除項
  • isEmpty(): 返回一個布爾值,指示鏈表是否爲空

printList():不是鏈表的原生方法,它將打印出咱們的鏈表,主要用於調試

構造函數

構造函數中須要三個信息:

  • head:對鏈表開頭節點的引用
  • tail:對鏈表末尾節點的引用
  • length:鏈表中有多少節點
class LinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.length = 0;
  }
}

IsEmpty

isEmpty() 方法是一個幫助函數,若是鏈表爲空,則返回true

isEmpty() {
  return this.length === 0;
}

printList

這個實用程序方法用於打印鏈表中的節點,僅用於調試目的。

printList () {
  const nodes = [];
  let current = this.head;
  while (current) {
    nodes.push(current.value);
    current = current.next;
  }
  return nodes.join(' -> ');
}

Push

在添加新節點以前,push 方法須要檢查鏈表是否爲空。如何知道鏈表是否爲空? 兩種方式:

  • isEmpty()方法返回true(鏈表的長度爲零)
  • head 指針爲空

對於這個例子,咱們使用 head是否爲null來判斷鏈表是否爲空。

若是鏈表中沒有項,咱們能夠簡單地將head 指針和tail指針都設置爲新節點並更新鏈表的長度。

if (this.head === null) {
  this.head = node;
  this.tail = node;
  this.length++;
  return node;
}

若是鏈表不是空的,咱們必須執行如下操做:

  • tail.next 指向新節點
  • tail 指向新節點
  • 更新鏈表長度

clipboard.png

如下是完整的 push 方法:

push(value) {
  const node = Node(value);
  // The list is empty
  if (this.head === null) {
    this.head = node;
    this.tail = node;
    this.length++;
    return node;
  }
  this.tail.next = node;
  this.tail = node;
  this.length++;
}

Pop

在刪除鏈表中的最後一項以前,咱們的pop方法須要檢查如下兩項內容:

  • 檢查鏈表是否爲空
  • 檢查鏈表中是否只有一項

可使用isEmpty方法檢查鏈表是否包含節點。

if (this.isEmpty()) {
  return null;
}

咱們如何知道鏈表中只有一個節點? 若是 headtail 指向同一個節點。可是在這種狀況下咱們須要作什麼呢? 刪除惟一的節點意味着咱們實際上要從新設置鏈表。

if (this.head === this.tail) {
  this.head = null;
  this.tail = null;
  this.length--;
  return nodeToRemove;
}

若是鏈表中有多個元素,咱們能夠執行如下操做

當鏈表中有節點時,
 若是鏈表中的下一個節點是 tail 
   更新 tail 指向當前節點
   當前節點設置爲 null,
   更新鏈表的長度
   返回前一個 tail 元素

它看起來像這樣:

1  let currentNode = this.head;
    2  let secondToLastNode;
    3
    4  //從前面開始並迭代直到找到倒數第二個節點
    5 
    6  while (currentNode) {
    7    if (currentNode.next === this.tail) {
    8      // 將第二個節點的指針移動到最後一個節點
    9      secondToLastNode = currentNode;
   10      break;
   11    }
   12    currentNode = currentNode.next;
   13  }
   14  // 彈出該節點
   15  secondToLastNode.next = null;
   16  // 將 tail 移動到倒數第二個節點
   17  this.tail = secondToLastNode;
   18  this.length--;
   19 
   20  // 初始化 this.tail
   21   return nodeToRemove;

若是你沒法想象這一點,那麼讓咱們來看看它。

第6-10行:若是鏈表中的下一個節點是最後一個項,那麼這個當前項目就是新tail,所以咱們須要保存它的引用。

if (currentNode.next === this.tail) {
  secondToLastNode = currentNode;
}

clipboard.png

第15行:將secondToLastNode更新爲null,這是從鏈表中「彈出」最後一個元素的行爲。

secondToLastNode.next = null;

clipboard.png

第17行:更新tail以指向secondToLastNode

this.tail = secondToLastNode;

clipboard.png

第18行:更新鏈表的長度,由於咱們剛刪除了一個節點。

第21行:返回剛剛彈出的節點。

如下是完整的pop方法:

pop() {
  if (this.isEmpty()) {
    return null;
  }
  const nodeToRemove = this.tail;
  // There's only one node!
  if (this.head === this.tail) {
    this.head = null;
    this.tail = null;
    this.length--;
    return nodeToRemove;
  }

  let currentNode = this.head;
  let secondToLastNode;

  // Start at the front and iterate until
  // we find the second to last node
  while (currentNode) {
    if (currentNode.next === this.tail) {
      // Move the pointer for the second to last node
      secondToLastNode = currentNode;
      break;
    }
    currentNode = currentNode.next;
  }
  // Pop off that node
  secondToLastNode.next = null;
  // Move the tail to the second to last node
  this.tail = secondToLastNode;
  this.length--;

  // Initialized to this.tail
  return nodeToRemove;
}

Get

get方法必須檢查三種狀況:

  • 索引是否超出了鏈表的範圍
  • 鏈表是否爲空
  • 查詢第一個元素

若是鏈表中不存在請求的索引,則返回null

// Index is outside the bounds of the list
if (index < 0 || index > this.length) {
  return null;
}

若是鏈表爲空,則返回null。你能夠把這些if語句組合起來,可是爲了保持清晰,我把它們分開了。

if (this.isEmpty()) {
  return null;
}

若是咱們請求第一個元素,返回 head

// We're at the head!
if (index === 0 )  {
  return this.head;
}

不然,咱們只是一個一個地遍歷鏈表,直到找到要查找的索引。

let current = this.head;
let iterator =  0;

while (iterator < index) {
  iterator++;
  current = current.next;
}

return current;

如下是完整的get(index)方法:

get(index) {
// Index is outside the bounds of the list
if (index < 0 || index > this.length) {
  return null;
}

if (this.isEmpty()) {
  return null;
}

// We're at the head!
if (index === 0 )  {
  return this.head;
}

let current = this.head;
let iterator =  0;

while (iterator < index) {
  iterator++;
  current = current.next;
}

return current;
}

Delete

delete方法須要考慮到三個地方

  • 刪除的索引超出了鏈表的範圍
  • 鏈表是否爲空
  • 咱們想要刪除 head

若是鏈表中不存在咱們要刪除的索引,則返回 null

// Index is outside the bounds of the list
if (index < 0 || index > this.length) {
  return null;
}

若是咱們想刪除head,將head設置爲鏈表中的下一個值,減少長度,並返回咱們剛剛刪除的值。

if (index === 0) {
  const nodeToDelete = this.head;
  this.head = this.head.next;
  this.length--;
  return nodeToDelete;
}

若是以上都 不是,則刪除節點的邏輯以下:

循環遍歷正在查找的索引

   增長索引值

   將前一個和當前指針向上移動一個

將當前值保存爲要刪除的節點

更新上一個節點的指針以指向下一個節點

若是下一個值爲 `null`

   將`tail`設置爲新的最後一個節點

更新鏈表長度

返回已刪除的節點

若是你須要可視化圖片,請參考Pop部分中的圖表。

如下是完整的 delete 方法:

delete(index) {
   // Index is outside the bounds of the list
  if (index < 0 || index > this.length - 1) {
    return null;
  }

  if (this.isEmpty()) {
    return null;
  }

  if (index === 0) {
    const nodeToDelete = this.head;
    this.head = this.head.next;
    this.length--;
    return nodeToDelete;
  }

  let current = this.head;
  let previous;
  let iterator = 0;

  while (iterator < index) {
    iterator++;
    previous = current;
    current = current.next;
  }
  const nodeToDelete = current;
  // Re-direct pointer to skip the element we're deleting
  previous.next = current.next;

  // We're at the end
  if(previous.next === null) {
    this.tail = previous;
  }

  this.length--;

  return nodeToDelete;
}

代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug

原文:

https://dev.to/macmacky/70-ja...


交流

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

https://github.com/qq44924588...

我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!

關注公衆號,後臺回覆福利,便可看到福利,你懂的。

clipboard.png

相關文章
相關標籤/搜索