用 JavaScript 實現常用資料結構
我們要談論的是什麼?
在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方法相同。
你可以認為它並不像最初看起來那樣複雜。這是一種到處使用的資料結構,也是是一個很好理解的結構!
資源搜尋網站大全 https://www.renrenfan.com.cn 廣州VI設計公司https://www.houdianzi.com
二叉搜尋樹
最後一個數據結構是臭名昭著的二叉搜尋樹。
在二叉搜尋樹中,每個節點具有零個、一個或兩個子節點。左邊的稱為左子節點,右邊的稱為右子節點。在二叉搜尋樹中,左側的子項必須小於右側的子項。
你可以像這樣描繪一個二叉搜尋樹:
樹的核心類如下:
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是二進位制搜尋樹的兩個核心方法。對這兩種方法的瞭解可以使你更好地解決日常工作中的問題。