JS數據結構與算法_集合&字典

上一篇:JS數據結構與算法_鏈表javascript

寫在前面

說明:JS數據結構與算法 系列文章的代碼和示例都可在此找到

1、集合Set

1.1 集合數據結構

集合 set是一種包含不一樣元素的數據結構。集合中的元素成爲成員。集合的兩個最重要特性是: 集合中的成員是無序的;集合中不容許相同成員存在

計算機中的集合與數學中集合的概念相同,有一些概念咱們必須知曉:html

  • 不包含任何成員的集合稱爲空集;包含一切可能的成員爲全集
  • 若是兩個成員徹底相同,則稱爲兩個集合相等
  • 若是一個集合中全部的成員都屬於另外一個集合,則前一個集合被稱爲後一個集合的子集

另外還有交集/並集/差集,下面會一一實現java

1.2 集合的實現

通常集合包含下面幾個方法:git

  • add 向集合添加一個新的項
  • remove 從集合移除一個值
  • has 若是值在集合中,返回true,不然返回false
  • clear 移除集合中的全部項
  • size 返回集合所包含元素的數量。與數組的length屬性相似
  • values 返回一個包含集合中全部值的數組
  • union 兩個集合的並集
  • intersection 兩個集合的交集
  • difference 兩個集合的差集
  • isSubsetOf 判斷是否爲子集

下面將基於對象實現基礎的集合(數組和隊列也可實現集合,點擊查看es6

class Set {
  constructor() {
    this._items = {};
    this._length = 0;
  }
  // 添加成員時,若是已有成員則不操做。以[value: value]的形式存儲在對象中
  add(value) {
    if (this.has(value)) return false;
    this._items[value] = value;
    this._length += 1;
    return true;
  }
  // 移除成員時,若是沒有對應成員則不操做
  remove(value) {
    if (!this.has(value)) return false;
    delete this._items[value];
    this._length -= 1;
    return true;
  }

  values() {
    return Object.values(this._items);
  }

  has(value) {
    return this._items.hasOwnProperty(value);
  }

  clear() {
    this._items = {};
    this._length = 0;
  }

  size() {
    return this._length;
  }

  isEmpty() {
    return !this._length;
  }
}

clipboard.png

(1)並集的實現github

將兩個集合中的元素依次添加至新的集合中,並返回改集合算法

// 並集
union(otherSet) {
  const unionSet = new Set();

  const values = this.values();
  values.forEach(item => unionSet.add(item));

  const otherValues = otherSet.values();
  otherValues.forEach(item => unionSet.add(item));

  return unionSet;
}

(2)交集的實現數據庫

以集合A做爲參考,遍歷集合B依次對比成員,B中的成員存在A中則添加至新集合C中,最後返回Csegmentfault

// 交集
intersection(otherSet) {
  const intersectionSet = new Set();

  const values = this.values();
  values.forEach(item => {
    if (otherSet.has(item)) {
      intersectionSet.add(item);
    }
  })

  return intersectionSet;
}

(3)差集的實現數組

以集合A做爲參考,遍歷集合B依次對比成員,B中的成員不存在A中則添加至新集合C中,最後返回C

// 差集
difference(otherSet) {
  const differenceSet = new Set();

  const values = this.values();
  values.forEach(item => {
    if (!otherSet.has(item)) {
      differenceSet.add(item);
    }
  })

  return differenceSet;
}

注意:A.difference(B)B.difference(A)計算參考不一樣

(4)子集的實現

以集合A做爲參考,遍歷集合B依次對比成員,B中的全部成員均存在A中則爲其子集,不然不是

// 子集
isSubsetOf(otherSet) {
  if (this.size() > otherSet.size()) return false;

  const values = this.values();
  for (let i = 0; i < values.length; i += 1) {
    const item = values[i];
    if (!otherSet.has(item)) return false;
  }

  return true;
}

1.3 ES6中的Set

ES6中提供了新的 數據結構Set,它相似於數組,可是成員的值都是惟一的,沒有重複的值

提供了一下幾個方法:

  • add(value) 添加某個值,返回Set結構自己
  • delete(value) 刪除某個值,返回一個布爾值,表示刪除是否成功
  • has(value) 返回一個布爾值,表示該值是否爲Set的成員
  • clear() 清除全部成員,沒有返回值
  • size 屬性,返回成員總數

建立:

  • 直接經過數組建立:new Set([1,2,3,4])
  • 先實例再添加:const set = new Set(); set.add(1);

遍歷:

  • keys() 返回鍵名的遍歷器
  • values() 返回鍵值的遍歷器
  • entries() 返回鍵值對的遍歷器
  • forEach()/for-of 使用回調函數遍歷每一個成員

2、字典Dictionary

2.1 字典數據結構

集合表示一組互不相同的元素(不重複的元素)。在字典中,存儲的是 鍵-值對,其中鍵名是用來查詢特定元素的。字典和集合很類似,集合以 值-值對的形式存儲元素,字典則是以 鍵-值對的形式來存儲元素。字典也稱做 映射

類比:電話號碼簿裏的名字和電話號碼。要找一個電話時,先找名字,名字找到了,緊挨着他的電話號碼也就想找到了,這裏的鍵是指你用來查找的東西,值時查找獲得的結果

2.2 字典的實現

通常字典包括下面幾種方法:

  • set(key,value) 向字典中添加新元素
  • remove(key) 經過使用鍵值來從字典中移除鍵值對應的數據值
  • has(key) 若是某個鍵值存在於這個字典中,則返回true,反之則返回false
  • get(key) 經過鍵值查找特定的數值並返回
  • clear() 將這個字典中的全部元素所有刪除
  • size() 返回字典所包含元素的數量。與數組的length屬性相似
  • keys() 將字典所包含的全部鍵名以數組形式返回
  • values() 將字典所包含的全部數值以數組形式返回

下面將基於對象實現基礎的字典

class Dictionary {
  constructor() {
    this._table = {};
    this._length = 0;
  }

  set(key, value) {
    if (!this.has(key)) {
      this._length += 1;
    }
    this._table[key] = value;
  }

  has(key) {
    return this._table.hasOwnProperty(key);
  }

  remove(key) {
    if (this.has(key)) {
      delete this._table[key];
      this._length -= 1;
      return true;
    }
    return false;
  }

  get(key) {
    return this._table[key];
  }

  clear() {
    this._table = {};
    this._length = 0;
  }

  size() {
    return this._length;
  }

  keys() {
    return Object.keys(this._table);
  }

  values() {
    return Object.values(this._table);
  }
}

這裏添加成員時,並未考慮key爲對象的狀況,以致於會出現以下狀況:

const obj = {};
obj[{a: 1}] = 1;
obj[{a: 2}] = 2;

console.log(obj[{a: 1}]); // 2

// 對象形式的鍵會以其toSting方法的結果存儲
obj; // {[object Object]: 2}

在ES6中支持key值爲對象形式的字典數據結構Map,其提供的方法以下:

提供了一下幾個方法:

  • set(key, value) set方法設置鍵名key對應的鍵值爲value,而後返回整個Map結構
  • get(key) get方法讀取key對應的鍵值,若是找不到key,返回undefined
  • delete(value) 刪除某個值,返回一個布爾值,表示刪除是否成功
  • has(value) 返回一個布爾值,表示該值是否爲Map的成員
  • clear() 清除全部成員,沒有返回值
  • size 屬性,返回成員總數

建立:

  • 直接經過數組建立:const map = new Map([ ['name', '張三'], ['title', 'Author'] ]);
  • 先實例再添加:const map = new Map();

遍歷:

  • keys() 返回鍵名的遍歷器
  • values() 返回鍵值的遍歷器
  • entries() 返回鍵值對的遍歷器
  • forEach()/for-of 使用回調函數遍歷每一個成員

3、哈希表/散列表

3.1 哈希表數據結構

散列表也叫哈希表( HashTable也叫 HashMap),是 Dictionary類的一種散列表實現方式

(1)哈希表有何特殊之處:

數組的特色是尋址方便,插入和刪除困難;而鏈表的特色是尋址困難,插入和刪除方便。哈希表正是綜合了二者的優勢,實現了尋址方便,插入刪除元素也方便的數據結構

(2)哈希表實現原理

哈希表就是把Key經過一個固定的算法函數既所謂的哈希函數轉換成一個整型數字,而後將該數字對數組長度進行取餘,取餘結果就看成數組的下標,將value存儲在以該數字爲下標的數組空間裏。而當使用哈希表進行查詢的時候,就是再次使用哈希函數將key轉換爲對應的數組下標,並定位到該空間獲取value,如此一來,就能夠充分利用到數組的定位性能進行數據定位

下面是將key中每一個字母的ASCII值之和做爲數組的索引(哈希函數)的圖例:

clipboard.png

(3)數組的長度爲何選擇質數

書中有以下說明:

散列函數的選擇依賴於鍵值的數據類型。若是鍵是整數,最簡單的散列函數就是以數組的長度對鍵取餘。在一些狀況下,好比數組的長度爲10,而鍵值都是10的倍數時,就不推薦使用這種方式了。這也是數組的長度爲何要是質數的緣由之一。若是鍵是隨機的整數,而散列函數應該更均勻地分佈這些鍵,這種散列方式稱爲 除留餘數法

3.2 哈希表的實現

咱們爲哈希表實現下面幾個方法:

  • hashMethod 哈希函數,將字符串轉換成索引
  • put 添加鍵值
  • get 由鍵獲取值
  • remove 移除鍵
class HashTable {
  constructor() {
    this._table = [];
  }

  // 哈希函數【社區中實踐較好的簡單哈希函數】
  hashMethod(key) {
    if (typeof key === 'number') return key;

    let hash = 5381;
    for (let i = 0; i < key.length; i += 1) {
      hash = hash * 33 + key.charCodeAt(i);
    }
    return hash % 1013;
  }

  put(key, value) {
    const pos = this.hashMethod(key);
    this._table[pos] = value;
  }

  get(key) {
    const pos = this.hashMethod(key);
    return this._table[pos];
  }

  remove(key) {
    const pos = this.hashMethod(key);
    delete this._table[pos];
  }

  print() {
    this._table.forEach((item, index) => {
      if (item !== undefined) {
        console.log(index + ' --> ' + item);
      }
    })
  }
}

固然了,一個簡單的哈希函數,將不一樣的字符串轉換成整數時,頗有可能會出現多個不一樣字符串轉換後對應同一個整數,這個就須要進行衝突的處理

3.3 處理衝突的方法

(1)分離連接

分離連接法包括爲散列表的每個位置建立一個鏈表並將元素存儲在裏面。它是解決衝突的
最簡單的方法,可是它在HashTable實例以外還須要額外的存儲空間

clipboard.png

(2)線性探查

當想向表中某個位置加入一個新元素的時候,若是索引 爲index的位置已經被佔據了,就嘗試index+1的位置。若是index+1的位置也被佔據了,就嘗試 index+2的位置,以此類推

clipboard.png

4、bitMap算法

4.1 bitMap數據結構

bitMap數據結構經常使用於大量整型數據作去重和查詢《Bitmap算法》這篇文章中是基於Java語言及數據庫優化進行解釋的圖文教程

bitMap是利用了二進制來描述狀態的一種數據結構,下面介紹其簡單的原理:

(1)思考下面的問題

街邊有8棧路燈,編號分別是1 2 3 4 5 6 7 8 ,其中2號,5號,7號,8號路燈是亮着的,其他的都處於不亮的狀態,請你設計一種簡單的方法來表示這8棧路燈亮與不亮的狀態。

路燈  1   2   3   4   5   6   7   8
狀態  0   1   0   0   1   0   1   1

將狀態轉化爲二進制parseInt(1001011, 2);結果爲75。一個Number類型的值爲32個字節,它能夠表示32棧路燈的狀態。這樣在大數據量的處理中,bitMap就有很大的優點。

(2)位運算介紹

  1. 按位與&: 3&7=3011 & 111 --> 011
  2. 按位或|: 3|7=7011 | 111 --> 111
  3. 左位移<<: 1<<3=81 --> 1000

(3)實踐

一組數,內容覺得 3,6,7,9,請用一個整數來表示這些四個數

var value = 0;
value = value | 1<<3; // 1000
value = value | 1<<6; // 1001000
value = value | 1<<7; // 11001000
value = value | 1<<9; // 1011001000
console.log(value); // 712

這樣,十進制數712的二進制形式對應的位數爲1的值便爲數組中的樹值

4.2 bitMap的實現

經過上面的介紹,咱們能夠實現一個簡單的bitMap類,有下面兩個方法:

  • addMember添加成員
  • isExist成員是否存在

分析:

  1. 單個數值既能表示0~32的值,若以數組做爲基礎,bitMap能容納的成員由數組長度決定64*數組長度
  2. addMember添加成員:數組/位數向下取整表示所在索引,數組/位數取餘表示所在二進制的位數
  3. isExist成員是否存在:添加成員的反向計算

咱們先實現基礎讀寫位的方法

export const BIT_SIZE = 32;

// 設置位的值
export function setBit(bitMap, bit) {
  const arrIndex = Math.floor(bit / BIT_SIZE);
  const bitIndex = Math.floor(bit % BIT_SIZE);
  bitMap[arrIndex] |= (1 << bitIndex);
}

// 讀取位的值
export function getBit(bitMap, bit) {
  const arrIndex = Math.floor(bit / BIT_SIZE);
  const bitIndex = Math.floor(bit % BIT_SIZE);
  return bitMap[arrIndex] & (1 << bitIndex);
}

進而根據上面的方法獲得下面的類

class BitMap {
  constructor(size) {
    this._bitArr = Array.from({
      length: size
    }, () => 0);
  }

  addMember(member) {
    setBit(this._bitArr, member);
  }

  isExist(member) {
    const isExist = getBit(this._bitArr, member);
    return Boolean(isExist);
  }
}

// 驗證
const bitMap = new BitMap(4);
const arr = [0, 3, 5, 6, 9, 34, 23, 78, 99];
for(var i = 0;i < arr.length;i++){
    bitMap.addMember(arr[i]);
}

console.log(bitMap.isExist(3)); // true
console.log(bitMap.isExist(7)); // false
console.log(bitMap.isExist(78)); // true

注意:這種結構也有其侷限性

  1. 數據集要求較爲緊湊,[1, 1000000]這種結構空間利用太低,不利於發揮bitMap的優點
  2. 僅對整數有效(固然,咱們能夠經過哈希函數將字符串轉換爲整型)

4.3 bitMap的應用

(1)大數據排序

要求:有多達10億無序整數,已知最大值爲15億,請對這個10億個數進行排序
分析:大數據的排序,傳統的排序方式相對內存佔用較大,使用bitMap僅佔原內存的(JS中爲1/64,Java中爲1/32)

實現:模擬大數據實現,以下(最大值爲99)

const arr = [0, 6, 88, 7, 73, 34, 10, 99, 22];
const MAX_NUMBER = 99;

const ret = [];
const bitMap = new BitMap(4);
arr.forEach(item => { bitMap.addMember(item); })

for (let i = 0; i <= MAX_NUMBER; i += 1) {
  if (bitMap.isExist(i)) ret.push(i);
}

console.log(ret); // [ 0, 6, 7, 10, 22, 34, 73, 88, 99 ]

(2)兩個集合取交集

要求:兩個數組,內容分別爲[1, 4, 6, 8, 9, 10, 15], [6, 14, 9, 2, 0, 7],請用BitMap計算他們的交集
分析:利用isExist()來篩選相同項
實現

const arr1 = [1, 4, 6, 8, 9, 10, 15];
const arr2 = [6, 14, 9, 2, 0, 7];
const intersectionArr = []

const bitMap = new BitMap();
arr1.forEach(item => bitMap.addMember(item))

arr2.forEach(item => {
  if (bitMap.isExist(item)) {
    intersectionArr.push(item);
  }
})

console.log(intersectionArr); // [6, 9]

BitMap數據結構的用法原不止如此,咱們能夠經過哈希函數將字符串轉換成整數,再進行處理。固然,咱們應該始終牢記BitMap必須是相對較爲緊密的數字,不然沒法發揮BitMap的最大功效

上一篇:JS數據結構與算法_鏈表

相關文章
相關標籤/搜索