JavaScript中的幾種資料結構簡介

lvwxx發表於2019-03-25

JavaScript中的資料結構

Intruduction

隨著業務邏輯越來越多的從後端轉向前端,專業的前端工程知識變的更加關鍵。作為前端的工程師,我們依賴像React這樣的庫來開發view層,同時又依賴Redux這樣的庫來管理資料狀態,兩者組合起來作為響應式程式設計,當資料動態變化時,UI層可以實時的更新。漸漸地,後端可以專注於api的開發,僅僅提供資料的檢索和更新。這樣實際上,後端只是將資料庫轉發到前端,前端工程師處理所有的邏輯,微服務和graphql的日益增長證明了這個趨勢。

如今,前端工程師不僅要精通html和css,也要精通JavaScript。隨著客戶端的資料儲存成為伺服器端資料庫的“副本”,熟悉慣用資料結構就變得至關重要。事實上,工程師的經驗水平可以從他/她區分何時以及為什麼使用特定資料結構的能力中推斷出來。

Bad programmers worry about the code. Good programmers worry about data structures and their relationships.  

— Linus Torvalds, Creator of Linux and Git
複製程式碼

在高等級上,有3中型別的資料結構, 棧和佇列是類陣列的結構,它們只是在插入和刪除資料上有所不同。連結串列、樹和圖是擁有節點的結構,並且節點有對其他節點的指標。雜湊表依賴雜湊函式儲存和定位資料。

就複雜性而言,佇列和棧是最簡單的,可以由連結串列構造,樹和圖是最複雜的,因為它們在連結串列的結構上進行了擴充套件。雜湊表需要利用這些資料結構來可靠地執行。就效率而言,連結串列最適合記錄和儲存資料,雜湊表最適合檢索資料。

下文將解釋並說明應該在何時使用這些資料結構。

Stack

可以說JavaScript中最重要的堆疊是呼叫堆疊,每當函式執行時,會把函式的作用域推入棧中。在程式設計方式上而言,棧只是一個包含pop和push操作的陣列結構,Push增加元素到陣列的頂端,Pop移除陣列元素在相同的位置,換句話說,棧結構遵循“後進先出”的原則(LIFO)。

class Stack {
  constructor() {
    this.list = []
  }

  push(...item) {
    this.list.push(...item)
  }

  pop() {
    this.list.pop()
  }
}
複製程式碼

Queue

JavaScript是一種事件驅動的程式語言,它支援非阻塞操作。在瀏覽器內部,只有一個執行緒來執行所有的JavaScript程式碼,使用事件迴圈來註冊事件,為了支援單執行緒環境中的非同步性(為了節省CPU資源和增強web體驗),回撥函式只有在呼叫堆疊為空時才會退出佇列並執行。Promise依賴於這個事件驅動的體系結構,允許非同步程式碼的“同步風格”執行,而不會阻塞其他操作。

在程式設計方式上而言,佇列是隻包含一個unshift和pop操作的陣列結構,Unshift將資料項加入佇列的末尾,Pop從陣列的頂部將元素出列,換句話說,佇列遵循“先進先出”的原則(FIFO)。

class Queue {
  constructor() {
    this.list = []
  }

  enqueue(...item) {
    this.list.unshift(...item)
  }

  dequeue() {
    this.list.pop()
  }
}
複製程式碼

Linked List

與陣列相似,連結串列按順序儲存資料元素。連結串列不儲存索引,而是儲存指向其他資料項的指標。第一個節點成為頭節點,最後一個節點成為尾節點。在單連結串列中,每個節點只有指向下一個節點的指標,頭部是每次檢索開始的地方,在雙連結串列中,每個節點還有指向前一個節點的指標,因此雙連結串列可以從尾部開始向前檢索。

連結串列在插入和刪除元素時有固定的時間,因為可以改變指標。但是在陣列中執行相同的操作需要線性時間,因為後續需要移位。此外,只要有空間,連結串列就可以增長。然而,即使是自動調整大小的“動態”陣列也可能變得異常昂貴。但是要查詢或編輯連結串列中的元素,我們可能需要遍歷整個長度,這等於線性時間。然而,對於陣列索引來說,這樣的操作是微不足道的。

與陣列一樣,單連結串列也可以作為堆疊來操作,只要讓頭部成為唯一可以插入和移除元素的地方。雙連結串列可以作為佇列來操作,只要在尾部插入元素,在頭部移除元素。對於大量的資料來說,這種實現佇列的方法比陣列效能更好,因為陣列的shiftunshift操作需要線性的時間在後續重新索引每個元素。

連結串列結構在客戶端和服務端都是常用的。在客戶端,像Rudex這樣的狀態管理庫以連結串列的方式構建其中介軟體邏輯。當actiondispatch後,它們從一箇中介軟體到另外一箇中介軟體直到到達ruducer。在服務端,像Express這樣的web框架也以類似的方式構造它的中介軟體邏輯,當一個request到達時,它會按順序從一箇中介軟體到另一箇中介軟體,直到發出響應。

單連結串列的簡單實現
class LinkList {
  constructor() {
    this.head = null
  }

  find(value) {
    let curNode = this.head
    while (curNode.value !== value) {
      curNode = curNode.next
    }
    return curNode
  }

  findPrev(value) {
    let curNode = this.head
    while (curNode.next!==null && curNode.next.value !== value) {
      curNode = curNode.next
    }
    return curNode
  }

  insert(newValue, value) {
    const newNode = new Node(newValue)
    const curNode = this.find(value)
    newNode.next = curNode.next
    curNode.next = newNode
  }

  delete(value) {
    const preNode = this.findPrev(value)
    const curNode = preNode.next
    preNode.next = preNode.next.next
    return curNode
  }
}

class Node {
  constructor(value, next) {
    this.value = value
    this.next = null
  }
}
複製程式碼

Hash Table

雜湊表類似於字典結構,由鍵值對組成。每個對在記憶體中的地址有一個雜湊函式確定,該函式接受一個key作為引數,並返回一個檢索該對的記憶體地址。如果兩個或者多個key轉為相同的地址,則可能會傳送衝突。為了健壯性,getter和setter應該預測這些事件,以確保所有資料都可以恢復,並且沒有覆蓋任何資料。

如果已經知道的地址是整數序列,可以簡單地使用陣列來儲存鍵值對。對於更復雜的對映,我們可以使用maps或者objects, 雜湊表的插入和查詢元素的時間平均為常數,如果key表示地址,就不需要雜湊,一個簡單的物件就足夠了。雜湊表實現鍵和值之間的簡單對應,鍵和地址之間的簡單關聯,但是犧牲了資料之間的關係。所以,雜湊表在儲存資料方面不是最優的。

如果一個應用傾向於檢索而不是儲存資料,那麼在查詢、插入和刪除方面,沒有其他資料結構能夠與雜湊表的速度相匹配。因此雜湊表被廣泛應用也就不足為奇了。從資料庫到服務端,再到客戶端,雜湊表尤其是雜湊函式對應用程式的效能和安全方面是至關重要的。資料庫的查詢速度很大程度上依賴於指向記錄的索引按順序儲存。這樣,二進位制搜尋就可以在對數時間內完成,特別是對於大的資料來說,這是一個巨大的效能優勢。

在客戶端和服務端,許多流行的庫都用快取來最大程度提升效能。通過在雜湊表中儲存輸入和輸出的記錄,對於相同的輸入,函式僅執行一次。流行的Reselect庫使用這種快取策略來優化啟動了Redux應用程式的mapStateToProps函式。實際上,JavaScript引擎還利用名為呼叫棧的雜湊表儲存所有我們建立的變數。這些變數可以通過呼叫棧上的指標被訪問到。

網際網路本身也依賴於雜湊演算法來安全執行。網際網路的結構是這樣的:任何計算機都可以通過互相連線的web裝置與其他計算機通訊。每當一個裝置登入到網際網路上,它也可以成為一個路由器,資料流可以通過它進行傳輸。然而這是一把雙刃劍。分散式架構意味著網路中的任何裝置都可以監聽並篡改它幫助轉發的資料包。MD5SHA256等雜湊函式在防止中間人攻擊方面發揮著關鍵作用。HTTPS上的電子商務之所以安全,只是因為使用了這些雜湊函式。

受Internet的啟發,區塊鏈技術通過使用雜湊函式對每個區塊的資料建立一個不可變的“指紋”,本質上建立了一個可以在web上被公開的完整資料庫,任何人都可以檢視和貢獻。從結構上看,區塊鏈就是加密雜湊的二叉樹單連結串列。雜湊非常神祕,任何人都可以建立和更新一個財務交易資料庫。曾經只有政府和中央銀行才能做到的事情,現在任何人都可以安全地創造自己的貨幣!

隨著越來越多的資料庫走向開放,要求前端工程師可以抽象出所有底層密碼的複雜性。在未來,應用程式主要的區別將是使用者體驗。

一個簡單的不做衝突處理的雜湊表

class HashTable {
  constructor(size) {
    this.table = new Array(size)
  }
  hash(key) { // hash函式
    // 將字串中的每個字元的ASCLL碼值相加,再對陣列的長度取餘
    let total = 0
    for (let i = 0; i < key.length; k++) {
      total += key.charCodeAt(i)
    }
    return total % this.table
  }
  insert(key, value) {
    const hashKey = this.hash(key)
    this.table[hashKey] = value
  }
  get(key) {
    const hashKey = this.hash(key)
    if (!this.table[hashKey]) {
      return null
    }
    return this.table[hashKey]
  }
  getAll() {
    const table = []
    for (let i = 0; i < this.table.length; i++) {
      if (this.table[i] != undefined) {
        table.push(this.table[i])
      }
    }
    return table
  }
}
複製程式碼

總結

這些資料結構可以在任何地方被找到,從資料庫到服務端再到前端,甚至JavaScript引擎自身。隨著邏輯層越來越多的從後端移向前端,前端的資料層變得至關重。對這一層的恰當的管理需要掌握邏輯所依賴的資料結構。沒有一種資料結構適合所有情況,因為對一個屬性進行優化總是會影響另外的屬性。一些資料結構對於儲存資料是非常高效的,然而另外的資料結構對於搜尋元素來說更加高效。在一種極端情況下,連結串列是儲存的最佳選擇,可以被分成堆疊和佇列(線性時間)。另一方面,沒有其他結構可以匹配雜湊表的搜尋速度(常數時間)。樹的結構效能位於兩者之間(對數時間),圖表可以描述自然界最複雜的結構。

相關文章