13161216443

您所在位置: 首頁> 學習課程> java培訓 | 常用數據結構的 JavaScript 實現代碼

java培訓 | 常用數據結構的 JavaScript 實現代碼

發布百知教育 來源:學習課程 2019-11-22

我們要談論的是什么?

在  JavaScript 中數據結構通??偸潜缓雎?,或者接觸得不多。但是對于許多大廠而言,一般都需要你深刻了解如何管理數據。掌握數據結構也能夠在解決問題時為你的工作提供幫助。

在本文中,我們將要討論并實現的數據結構是:

  • 隊列

  • 鏈表

  • 哈希表

第一個數據結構是棧。它與隊列非常相似,你之前可能聽說過調用棧,這是 JavaScript 用于處理事件的方法。

??雌饋硐襁@樣:


java培訓班





最后一個存入棧里的項目將是第一個被移除的項目。這被稱為后進先出(LIFO)。Web 瀏覽器中的后退按鈕就是一個很好的例子:將你查看的每個頁面添加到棧中,當你單擊“返回”時,將從棧中彈出當前頁面(最后添加的頁面)。

理論足夠多了。接下來看一些代碼:

 1class Stack {
2  constructor() {
3    // 創建棧結構,這是一個空對象
4    this.stack = {}
5  }
6  // 把一個值壓入棧的頂部
7  push(value) {
8
9  }
10  // 彈出棧頂的值并返回
11  pop() {
12
13  }
14
15  // 讀取棧中的最后一個值,但是不刪除
16  peek() {
17
18  }
19}


我已經對上面的代碼進行了注釋,現在咱們一起對其進行實現。第一種方法是 push。

先思考一下需要這個方法做的事情:

  • 我們需要接受一個值

  • 然后將該值添加到棧的頂部

  • 還應該跟蹤棧的長度,以便知道棧的索引

如果你能夠先自己嘗試一下,那就太好了,完整的 push 方法實現如下:

 1class Stack {
2  constructor() {
3    this._storage = {};
4    this._length = 0// 這是棧的大小
5  }
6
7  push(value) {
8    // 將值添加到棧頂
9    this._storage[this._length] = value;
10    // 因為增加了一個值,所以也應該將長度加1
11    this._length++;
12  }
13  /// .....
14}

我敢打賭,這比你想象的要容易。有許多類似這樣的結構,它們聽起來比實際上要復雜得多。

現在是 pop 方法。pop 方法的目標是刪除最后一個添加到棧中的值,然后返回它。如果可以的話,請先自己嘗試實現:

 1class Stack {
2  constructor() {
3    this._storage = {};
4    this._length = 0;
5  }
6
7  pop() {
8    // we first get the last val so we have it to return
9    const lastVal = this._storage[--this._length]
10    // now remove the item which is the length - 1 
11    delete this._storage[--this._length]
12    // decrement the length
13    this._length--;
14    // now return the last value
15    return lastVal
16  }
17}

酷!快要完成了。最后一個是 peek 函數,該函數查看棧中的最后一項。這是最簡單的功能:只需要返回最后一個值。實現是:

 1class Stack {
2  constructor() {
3    this._storage = {};
4    this._length = 0;
5  }
6  /*
7  * Adds a new value at the end of the stack
8  * @param {*} value the value to push
9  */

10
11  peek() {
12    const lastVal = this._storage[--this._length]
13    return lastVal
14  }
15}

所以它與 pop 方法非常相似,但不刪除最后一項。

Yes!第一個數據結構已經實現。接著是隊列,隊列與棧非常相似。

隊列

接下來討論隊列——希望棧在你的腦子仍然記得很清楚,因為它和隊列非常相似。棧和隊列之間的主要區別在于隊列是先進先出(FIFO)。

可以用圖形這樣表示:


java培訓班


隊列的可視化表示

所以兩個主要方法是 enqueue  dequeue。數據被添加到隊尾,并從隊首移除。為了更好的理解它,下面開始實現隊列。

核心代碼結構如下所示:

 1class Queue {
2  constructor() {
3    // 與前面類似,我們為數據結構提供了一個對象
4    // 并且還有一個變量來保存長度
5    this.queue = {}
6    this.length = 0
7    // 這是一個跟蹤頭部的新變量
8    this.head = 0
9  }
10
11  enqueue(value) {
12
13  }
14
15  dequeue() {
16
17  }
18
19  peek() {
20
21  }
22}

首先實現 enqueue 方法。其目的是向隊尾添加一個項目。

1enqueue(value) {
2  // 使用 value 參數將 length + head 的鍵添加到對象
3  this.queue[this.length + this.head] = value;
4  this.length++
5}

這是一個非常簡單的方法,可以在隊列的末尾添加一個值,但是你可能會對this.queue[this.length + this.head] = value;感到困惑。

假設隊列看起來像這樣的  {14 : 'randomVal'}。在添加這個內容時,我們希望的下一個值為15,所以應該是  length(1) + head(14),即為 15。

下一個要實現的是 dequeue 

1dequeue() {
2  // 獲取第一個值的引用,以便將其返回
3  const firstVal = this.queue[this.head]
4  // 現在將其從隊列中刪除
5  delete this.queue[this.head]
6  this.length--;
7  // 最終增加我們的頭成為下一個節點
8  this.head++;
9}

最后要實現的是 peek 方法,非常簡單:

1peek() {
2  // 只需要把值返回即可
3  return this.queue[this.head];
4}

隊列實現完畢。

鏈表

先讓我們討論一下強大的鏈表。這比上面的結構要復雜得多。

可能你第一個問題是為什么要使用鏈表?鏈表主要用于沒有動態大小調整數組的語言。鏈表按順序組織項目,一個項目指向下一個項目。

鏈表中的每個節點都有一個 data 值和一個 next 值。下圖中 5  data 值,next 值指向下一個節點,即值為 10 的節點。

在視覺上,它看起來像這樣:


java培訓班


鏈表的可視化表示

在一個對象中,上面的 LinkedList 看起來像下面的樣子


java培訓班




對象中的鏈表

你會看到最后一個值 1  next 值為 null,因為這是 LinkedList 的結尾。

那么該如何實現呢?

讓我們創建一個具有值 1、 2 和  37 LinkedList。

 1const myLinkedList = {
2    head: {
3        value1
4        next: {
5            value2           
6            next: {
7                value37
8                next: null
9            }
10        }
11    }
12};

現在我們知道了該怎樣手動創建 LinkedList,但是還需要編碼實現 LinkedList 的方法。

首先要注意的是,LinkedList 只是一堆嵌套對象!

當構造一個 LinkedList 時,我們需要一個 head 和一個 tail,它們最初都會指向頭部(因為 head 是第一個也是最后一個)。

1class LinkedList {
2  constructor(value) {
3    this.head = {value, nextnull}
4    this.tail = this.head
5  }
6}

第一個要實現的方法是 insert ,該方法用來在鏈表的末尾插入一個值。

1// insert 將添加到鏈接列表的末尾
2insert(value) {
3  /* 創建一個節點 */
4  const node = {value, nextnull}
5  /* 把 tail 的 next 屬性設置為新節點的引用 */
6  this.tail.next = node;
7  /* 新節點現在是尾節點 */
8  this.tail = node;
9}

上面最混亂的一行可能是 this.tail.next = node。之所以這樣做,是因為當添加一個新節點時,我們還希望當前的 tail 指向新的 node,該節點將成為新的 tail。第一次插入 node時,頭部的 next 指針將指向新節點,就像在構造函數中那樣,在其中設置了 this.tail = this.head。

你還可以到這個網站來查看圖形化的演示,這將幫你了解插入的過程(按 esc 擺脫煩人的彈出窗口)。

下一個方法是刪除節點。我們首先要決定參數是值( value) 還是對節點(node)的引用(在面試中,最好先問問面試官)。我們的代碼中傳遞了一個“值”。按值從列表中刪除節點是一個緩慢的過程,因為必須要遍歷整個列表才能找到值。

我這樣做是這樣的:

 1removeNode(val) {
2  /* 從 head 開始 */
3  let currentNode = this.head
4  /* 我們需要保留對上一個節點的引用 */
5  let previousNode
6  /* 當存在一個節點時,意味著沒有到達尾部 */
7  while(currentNode) {
8     /* 如果發現自己想要的那個值,那么就退出循環 */
9     if(currentNode.value === val) {
10        break;
11     }
12    /* 沒有找到值就將 currentNode 設置為 previousNode */
13    previousNode = currentNode
14    /* 得到下一個節點并將其分配給currentNode  */
15    currentNode = currentNode.next
16  }
17  /* 返回undefined,因為沒有找到具有該值的節點  */
18  if (currentNode=== null) {
19    return false;
20  }
21
22  // 如果節點是 head ,那么將 head 設置為下一個值
23  頭節點的
24  if (currentNode === this.head) {
25    this.head = this.head.next;
26    return;
27  }
28  /* 通過將節點設置為前面的節點來刪除節點 */  
29  previousNode.next = currentNode.next
30}

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):

 1removeTail() {
2  let currentNode = this.head;
3  let previousNode;
4  while (currentNode) {
5    /* 尾部是唯一沒有下一個值的節點,所以如果不存在下一個值,那么該節點就是尾部 */
6    if (!currentNode.next) {
7      break;
8    }
9    // 獲取先前節點的引用
10    previousNode = currentNode;
11    // 移至下一個節點
12    currentNode = currentNode.next;
13  }
14  // 要刪除尾部,將 previousNode.next 設置為 null
15  previousNode.next = null;
16}

這些就是 LinkedList 的一些主要方法。鏈表還有各種方法,但是利用以上學到的知識,你應該能夠自己實現它們。

哈希表

接下來是強大的哈希表。

哈希表是一種實現關聯數組的數據結構,這意味著它把鍵映射到值。JavaScript 對象就是一個“哈希表”,因為它存儲鍵值對。

在視覺上,可以這樣表示:


java培訓班


哈希表的可視化表示

在討論如何實現哈希表之前,需要討論討論哈希函數的重要性。哈希函數的核心概念是它接受任意大小的輸入并返回固定長度的哈希值。

1hashThis('i want to hash this') => 7

哈希函數可能非常復雜或直接。GitHub 上的每個文件都經過了哈希處理,這使得每個文件的查找都非???。哈希函數背后的核心思想是,給定相同的輸入將返回相同的輸出。

在介紹了哈希功能之后,該討論一下如何實現哈希表了。

將要討論的三個操作是 insert、get最后是 remove。

實現哈希表的核心代碼如下:

 1class HashTable {
2  constructor(size) {
3    // 定義哈希表的大小,將在哈希函數中使用
4    this.size = size;
5    this.storage = [];
6  }
7  insert(key, value) { }
8  get() {}
9  remove() {}
10  // 這是計算散列密鑰的方式
11  myHashingFunction(str, n) {
12    let sum = 0;
13    for (let i = 0; i < str.length; i++) {
14      sum += str.charCodeAt(i) * 3;
15    }
16    return sum % n;
17  }
18}

現在解決第一個方法,即 insert。insert 到哈希表中的代碼如下(為簡單起見,此方法將簡單的處理沖突問題):

 1insert(key, value) {
2  // 得到數組中的索引
3  const index = this.myHashingFunction(key, this.size);
4  // 處理沖突 - 如果哈希函數為不同的鍵返回相同的索引,
5  // 在復雜的哈希函數中,很可能發生沖突
6  if (!this.storage[index]) {
7    this.storage[index] = [];
8  }
9  // push 新的鍵值對
10  this.storage[index].push([key, value]);
11}

像這樣調用 insert 方法:

1const myHT = new HashTable(5);
2myHT.insert("a"1);
3myHT.insert("b"2);

你認為我們的哈希表會是什么樣的?

哈希表示例代碼

你可以看到鍵值對已插入到表中的索引 1  4 處。

現在實現從哈希表中刪除

 1remove(key) {
2    // 首先要獲取 key 的索引,請記住,
3    // 哈希函數將始終為同一 key 返回相同的索引
4    const index = this.myHashingFunction(key, this.size);
5    // 記住我們在一個索引處可以有多個數組(不太可能)
6    let arrayAtIndex = this.storage[index];
7    if (arrayAtIndex) {
8      // 遍歷該索引處的所有數組
9      for (let i = 0; i < arrayAtIndex.length; i++) {
10        // get the pair (a, 1)
11        let pair = arrayAtIndex[i];
12        // 檢查 key 是否與參數 key 匹配
13        if (pair[0] === key) {
14          delete arrayAtIndex[i];
15          // 工作已經完成,所以要退出循環
16          break;
17        }
18      }
19    }
20}

最后是  get 方法。這和 remove 方法一樣,但是這次,我們返回 pair 而不是刪除它。

 1 get(key) {
2    const index = this.myHashingFunction(key, this.size);
3    let arrayAtIndex = this.storage[index];
4    if (arrayAtIndex) {
5      for (let i = 0; i < arrayAtIndex.length; i++) {
6        const pair = arrayAtIndex[i];
7        if (pair[0] === key) {
8          return pair[1];
9        }
10      }
11    }
12  }

我認為不需要執行這個操作,因為它的作用與 remove 方法相同。

你可以認為它并不像最初看起來那樣復雜。這是一種到處使用的數據結構,也是是一個很好理解的結構!

二叉搜索樹

最后一個數據結構是臭名昭著的二叉搜索樹。

在二叉搜索樹中,每個節點具有零個、一個或兩個子節點。左邊的稱為左子節點,右邊的稱為右子節點。在二叉搜索樹中,左側的子項必須小于右側的子項。

你可以像這樣描繪一個二叉搜索樹:


java培訓班


二進制搜索樹的可視化表示

樹的核心類如下:

 1class Tree {
2   constructor(value) {
3     this.root = null
4   }
5
6   add(value) {
7    // 我們將在下面實現
8   }
9
10}

我們還將創建一個 Node 類來代表每個節點。

1class Node {
2  constructor(value, left = null, right = null) {
3    this.value = value;
4    this.left = left;
5    this.right = right;
6  }
7}

下面實現 add 方法。我已經對代碼進行了注釋,但是如果你發現使你感到困惑,請記住,我們要做的只是從根開始并檢查每個節點的 left  right。

 1add(value) {
2    // 如果沒有根,那么就創建一個
3    if (this.root === null) {
4      this.root = new Node(value);
5      return;
6    }
7    let current = this.root;
8    // keep looping
9    while (true) {
10      // 如果當前值大于傳入的值,則向左
11      if (current.value > value) {
12        // 如果存在左子節點,則再次進行循環
13        if (current.left) {
14          current = current.left;
15        } else {
16          current.left = new Node(value);
17          return;
18        }
19      }
20      // 值較小,所以我們走對了
21      else {
22        // 向右
23        // 如果存在左子節點,則再次運行循環
24        if (current.right) {
25          current = current.right;
26        } else {
27          current.right = new Node(value);
28          return;
29        }
30      }
31    }
32}

測試新的 add 方法:

1const t = new Tree();
2t.add(2);
3t.add(5);
4t.add(3);

現在樹看起來是這樣:


java培訓班



二叉搜索樹示例

為了更好的理解,讓我們實現一個檢查樹中是否包含值的方法。

 1contains(value) {
2  // 獲取根節點
3  let current = this.root;
4  // 當存在節點時
5  while (current) {
6    // 檢查當前節點是否為該值
7    if (value === current.value) {
8      return true// 退出函數
9    }
10    // 通過將我們的值與 current.value 進行比較來決定下一個當前節點
11    // 如果小則往左,否則往右
12    current = value < current.value ? current.left : current.right;
13  }
14  return false;
15}

Add  Contains 是二進制搜索樹的兩個核心方法。對這兩種方法的了解可以使你更好地解決日常工作中的問題。

總結

我已經在本文中介紹了很多內容,并且掌握這些知識后在面試中將使你處于有利位置。希望你能夠學到一些東西,并能夠輕松地通過技術面試(尤其是討厭的白板面試)。


java培訓班:http://www.akpsimsu.com/java2019


注釋:本文內容來自公眾號前端先鋒



上一篇:大數據培訓:專員=磚員?如何找到自己的第一個數據分析項目

下一篇:應屆生去公司找個Java程序員的職位需要什么技能?

相關推薦

www.akpsimsu.com

有位老師想和您聊一聊

關閉

立即申請