TypeScript實現Map與HashMap

前言

字典(Map)與散列表(HashMap)是一種採用[鍵(key),值(value)]對的形式來存儲數據的數據結構。前端

本文將詳細講解字典與散列表的實現思路並使用TypeScript將其實現,歡迎各位感興趣的前端開發者閱讀本文。git

實現思路

字典與散列表存儲數據的方式是鍵值對的形式來存儲,所以咱們可使用JavaScript中的對象來實現。github

字典的實現

字典經過鍵值對的形式來存儲數據,它的鍵是字符串類型,調用者傳的key是什麼,它的鍵就是什麼。web

一個完整的字典類須要具有:判斷一個鍵是否在字典中、向字典中添加元素、根據key移除字典中存的元素、根據key查找字典中的元素、獲取字典中存儲的全部元素等方法,接下來咱們來分析下這些方法的實現思路。數組

  • 向字典中添加元素(set數據結構

    • set方法接收兩個參數:key & value
    • 判斷參數的有效性,key & value不爲null | undefined時向字典中添加元素,不然直接返回false
    • 參數有效時,將參數key轉爲字符串
    • 將轉換爲字符串的key做爲字典中的key,將key & value放進一個對象中,將這個對象存進轉換爲字符串的key中。
    • 講過上述操做後,咱們就成功的向字典中添加了一個元素,返回true。
  • 判斷一個鍵是否在字典中 (hasKey)編輯器

    • hasKey方法接收一個參數:key
    • 因爲字典中的數據是以對象的形式存儲的,所以咱們能夠直接將key轉爲字符串,而後將其做爲屬性傳給字典對象,判斷其返回結果是否爲 undefined | null就能夠知道這個key是否在字典中了。
  • 根據key獲取字典中存儲的value值 (get)ide

    • get方法接收一個參數:key
    • 將key轉爲字符串,將其做爲屬性傳給字典對象,用一個變量來接收其返回值。
    • 判斷返回值是否null | undefined
    • 若是返回值不爲null | undefined則返回其對象中的value值,不然返回undefined。
  • 根據key從字典中移除一個元素 (remove)函數

    • remove方法接收一個參數:key
    • 判斷目標參數是否存在於字典對象中(調用hasKey方法),若是不存在直接返回false
    • 若是目標元素存在於字典對象中,將key轉爲字符串,而後將其做爲參數傳給字典對象,最後調用對象的delete方法刪除目標key,返回true
  • 獲取字典中存儲的全部對象 (keyValues)post

    • keyValues方法不接收任何參數,返回值爲一個對象數組
    • 首先,聲明一個數組變量(valuePairs)用於存儲獲取到的對象
    • 獲取字典對象中全部的key
    • 遍歷獲取到的key,將遍歷到的key做爲參數傳給字典對象。
    • 將字典對象返回的值放進valuePairs中,將其返回。
  • 獲取字典中存儲的全部key (keys) & 獲取字典中存儲的全部value (value)

    • keys方法接收任何參數
    • 聲明一個數組變量(keys)用於存儲獲取到的key | 聲明一個數組變量(values)用於存儲獲取到的value
    • 獲取字典中存儲的全部對象(調用keyValues方法)
    • 遍歷獲取到的對象數組
    • 若是想獲取key則將當前遍歷到的元素的key的值放進keys數組中,不然將values的值放進values數組中。
    • 返回 keys | values
  • 迭代字典中的數據(forEach)

    • forEach方法接收一個回調函數做爲參數,其回調函數有兩個參數:key & value
    • 獲取字典中的全部數據
    • 遍歷獲取到的數據,調用回調函數參數將當前遍歷到的對象的key和value傳給回調函數,用一個變量(result)保存其結果
    • 若是result爲false時,表明字典中的元素已經遍歷完,退出循環
  • 獲取字典的大小 (size),調用keyValues方法,返回其數組長度

  • 判斷字典是否爲空 (isEmpty),調用size方法,判斷其是否0,返回判斷結果。

  • 清空字典(clear),直接將字典對象初始化爲空對象便可

  • 將字典中的數據轉爲字符串 (toString)

    • toString方法不接收任何參數
    • 若是字典爲空,則直接返回空字符串。
    • 字典不爲空時,獲取字典中的全部數據。
    • 聲明一個變量(objString),用於存放字典中的每一個對象,其初始值爲字典對象數組中的0號
    • 遍歷獲取到的對象,將objString與遍歷到的數據進行拼接,返回objString。

散列表的實現

散列表又叫哈希表,它是字典的另外一種實現,它與字典的不一樣之處在於其key值的存儲。

字典存儲元素時會將key轉爲字符串後再存儲元素。

散列表存儲元素時會將key進行hash計算,獲得hash值後再存儲元素。

在查找元素時,字典須要去迭代整個數據結構來查找目標元素,而散列表是經過hash值來存儲的,咱們只須要對目標元素進行hash值計算,就能夠快速找到目標元素的位置。所以,散列表的效率要比字典的效率高。

因爲散列表相比哈希表有着許多相同的地方,可是他們存儲數據的key不同,所以咱們須要把其用到的方法寫到一個接口裏,分別根據各自的特色來實現這個接口,接下來咱們來分析下哈希表中與字典中不一樣的地方其方法的實現。

  • 向哈希表中添加元素(put)

    • 跟字典的實現同樣,一樣也是接收兩個參數,判斷其是否有效
    • 以key爲參數,調用hashCode函數(咱們本身來實現)計算其hash值
    • 將獲得的哈希值做爲key存進哈希表中,其值與字典的保持一致
  • 計算hash值(hashCode)

    • 生成hash值有多種解決方案,本文只介紹兩種最經常使用的。
    • 調用須要使用的hash函數(loseloseHashCode | djb2HashCode)
  • loseloseHashCode計算哈希值

    • 首先,咱們判斷下key是否爲數字,若是爲數字不執行直接將其返回,不執行哈希運算
    • 將key轉爲字符串,聲明一個變量(hash)用於存儲hash值
    • 遍歷轉爲字符串的key,調用js的charCodeAt函數求出每一個字符的Unicode編碼,將獲取到的Unicode碼與hash相加
    • 遍歷結束後,爲了不數值過大,將hash值除以37取其他數,返回hash。
  • djb2HashCode計算哈希值

    • 與loseloseHashCode方法同樣,判斷key是否爲數字,而後將其轉爲字符串
    • 不一樣的地方是,hash有一個初始值 5381
    • 遍歷轉爲字符串後的key,獲取遍歷到的每一個字符的Unicode碼,將hash值乘以33而後將獲取到的Unicode碼與其相加
    • 遍歷結束後,將hash值除以1013取其他數,返回hash
  • 根據key獲取哈希表中的元素 (get)

    • 將key進行hash運算,獲得結果,將其做爲參數傳給哈希表對象,獲取目標key存在哈希表中的元素
    • 判斷其結果是否爲 null | undefined,若是是則返回undefined,不然返回其value值
  • 根據key移除哈希表中的元素 (remove)

    • 將key進行hash運算,判斷其哈希值是否在哈希中,若是不在則返回false
    • key在哈希表中,將計算出來的hash值看成屬性傳給哈希表,調用delete方法刪除目標元素的key,返回true
  • 其餘方法與字典中的實現基本同樣,惟一不一樣的地方在於它們對鍵的處理。

處理哈希表中Hash值衝突

咱們在使用HashMap時,若是調用的是loseloseHashCode方法來計算的哈希值,那麼其衝突率會很高,此處介紹兩種比較經常使用的處理哈希衝突問題的方法。

分離連接

分離連接法,會爲散列表的每個位置建立一個鏈表並將元素存儲在裏面。他是解決衝突最簡單的方法,可是它會佔用額外的存儲空間。

因爲分離連接的方法只是改變了HashMap的存儲結構,所以咱們能夠繼承HashMap重寫與HashMap不一樣的方法便可。

  • 更換私有屬性表的變量名,因爲分離連接方法其value是一個鏈表類型而HashMap用的是ValuePair類型,js裏沒有真正的私有屬性,繼承時不能改變其表屬性的類型,所以咱們須要更換變量名(tableLink)

  • 重寫put方法

    • 與HashMap同樣,判斷其key & value的有效性
    • 計算key的hash值,用一個變量(position)存起來
    • 將position做爲參數傳給tableLink判斷其是否爲null | undefined
    • 若是不爲null | undefined,在tableLink的position位置建立一個鏈表
    • 在在tableLink的position位置的鏈表中添加Key & value對象
    • 添加成功,返回true
  • 重寫get方法 (須要從鏈表中獲取元素)

    • 計算key的hash值,用一個變量(position)存起來
    • 獲取position位置存儲的鏈表結構元素
    • 若是鏈表不爲空,從鏈表頭部開始遍歷,直至當前遍歷到的鏈表元素的key與目標參數的key相同,則返回其對應的value值
    • 鏈表爲空則返回undefined
  • 重寫remove方法 (須要從鏈表中移除元素)

    • 計算key的hash值,用一個變量(position)存起來
    • 獲取position位置的鏈表結構元素
    • 若是鏈表不爲空,從鏈表頭部開始遍歷。
    • 當前遍歷到的鏈表元素與目標參數key相同,則將當前鏈表中的元素從鏈表中移除。
    • 移除後,若是鏈表爲空,直接刪除tableLink的position位置元素
    • 鏈表爲空則返回undefined
  • 重寫clear方法,將tableLink指向空對象便可

  • 重寫keyValues方法,HashMap中存儲的是鏈表,須要從鏈表中獲取存儲的對象(valuePair)

    • 聲明一個數組變量(valuePairs)用於存儲獲取到的ValuePair對象
    • 獲取tableLink中的全部key,將其轉爲int類型後,用一個變量存起來(keys)
    • 遍歷keys,獲取當前遍歷到的鏈表結構元素數據,用變量(linkedList)存起來
    • 若是linkedList不爲空,從鏈表頭部開始遍歷鏈表中的數據,獲取當前遍歷到的鏈表中的元素,將其存進valuePairs中。
    • 返回valuePairs
線性探查

另外一種解決衝突的方法是線性探查,之因此成爲線性,是由於它處理衝突的方法是將元素直接存到表中,而不是在單獨的數據結構中。

當想向表中某個位置添加一個新元素的時候,若是索引爲position的位置已經被佔據了,就嘗試position + 1的位置,若是position + 1的位置也被佔據了,就嘗試position + 2的位置,以此類推,直到在哈希表中找到一個空閒的位置。

接下來,咱們就來看下用線性探查解決衝突,須要重寫哪些方法

  • 重寫put方法

    • 與HashMap同樣,須要判斷其參數的有效性以及傳的參數數量
    • 計算key的hash值,用一個變量存起來(position)
    • 判斷table的position位置是否被佔據
    • 若是沒有被佔據,則在position位置新建一個對象將key & value存進去
    • 若是被佔據,用一個變量(index)來接收position+1位置的值
    • 遍歷table的index位置的值,若是不爲空index就一直自增。
    • 當找到table中的空餘位置時,在table的index位置新建一個對象將Key與Value存進去,返回true
  • 重寫get方法

    • 計算key的hash值,用一個變量存起來(position)
    • 判斷table的position位置的元素是否爲null | undefined,若是不是則返回undefined
    • 判斷table的position位置元素的key是否等於目標參數的key,若是等於則直接返回position位置的value值
    • 若是不等於,用一個變量(index)來存儲position+1位置的值
    • 遍歷table的index位置的值,若是index位置的值不爲空而且index位置的key不等於目標參數的key,index就自增
    • 循環結束後,判斷當前table的index位置元素的key是否等於目標參數的key,若是相等則返回index位置的value值
  • 重寫remove方法

    • 計算key的hash值,用一個變量存起來(position)
    • 判斷table的position位置是否爲null,若是爲null直接返回false
    • 若是table的position位置的key等於目標參數的key,刪除position位置的元素,驗證本次刪除是否有反作用,調整元素的位置,返回true
    • 若是不相等,須要聲明一個變量(index)查找position後面的值,默認值爲position + 1
    • 遍歷table的index位置的元素,若是其不爲null而且其key與目標key不相等,index就自增
    • 遍歷技術後,若是index位置的元素不爲null且index位置元素的key等於目標參數的key,則刪除table中index位置的元素,驗證本次刪除是否有反作用,調整元素的位置,返回true
  • 新增驗證刪除操做是否有反作用方法 (verifyRemoveSideEffect),若是元素刪除後產生了衝突,就須要將衝突的元素移動至一個以前的位置,這樣就不會產生空位置。

    • verifyRemoveSideEffect方法接收來那個參數:被刪除的key,被刪除key在table中的位置(removedPosition)
    • 計算key的hash值,用一個變量存起來(hash)
    • 用一個變量接收被刪除key位置的下一個位置(index),默認爲removedPosition+1
    • 遍歷表,若是index位置的元素不爲null,獲取當前index位置的key的hash值,將其存進一個變量裏(posHash)
    • 若是posHash小於等於hash 或者 posHash小於等於removedPosition,將table中index位置的元素賦值給removedPosition位置
    • 刪除table中index位置的元素
    • 將index賦值給removedPosition
    • 繼續下一輪循環判斷

實現代碼

通過分析後咱們獲得了實現思路,接下來咱們就將上述思路轉換爲代碼。

編寫Map接口

咱們知道字典和散列表有着不少共有方法,所以咱們須要將共有方法分離成接口,而後根據不一樣的需求來實現不一樣的接口便可。

  • 新建Map.ts文件
  • 新建dictionary-list-models.ts文件
  • 在dictionary-list-models中添加一個ValuePair類並將其導出,這個類用於存儲字典中value值
// 生成一個對象
export class ValuePair<K,V>{  constructor(public key: K,public value: V) {}   toString(){  return `[#${this.key}: ${this.value}]`;  } } 複製代碼
  • 在Map中導入ValuePair類,添加並定義咱們後面須要用到的方法並規定其返回值類型
import {ValuePair} from "../../utils/dictionary-list-models.ts";
 export default interface Map<K,V> {  hasKey(key: K): boolean;  set?(key: K, value: V): boolean;  put?(key: K, value: V): boolean;  hashCode?(key: K): number;  remove(key: K): boolean;  get(key: K): V|undefined;  keyValues(): ValuePair<K, V>[];  keys(): K[];  values(): V[];  forEach(callbackFn: (key: K, value: V) => any): void;  size(): number;  isEmpty(): boolean;  clear():void;  toString():string; } 複製代碼

實現字典類

  • 新建Dictionary.ts文件,添加Dictionary類實現Map接口
export default class Dictionary<K, V> implements Map<K, V> {
 } 複製代碼
  • 聲明私有屬性table,用於字典的存儲
private table: { [key: string]: ValuePair<K, V> };
複製代碼
  • 在構造器中初始化table,聲明值轉字符串函數並賦予其默認值
// toStrFn用於將一個值轉爲字符串,能夠本身來實現這部分邏輯,實例化時傳進來
 constructor(private toStrFn: (key: K) => string = defaultToString) {  this.table = {};  } 複製代碼
  • 實現set方法
// 向字典中添加元素
 set(key: K, value: V) {  if (key != null && value != null) {  // 將key轉爲字符串,字典中須要的key爲字符串形式  const tableKey = this.toStrFn(key);  this.table[tableKey] = new ValuePair(key, value);  return true;  }  return false;  } 複製代碼
  • 實現hasKey方法
hasKey(key: K) {
 return this.table[this.toStrFn(key)] != null;  } 複製代碼
  • 實現get方法
get(key: K) {
 const valuePair = this.table[this.toStrFn(key)];  return valuePair == null ? undefined : valuePair.value;  } 複製代碼
  • 實現remove方法
remove(key: K) {
 if (this.hasKey(key)) {  delete this.table[this.toStrFn(key)];  return true;  }  return false;  } 複製代碼
  • 實現keyValues方法
keyValues(): ValuePair<K, V>[] {
 /* 使用ES2017引入的Object.values方法能夠直接獲取對象裏存儲的全部對應key的value值存進數組中 */  const valuePairs = [];  const keys = Object.keys(this.table);  for (let i = 0; i < keys.length; i++){  valuePairs.push(this.table[keys[i]])  }  return valuePairs;  } 複製代碼
  • 實現keys方法
keys() {
 // 能夠直接使用map獲取對象的key  // return this.keyValues().map(valuePair=> valuePair.key);  const keys = [];  const valuePairs = this.keyValues();  for (let i = 0; i < valuePairs.length; i++) {  keys.push(valuePairs[i].key);  }  return keys;  } 複製代碼
  • 實現values方法
values() {
 const values = [];  const valuePairs = this.keyValues();  for (let i = 0; i < valuePairs.length; i++) {  values.push(valuePairs[i].value);  }  return values;  } 複製代碼
  • 實現forEach方法
forEach(callbackFn: (key: K, value: V) => any) {
 const valuePairs = this.keyValues();  for (let i = 0; i < valuePairs.length; i++) {  const result = callbackFn(valuePairs[i].key, valuePairs[i].value);  if (result === false) {  break;  }  }  } 複製代碼
  • 實現size、isEmpty、clear方法
size() {
 return this.keyValues().length;  }   isEmpty() {  return this.size() === 0;  }   clear() {  this.table = {};  } 複製代碼
  • 實現toString方法
toString() {
 if (this.isEmpty()) {  return '';  }   const valuePairs = this.keyValues();  let objString = `${valuePairs[0].toString()}`;  for (let i = 1; i < valuePairs.length; i++) {  objString = `${objString},${valuePairs[i].toString()}`;  }  return objString;  } 複製代碼

完整代碼請移步: Dictionary.ts

編寫測試代碼

上面咱們實現了字典類,接下來咱們來測試下上述代碼是否都執行正常

const dictionary = new Dictionary();
dictionary.set("name","張三"); dictionary.set("age",20); dictionary.set("id",198); console.log("判斷name是否在dictionary中",dictionary.hasKey("name")); // 移除名爲id的key dictionary.remove("id"); console.log("判斷id是否爲dictionary中",dictionary.hasKey("id")); console.log("將字典中存儲的數據轉爲字符串",dictionary.toString()) // 獲取dictionary中名爲name的值 console.log("dictionary中名爲name的值",dictionary.get("name")); // 獲取字典中全部存儲的值 console.log("dictionary中全部存儲的值",dictionary.keyValues()); // 獲取字典中全部的鍵 console.log("dictionary中全部存儲的鍵",dictionary.keys()); // 獲取字典中全部的值 console.log("dictionary中全部存儲的值",dictionary.values()); // 迭代字典中的每一個鍵值對 const obj = {}; dictionary.forEach(function (key,value) {  obj[key] = value; }) console.log(obj) 複製代碼

完整代碼請移步:DictionaryTest.js

實現哈希表

  • 新建HashMap類,實現Map接口。
export class HashMap<K,V> implements Map<K, V>{
 } 複製代碼
  • 聲明table,並規定其類型
protected table:{ [key:number]: ValuePair<K, V> };
複製代碼
  • 在構造器中初始化table,並規定值轉字符串函數,容許調用者傳一個值字符串函數
constructor(protected toStrFn: (key: K) => string = defaultToString) {
 this.table = {};  } 複製代碼
  • 實現hashCode、loseloseHashCode、djb2HashCode方法
// 生成哈希碼
 hashCode(key: K): number {  return this.loseloseHashCode(key);  }   // loselose實現哈希函數  loseloseHashCode(key: K): number {  if (typeof key === "number"){  return key;  }  const tableKey = this.toStrFn(key);  let hash = 0;  for (let i = 0; i < tableKey.length; i++){  // 獲取每一個字符的ASCII碼將其拼接至hash中  hash += tableKey.charCodeAt(i);  }  return hash % 37;  }   // djb2實現哈希函數  djb2HashCode(key: K): number {  if (typeof key === "number"){  return key;  }   // 將參數轉爲字符串  const tableKey = this.toStrFn(key);  let hash = 5381;  for (let i = 0; i < tableKey.length; i++){  hash = (hash * 33) + tableKey.charCodeAt(i);  }  return hash % 1013;  } 複製代碼
  • 實現put方法
put(key: K, value: V): boolean {
 if (key != null && value != null){  const position = this.hashCode(key);  this.table[position] = new ValuePair(key, value);  return true;  }  return false;  } 複製代碼
  • 實現get方法
get(key: K): V|undefined {
 const valuePair = this.table[this.hashCode(key)];  return valuePair == null ? undefined : valuePair.value;  } 複製代碼
  • 實現hasKey方法
hasKey(key: K): boolean {
 return this.table[this.hashCode(key)] != null;  } 複製代碼
  • 實現remove方法
remove(key: K): boolean {
 if(this.hasKey(key)){  delete this.table[this.hashCode(key)];  return true;  }  return false;  } 複製代碼
  • 實現keyValues、keys、values方法
keyValues(): ValuePair<K, V>[] {
 const valuePairs = [];  // 獲取對象中的全部key並將其轉爲int類型數組  const keys = Object.keys(this.table).map(item => parseInt(item));  for (let i = 0; i < keys.length; i++){  valuePairs.push(this.table[keys[i]]);  }  return valuePairs;  }   keys(): K[] {  const keys = [];  const valuePairs = this.keyValues();  for (let i = 0; i < valuePairs.length; i++){  keys.push(valuePairs[i].key);  }  return keys;  }   values(): V[] {  const values = [];  const valuePairs = this.keyValues();  for (let i = 0; i < valuePairs.length; i++){  values.push(valuePairs[i].value);  }  return values;  } 複製代碼
  • 實現isEmpty、size、clear方法
isEmpty(): boolean {
 return this.values().length === 0;  }   size(): number {  return this.keyValues().length;  }   clear(): void {  this.table= {};  } 複製代碼
  • 實現forEach方法
forEach(callbackFn: (key: K, value: V) => any): void {
 const valuePairs = this.keyValues();  for (let i = 0; i < valuePairs.length; i++){  const result = callbackFn(valuePairs[i].key,valuePairs[i].value);  if (result === false) {  break;  }  }  } 複製代碼
  • 實現toString方法
toString(): string {
 if (this.isEmpty()){  return ``  }   const valuePairs = this.keyValues();  let objString = `${valuePairs[0].toString()}`;  for (let i = 1; i < valuePairs.length; i++){  objString = `${objString},${valuePairs[i].toString()}`;  }  return objString;  } 複製代碼

完整代碼請移步: HashMap.ts

編寫測試代碼

咱們測試下上述代碼是否都正常執行

const hashMap = new HashMap();
hashMap.put("name", "張三"); hashMap.put("id", 1); hashMap.put("class", "產品"); console.log("判斷class是否存在與HashMap中", hashMap.hasKey("class")); hashMap.remove("id"); console.log("判斷id是否存在於HashMap中", hashMap.hasKey("id")) console.log(hashMap.get("name")); hashMap.forEach(((key, value) => {  console.log(key +"="+ value); })) console.log("判斷HashMap中的數據是否爲空",hashMap.isEmpty()); console.log("輸出HashMap中全部key對應的value",hashMap.keyValues()); console.log("獲取HashMap中的全部key值",hashMap.keys()); console.log("獲取HashMap中的全部Value值",hashMap.values()); console.log("獲取HashMap的大小",hashMap.size()); console.log("HashMap中的數據轉字符串輸出",hashMap.toString()); console.log("清空HashMap中的數據"); hashMap.clear(); // 測試hash值衝突問題 hashMap.put('Ygritte', 'ygritte@email.com'); hashMap.put('Jonathan', 'jonathan@email.com'); hashMap.put('Jamie', 'jamie@email.com'); hashMap.put('Jack', 'jack@email.com'); hashMap.put('Jasmine', 'jasmine@email.com'); hashMap.put('Jake', 'jake@email.com'); hashMap.put('Nathan', 'nathan@email.com'); hashMap.put('Athelstan', 'athelstan@email.com'); hashMap.put('Sue', 'sue@email.com'); hashMap.put('Aethelwulf', 'aethelwulf@email.com'); hashMap.put('Sargeras', 'sargeras@email.com'); console.log(hashMap.toString()); 複製代碼

完整代碼請移步:HashMapTest.js

分離鏈表法解決哈希衝突問題

執行上述測試代碼後咱們發現,有一些值衝突了,被替換掉了,產生了數據丟失問題。

咱們來看看如何結合鏈表如何解決衝突問題。

  • 新建HashMapSeparateChaining.ts文件,添加HashMapSeparateChaining類繼承自HashMap
export default class HashMapSeparateChaining<K,V> extends HashMap<K, V> {
 } 複製代碼
  • 聲明私有屬性tableLink,並定義其類型,用於表的存儲。
private tableLink:{ [key: number]: LinkedList<ValuePair<K, V>> };
複製代碼
  • 在構造器中聲明toStrFn方法賦予默認值,初始化父類和tableLink
constructor(protected toStrFn: (key: K) => string = defaultToString) {
 super(toStrFn);  this.tableLink = {};  } 複製代碼
  • 重寫put方法
put(key: K, value: V): boolean {
 if (key != null && value != null) {  const position = this.hashCode(key);  if (this.tableLink[position] == null){  // 若是當前要添加元素的位置爲空則建立一個鏈表  this.tableLink[position] = new LinkedList<ValuePair<K, V>>();  }  // 往當前要添加元素的鏈表中添加當前當前元素  this.tableLink[position].push(new ValuePair(key,value));  return true;  }  return false;  } 複製代碼
  • 重寫get方法
get(key: K): V | undefined {
 // 獲取參數的hash值  const position = this.hashCode(key);  // 獲取目標元素位置存儲的鏈表結構元素  const linkedList = this.tableLink[position];  if (linkedList !=null && !linkedList.isEmpty()){  // 獲取鏈表頭部數據  let current = linkedList.getHead();  while (current != null){  // 遍歷鏈表,找到鏈表中與目標參數相同的數據  if (current.element.key === key){  // 返回目標key對應的value值  return current.element.value;  }  current = current.next;  }  }  return undefined;  } 複製代碼
  • 重寫remove方法
remove(key: K): boolean {
 const position = this.hashCode(key);  // 獲取目標元素位置存儲的鏈表結構元素  const linkedList = this.tableLink[position];  if (linkedList != null && !linkedList.isEmpty()){  // 獲取鏈表頭部元素  let current = linkedList.getHead();  while (current != null){  // 遍歷鏈表,找到與目標元素相同的數據  if (current.element.key === key){  // 將當前鏈表中的元素從鏈表中移除  linkedList.remove(current.element);  if (linkedList.isEmpty()){  // 鏈表爲空,刪除目標位置元素  delete this.tableLink[position];  }  return true;  }  current = current.next;  }  }  return false;  } 複製代碼
  • 重寫clear方法
clear() {
 this.tableLink = {};  } 複製代碼
  • 重寫keyValues方法
keyValues(): ValuePair<K, V>[] {
 const valuePairs = [];  // 獲取tableLink中的全部key並轉爲int類型  const keys = Object.keys(this.tableLink).map(item=>parseInt(item));  for (let i = 0; i < keys.length; i++){  const linkedList = this.tableLink[keys[i]];  if (linkedList != null && !linkedList.isEmpty()){  // 遍歷鏈表中的數據,將鏈表中的數據放進valuePairs中  let current = linkedList.getHead();  while (current != null){  valuePairs.push(current.element);  current = current.next;  }  }  }  return valuePairs;  } 複製代碼

完整代碼請移步:HashMapSeparateChaining.ts

編寫測試代碼

咱們測試下上述方法是否都能正常執行

const hashMapSC = new HashMapSeparateChaining();
hashMapSC.put("name","張三"); hashMapSC.put("id",11); hashMapSC.put("age",22); hashMapSC.put("phone","09871588"); hashMapSC.remove("id"); console.log(hashMapSC.get("name")); console.log("判斷hashMap中的數據是否爲空", hashMapSC.isEmpty()); console.log(hashMapSC.toString()); console.log("使用forEach遍歷hashMap中的數據"); hashMapSC.forEach((key,value)=>{  console.log(`${key} = ${value}`); }) console.log("獲取hashMap中存儲的全部key",hashMapSC.keys()); console.log("獲取hashMap中存儲的全部value",hashMapSC.values()); console.log("判斷id是否在hashMap中",hashMapSC.hasKey("id")); console.log("清空HashMap中的數據"); hashMapSC.clear(); console.log("判斷HashMap中的數據是否爲空", hashMapSC.isEmpty()); console.log("衝突測試") hashMapSC.put('Ygritte', 'ygritte@email.com'); hashMapSC.put('Jonathan', 'jonathan@email.com'); hashMapSC.put('Jamie', 'jamie@email.com'); hashMapSC.put('Jack', 'jack@email.com'); hashMapSC.put('Jasmine', 'jasmine@email.com'); hashMapSC.put('Jake', 'jake@email.com'); hashMapSC.put('Nathan', 'nathan@email.com'); hashMapSC.put('Athelstan', 'athelstan@email.com'); hashMapSC.put('Sue', 'sue@email.com'); hashMapSC.put('Aethelwulf', 'aethelwulf@email.com'); hashMapSC.put('Sargeras', 'sargeras@email.com'); console.log(hashMapSC.toString()); 複製代碼

完整代碼請移步:HashMapSeparateChainingTest.js

線性探查解決哈希衝突問題

  • 新建HashMapLinearProbing.ts文件,添加HashMapLinearProbing類繼承自HashMap
export default class HashMapLinearProbing<K,V> extends HashMap<K, V>{
 } 複製代碼
  • 構造器中初始化父類
constructor() {
 super();  } 複製代碼
  • 重寫put方法
put(key: K, value: V): boolean {
 if (key != null && value!= null){  const position = this.hashCode(key);  // 判斷當前要插入的位置在表中是否被佔據  if (this.table[position] == null){  // 當前位置沒有被佔據,將Key & value放進ValuePair中賦值給當前表中要插入位置的元素  this.table[position] = new ValuePair(key,value);  } else{  // 位置被佔據,遞增index直至找到沒有被佔據的位置  let index = position + 1;  while (this.table[index] != null){  index++;  }  // 找到沒有被佔據的位置,將Key & value放進ValuePair中賦值給當前表中要插入位置的元素  this.table[index] = new ValuePair(key,value);  }  return true;  }  return false;  } 複製代碼
  • 重寫get方法
get(key: K): V | undefined {
 const position = this.hashCode(key);  if(this.table[position] != null) {  // 若是當前位置元素的key等於目標元素的key直接返回當前位置元素的value  if (this.table[position].key === key){  return this.table[position].value;  }  // 位置遞增直至找到咱們要找的元素或者找到一個空位置  let index = position + 1;  while (this.table[index] != null && this.table[index].key !== key){  index++;  }  // 遞增結束後,判斷當前表中index的key是否等於目標key  if (this.table[index] != null && this.table[index].key === key){  return this.table[index].value;  }  }  return undefined;  } 複製代碼
  • 重寫remove方法
remove(key: K): boolean {
 const position = this.hashCode(key);  if (this.table[position] != null){  if (this.table[position].key === key){  delete this.table[position];  // 刪除後,驗證本次刪除是否有反作用,調整元素位置  this.verifyRemoveSideEffect(key,position);  return true;  }  let index = position + 1;  while (this.table[index] != null && this.table[index].key !== key){  index++;  }  if (this.table[index] != null && this.table[index].key === key){  delete this.table[index];  this.verifyRemoveSideEffect(key,index);  return true;  }  }  return false;  } 複製代碼
  • 實現verifyRemoveSideEffect方法
// 驗證刪除操做是否有反作用
 private verifyRemoveSideEffect(key: K,removedPosition: number){  // 計算被刪除key的哈希值  const hash = this.hashCode(key);  // 從被刪除元素位置的下一個開始遍歷表直至找到一個空位置  // 當找到一個空位置後即表示元素在合適的位置上不須要移動  let index = removedPosition + 1;  while (this.table[index] != null){  // 計算當前遍歷到的元素key的hash值  const posHash = this.hashCode(this.table[index].key);  console.log(`當前遍歷到的元素的hash= ${posHash} , 上一個被移除key的hash = ${removedPosition}`)  if (posHash <= hash || posHash <= removedPosition){  // 若是當前遍歷到的元素的哈希值小於等於被刪除元素的哈希值或者小於等於上一個被移除key的哈希值(removedPosition)  // 須要將當前元素移動至removedPosition位置  this.table[removedPosition] = this.table[index];  // 移動完成後,刪除當前index位置的元素  delete this.table[index];  // 更新removedPosition的值爲index  removedPosition = index;  }  index++;  }  } 複製代碼

完整代碼請移步:HashMapLinearProbing.ts

編寫測試代碼

測試下上述方法是否都正常執行

const hashMapLP = new HashMapLinearProbing();
console.log("衝突元素刪除測試"); hashMapLP.put('Ygritte', 'ygritte@email.com'); hashMapLP.put('Jonathan', 'jonathan@email.com'); hashMapLP.put('Jamie', 'jamie@email.com'); hashMapLP.put('Jack', 'jack@email.com'); hashMapLP.put('Jasmine', 'jasmine@email.com'); hashMapLP.put('Jake', 'jake@email.com'); hashMapLP.put('Nathan', 'nathan@email.com'); hashMapLP.put('Athelstan', 'athelstan@email.com'); hashMapLP.put('Sue', 'sue@email.com'); hashMapLP.put('Aethelwulf', 'aethelwulf@email.com'); hashMapLP.put('Sargeras', 'sargeras@email.com'); // hashMapLP.remove("Ygritte"); hashMapLP.remove("Jonathan"); console.log(hashMapLP.toString()); 複製代碼

完整代碼請移步:HashMapLinearProbing.ts

更好的散列函數

代碼中咱們使用的是loseloseHashCode來生成hash值,這種方法會生成比較多的重複元素,所以不建議使用此方法,由於處理衝突會消耗不少的性能。

咱們在上述代碼中實現了djb2HashCode方法,此方法產生重複的hash值的機率很小,所以咱們應該使用此方法來生成,接下來咱們將hashCode使用的方法改成djb2HashCode,測試下HashMap的執行結果。

hashCode(key: K): number {
 return this.djb2HashCode(key);  } 複製代碼

結果在咱們的預料以內,它沒有產生重複的hash值,全部的元素都保存進去了。

寫在最後

  • 文中若有錯誤,歡迎在評論區指正,若是這篇文章幫到了你,歡迎點贊和關注😊
  • 本文首發於掘金,未經許可禁止轉載💌
相關文章
相關標籤/搜索