[譯文] 初學者應該瞭解的數據結構:Array、HashMap 與 List

原文連接:Data Structures for Beginners: Arrays, HashMaps, and Listsjavascript

衆成翻譯地址:初學者應該瞭解的數據結構:Array、HashMap 與 Listjava

校對:Volongnode

挺長的一篇文章,建議不太熟悉數據結構的同窗慢慢閱讀一下這篇文章,但願對你有所幫助~如下是譯文正文:git

Data Structures for Beginners: Arrays, HashMaps, and Lists

當開發程序時,咱們(一般)須要在內存中存儲數據。根據操做數據方式的不一樣,可能會選擇不一樣的數據結構。有不少經常使用的數據結構,如:Array、Map、Set、List、Tree、Graph 等等。(然而)爲程序選取合適的數據結構可能並不容易。所以,但願這篇文章能幫助你瞭解(不一樣數據結構的)表現,以求在工做中合理地使用它們。程序員

本文主要聚焦於線性的數據結構,如:Array、Set、List、Sets、Stacks、Queues 等等。github


本篇是如下教程的一部分(譯者注:若是你們以爲還不錯,我會翻譯整個系列的文章):算法

初學者應該瞭解的數據結構與算法(DSA)編程

  1. 算法的時間複雜性與大 O 符號
  2. 每一個程序員應該知道的八種時間複雜度
  3. 初學者應該瞭解的數據結構:Array、HashMap 與 List 👈 即本文
  4. 初學者應該瞭解的數據結構: Graph (譯文)
  5. 初學者應該瞭解的數據結構:Tree (敬請期待)
  6. 附錄 I:遞歸算法分析

(操做)數據結構的時間複雜度

下表是本文所討論內容的歸納。數組

加個書籤、收藏或分享本文,以便不時之需。bash

* = 運行時分攤

數據結構 插入 訪問 查找 刪除 備註
Array O(n) O(1) O(n) O(n) 插入最後位置複雜度爲 O(1)
(Hash)Map O(1)* O(1)* O(1)* O(1)* 從新計算哈希會影響插入時間。
Map O(log(n)) - O(log(n)) O(log(n)) 經過二叉搜索樹實現
Set(使用 HashMap) O(1)* - O(1)* O(1)* 由 HashMap 實現
Set (使用 List) O(n) - O(n)] O(n) 經過 List 實現
Set (使用二叉搜索樹) O(log(n)) - O(log(n)) O(log(n)) 經過二叉搜索樹實現
Linked List (單向) O(n) - O(n) O(n) 在起始位置添加或刪除元素,複雜度爲 O(1)
Linked List (雙向) O(n) - O(n) O(n) 在起始或結尾添加或刪除元素,複雜度爲 O(1)。然而在中間位置是 O(n)
Stack (由 Array 實現) O(1) - - O(1)] 插入與刪除都遵循與後進先出(LIFO)
Queue (簡單地由 Array 實現) O(n) - - O(1) 插入(Array.shift)操做的複雜度是 O(n)
Queue (由 Array 實現,但進行了改進) O(1)* - - O(1) 插入操做的最差狀況複雜度是 O(n)。然而分攤後是 O(1)
Queue (由 List 實現) O(1) - - O(1) 使用雙向鏈表

注意: 二叉搜索樹 與其餘樹結構、圖結構,將在另外一篇文章中討論。

原始數據類型

原始數據類型是構成數據結構最基礎的元素。下面列舉出一些原始原始數據類型:

  • 整數,如:1, 2, 3, …
  • 字符,如:a, b, "1", "*"
  • 布爾值, true 與 false.
  • 浮點數 ,如:3.14159, 1483e-2.

Array

數組可由零個或多個元素組成。因爲數組易於使用且檢索性能優越,它是最經常使用的數據結構之一。

你能夠將數組想象成一個抽屜,能夠將數據存到匣子中。

數組就像將東西存到匣子中的抽屜

Array is like a drawer that stores things on bins

當你想查找某個元素時,你能夠直接打開對應編號的匣子(時間複雜度爲 O(1))。然而,若是你忘記了匣子裏存着什麼,就必須逐個打開全部的匣子(時間複雜度爲 O(n)),直到找到所需的東西。數組也是如此。

根據編程語言的不一樣,數組存在一些差別。對於 JavaScript 和 Ruby 等動態語言而言,數組能夠包含不一樣的數據類型:數字,字符串,對象甚至函數。而在 Java 、 C 、C ++ 之類的強類型語言中,你必須在使用數組以前,定好它的長度與數據類型。JavaScript 會在須要時自動增長數組的長度。

Array 的內置方法

根據編程序言的不一樣,數組(方法)的實現稍有不一樣。

好比在 JavaScript 中,咱們可使用 unshiftpush 添加元素到數組的頭或尾,同時也可使用 shiftpop 刪除數組的首個或最後一個元素。讓咱們來定義一些本文用到的數組經常使用方法。

經常使用的 JS 數組內置函數

函數 複雜度 描述
array.push(element1[, …[, elementN]]) O(1) 將一個或多個元素添加到數組的末尾
array.pop() O(1) 移除數組末尾的元素
array.shift() O(n) 移除數組開頭的元素
array.unshift(element1[, …[, elementN]]) O(n) 將一個或多個元素添加到數組的開頭
array.slice([beginning[, end]]) O(n) 返回淺拷貝原數組從 beginningend(不包括 end)部分組成的新數組
array.splice(start[, deleteCount[, item1[,…]]]) O(n) 改變 (插入或刪除) 數組

向數組插入元素

將元素插入到數組有不少方式。你能夠將新數據添加到數組末尾,也能夠添加到數組開頭。

先看看如何添加到末尾:

function insertToTail(array, element) {
  array.push(element);
  return array;
}

const array = [1, 2, 3];
console.log(insertToTail(array, 4)); // => [ 1, 2, 3, 4 ]
複製代碼

根據規範push 操做只是將一個新元素添加到數組的末尾。所以,

Array.push 的時間複雜度度是 O(1)

如今看看如添加到開頭:

function insertToHead(array, element) {
  array.unshift(element);
  return array;
}

const array = [1, 2, 3];
console.log(insertToHead(array, 0));// => [ 0, 1, 2, 3, ]
複製代碼

你以爲 insertToHead 函數,時間複雜度是什麼呢?看起來和上面(push)差很少,除了調用的方法是 unshift 而不是 push。但這有個問題,unshift 是經過將數組的每一項移到下一項,騰出首項的空間來容納新添加的元素。因此它是遍歷了一次數組的。

Array.unshift 的時間複雜度度是 O(n)

訪問數組中的元素

若是你知道待查找元素在數組中的索引,那你能夠經過如下方法直接訪問該元素:

function access(array, index) {
  return array[index];
}

const array = [1, 'word', 3.14, { a: 1 }];
access(array, 0);// => 1
access(array, 3);// => {a: 1}
複製代碼

正如上面你所看到的的代碼同樣,訪問數組中的元素時間是恆定的:

訪問數組中元素的時間複雜度是 O(1)

注意:經過索引修改數組的值所花費的時間也是恆定的。

在數組中查找元素

若是你想查找某個元素但不知道對應的索引時,那隻能經過遍歷數組的每一個元素,直到找到爲止。

function search(array, element) {
  for (let index = 0;
       index < array.length;
       index++) {
    if (element === array[index]) {
      return index;
    }
  }
}

const array = [1, 'word', 3.14, { a: 1 }];
console.log(search(array, 'word'));// => 1
console.log(search(array, 3.14));// => 2
複製代碼

鑑於使用了 for 循環,那麼:

在數組中查找元素的時間複雜度是 O(n)

在數組中刪除元素

你以爲從數組中刪除元素的時間複雜度是什麼呢?

先一塊兒思考下這兩種狀況:

  1. 從數組的末尾刪除元素所需時間是恆定的,也就是 O(1)
  2. 然而,不管是從數組的開頭或是中間位置刪除元素,你都須要調整(刪除元素後面的)元素位置。所以複雜度爲 O(n)

Talk is cheap, let’s do the code!

function remove(array, element) {
  const index = search(array, element);
  array.splice(index, 1);
  return array;
}

const array1 = [0, 1, 2, 3];
console.log(remove(array1, 1));// => [ 0, 2, 3 ]
複製代碼

咱們使用了上面定義的 search 函數來查找元素的的索引,複雜度爲 O(n)。而後使用JS 內置的 splice 方法,它的複雜度也是 O(n)。那(刪除函數)總的時間複雜度不是 O(2n) 嗎?記住,(對於時間複雜度而言,)咱們並不關心常量。

對於上面列舉的兩種狀況,考慮最壞的狀況:

在數組中刪除某項元素的時間複雜度是 O(n)

數組方法的時間複雜度

在下表中,小結了數組(方法)的時間複雜度:

數組方法的時間複雜度

操做方法 最壞狀況
訪問 (Array.[]) O(1)
添加新元素至開頭 (Array.unshift) O(n)
添加新元素至末尾 (Array.push) O(1)
查找 (經過值而非索引) O(n)
刪除 (Array.splice) O(n)

HashMaps

HashMap有不少名字,如 HashTableHashMap、Map、Dictionary、Associative Array 等。概念上它們都是一致的,實現上稍有不一樣。

哈希表是一種將鍵 映射到 值的數據結構。

回想一下關於抽屜的比喻,如今匣子有了標籤,再也不是按數字順序了。

HashMap 也和抽屜同樣存儲東西,經過不一樣標識來區分不一樣匣子。

HashMap is like a drawer that stores things on bins and label them

此例中,若是你要找一個玩具,你不須要依次打開第一個、第二個和第三個匣子來查看玩具是否在內。直接代開被標識爲「玩具」的匣子便可。這是一個巨大的進步,查找元素的時間複雜度從 O(n) 降爲 O(1) 了。

數字是數組的索引,而標識則做爲 HashMap 存儲數據的鍵。HashMap 內部經過 哈希函數 將鍵(也就是標識)轉化爲索引。

至少有兩種方式能夠實現 hashmap:

  1. 數組:經過哈希函數將鍵映射爲數組的索引。(查找)最差狀況: O(n),平均: O(1)。
  2. 二叉搜索樹: 使用自平衡二叉搜索樹查找值(另外的文章會詳細介紹)。 (查找)最差狀況: O(log n),平均:O(log n)

咱們會介紹樹與二叉搜索樹,如今先不用擔憂太多。實現 Map 最經常使用的方式是使用 數組與哈希轉換函數。讓咱們(經過數組)來實現它吧

經過數組實現 HashMap

HashMap: hash function translates keys into bucket (array) indexes

正如上圖所示,每一個鍵都被轉換爲一個 hash code。因爲數組的大小是有限的(如此例中是10),(如發生衝突,)咱們必須使用求模函數找到對應的桶(譯者注:桶指的是數組的項),再循環遍歷該桶(來尋找待查詢的值)。每一個桶內,咱們存儲的是一組組的鍵值對,若是桶內存儲了多個鍵值對,將採用集合來存儲它們。

咱們將講述 HashMap 的組成,讓咱們先從哈希函數開始吧。

哈希函數

實現 HashMap 的第一步是寫出一個哈希函數。這個函數會將鍵映射爲對應(索引的)值。

完美的哈希函數 是爲每個不一樣的鍵映射爲不一樣的索引。

藉助理想的哈希函數,能夠實現訪問與查找在恆定時間內完成。然而,完美的哈希函數在實踐中是難以實現的。你極可能會碰到兩個不一樣的鍵被映射爲同一索引的狀況,也就是 衝突

當使用相似數組之類的數據結構做爲 HashMap 的實現時,衝突是難以免的。所以,解決衝突的其中一種方式是在同一個桶中存儲多個值。當咱們試圖訪問某個鍵對應的值時,若是在對應的桶中發現多組鍵值對,則須要遍歷它們(以尋找該鍵對應的值),時間複雜度爲 O(n)。然而,在大多數(HashMap)的實現中, HashMap 會動態調整數組的長度以避免衝突發生過多。所以咱們能夠說分攤後的查找時間爲 O(1)。本文中咱們將經過一個例子,講述分攤的含義。

HashMap 的簡單實現

一個簡單(但糟糕)的哈希函數能夠是這樣的:

class NaiveHashMap {

  constructor(initialCapacity = 2) {
    this.buckets = new Array(initialCapacity);
  }

  set(key, value) {
    const index = this.getIndex(key);
    this.buckets[index] = value;
  }

  get(key) {
    const index = this.getIndex(key);
    return this.buckets[index];
  }

  hash(key) {
    return key.toString().length;
  }

  getIndex(key) {
    const indexHash = this.hash(key);
    const index = indexHash % this.buckets.length;
    return index;
  }
}
複製代碼

完整代碼

咱們直接使用 buckets 而不是抽屜與匣子,相信你能明白喻義的意思 :)

HashMap 的初始容量(譯者注:容量指的是用於存儲數據的數組長度,即桶的數量)是2(兩個桶)。當咱們往裏面存儲多個元素時,經過求模 % 計算出該鍵應存入桶的編號(,並將數據存入該桶中)。

留意代碼的第18行(即 return key.toString().length;)。以後咱們會對此進行一點討論。如今先讓咱們使用一下這個新的 HashMap 吧。

// Usage:
const assert = require('assert');
const hashMap = new NaiveHashMap();
hashMap.set('cat', 2);
hashMap.set('rat', 7);
hashMap.set('dog', 1);
hashMap.set('art', 8);
console.log(hashMap.buckets);
/*
  bucket #0: <1 empty item>,
  bucket #1: 8
*/
assert.equal(hashMap.get('art'), 8); // this one is ok
assert.equal(hashMap.get('cat'), 8); // got overwritten by art 😱
assert.equal(hashMap.get('rat'), 8); // got overwritten by art 😱
assert.equal(hashMap.get('dog'), 8); // got overwritten by art 😱
複製代碼

這個 HashMap 容許咱們經過 set 方法設置一組鍵值對,經過往 get 方法傳入一個鍵來獲取對應的值。其中的關鍵是哈希函數,當咱們存入多組鍵值時,看看這 HashMap 的表現。

你能說出這個簡單實現的 HashMap 存在的問題嗎?

1) Hash function 轉換出太多相同的索引。如:

hash('cat') // 3
hash('dog') // 3
複製代碼

這會產生很是多的衝突。

2) 徹底不處理衝突的狀況。catdog 會重寫彼此在 HashMap 中的值(它們均在桶 #1 中)。

3 數組長度。 即便咱們有一個更好的哈希函數,因爲數組的長度是2,少於存入元素的數量,仍是會產生不少衝突。咱們但願 HashMap 的初始容量大於咱們存入數據的數量。

改進哈希函數

HashMap 的主要目標是將數組查找與訪問的時間複雜度,從 O(n) 降至 O(1)

爲此,咱們須要:

  1. 一個合適的哈希函數,儘量地減小衝突。
  2. 一個長度足夠大的數組用於保存數據。

讓咱們從新設計哈希函數,再也不採用字符串的長度爲 hash code,取而代之是使用字符串中每一個字符的ascii 碼的總和爲 hash code。

hash(key) {
  let hashValue = 0;
  const stringKey = key.toString();
  for (let index = 0; index < stringKey.length; index++) {
    const charCode = stringKey.charCodeAt(index);
    hashValue += charCode;
  }
  return hashValue;
}
複製代碼

再試一次:

hash('cat') // 312  (c=99 + a=97 + t=116)
hash('dog') // 314 (d=100 + o=111 + g=103)
複製代碼

這函數比以前的要好!這是由於相同長度的單詞由不同的字母組成,於是 ascii 碼的總和不同。

然而,仍然有問題!單詞 rat 與 art 轉換後都是327,產生衝突了! 💥

能夠經過偏移每一個字符的 ascii 碼再求和來解決:

hash(key) {
  let hashValue = 0;
  const stringKey = `${key}`;
  for (let index = 0; index < stringKey.length; index++) {
    const charCode = stringKey.charCodeAt(index);
    hashValue += charCode << (index * 8);
  }
  return hashValue;
}
複製代碼

如今繼續試驗,下面列舉出了十六進制的數字,這樣能夠方便咱們觀察位移。

// r = 114 or 0x72; a = 97 or 0x61; t = 116 or 0x74

hash('rat'); // 7,627,122 (r: 114 * 1 + a: 97 * 256 + t: 116 * 65,536) or in hex: 0x726174 (r: 0x72 + a: 0x6100 + t: 0x740000)

hash('art'); // 7,631,457 or 0x617274
複製代碼

然而,若是數據類型不一樣會有怎樣的表現呢?

hash(1); // 49
hash('1'); // 49
hash('1,2,3'); // 741485668
hash([1,2,3]); // 741485668
hash('undefined') // 3402815551
hash(undefined) // 3402815551
複製代碼

天啊,仍然有問題!!不一樣的數據類型不該該返回相同的 hash code!

該如何解決呢?

其中一種方式是在哈希函數中,將數據的類型做爲轉換 hash code 的一部分。

hash(key) {
  let hashValue = 0;
  const stringTypeKey = `${key}${typeof key}`;
  for (let index = 0; index < stringTypeKey.length; index++) {
    const charCode = stringTypeKey.charCodeAt(index);
    hashValue += charCode << (index * 8);
  }
  return hashValue;
}
複製代碼

讓咱們讓咱們再試一次:

console.log(hash(1)); // 1843909523
console.log(hash('1')); // 1927012762
console.log(hash('1,2,3')); // 2668498381
console.log(hash([1,2,3])); // 2533949129
console.log(hash('undefined')); // 5329828264
console.log(hash(undefined)); // 6940203017
複製代碼

Yay!!! 🎉 咱們終於有了更好的哈希函數!

同時,咱們能夠改變 HashMap 的原始容量以減小衝突,讓咱們在下一節中優化 HashMap。

更完善的 HashMap 實現

經過優化好的哈希函數,HashMap 能夠表現得更好。

儘管衝突仍可能發生,但經過一些方式能夠很好地處理它們。

對於咱們的 HashMap,但願有如下改進:

  • 哈希函數, 檢查類型與計算各字符(ascii 碼的總和)以減小衝突的發生。
  • 解決衝突,經過將值添加到集合中來解決這問題,同時須要一個計數器追蹤衝突的數量。

更完善 HashMap 實現完整代碼

class DecentHashMap {
  constructor(initialCapacity = 2) {
    this.buckets = new Array(initialCapacity);
    this.collisions = 0;
  }
  set(key, value) {
    const bucketIndex = this.getIndex(key);
    if(this.buckets[bucketIndex]) {
      this.buckets[bucketIndex].push({key, value});
      if(this.buckets[bucketIndex].length > 1) { this.collisions++; }
    } else {
      this.buckets[bucketIndex] = [{key, value}];
    }
    return this;
  }
  get(key) {
    const bucketIndex = this.getIndex(key);
    for (let arrayIndex = 0; arrayIndex < this.buckets[bucketIndex].length; arrayIndex++) {
      const entry = this.buckets[bucketIndex][arrayIndex];
      if(entry.key === key) {
        return entry.value
      }
    }
  }
  hash(key) {
    let hashValue = 0;
    const stringTypeKey = `${key}${typeof key}`;
    for (let index = 0; index < stringTypeKey.length; index++) {
      const charCode = stringTypeKey.charCodeAt(index);
      hashValue += charCode << (index * 8);
    }
    return hashValue;
  }
  getIndex(key) {
    const indexHash = this.hash(key);
    const index = indexHash % this.buckets.length;
    return index;
  }
}
複製代碼

看看這個 HashMap 表現如何:

// Usage:
const assert = require('assert');
const hashMap = new DecentHashMap();
hashMap.set('cat', 2);
hashMap.set('rat', 7);
hashMap.set('dog', 1);
hashMap.set('art', 8);
console.log('collisions: ', hashMap.collisions); // 2
console.log(hashMap.buckets);
/*
  bucket #0: [ { key: 'cat', value: 2 }, { key: 'art', value: 8 } ]
  bucket #1: [ { key: 'rat', value: 7 }, { key: 'dog', value: 1 } ]
*/
assert.equal(hashMap.get('art'), 8); // this one is ok
assert.equal(hashMap.get('cat'), 2); // Good. Didn't got overwritten by art assert.equal(hashMap.get('rat'), 7); // Good. Didn't got overwritten by art
assert.equal(hashMap.get('dog'), 1); // Good. Didn't got overwritten by art 複製代碼

完善後的 HashMap 很好地完成了工做,但仍然有一些問題。使用改良後的哈希函數不容易產生重複的值,這很是好。然而,在桶#0與桶#1中都有兩個值。這是爲何呢??

因爲 HashMap 的容量是2,儘管算出來的 hash code 是不同的,當求餘後算出所需放進桶的編號時,結果不是桶#0就是桶#1。

hash('cat') => 3789411390; bucketIndex => 3789411390 % 2 = 0
hash('art') => 3789415740; bucketIndex => 3789415740 % 2 = 0
hash('dog') => 3788563007; bucketIndex => 3788563007 % 2 = 1
hash('rat') => 3789411405; bucketIndex => 3789411405 % 2 = 1
複製代碼

很天然地想到,能夠經過增長 HashMap 的原始容量來解決這個問題,但原始容量應該是多少呢?先來看看容量是如何影響 HashMap 的表現的。

若是初始容量是1,那麼全部的鍵值對都會被存入同一個桶,即桶#0。查找操做並不比純粹用數組存儲數據的時間複雜度簡單,它們都是 O(n)

而假設將初始容量定爲10:

const hashMapSize10 = new DecentHashMap(10);
hashMapSize10.set('cat', 2);
hashMapSize10.set('rat', 7);
hashMapSize10.set('dog', 1);
hashMapSize10.set('art', 8);
console.log('collisions: ', hashMapSize10.collisions); // 1
console.log('hashMapSize10\n', hashMapSize10.buckets);
/*
  bucket#0: [ { key: 'cat', value: 2 }, { key: 'art', value: 8 } ],
            <4 empty items>,
  bucket#5: [ { key: 'rat', value: 7 } ],
            <1 empty item>,
  bucket#7: [ { key: 'dog', value: 1 } ],
            <2 empty items>
*/
複製代碼

換個角度看:

HashMap: hash function translates keys into bucket (array) indexes

正如你所看到的,經過增長 HashMap 的容量,能有效減小衝突次數。

那換個更大的試試怎樣,好比 💯:

const hashMapSize100 = new DecentHashMap(100);
hashMapSize100.set('cat', 2);
hashMapSize100.set('rat', 7);
hashMapSize100.set('dog', 1);
hashMapSize100.set('art', 8);
console.log('collisions: ', hashMapSize100.collisions); // 0
console.log('hashMapSize100\n', hashMapSize100.buckets);
/*
            <5 empty items>,
  bucket#5: [ { key: 'rat', value: 7 } ],
            <1 empty item>,
  bucket#7: [ { key: 'dog', value: 1 } ],
            <32 empty items>,
  bucket#41: [ { key: 'art', value: 8 } ],
            <49 empty items>,
  bucket#90: [ { key: 'cat', value: 2 } ],
            <9 empty items>
*/
複製代碼

Yay! 🎊 沒有衝突!

經過增長初始容量,能夠很好的減小衝突,但會消耗更多的內存,並且極可能許多桶都沒被使用。

若是咱們的 HashMap 能根據須要自動調整容量,這不是更好嗎?這就是所謂的rehash(從新計算哈希值),咱們將在下一節將實現它!

優化HashMap 的實現

若是 HashMap 的容量足夠大,那就不會產生任何衝突,所以查找操做的時間複雜度爲 O(1)。然而,咱們怎麼知道容量多大才是足夠呢,100?1000?仍是一百萬?

(從開始就)分配大量的內存(去創建數組)是不合理的。所以,咱們能作的是根據裝載因子動態地調整容量。這操做被稱爲 rehash

裝載因子是用於衡量一個 HashMap 滿的程度,能夠經過存儲鍵值對的數量除以 HashMap 的容量獲得它。

根據這思路,咱們將實現最終版的 HashMap:

最佳的 HasnMap 實現

class HashMap {
  constructor(initialCapacity = 16, loadFactor = 0.75) {
    this.buckets = new Array(initialCapacity);
    this.loadFactor = loadFactor;
    this.size = 0;
    this.collisions = 0;
    this.keys = [];
  }
  hash(key) {
    let hashValue = 0;
    const stringTypeKey = `${key}${typeof key}`;
    for (let index = 0; index < stringTypeKey.length; index++) {
      const charCode = stringTypeKey.charCodeAt(index);
      hashValue += charCode << (index * 8);
    }
    return hashValue;
  }
  _getBucketIndex(key) {
    const hashValue = this.hash(key);
    const bucketIndex = hashValue % this.buckets.length;
    return bucketIndex;
  }
  set(key, value) {
    const {bucketIndex, entryIndex} = this._getIndexes(key);
    if(entryIndex === undefined) {
      // initialize array and save key/value
      const keyIndex = this.keys.push({content: key}) - 1; // keep track of the key index
      this.buckets[bucketIndex] = this.buckets[bucketIndex] || [];
      this.buckets[bucketIndex].push({key, value, keyIndex});
      this.size++;
      // Optional: keep count of collisions
      if(this.buckets[bucketIndex].length > 1) { this.collisions++; }
    } else {
      // override existing value
      this.buckets[bucketIndex][entryIndex].value = value;
    }
    // check if a rehash is due
    if(this.loadFactor > 0 && this.getLoadFactor() > this.loadFactor) {
      this.rehash(this.buckets.length * 2);
    }
    return this;
  }
  get(key) {
    const {bucketIndex, entryIndex} = this._getIndexes(key);
    if(entryIndex === undefined) {
      return;
    }
    return this.buckets[bucketIndex][entryIndex].value;
  }
  has(key) {
    return !!this.get(key);
  }
  _getIndexes(key) {
    const bucketIndex = this._getBucketIndex(key);
    const values = this.buckets[bucketIndex] || [];
    for (let entryIndex = 0; entryIndex < values.length; entryIndex++) {
      const entry = values[entryIndex];
      if(entry.key === key) {
        return {bucketIndex, entryIndex};
      }
    }
    return {bucketIndex};
  }
  delete(key) {
    const {bucketIndex, entryIndex, keyIndex} = this._getIndexes(key);
    if(entryIndex === undefined) {
      return false;
    }
    this.buckets[bucketIndex].splice(entryIndex, 1);
    delete this.keys[keyIndex];
    this.size--;
    return true;
  }
  rehash(newCapacity) {
    const newMap = new HashMap(newCapacity);
    this.keys.forEach(key => {
      if(key) {
        newMap.set(key.content, this.get(key.content));
      }
    });
    // update bucket
    this.buckets = newMap.buckets;
    this.collisions = newMap.collisions;
    // Optional: both `keys` has the same content except that the new one doesn't have empty spaces from deletions this.keys = newMap.keys; } getLoadFactor() { return this.size / this.buckets.length; } } 複製代碼

完整代碼 (譯者注:其實 has 方法有問題,只是不影響閱讀。)

注意第99行至第114行(即 rehash 函數),那裏是 rehash 魔法發生的地方。咱們創造了一個新的 HashMap,它擁有原來 HashMap兩倍的容量。

測試一下上面的新實現吧:

const assert = require('assert');
const hashMap = new HashMap();
assert.equal(hashMap.getLoadFactor(), 0);
hashMap.set('songs', 2);
hashMap.set('pets', 7);
hashMap.set('tests', 1);
hashMap.set('art', 8);
assert.equal(hashMap.getLoadFactor(), 4/16);
hashMap.set('Pineapple', 'Pen Pineapple Apple Pen');
hashMap.set('Despacito', 'Luis Fonsi');
hashMap.set('Bailando', 'Enrique Iglesias');
hashMap.set('Dura', 'Daddy Yankee');
hashMap.set('Lean On', 'Major Lazer');
hashMap.set('Hello', 'Adele');
hashMap.set('All About That Bass', 'Meghan Trainor');
hashMap.set('This Is What You Came For', 'Calvin Harris ');
assert.equal(hashMap.collisions, 2);
assert.equal(hashMap.getLoadFactor(), 0.75);
assert.equal(hashMap.buckets.length, 16);
hashMap.set('Wake Me Up', 'Avicii'); // <--- Trigger REHASH
assert.equal(hashMap.collisions, 0);
assert.equal(hashMap.getLoadFactor(), 0.40625);
assert.equal(hashMap.buckets.length, 32);
複製代碼

注意,在往 HashMap 存儲第 12 個元素的時候,裝載因子將超過0.75,於是觸發 rehash,HashMap 容量加倍(從16到32)。同時,咱們也能看到衝突從2下降爲0。

這版本實現的 HashMap 能以很低的時間複雜度進行常見的操做,如:插入、查找、刪除、編輯等。

小結一下,HashMap 的性能取決於:

  1. 哈希函數能根據不一樣的鍵輸出不一樣的值。
  2. HashMap 容量的大小。

咱們終於處理好了各類問題 🔨。有了不錯的哈希函數,能夠根據不一樣輸入返回不一樣輸出。同時,咱們也有 rehash 函數根據須要動態地調整 HashMap的容量。這實在太好了!

HashMap 中插入元素的時間複雜度

往一個 HashMap 插入元素須要兩樣東西:一個鍵與一個值。可使用上文開發優化後的 HashMap 或內置的對象進行操做:

function insert(object, key, value) {
  object[key] = value;
  return object;
}
const hash = {};
console.log(insert(hash, 'word', 1)); // => { word: 1 }

複製代碼

在新版的 JavaScript 中,你可使用 Map。

function insertMap(map, key, value) {
  map.set(key, value);
  return map;
}
const map = new Map();
console.log(insertMap(map, 'word', 1)); // Map { 'word' => 1 }
複製代碼

注意。咱們將使用 Map 而不是普通的對象,這是因爲 Map 的鍵能夠是任何東西而對象的鍵只能是字符串或者數字。此外,Map 能夠保持插入的順序。

進一步說,Map.set 只是將元素插入到數組(如上文 DecentHashMap.set 所示),相似於 Array.push,所以能夠說:

往 HashMap 中插入元素,時間複雜度是 O(1)。若是須要 rehash,那麼複雜度則是 O(n)

rehash 能將衝突可能性降至最低。rehash 操做時間複雜度是 O(n) ,但不是每次插入操做都要執行,僅在須要時執行。

HashMap 中查找或訪問元素的時間複雜度

這是 HashMap.get 方法,咱們經過往裏面傳遞一個鍵來獲取對應的值。讓咱們回顧一下 DecentHashMap.get 的實現:

get(key){
  const hashIndex = this .getIndex(key);
  const values = this .array [hashIndex];
  forlet index = 0 ; index <values.length; index ++){
    const entry = values [index];
    if(entry.key === key){
      返回 entry.value
    }
  }
}
複製代碼

若是並未發生衝突,那麼 values 只會有一個值,訪問的時間複雜度是 O(1)。但咱們也知道,衝突老是會發生的。若是 HashMap 的初始容量過小或哈希函數設計糟糕,那麼大多數元素訪問的時間複雜度是 O(n)

HashMap 訪問操做的時間複雜度平均是 O(1),最壞狀況是 O(n)

**特別注意:**另外一個(將訪問操做的)時間複雜度從 O(n) 降至 O(log n) 的方法是使用 二叉搜索樹 而不是數組進行底層存儲。事實上,當存儲的元素超過8 個時, Java HashMap 的底層實現會從數組轉爲樹。

HashMap 中修改或刪除元素的時間複雜度

修改(HashMap.set)或刪除(HashMap.delete)鍵值對,分攤後的時間複雜度是 O(1)。若是衝突不少,可能面對的就是最壞狀況,複雜度爲 O(n)。然而伴隨着 rehash 操做,能夠大大減小最壞狀況的發生的概率。

HashMap 修改或刪除操做的時間複雜度平均是 O(1) ,最壞狀況是 O(n)

HashMap 方法的時間複雜度

在下表中,小結了 HashMap(方法)的時間複雜度:

HashMap 方法的時間複雜度

操做方法 最壞狀況 平均 備註
訪問或查找 (HashMap.get) O(n) O(1) O(n) 是衝突極多的極端狀況
插入或修改 (HashMap.set) O(n) O(1) O(n) 只發生在裝載因子超過0.75,觸發 rehash 時
刪除 (HashMap.delete) O(n) O(1) O(n) 是衝突極多的極端狀況

Sets

集合跟數組很是相像。它們的區別是集合中的元素是惟一的。

咱們該如何實現一個集合呢(也就是沒有重複項的數組)?可使用數組實現,在插入新元素前先檢查該元素是否存在。但檢查是否存在的時間複雜度是 O(n)。能對此進行優化嗎?以前開發的 Map (插入操做)分攤後時間複雜度度才 O(1)

Set 的實現

可使用 JavaScript 內置的 Set。然而經過本身實現它,能夠更直觀地瞭解它的時間複雜度。咱們將使用上文優化後帶有 rehash 功能的 HashMap 來實現它。

const HashMap = require('../hash-maps/hash-map');
class MySet {
  constructor() {
    this.hashMap = new HashMap();
  }
  add(value) {
    this.hashMap.set(value);
  }
  has(value) {
    return this.hashMap.has(value);
  }
  get size() {
    return this.hashMap.size;
  }
  delete(value) {
    return this.hashMap.delete(value);
  }
  entries() {
    return this.hashMap.keys.reduce((acc, key) => {
      if(key !== undefined) {
        acc.push(key.content);
      }
      return acc
    }, []);
  }
}
複製代碼

(譯者注:因爲 HashMap 的 has 方法有問題,致使 Set 的 has 方法也有問題)

咱們使用 HashMap.set 爲集合不重複地添加元素。咱們將待存儲的值做爲 HashMap的鍵,因爲哈希函數會將鍵映射爲惟一的索引,於是起到排重的效果。

檢查一個元素是否已存在於集合中,可使用 hashMap.has 方法,它的時間複雜度平均是 O(1)。集合中絕大多數的方法分攤後時間複雜度爲 O(1),除了 entries 方法,它的事件複雜度是 O(n)

注意:使用 JavaScript 內置的集合時,它的 Set.has 方法時間複雜度是 O(n)。這是因爲它的使用了 List 做爲內部實現,須要檢查每個元素。你能夠在查閱相關的細節。

下面有些例子,說明如何使用這個集合:

const assert = require('assert');
// const set = new Set(); // Using the built-in
const set = new MySet(); // Using our own implementation
set.add('one');
set.add('uno');
set.add('one'); // should NOT add this one twice
assert.equal(set.has('one'), true);
assert.equal(set.has('dos'), false);
assert.equal(set.size, 2);
// assert.deepEqual(Array.from(set), ['one', 'uno']);
assert.equal(set.delete('one'), true);
assert.equal(set.delete('one'), false);
assert.equal(set.has('one'), false);
assert.equal(set.size, 1);
複製代碼

這個例子中,MySet 與 JavaScript 中內置的 Set 都可做爲容器。

Set 方法的時間複雜度

根據 HashMap 實現的的 Set,能夠小結出的時間複雜度以下(與 HashMap 很是類似):

Set 方法的時間複雜度

操做方法 最壞狀況 平均 備註
訪問或查找 (Set.has) O(n) O(1) O(n) 是衝突極多的極端狀況
插入或修改 (Set.add) O(n) O(1) O(n) 只發生在裝載因子超過0.75,觸發 rehash 時
刪除 (Set.delete) O(n) O(1) _O(n)_是衝突極多的極端狀況)

Linked Lists

鏈表是一種一個節點連接到下一個節點的數據結構。

LinkedList

鏈表是(本文)第一種不用數組(做爲底層)實現的數據結構。咱們使用節點來實現,節點存儲了一個元素,並指向下一個節點(若沒有下一個節點,則爲空)。

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}
複製代碼

當每一個節點都指向它的下了一個節點時,咱們就擁有了一條由若干節點組成鏈條,即單向鏈表

Singly Linked Lists

對於單向鏈表而言,咱們只需關心每一個節點都有指向下一個節點的引用。

從首個節點或稱之爲根節點開始構建(單向鏈表)。

class LinkedList {
  constructor() {
    this.root = null;
  }
  // ...
}
複製代碼

每一個鏈表都有四個基礎操做:

  1. addLast:將一個元素添加至鏈表尾部。
  2. removeLast:刪除鏈表的最後一個元素。
  3. addFirst:將一個元素添加到鏈表的首部。
  4. removeFirst:刪除鏈表的首個元素。

向鏈表末尾添加與刪除一個元素

(對添加操做而言,)有兩種狀況。1)若是鏈表根節點不存在,那麼將新節點設置爲鏈表的根節點。2)若存在根節點,則必須不斷查詢下一個節點,直到鏈表的末尾,並將新節點添加到最後。

addLast(value) { // similar Array.push
  const node = new Node(value);
  if(this.root) {
    let currentNode = this.root;
    while(currentNode && currentNode.next) {
      currentNode = currentNode.next;
    }
    currentNode.next = node;
  } else {
    this.root = node;
  }
}
複製代碼

上述代碼的時間複雜度是多少呢?若是是做爲根節點添加進鏈表,時間複雜度是 O(1),然而尋找最後一個節點的時間複雜度是 O(n).。

刪除末尾的節點與上述代碼相差無幾。

removeLast() {
  let current = this.root;
  let target;
  if(current && current.next) {
    while(current && current.next && current.next.next) {
      current = current.next;
    }
    target = current.next;
    current.next = null;
  } else {
    this.root = null;
    target = current;
  }
  if(target) {
    return target.value;
  }
}
複製代碼

時間複雜度也是 O(n)。這是因爲咱們必須依次往下,直到找到倒數第二個節點,並將它 next 的引用指向 null

向鏈表開頭添加與刪除一個元素

往鏈表開頭添加一個元素(的代碼)以下所示:

addFirst(value) {
  const node = new Node(value);
  node.next = this.first;
  this.first = node;
}
複製代碼

向鏈表的開頭進行增刪操做,所耗費的時間是恆定的,由於咱們持有根元素的引用:

removeFirst(value) {
  const target = this.first;
  this.first = target ? target.next : null;
  return target.value;
}
複製代碼

(譯者注:做者原文 removeFirst 的代碼放錯了,上述代碼是譯者實現的)

如你所見,對鏈表的開頭進行增刪操做,時間複雜度永遠是 O(1)

從鏈表的任意位置刪除元素

刪除鏈表首尾的元素,可使用 removeFirstremoveLast。然而,如若移除的節點在鏈表的中間,則須要將待刪除節點的前一個節點指向待刪除節點的下一個節點,從而從鏈表中刪除該節點:

remove(index = 0) {
  if(index === 0) {
    return this.removeFirst();
  }
  let current;
  let target = this.first;
  for (let i = 0; target;  i++, current = target, target = target.next) {
    if(i === index) {
      if(!target.next) { // if it doesn't have next it means that it is the last return this.removeLast(); } current.next = target.next; this.size--; return target.value; } } } 複製代碼

(譯者注:原文實現有點問題,譯者稍做了點修改。removeLast 的調用其實浪費了性能,但可讀性上增長了,於是此處未做修改。)

注意, index 是從0開始的:0是第一個節點,1是第二個,如此類推。

在鏈表任意一處刪除節點,時間複雜度爲 O(n).

在鏈表中查找元素

在鏈表中查找一個元素與刪除元素的代碼差很少:

contains(value) {
  for (let current = this.first, index = 0; current;  index++, current = current.next) {
    if(current.value === value) {
      return index;
    }
  }
}
複製代碼

這個方法查找鏈表中第一個與給定值相等的節點(的索引)。

在鏈表中查找一個元素,時間複雜度是 O(n)

單向鏈表操做方法的時間複雜度

在下表中,小結了單向鏈表(方法)的時間複雜度:

操做方法 時間複雜度 註釋
addFirst O(1) 將元素插入到鏈表的開頭
addLast O(n) 將元素插入到鏈表的末尾
add O(n) 將元素插入到鏈表的任意地方
removeFirst O(1) 刪除鏈表的首個元素
removeLast O(n) 刪除鏈表最後一個元素
remove O(n) 刪除鏈表中任意一個元素
contains O(n) 在鏈表中查找任意元素

注意,當咱們增刪鏈表的最後一個元素時,該操做的時間複雜度是 O(n)

但只要持有最後一個節點的引用,能夠從原來的 O(n),降至與增刪首個元素一致,變爲 O(1)

咱們將在下一節實現這功能!

Doubly Linked Lists

當咱們有一串節點,每個都有指向下一個節點的引用,也就是擁有了一個單向鏈表。而當一串節點,每個既有指向下一個節點的引用,也有指向上一個節點的引用時,這串節點就是雙向鏈表

Doubly Linked List

雙向鏈表的節點有兩個引用(分別指向前一個和後一個節點),所以須要保持追蹤首個與最後一個節點。

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
    this.previous = null;
  }
}
class LinkedList {
  constructor() {
    this.first = null; // head/root element
    this.last = null; // last element of the list
    this.size = 0; // total number of elements in the list
  }
  // ...
}
複製代碼

(雙向鏈表的完整代碼

添加或刪除鏈表的首個元素

因爲持有首個節點的引用,於是添加或刪除首個元素的操做是十分簡單的:

addFirst(value) {
  const node = new Node(value);
  node.next = this.first;
  if(this.first) {
    this.first.previous = node;
  } else {
    this.last = node;
  }
  this.first = node; // update head
  this.size++;
  return node;
}
複製代碼

LinkedList.prototype.addFirst完整代碼

注意,咱們須要十分謹慎地更新節點的 previous 引用、雙向鏈表的 size 與雙向鏈表最後一個節點的引用。

removeFirst() {
  const first = this.first;
  if(first) {
    this.first = first.next;
    if(this.first) {
      this.first.previous = null;
    }
    this.size--;
    return first.value;
  } else {
    this.last = null;
  }
}
複製代碼

LinkedList.prototype.removeFirst完整代碼

時間複雜度是什麼呢?

不管是單向鏈表仍是雙向鏈表,添加與刪除首個節點的操做耗費時間都是恆定的,時間複雜度爲 O(1)

添加或刪除鏈表的最後一個元素

_從雙向鏈表的末尾_添加或刪除一個元素稍有點麻煩。當你查詢單向鏈表(操做的時間複雜度)時,這兩個操做都是 O(n),這是因爲須要遍歷整條鏈表,直至找到最後一個元素。然而,雙向鏈表持有最後一個節點的引用:

addLast(value) {
  const node = new Node(value);
  if(this.first) {
    node.previous = this.last;
    this.last.next = node;
    this.last = node;
  } else {
    this.first = node;
    this.last = node;
  }
  this.size++;
  return node;
}
複製代碼

LinkedList.prototype.addLast完整代碼

一樣,咱們須要當心地更新引用與處理一些特殊狀況,如鏈表中只有一個元素時。

removeLast() {
  let current = this.first;
  let target;
  if(current && current.next) {
    target = this.last;
    current = target.previous;
    this.last = current;
    current.next = null;
  } else {
    this.first = null;
    this.last = null;
    target = current;
  }
  if(target) {
    this.size--;
    return target.value;
  }
}
複製代碼

LinkedList.prototype.removeLast完整代碼

使用了雙向鏈表,咱們再也不須要遍歷整個鏈表直至找到倒數第二個元素。能夠直接使用 this.last.previous 來找到它,時間複雜度是 O(1)

下文將介紹隊列相關的知識,本文中隊列是使用兩個數組實現的。能夠改成使用雙向鏈表實現隊列,由於(雙向鏈表)添加首個元素與刪除最後一個元素時間複雜度都是 O(1)

添加一個元素至鏈表任意一處

藉助 addFirstaddLast,能夠實現將一個元素添加到鏈表任意一處,實現以下:

add(value, index = 0) {
  if(index === 0) {
    return this.addFirst(value);
  }
  for (let current = this.first, i = 0; i <= this.size;  i++, current = (current && current.next)) {
    if(i === index) {
      if(i === this.size) { // if it doesn't have next it means that it is the last return this.addLast(value); } const newNode = new Node(value); newNode.previous = current.previous; newNode.next = current; current.previous.next = newNode; if(current.next) { current.next.previous = newNode; } this.size++; return newNode; } } } 複製代碼

LinkedList.prototype.add完整代碼

若是添加元素的位置是在鏈表中間,咱們就必須更新該元素先後節點的 nextprevious 引用。

添加一個元素至鏈表任意一處的時間複雜度是 O(n).

雙向鏈表方法的時間複雜度

雙向鏈表每一個方法的時間複雜度以下表:

操做方法 時間複雜度 註釋
addFirst O(1) 將元素插入到鏈表的開頭
addLast O(1) 將元素插入到鏈表的末尾
add O(n) 將元素插入到鏈表的任意地方
removeFirst O(1) 刪除鏈表的首個元素
removeLast O(1) 刪除鏈表最後一個元素
remove O(n) 刪除鏈表中任意一個元素
contains O(n) 在鏈表中查找任意元素

與單向鏈表相比,有了很大的改進(譯者注:其實看場景,不要盲目認爲雙向鏈表必定比單向鏈表強)!(addLastremoveLast)操做時間複雜度從 O(n) 降至 O(1) ,這是因爲:

  • 添加對前一個節點的引用。
  • 持有鏈表最後一個節點的引用。

刪除首個或最後一個節點能夠在恆定時間內完成,然而刪除中間的節點時間複雜度仍然是 O(n)

Stacks

棧是一種越後被添加的元素,越先被彈出的數據結構。也就是後進先出(LIFO).

Stack: push and pop

讓咱們從零開始實現一個棧!

class Stack {
  constructor() {
    this.input = [];
  }
  push(element) {
    this.input.push(element);
    return this;
  }
  pop() {
    return this.input.pop();
  }
}
複製代碼

正如你看到的,若是使用內置的 Array.pushArray.pop 實現一個棧,那是十分簡單的。兩個方法的時間複雜度都是 O(1)

下面來看看棧的具體使用:

const stack = new Stack();
stack.push('a');
stack.push('b');
stack.push('c');
stack.pop(); // c
stack.pop(); // b
stack.pop(); // a
複製代碼

最早被加入進去的元素 a 直到最後才被彈出。棧也能夠經過鏈表來實現,對應方法的時間複雜度是同樣的。

這就是棧的所有內容啦!

Queues

隊列是一種越先被添加的元素,越先被出列的數據結構。也就是先進先出(FIFO)。就如現實中排成一條隊的人們同樣,先排隊的先被服務(也就是出列)。

Queue: enqueue and dequeue

能夠經過數組來實現一個隊列,代碼與棧的實現相相似。

經過數組實現隊列

經過 Array.pushArray.shift 能夠實現一個簡單(譯者注:即不是最優的實現方式)的隊列:

class Queue {
  constructor() {
    this.input = [];
  }
  add(element) {
    this.input.push(element);
  }
  remove() {
    return this.input.shift();
  }
}
複製代碼

Queue.addQueue.remove 的時間複雜度是什麼呢?

  • Queue.add 使用 Array.push 實現,能夠在恆定時間內完成。這很是不錯!
  • Queue.remove 使用 Array.shift 實現,Array.shift 耗時是線性的(即 O(n))。咱們能夠減小 Queue.remove 的耗時嗎?

試想一下,若是隻用 Array.pushArray.pop 能實現一個隊列嗎?

class Queue {
  constructor() {
    this.input = [];
    this.output = [];
  }
  add(element) {
    this.input.push(element);
  }
  remove() {
    if(!this.output.length) {
      while(this.input.length) {
        this.output.push(this.input.pop());
      }
    }
    return this.output.pop();
  }
}
複製代碼

如今,咱們使用兩個而不是一個數組來實現一個隊列。

const queue = new Queue();
queue.add('a');
queue.add('b');
queue.remove() // a
queue.add('c');
queue.remove() // b
queue.remove() // c
複製代碼

當咱們第一次執行出列操做時,output 數組是空的,所以將 input 數組的內容反向添加到 output 中,此時 output 的值是 ['b', 'a']。而後再從 output 中彈出元素。正如你所看到的,經過這個技巧實現的隊列,元素輸出的順序也是先進先出(FIFO)的。

那時間複雜度是什麼呢?

若是 output 數組已經有元素了,那麼出列操做就是恆定的 O(1)。而當 output 須要被填充(即裏面沒有元素)時,時間複雜度變爲 O(n)output 被填充後,出列操做耗時再次變爲恆定。所以分攤後是 O(1)

也能夠經過鏈表來實現隊列,相關操做耗時也是恆定的。下一節將帶來具體的實現。

經過雙向鏈表實現隊列

若是但願隊列有最好的性能,就須要經過雙向鏈表而不是數組來實現(譯者注:並不是數組實現就徹底很差,空間上雙向鏈表就不佔優點,仍是具體問題具體分析)。

const LinkedList = require('../linked-lists/linked-list');
class Queue {
  constructor() {
    this.input = new LinkedList();
  }
  add(element) {
    this.input.addFirst(element);
  }
  remove() {
    return this.input.removeLast();
  }
  get size() {
    return this.input.size;
  }
}
複製代碼

經過雙向鏈表實現的隊列,咱們持有(雙向鏈表中)首個與最後一個節點的引用,所以入列與出列的時間複雜度都是 O(1)。這就是爲遇到的問題選擇合適數據結構的重要性 💪。

總結

咱們討論了大部分線性的數據結構。能夠看出,根據實現方法的不一樣,相同的數據結構也會有不一樣的時間複雜度。

如下是本文討論內容的總結:

時間複雜度

* = 運行時分攤

數據結構 插入 訪問 查找 刪除 備註
Array O(n) O(1) O(n) O(n) 插入最後位置複雜度爲 O(1)
(Hash)Map O(1)* O(1)* O(1)* O(1)* 從新計算哈希會影響插入時間。
Map O(log(n)) - O(log(n)) O(log(n)) 經過二叉搜索樹實現
Set(使用 HashMap) O(1)* - O(1)* O(1)* 由 HashMap 實現
Set (使用 List) O(n) - O(n)] O(n) 經過 List 實現
Set (使用二叉搜索樹) O(log(n)) - O(log(n)) O(log(n)) 經過二叉搜索樹實現
Linked List (單向) O(n) - O(n) O(n) 在起始位置添加或刪除元素,複雜度爲 O(1)
Linked List (雙向) O(n) - O(n) O(n) 在起始或結尾添加或刪除元素,複雜度爲 O(1)。然而在其餘位置是 O(n)
Stack (由 Array 實現) O(1) - - O(1)] 插入與刪除都遵循與後進先出(LIFO)
Queue (簡單地由 Array 實現) O(n) - - O(1) 插入(Array.shift)操做的複雜度是 O(n)
Queue (由 Array 實現,但進行了改進) O(1)* - - O(1) 插入操做的最差狀況複雜度是 O(n)。然而分攤後是 O(1)
Queue (由 List 實現) O(1) - - O(1) 使用雙向鏈表

注意: 二叉搜索樹 與其餘樹結構、圖結構,將在另外一篇文章中討論。

相關文章
相關標籤/搜索