|
5 | 5 | import standardEqualsFunction from "../utils/standardEqualsFunction.js";
|
6 | 6 | import standardHashCodeFunction from "../utils/standardHashCodeFunction.js";
|
7 | 7 |
|
8 |
| -const HASH_KEY_PREFIX = "h-"; |
| 8 | +const DEFAULT_LOAD_FACTOR = 0.75; |
| 9 | +const INITIAL_CAPACITY = 16 |
9 | 10 |
|
10 | 11 | export default class HashMap {
|
11 | 12 |
|
12 | 13 | constructor(hashFunction, equalsFunction) {
|
13 |
| - this.data = {}; |
| 14 | + this.buckets = new Array(INITIAL_CAPACITY); |
| 15 | + this.threshold = Math.floor(INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); |
| 16 | + this.itemCount = 0; |
14 | 17 | this.hashFunction = hashFunction || standardHashCodeFunction;
|
15 | 18 | this.equalsFunction = equalsFunction || standardEqualsFunction;
|
16 | 19 | }
|
17 | 20 |
|
18 | 21 | set(key, value) {
|
19 |
| - const hashKey = HASH_KEY_PREFIX + this.hashFunction(key); |
20 |
| - if (hashKey in this.data) { |
21 |
| - const entries = this.data[hashKey]; |
22 |
| - for (let i = 0; i < entries.length; i++) { |
23 |
| - const entry = entries[i]; |
24 |
| - if (this.equalsFunction(key, entry.key)) { |
25 |
| - const oldValue = entry.value; |
26 |
| - entry.value = value; |
27 |
| - return oldValue; |
28 |
| - } |
29 |
| - } |
30 |
| - entries.push({key:key, value:value}); |
| 22 | + this._expand(); |
| 23 | + const slot = this._getSlot(key); |
| 24 | + let bucket = this.buckets[slot]; |
| 25 | + if (!bucket) { |
| 26 | + bucket = [[key, value]]; |
| 27 | + this.buckets[slot] = bucket; |
| 28 | + this.itemCount++; |
31 | 29 | return value;
|
| 30 | + } |
| 31 | + const existing = bucket.find(pair => this.equalsFunction(pair[0], key), this); |
| 32 | + if(existing) { |
| 33 | + const result = existing[1]; |
| 34 | + existing[1] = value; |
| 35 | + return result; |
32 | 36 | } else {
|
33 |
| - this.data[hashKey] = [{key:key, value:value}]; |
| 37 | + bucket.push([key, value]); |
| 38 | + this.itemCount++; |
34 | 39 | return value;
|
35 | 40 | }
|
36 | 41 | }
|
37 | 42 |
|
38 | 43 | containsKey(key) {
|
39 |
| - const hashKey = HASH_KEY_PREFIX + this.hashFunction(key); |
40 |
| - if(hashKey in this.data) { |
41 |
| - const entries = this.data[hashKey]; |
42 |
| - for (let i = 0; i < entries.length; i++) { |
43 |
| - const entry = entries[i]; |
44 |
| - if (this.equalsFunction(key, entry.key)) |
45 |
| - return true; |
46 |
| - } |
| 44 | + const bucket = this._getBucket(key); |
| 45 | + if(!bucket) { |
| 46 | + return false; |
47 | 47 | }
|
48 |
| - return false; |
| 48 | + const existing = bucket.find(pair => this.equalsFunction(pair[0], key), this); |
| 49 | + return !!existing; |
49 | 50 | }
|
50 | 51 |
|
51 | 52 | get(key) {
|
52 |
| - const hashKey = HASH_KEY_PREFIX + this.hashFunction(key); |
53 |
| - if(hashKey in this.data) { |
54 |
| - const entries = this.data[hashKey]; |
55 |
| - for (let i = 0; i < entries.length; i++) { |
56 |
| - const entry = entries[i]; |
57 |
| - if (this.equalsFunction(key, entry.key)) |
58 |
| - return entry.value; |
59 |
| - } |
| 53 | + const bucket = this._getBucket(key); |
| 54 | + if(!bucket) { |
| 55 | + return null; |
60 | 56 | }
|
61 |
| - return null; |
| 57 | + const existing = bucket.find(pair => this.equalsFunction(pair[0], key), this); |
| 58 | + return existing ? existing[1] : null; |
62 | 59 | }
|
63 | 60 |
|
64 | 61 | entries() {
|
65 |
| - return Object.keys(this.data).filter(key => key.startsWith(HASH_KEY_PREFIX)).flatMap(key => this.data[key], this); |
| 62 | + return this.buckets.filter(b => b != null).flat(1); |
66 | 63 | }
|
67 | 64 |
|
68 | 65 | getKeys() {
|
69 |
| - return this.entries().map(e => e.key); |
| 66 | + return this.entries().map(pair => pair[0]); |
70 | 67 | }
|
71 | 68 |
|
72 | 69 | getValues() {
|
73 |
| - return this.entries().map(e => e.value); |
| 70 | + return this.entries().map(pair => pair[1]); |
74 | 71 | }
|
75 | 72 |
|
76 | 73 | toString() {
|
77 |
| - const ss = this.entries().map(e => '{' + e.key + ':' + e.value + '}'); |
| 74 | + const ss = this.entries().map(e => '{' + e[0] + ':' + e[1] + '}'); |
78 | 75 | return '[' + ss.join(", ") + ']';
|
79 | 76 | }
|
80 | 77 |
|
81 | 78 | get length() {
|
82 |
| - return Object.keys(this.data).filter(key => key.startsWith(HASH_KEY_PREFIX)).map(key => this.data[key].length, this).reduce((accum, item) => accum + item, 0); |
| 79 | + return this.itemCount; |
| 80 | + } |
| 81 | + |
| 82 | + _getSlot(key) { |
| 83 | + const hash = this.hashFunction(key); |
| 84 | + return hash & this.buckets.length - 1; |
| 85 | + } |
| 86 | + _getBucket(key) { |
| 87 | + return this.buckets[this._getSlot(key)]; |
83 | 88 | }
|
| 89 | + |
| 90 | + _expand() { |
| 91 | + if (this.itemCount <= this.threshold) { |
| 92 | + return; |
| 93 | + } |
| 94 | + const old_buckets = this.buckets; |
| 95 | + const newCapacity = this.buckets.length * 2; |
| 96 | + this.buckets = new Array(newCapacity); |
| 97 | + this.threshold = Math.floor(newCapacity * DEFAULT_LOAD_FACTOR); |
| 98 | + for (const bucket of old_buckets) { |
| 99 | + if (!bucket) { |
| 100 | + continue; |
| 101 | + } |
| 102 | + for (const pair of bucket) { |
| 103 | + const slot = this._getSlot(pair[0]); |
| 104 | + let newBucket = this.buckets[slot]; |
| 105 | + if (!newBucket) { |
| 106 | + newBucket = []; |
| 107 | + this.buckets[slot] = newBucket; |
| 108 | + } |
| 109 | + newBucket.push(pair); |
| 110 | + } |
| 111 | + } |
| 112 | + } |
| 113 | + |
84 | 114 | }
|
0 commit comments