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