字典(Map)與散列表(HashMap)是一種採用[鍵(key),值(value)]對的形式來存儲數據的數據結構。前端
本文將詳細講解字典與散列表的實現思路並使用TypeScript將其實現,歡迎各位感興趣的前端開發者閱讀本文。git
字典與散列表存儲數據的方式是鍵值對的形式來存儲,所以咱們可使用JavaScript中的對象來實現。github
字典經過鍵值對的形式來存儲數據,它的鍵是字符串類型,調用者傳的key是什麼,它的鍵就是什麼。web
一個完整的字典類須要具有:判斷一個鍵是否在字典中、向字典中添加元素、根據key移除字典中存的元素、根據key查找字典中的元素、獲取字典中存儲的全部元素等方法,接下來咱們來分析下這些方法的實現思路。數組
向字典中添加元素(set)數據結構
判斷一個鍵是否在字典中 (hasKey)編輯器
根據key獲取字典中存儲的value值 (get)ide
根據key從字典中移除一個元素 (remove)函數
獲取字典中存儲的全部對象 (keyValues)post
獲取字典中存儲的全部key (keys) & 獲取字典中存儲的全部value (value)
迭代字典中的數據(forEach)
獲取字典的大小 (size),調用keyValues方法,返回其數組長度
判斷字典是否爲空 (isEmpty),調用size方法,判斷其是否0,返回判斷結果。
清空字典(clear),直接將字典對象初始化爲空對象便可
將字典中的數據轉爲字符串 (toString)
散列表又叫哈希表,它是字典的另外一種實現,它與字典的不一樣之處在於其key值的存儲。
字典存儲元素時會將key轉爲字符串後再存儲元素。
散列表存儲元素時會將key進行hash計算,獲得hash值後再存儲元素。
在查找元素時,字典須要去迭代整個數據結構來查找目標元素,而散列表是經過hash值來存儲的,咱們只須要對目標元素進行hash值計算,就能夠快速找到目標元素的位置。所以,散列表的效率要比字典的效率高。
因爲散列表相比哈希表有着許多相同的地方,可是他們存儲數據的key不同,所以咱們須要把其用到的方法寫到一個接口裏,分別根據各自的特色來實現這個接口,接下來咱們來分析下哈希表中與字典中不一樣的地方其方法的實現。
向哈希表中添加元素(put)
計算hash值(hashCode)
loseloseHashCode計算哈希值
djb2HashCode計算哈希值
根據key獲取哈希表中的元素 (get)
根據key移除哈希表中的元素 (remove)
其餘方法與字典中的實現基本同樣,惟一不一樣的地方在於它們對鍵的處理。
咱們在使用HashMap時,若是調用的是loseloseHashCode方法來計算的哈希值,那麼其衝突率會很高,此處介紹兩種比較經常使用的處理哈希衝突問題的方法。
分離連接法,會爲散列表的每個位置建立一個鏈表並將元素存儲在裏面。他是解決衝突最簡單的方法,可是它會佔用額外的存儲空間。
因爲分離連接的方法只是改變了HashMap的存儲結構,所以咱們能夠繼承HashMap重寫與HashMap不一樣的方法便可。
更換私有屬性表的變量名,因爲分離連接方法其value是一個鏈表類型而HashMap用的是ValuePair類型,js裏沒有真正的私有屬性,繼承時不能改變其表屬性的類型,所以咱們須要更換變量名(tableLink)
重寫put方法
重寫get方法 (須要從鏈表中獲取元素)
重寫remove方法 (須要從鏈表中移除元素)
重寫clear方法,將tableLink指向空對象便可
重寫keyValues方法,HashMap中存儲的是鏈表,須要從鏈表中獲取存儲的對象(valuePair)
另外一種解決衝突的方法是線性探查,之因此成爲線性,是由於它處理衝突的方法是將元素直接存到表中,而不是在單獨的數據結構中。
當想向表中某個位置添加一個新元素的時候,若是索引爲position的位置已經被佔據了,就嘗試position + 1的位置,若是position + 1的位置也被佔據了,就嘗試position + 2的位置,以此類推,直到在哈希表中找到一個空閒的位置。
接下來,咱們就來看下用線性探查解決衝突,須要重寫哪些方法
重寫put方法
重寫get方法
重寫remove方法
新增驗證刪除操做是否有反作用方法 (verifyRemoveSideEffect),若是元素刪除後產生了衝突,就須要將衝突的元素移動至一個以前的位置,這樣就不會產生空位置。
通過分析後咱們獲得了實現思路,接下來咱們就將上述思路轉換爲代碼。
咱們知道字典和散列表有着不少共有方法,所以咱們須要將共有方法分離成接口,而後根據不一樣的需求來實現不一樣的接口便可。
// 生成一個對象
export class ValuePair<K,V>{ constructor(public key: K,public value: V) {} toString(){ return `[#${this.key}: ${this.value}]`; } } 複製代碼
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; } 複製代碼
export default class Dictionary<K, V> implements Map<K, V> {
} 複製代碼
private table: { [key: string]: ValuePair<K, V> };
複製代碼
// toStrFn用於將一個值轉爲字符串,能夠本身來實現這部分邏輯,實例化時傳進來
constructor(private toStrFn: (key: K) => string = defaultToString) { this.table = {}; } 複製代碼
// 向字典中添加元素
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(key: K) {
return this.table[this.toStrFn(key)] != null; } 複製代碼
get(key: K) {
const valuePair = this.table[this.toStrFn(key)]; return valuePair == null ? undefined : valuePair.value; } 複製代碼
remove(key: K) {
if (this.hasKey(key)) { delete this.table[this.toStrFn(key)]; return true; } return false; } 複製代碼
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() {
// 能夠直接使用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() {
const values = []; const valuePairs = this.keyValues(); for (let i = 0; i < valuePairs.length; i++) { values.push(valuePairs[i].value); } return values; } 複製代碼
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() {
return this.keyValues().length; } isEmpty() { return this.size() === 0; } clear() { this.table = {}; } 複製代碼
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
export class HashMap<K,V> implements Map<K, V>{
} 複製代碼
protected table:{ [key:number]: ValuePair<K, V> };
複製代碼
constructor(protected toStrFn: (key: K) => string = defaultToString) {
this.table = {}; } 複製代碼
// 生成哈希碼
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(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(key: K): V|undefined {
const valuePair = this.table[this.hashCode(key)]; return valuePair == null ? undefined : valuePair.value; } 複製代碼
hasKey(key: K): boolean {
return this.table[this.hashCode(key)] != null; } 複製代碼
remove(key: K): boolean {
if(this.hasKey(key)){ delete this.table[this.hashCode(key)]; return true; } return false; } 複製代碼
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(): boolean {
return this.values().length === 0; } size(): number { return this.keyValues().length; } clear(): void { this.table= {}; } 複製代碼
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(): 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
執行上述測試代碼後咱們發現,有一些值衝突了,被替換掉了,產生了數據丟失問題。
咱們來看看如何結合鏈表如何解決衝突問題。
export default class HashMapSeparateChaining<K,V> extends HashMap<K, V> {
} 複製代碼
private tableLink:{ [key: number]: LinkedList<ValuePair<K, V>> };
複製代碼
constructor(protected toStrFn: (key: K) => string = defaultToString) {
super(toStrFn); this.tableLink = {}; } 複製代碼
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(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(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() {
this.tableLink = {}; } 複製代碼
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
export default class HashMapLinearProbing<K,V> extends HashMap<K, V>{
} 複製代碼
constructor() {
super(); } 複製代碼
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(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(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; } 複製代碼
// 驗證刪除操做是否有反作用
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值,全部的元素都保存進去了。