ES6的JavaScript数据结构实现之字典与散列表
内容:字典和散列表。(未完成,待继续)
所有源码在我的Github上(如果觉得不错记得给星鼓励我哦):ES6的JavaScript数据结构实现之字典与散列表
注:ES6也是新增加了Map类。此外还提供了WeakMap和WeakSet。基本上,Map和Set与其弱化版本之间仅有的区别是:WeakMap和WeakSet类没有entries、keys、values等方法;只能用对象作为键。而这两个弱化类是为了性能,可以使得JavaScript的垃圾回收器可以从中清除整个入口。此外,由于弱化类键只能是对象,所以不知道键就没办法找到值,这个性质可以用于封装ES6类的私有属性。
一、基础数据结构
1、字典(以[键,值]对的形式存储数据,table[key]={key, value})
//注:由于JavaScript不是强类型的语言,不能保证键一定是字符串,所以需要把所有键名的传入的对象转换为字符串,使得Dictionary类中搜索和获取值更简单。
1 class ValuePair { 2 constructor(key, value) { 3 this.key = key; 4 this.value = value; 5 } 6 7 toString() { 8 return `[#${this.key}: ${this.value}]`; 9 } 10 } 11 12 function defaultToString(item) { 13 if (item === null) { 14 return 'NULL'; 15 } if (item === undefined) { 16 return 'UNDEFINED'; 17 } if (typeof item === 'string' || item instanceof String) { 18 return `${item}`; 19 } 20 return item.toString(); 21 } 22 23 class Dictionary { 24 constructor(toStrFn = defaultToString) { 25 this.toStrFn = toStrFn; 26 this.table = {}; 27 } 28 hasKey(key) { 29 return this.table[this.toStrFn(key)] != null; 30 } 31 set(key, value) { 32 if (key != null && value != null) { 33 const tableKey = this.toStrFn(key); 34 this.table[tableKey] = new ValuePair(key,value); 35 return true; 36 } 37 return false; 38 } 39 remove(key) { 40 if (this.hasKey(key)) { 41 delete this.table[this.toStrFn(key)]; 42 return true; 43 } 44 return false; 45 } 46 get(key) { 47 const valuePair = this.table[this.toStrFn(key)]; 48 return valuePair == null ? undefined : valuePair.value; 49 } 50 keyValues() { 51 return Object.values(this.table); 52 } 53 values() { 54 return this.keyValues().map(valuePair => valuePair.value); 55 } 56 57 keys() { 58 return this.keyValues().map(valuePair => valuePair.key); 59 } 60 forEach(callbackFn) { 61 const valuePairs = this.keyValues(); 62 for (let i = 0; i < valuePairs.length; i++) { 63 const result = callbackFn(valuePairs[i].key, valuePairs[i].value); 64 if (result === false) { 65 break; 66 } 67 } 68 } 69 70 isEmpty() { 71 return this.size() === 0; 72 } 73 74 size() { 75 return Object.keys(this.table).length; 76 } 77 78 clear() { 79 this.table = {}; 80 } 81 82 toString() { 83 if (this.isEmpty()) { 84 return ''; 85 } 86 const valuePairs = this.keyValues(); 87 let objString = `${valuePairs[0].toString()}`; 88 for (let i = 1; i < valuePairs.length; i++) { 89 objString = `${objString},${valuePairs[i].toString()}`; 90 } 91 return objString; 92 } 93 94 95 } 96 97 const dictionary = new Dictionary(); 98 dictionary.set('Bob','student'); 99 dictionary.set('Alice','teacher'); 100 dictionary.set('Jack','student'); 101 console.log(dictionary); 102 console.log(dictionary.hasKey('Bob')); 103 dictionary.remove('Jack'); 104 console.log(dictionary); 105 console.log(dictionary.get('Alice')); 106 console.log(dictionary.keyValues()); 107 console.log(dictionary.keys()); 108 console.log(dictionary.values()); 109 dictionary.forEach((k, v) => { 110 console.log(`forEach: `, `key: ${k},value: ${v}`); 111 });
2、散列表(HashTable类或HashMap类,它是Dictionary类的一种散列表实现方式)
1 class ValuePair { 2 constructor(key, value) { 3 this.key = key; 4 this.value = value; 5 } 6 7 toString() { 8 return `[#${this.key}: ${this.value}]`; 9 } 10 } 11 12 function defaultToString(item) { 13 if (item === null) { 14 return 'NULL'; 15 } if (item === undefined) { 16 return 'UNDEFINED'; 17 } if (typeof item === 'string' || item instanceof String) { 18 return `${item}`; 19 } 20 return item.toString(); 21 } 22 23 class HashTable { 24 constructor(toStrFn = defaultToString) { 25 this.toStrFn = toStrFn; 26 this.table = {}; 27 } 28 loseloseHashCode(key) { 29 if (typeof key === 'number') { 30 return key; 31 } 32 const tableKey = this.toStrFn(key); 33 let hash = 0; 34 for (let i = 0; i < tableKey.length; i++) { 35 hash += tableKey.charCodeAt(i); 36 } 37 return hash % 37; 38 } 39 hashCode(key) { 40 return this.loseloseHashCode(key); 41 } 42 put(key,value) { 43 if (key != null && value != null) { 44 const position = this.hashCode(key); 45 this.table[position] = new ValuePair(key, value); 46 return true; 47 } 48 return false; 49 } 50 get(key) { 51 const valuepair = this.table[this.hashCode(key)]; 52 return valuepair == null ? undefined : valuepair.value; 53 } 54 remove(key) { 55 const hash = this.hashCode(key); 56 const valuepair = this.table[hash]; 57 if (valuepair != null ) { 58 delete this.table[hash]; 59 return true; 60 } 61 return false; 62 } 63 getTable() { 64 return this.table; 65 } 66 67 isEmpty() { 68 return this.size() === 0; 69 } 70 71 size() { 72 return Object.keys(this.table).length; 73 } 74 75 clear() { 76 this.table = {}; 77 } 78 79 toString() { 80 if (this.isEmpty()) { 81 return ''; 82 } 83 const keys = Object.keys(this.table); 84 let objString = `{${keys[0]} => ${this.table[keys[0]].toString()}}`; 85 for (let i = 1; i < keys.length; i++) { 86 objString = `${objString},{${keys[i]} => ${this.table[keys[i]].toString()}}`; 87 } 88 return objString; 89 } 90 91 92 93 } 94 95 const hash = new HashTable(); 96 console.log(hash.hashCode('Bob') + '-Bob'); 97 hash.put('Bob', 20); 98 console.log(hash); 99 hash.put('Alice', 21); 100 hash.put('Jack', 19); 101 console.log(hash); 102 console.log(hash.get('Alice')); 103 hash.remove('Jack'); 104 console.log(hash);
二、散列表的扩展
问题:有时候,一些键会有相同的散列值,不同的值在散列表中对应相同位置的时候,称为冲突。(冲突会导致散列表只保存最新的值,旧的值会被覆盖)
解决冲突:分离链接、线性探查和双散列法。(我们研究前两个)
1、分离链接
//注:分离链接法包括为散列表的每一个位置创建一个链表并将元素存储在里面。(优点:简单;缺点:需要额外的存储空间)
//本质是将Hash表的位置当做链表处理,先找到位置,对元素进行处理
1 class ValuePair { 2 constructor(key, value) { 3 this.key = key; 4 this.value = value; 5 } 6 7 toString() { 8 return `[#${this.key}: ${this.value}]`; 9 } 10 } 11 12 function defaultToString(item) { 13 if (item === null) { 14 return 'NULL'; 15 } if (item === undefined) { 16 return 'UNDEFINED'; 17 } if (typeof item === 'string' || item instanceof String) { 18 return `${item}`; 19 } 20 return item.toString(); 21 } 22 //载入之前我们写好的Linkedlist 23 class Node { 24 constructor(element, next) { 25 this.element = element; 26 this.next = next; 27 } 28 } 29 30 function defaultEquals(a, b) { 31 return a === b; 32 } 33 34 class LinkedList { 35 constructor(equalsFn = defaultEquals) { 36 this.equalsFn = equalsFn; 37 this.count = 0; 38 this.head = undefined; 39 } 40 41 push(element){ 42 const node = new Node(element); 43 let current; 44 if (this.head == null){ 45 this.head = node; 46 } else { 47 current = this.head; 48 while (current.next != null) { 49 current = current.next; 50 } 51 current.next = node; 52 } 53 this.count++; 54 } 55 56 getElementAt(index) { 57 if (index >= 0 && index <= this.count) { 58 let node = this.head; 59 for (let i = 0; i < index && node != null; i++) { 60 node = node.next; 61 } 62 return node; 63 } 64 return undefined; 65 } 66 67 insert(element, index) { 68 if (index >= 0 && index <= this.count) { 69 const node = new Node(element); 70 if (index ==0 ) { 71 const current = this.head; 72 node.next = current; 73 this.head = node; 74 } else { 75 const previous = this.getElementAt(index - 1); 76 node.next = previous.next; 77 previous.next = node; 78 } 79 this.count++; 80 return true; 81 } 82 return false; 83 } 84 85 removeAt(index) { 86 if (index >= 0 && index < this.count) { 87 let current = this.head; 88 if (index == 0) { 89 this.head = current.next; 90 }else { 91 const previous = this.getElementAt(index - 1); 92 current = previous.next; 93 previous.next = current; 94 95 } 96 this.count--; 97 return current.element; 98 } 99 return undefined; 100 } 101 102 remove(element) { 103 const index = this.indexOf(element); 104 return this.removeAt(index); 105 } 106 107 indexOf(element) { 108 let current = this.head; 109 for (let i = 0; i < this.count && current != null; i++) { 110 if (this.equalsFn(element, current.element)) { 111 return i; 112 } 113 current = current.next; 114 } 115 return -1; 116 } 117 118 isEmpty() { 119 return this.size() === 0; 120 } 121 122 size() { 123 return this.count; 124 } 125 126 getHead() { 127 return this.head; 128 } 129 130 clear() { 131 this.head = undefined; 132 this.count = 0; 133 } 134 135 toString() { 136 if (this.head == null) { 137 return ''; 138 } 139 let objString = `${this.head.element}`; 140 let current = this.head.next; 141 for (let i = 1; i < this.size() && current != null; i++) { 142 objString = `${objString},${current.element}`; 143 current = current.next; 144 } 145 return objString; 146 } 147 } 148 149 //分离链接 150 class HashTableSeparateChaining { 151 constructor(toStrFn = defaultToString) { 152 this.toStrFn = toStrFn; 153 this.table = {}; 154 } 155 156 loseloseHashCode(key) { 157 if (typeof key === 'number') { 158 return key; 159 } 160 const tableKey = this.toStrFn(key); 161 let hash = 0; 162 for (let i = 0; i < tableKey.length; i++) { 163 hash += tableKey.charCodeAt(i); 164 } 165 return hash % 37; 166 } 167 168 hashCode(key) { 169 return this.loseloseHashCode(key); 170 } 171 172 put(key, value) { 173 if (key != null && value != null) { 174 const position = this.hashCode(key); 175 if (this.table[position] == null) { 176 this.table[position] = new LinkedList(); 177 } 178 this.table[position].push(new ValuePair(key, value)); 179 return true; 180 } 181 return false; 182 } 183 184 get(key) { 185 const position = this.hashCode(key); 186 const linkedList = this.table[position]; 187 if (linkedList != null && !linkedList.isEmpty()) { 188 let current = linkedList.getHead(); 189 while (current != null) { 190 if (current.element.key === key) { 191 return current.element.value; 192 } 193 current = current.next; 194 } 195 } 196 return undefined; 197 } 198 199 remove(key) { 200 const position = this.hashCode(key); 201 const linkedList = this.table[position]; 202 if (linkedList != null && !linkedList.isEmpty()) { 203 let current = linkedList.getHead(); 204 while (current != null) { 205 if (current.element.key === key) { 206 linkedList.remove(current.element); 207 if (linkedList.isEmpty()) { 208 delete this.table[position]; 209 } 210 return true; 211 } 212 current = current.next; 213 } 214 } 215 return false; 216 } 217 218 isEmpty() { 219 return this.size() === 0; 220 } 221 222 size() { 223 let count = 0; 224 Object.values(this.table).forEach(linkedList => { 225 count += linkedList.size(); 226 }); 227 return count; 228 } 229 230 clear() { 231 this.table = {}; 232 } 233 234 getTable() { 235 return this.table; 236 } 237 238 toString() { 239 if (this.isEmpty()) { 240 return ''; 241 } 242 const keys = Object.keys(this.table); 243 let objString = `{${keys[0]} => ${this.table[keys[0]].toString()}}`; 244 for (let i = 1; i < keys.length; i++) { 245 objString = `${objString},{${keys[i]} => ${this.table[ 246 keys[i] 247 ].toString()}}`; 248 } 249 return objString; 250 } 251 } 252 253 const hashTable = new HashTableSeparateChaining(); 254 255 hashTable.put('Ygritte', 'ygritte@email.com'); 256 hashTable.put('Jonathan', 'jonathan@email.com'); 257 hashTable.put('Jamie', 'jamie@email.com'); 258 hashTable.put('Jack', 'jack@email.com'); 259 hashTable.put('Jasmine', 'jasmine@email.com'); 260 hashTable.put('Jake', 'jake@email.com'); 261 hashTable.put('Nathan', 'nathan@email.com'); 262 hashTable.put('Athelstan', 'athelstan@email.com'); 263 hashTable.put('Sue', 'sue@email.com'); 264 hashTable.put('Aethelwulf', 'aethelwulf@email.com'); 265 hashTable.put('Sargeras', 'sargeras@email.com'); 266 267 console.log('**** Printing Hash **** '); 268 269 console.log(hashTable.toString());
2、线性探查
//注:其处理冲突的方法是将元素直接存储到表中,而不是单独的数据结构中。当在某个位置position添加新元素,出现冲突时,就尝试添加到position+1的位置,如果position+1的位置也被某元素占据,则继续迭代,直到一个“空闲”的位置。
//采用需要检验是否有必要将一个或多个元素移动到之前的位置的方法。
1 class ValuePair { 2 constructor(key, value) { 3 this.key = key; 4 this.value = value; 5 } 6 7 toString() { 8 return `[#${this.key}: ${this.value}]`; 9 } 10 } 11 12 function defaultToString(item) { 13 if (item === null) { 14 return 'NULL'; 15 } if (item === undefined) { 16 return 'UNDEFINED'; 17 } if (typeof item === 'string' || item instanceof String) { 18 return `${item}`; 19 } 20 return item.toString(); 21 } 22 23 //线性探查 24 class HashTableLinearProbing { 25 constructor(toStrFn = defaultToString) { 26 this.toStrFn = toStrFn; 27 this.table = {}; 28 } 29 30 loseloseHashCode(key) { 31 if (typeof key === 'number') { 32 return key; 33 } 34 const tableKey = this.toStrFn(key); 35 let hash = 0; 36 for (let i = 0; i < tableKey.length; i++) { 37 hash += tableKey.charCodeAt(i); 38 } 39 return hash % 37; 40 } 41 42 hashCode(key) { 43 return this.loseloseHashCode(key); 44 } 45 46 put(key, value) { 47 if (key != null && value != null) { 48 const position = this.hashCode(key); 49 if (this.table[position] == null) { 50 this.table[position] = new ValuePair(key, value); 51 } else { 52 let index = position + 1; 53 while (this.table[index] != null) { 54 index++; 55 } 56 this.table[index] = new ValuePair(key, value); 57 } 58 return true; 59 } 60 return false; 61 } 62 63 get(key) { 64 const position = this.hashCode(key); 65 if (this.table[position] != null) { 66 if (this.table[position].key === key) { 67 return this.table[position].value; 68 } 69 let index = position + 1; 70 while (this.table[index] != null && this.table[index].key !== key) { 71 index++; 72 } 73 if (this.table[index] != null && this.table[index].key === key) { 74 return this.table[position].value; 75 } 76 } 77 return undefined; 78 } 79 80 remove(key) { 81 const position = this.hashCode(key); 82 if (this.table[position] != null) { 83 if (this.table[position].key === key) { 84 delete this.table[position]; 85 this.verifyRemoveSideEffect(key, position); 86 return true; 87 } 88 let index = position + 1; 89 while (this.table[index] != null && this.table[index].key !== key) { 90 index++; 91 } 92 if (this.table[index] != null && this.table[index].key === key) { 93 delete this.table[index]; 94 this.verifyRemoveSideEffect(key, index); 95 return true; 96 } 97 } 98 return false; 99 } 100 101 verifyRemoveSideEffect(key, removedPosition) { 102 const hash = this.hashCode(key); 103 let index = removedPosition + 1; 104 while (this.table[index] != null) { 105 const posHash = this.hashCode(this.table[index].key); 106 if (posHash <= hash || posHash <= removedPosition) { 107 this.table[removedPosition] = this.table[index]; 108 delete this.table[index]; 109 removedPosition = index; 110 } 111 index++; 112 } 113 } 114 115 isEmpty() { 116 return this.size() === 0; 117 } 118 119 size() { 120 return Object.keys(this.table).length; 121 } 122 123 clear() { 124 this.table = {}; 125 } 126 127 getTable() { 128 return this.table; 129 } 130 131 toString() { 132 if (this.isEmpty()) { 133 return ''; 134 } 135 const keys = Object.keys(this.table); 136 let objString = `{${keys[0]} => ${this.table[keys[0]].toString()}}`; 137 for (let i = 1; i < keys.length; i++) { 138 objString = `${objString},{${keys[i]} => ${this.table[ 139 keys[i] 140 ].toString()}}`; 141 } 142 return objString; 143 } 144 } 145 146 147 const hashTable = new HashTableLinearProbing(); 148 hashTable.put('Jamie', 'jamie@email.com'); 149 hashTable.put('Jack', 'jack@email.com'); 150 hashTable.put('Jasmine', 'jasmine@email.com'); 151 hashTable.put('Jake', 'jake@email.com'); 152 hashTable.put('Nathan', 'nathan@email.com'); 153 hashTable.put('Athelstan', 'athelstan@email.com'); 154 hashTable.put('Sue', 'sue@email.com'); 155 hashTable.put('Aethelwulf', 'aethelwulf@email.com'); 156 hashTable.put('Sargeras', 'sargeras@email.com'); 157 158 console.log(hashTable); 159 console.log(hashTable.toString()); 160 console.log(hashTable.get('Nathan')); // nathan@email.com 161 162 hashTable.remove('Ygritte'); 163 console.log(hashTable.get('Ygritte')); // undefined 164 console.log(hashTable.toString());
三、小结
实现的lose lose散列函数并不是一个表现良好的散列函数,因为它会产生太多的冲突。一个表现良好的散列函数是由几个方面构成的:插入和检索元素的时间(即性能),以及较低的冲突可能性。例如djb2函数。
1 djb2HashCode(key) { 2 const tableKey = this.toStrFn(key); 3 let hash = 5381; 4 for(let i = 0; i < tableKey.length; i++) { 5 hash = (hash * 33) + tableKey.charCodeAt(i); 6 } 7 return hash % 1013; 8 }