相關推薦:《》
在 JavaScript 中資料結構通常總是被忽略,或者接觸得不多。但是對於許多大廠而言,一般都需要你深刻了解如何管理資料。掌握資料結構也能夠在解決問題時為你的工作提供幫助。
在本文中,我們將要討論並實現的資料結構是:
第一個資料結構是棧。它與佇列非常相似,你之前可能聽說過呼叫棧,這是 JavaScript 用於處理事件的方法。
棧看起來像這樣:
最後一個存入棧裡的專案將是第一個被移除的專案。這被稱為後進先出(LIFO)。 Web 瀏覽器中的後退按鈕就是一個很好的例子:將你檢視的每個頁面新增到棧中,當你單擊「返回」時,將從棧中彈出當前頁面(最後新增的頁面)。
理論足夠多了。接下來看一些程式碼:
class Stack { constructor() { // 建立棧結構,這是一個空物件 this.stack = {} } // 把一個值壓入棧的頂部 push(value) { } // 彈出棧頂的值並返回 pop() { } // 讀取棧中的最後一個值,但是不刪除 peek() { } }
我已經對上面的程式碼進行了註釋,現在咱們一起對其進行實現。第一種方法是 push
。
先思考一下需要這個方法做的事情:
如果你能夠先自己嘗試一下,那就太好了,完整的 push
方法實現如下:
class Stack { constructor() { this._storage = {}; this._length = 0; // 這是棧的大小 } push(value) { // 將值新增到棧頂 this._storage[this._length] = value; // 因為增加了一個值,所以也應該將長度加1 this._length++; } /// ..... }
我敢打賭,這比你想象的要容易。有許多類似這樣的結構,它們聽起來比實際上要複雜得多。
現在是 pop
方法。 pop
方法的目標是刪除最後一個新增到棧中的值,然後返回它。如果可以的話,請先自己嘗試實現:
class Stack { constructor() { this._storage = {}; this._length = 0; } pop() { // we first get the last val so we have it to return const lastVal = this._storage[--this._length] // now remove the item which is the length - 1 delete this._storage[--this._length] // decrement the length this._length--; // now return the last value return lastVal } }
酷!快要完成了。最後一個是 peek
函數,該函數檢視棧中的最後一項。這是最簡單的功能:只需要返回最後一個值。實現是:
class Stack { constructor() { this._storage = {}; this._length = 0; } /* * Adds a new value at the end of the stack * @param {*} value the value to push */ peek() { const lastVal = this._storage[--this._length] return lastVal } }
所以它與 pop
方法非常相似,但不刪除最後一項。
Yes!第一個資料結構已經實現。接著是佇列,佇列與棧非常相似。
接下來討論佇列——希望棧在你的腦子仍然記得很清楚,因為它和佇列非常相似。棧和佇列之間的主要區別在於佇列是先進先出(FIFO)。
可以用圖形這樣表示:
所以兩個主要方法是 enqueue
與 dequeue
。資料被新增到隊尾,並從隊首移除。為了更好的理解它,下面開始實現佇列。
核心程式碼結構如下所示:
class Queue { constructor() { // 與前面類似,我們為資料結構提供了一個物件 // 並且還有一個變數來儲存長度 this.queue = {} this.length = 0 // 這是一個跟蹤頭部的新變數 this.head = 0 } enqueue(value) { } dequeue() { } peek() { } }
首先實現 enqueue
方法。其目的是向隊尾新增一個專案。
enqueue(value) { // 使用 value 引數將 length + head 的鍵新增到物件 this.queue[this.length + this.head] = value; this.length++ }
這是一個非常簡單的方法,可以在佇列的末尾新增一個值,但是你可能會對this.queue[this.length + this.head] = value;
感到困惑。
假設佇列看起來像這樣的 {14 : 'randomVal'}
。在新增這個內容時,我們希望的下一個值為 15
,所以應該是 length(1) + head(14),即為 15
。
下一個要實現的是 dequeue
:
dequeue() { // 獲取第一個值的參照,以便將其返回 const firstVal = this.queue[this.head] // 現在將其從佇列中刪除 delete this.queue[this.head] this.length--; // 最終增加我們的頭成為下一個節點 this.head++; }
最後要實現的是 peek
方法,非常簡單:
peek() { // 只需要把值返回即可 return this.queue[this.head]; }
佇列實現完畢。
先讓我們討論一下強大的連結串列。這比上面的結構要複雜得多。
可能你第一個問題是為什麼要使用連結串列?連結串列主要用於沒有動態大小調整陣列的語言。連結串列按順序組織專案,一個專案指向下一個專案。
連結串列中的每個節點都有一個 data
值和一個 next
值。下圖中 5
是 data
值,next
值指向下一個節點,即值為 10
的節點。
在視覺上,它看起來像這樣:
在一個物件中,上面的 LinkedList
看起來像下面的樣子
你會看到最後一個值 1
的 next
值為 null
,因為這是 LinkedList
的結尾。
那麼該如何實現呢?
讓我們建立一個具有值 1
、 2
和 37
的 LinkedList
。
const myLinkedList = { head: { value: 1 next: { value: 2 next: { value: 37 next: null } } } };
現在我們知道了該怎樣手動建立 LinkedList
,但是還需要編碼實現 LinkedList
的方法。
首先要注意的是,LinkedList
只是一堆巢狀物件!
當構造一個 LinkedList
時,我們需要一個 head
和一個 tail
,它們最初都會指向頭部(因為 head
是第一個也是最後一個)。
class LinkedList { constructor(value) { this.head = {value, next: null} this.tail = this.head } }
第一個要實現的方法是 insert
,該方法用來在連結串列的末尾插入一個值。
// insert 將新增到連結列表的末尾 insert(value) { /* 建立一個節點 */ const node = {value, next: null} /* 把 tail 的 next 屬性設定為新節點的參照 */ this.tail.next = node; /* 新節點現在是尾節點 */ this.tail = node; }
上面最混亂的一行可能是 this.tail.next = node
。之所以這樣做,是因為當新增一個新節點時,我們還希望當前的 tail
指向新的 node
,該節點將成為新的 tail
。第一次插入 node
時,頭部的 next
指標將指向新節點,就像在建構函式中那樣,在其中設定了 this.tail = this.head
。
你還可以到這個網站來檢檢視形化的演示,這將幫你瞭解插入的過程(按 esc
擺脫煩人的彈出視窗)。
下一個方法是刪除節點。我們首先要決定引數是值( value
) 還是對節點(node
)的參照(在面試中,最好先問問面試官)。我們的程式碼中傳遞了一個「值」。按值從列表中刪除節點是一個緩慢的過程,因為必須要遍歷整個列表才能找到值。
我這樣做是這樣的:
removeNode(val) { /* 從 head 開始 */ let currentNode = this.head /* 我們需要保留對上一個節點的參照 */ let previousNode /* 當存在一個節點時,意味著沒有到達尾部 */ while(currentNode) { /* 如果發現自己想要的那個值,那麼就退出迴圈 */ if(currentNode.value === val) { break; } /* 沒有找到值就將 currentNode 設定為 previousNode */ previousNode = currentNode /* 得到下一個節點並將其分配給currentNode */ currentNode = currentNode.next } /* 返回undefined,因為沒有找到具有該值的節點 */ if (currentNode=== null) { return false; } // 如果節點是 head ,那麼將 head 設定為下一個值 頭節點的 if (currentNode === this.head) { this.head = this.head.next; return; } /* 通過將節點設定為前面的節點來刪除節點 */ previousNode.next = currentNode.next }
removeNode
方法使我們對 LinkedList
的工作方式有了很好的瞭解。
所以再次說明一下,首先將變數 currentNode
設定為 LinkedList
的 head
,因為這是第一個節點。然後建立一個名為 previousNode
的預留位置變數,該變數將在 while
迴圈中使用。從條件 currentNode
開始 while
迴圈,只要存在 currentNode
,就會一直執行。
在 while
迴圈中第一步是檢查是否有值。如果不是,則將 previousNode
設定為 currentNode
,並將 currentNode
設定為列表中的下一個節點。繼續進行此過程,直到找到我需要找的值或遍歷完節點為止。
在 while
迴圈之後,如果沒有 currentNode
,則返回 false
,這意味著沒有找到任何節點。如果確實存在一個 currentNode
,則檢查的 currentNode
是否為 head
。如果是的話就把 LinkedList
的 head
設定為第二個節點,它將成為 head
。
最後,如果 currentNode
不是頭,就把 previousNode
設定為指向 currentNode
前面的 node
,這將會從物件中刪除 currentNode
。
另一個常用的方法(面試官可能還會問你)是 removeTail
。這個方法如其所言,只是去掉了 LinkedList
的尾節點。這比上面的方法容易得多,但工作原理類似。
我建議你先自己嘗試一下,然後再看下面的程式碼(為了使其更復雜一點,我們在建構函式中不使用 tail
):
removeTail() { let currentNode = this.head; let previousNode; while (currentNode) { /* 尾部是唯一沒有下一個值的節點,所以如果不存在下一個值,那麼該節點就是尾部 */ if (!currentNode.next) { break; } // 獲取先前節點的參照 previousNode = currentNode; // 移至下一個節點 currentNode = currentNode.next; } // 要刪除尾部,將 previousNode.next 設定為 null previousNode.next = null; }
這些就是 LinkedList
的一些主要方法。連結串列還有各種方法,但是利用以上學到的知識,你應該能夠自己實現它們。
接下來是強大的雜湊表。
雜湊表是一種實現關聯陣列的資料結構,這意味著它把鍵對映到值。 JavaScript 物件就是一個「雜湊表」,因為它儲存鍵值對。
在視覺上,可以這樣表示:
在討論如何實現雜湊表之前,需要討論討論雜湊函數的重要性。雜湊函數的核心概念是它接受任意大小的輸入並返回固定長度的雜湊值。
hashThis('i want to hash this') => 7
雜湊函數可能非常複雜或直接。 GitHub 上的每個檔案都經過了雜湊處理,這使得每個檔案的查詢都非常快。雜湊函數背後的核心思想是,給定相同的輸入將返回相同的輸出。
在介紹了雜湊功能之後,該討論一下如何實現雜湊表了。
將要討論的三個操作是 insert
、get
最後是 remove
。
實現雜湊表的核心程式碼如下:
class HashTable { constructor(size) { // 定義雜湊表的大小,將在雜湊函數中使用 this.size = size; this.storage = []; } insert(key, value) { } get() {} remove() {} // 這是計算雜湊金鑰的方式 myHashingFunction(str, n) { let sum = 0; for (let i = 0; i < str.length; i++) { sum += str.charCodeAt(i) * 3; } return sum % n; } }
現在解決第一個方法,即 insert
。insert
到雜湊表中的程式碼如下(為簡單起見,此方法將簡單的處理衝突問題):
insert(key, value) { // 得到陣列中的索引 const index = this.myHashingFunction(key, this.size); // 處理衝突 - 如果雜湊函數為不同的鍵返回相同的索引, // 在複雜的雜湊函數中,很可能發生衝突 if (!this.storage[index]) { this.storage[index] = []; } // push 新的鍵值對 this.storage[index].push([key, value]); }
像這樣呼叫 insert
方法:
const myHT = new HashTable(5); myHT.insert("a", 1); myHT.insert("b", 2);
你認為我們的雜湊表會是什麼樣的?
你可以看到鍵值對已插入到表中的索引 1
和 4
處。
現在實現從雜湊表中刪除
remove(key) { // 首先要獲取 key 的索引,請記住, // 雜湊函數將始終為同一 key 返回相同的索引 const index = this.myHashingFunction(key, this.size); // 記住我們在一個索引處可以有多個陣列(不太可能) let arrayAtIndex = this.storage[index]; if (arrayAtIndex) { // 遍歷該索引處的所有陣列 for (let i = 0; i < arrayAtIndex.length; i++) { // get the pair (a, 1) let pair = arrayAtIndex[i]; // 檢查 key 是否與引數 key 匹配 if (pair[0] === key) { delete arrayAtIndex[i]; // 工作已經完成,所以要退出迴圈 break; } } } }
最後是 get
方法。這和 remove
方法一樣,但是這次,我們返回 pair
而不是刪除它。
get(key) { const index = this.myHashingFunction(key, this.size); let arrayAtIndex = this.storage[index]; if (arrayAtIndex) { for (let i = 0; i < arrayAtIndex.length; i++) { const pair = arrayAtIndex[i]; if (pair[0] === key) { return pair[1]; } } } }
我認為不需要執行這個操作,因為它的作用與 remove
方法相同。
你可以認為它並不像最初看起來那樣複雜。這是一種到處使用的資料結構,也是是一個很好理解的結構!
最後一個資料結構是臭名昭著的二元搜尋樹。
在二元搜尋樹中,每個節點具有零個、一個或兩個子節點。左邊的稱為左子節點,右邊的稱為右子節點。在二元搜尋樹中,左側的子項必須小於右側的子項。
你可以像這樣描繪一個二元搜尋樹:
樹的核心類如下:
class Tree { constructor(value) { this.root = null } add(value) { // 我們將在下面實現 } }
我們還將建立一個 Node
類來代表每個節點。
class Node { constructor(value, left = null, right = null) { this.value = value; this.left = left; this.right = right; } }
下面實現 add
方法。我已經對程式碼進行了註釋,但是如果你發現使你感到困惑,請記住,我們要做的只是從根開始並檢查每個節點的 left
和 right
。
add(value) { // 如果沒有根,那麼就建立一個 if (this.root === null) { this.root = new Node(value); return; } let current = this.root; // keep looping while (true) { // 如果當前值大於傳入的值,則向左 if (current.value > value) { // 如果存在左子節點,則再次進行迴圈 if (current.left) { current = current.left; } else { current.left = new Node(value); return; } } // 值較小,所以我們走對了 else { // 向右 // 如果存在左子節點,則再次執行迴圈 if (current.right) { current = current.right; } else { current.right = new Node(value); return; } } } }
測試新的 add
方法:
const t = new Tree(); t.add(2); t.add(5); t.add(3);
現在樹看起來是這樣:
為了更好的理解,讓我們實現一個檢查樹中是否包含值的方法。
contains(value) { // 獲取根節點 let current = this.root; // 當存在節點時 while (current) { // 檢查當前節點是否為該值 if (value === current.value) { return true; // 退出函數 } // 通過將我們的值與 current.value 進行比較來決定下一個當前節點 // 如果小則往左,否則往右 current = value < current.value ? current.left : current.right; } return false; }
Add
和 Contains
是二進位制搜尋樹的兩個核心方法。對這兩種方法的瞭解可以使你更好地解決日常工作中的問題。
我已經在本文中介紹了很多內容,並且掌握這些知識後在面試中將使你處於有利位置。希望你能夠學到一些東西,並能夠輕鬆地通過技術面試(尤其是討厭的白板面試)。
更多程式設計相關知識,請存取:!!
以上就是JavaScript實現常用資料結構(棧、佇列、連結串列、雜湊表、樹)的詳細內容,更多請關注TW511.COM其它相關文章!