Skip to content

Commit 6fffa79

Browse files
authored
structure: hash table (TheAlgorithms#108)
* structure: hash table * fix: requested changes & dynamic capacity * feat: adds test for overwriting existing values
1 parent 957100c commit 6fffa79

File tree

2 files changed

+326
-0
lines changed

2 files changed

+326
-0
lines changed

data_structures/hashing/hash_table.ts

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
/**
2+
* Represents a hash table.
3+
* Time complexity:
4+
* - Set, Get, Delete, Has: O(1) on average, O(n) in the worst case.
5+
* - Clear: O(m) where m is the number of buckets.
6+
* - Keys, Values, Entires: O(n + m).
7+
*
8+
* @template K The key type.
9+
* @template V The value type.
10+
* @param size The size of the hash table.
11+
* @param buckets The buckets in which to store the key-value pairs.
12+
* @param loadFactor The load factor to determine when to resize the hash table.
13+
*/
14+
export class HashTable<K, V> {
15+
private size!: number;
16+
private buckets!: HashTableEntry<K, V>[][];
17+
private readonly loadFactor = 0.75;
18+
19+
constructor() {
20+
this.clear();
21+
}
22+
23+
/**
24+
* Gets the size.
25+
*
26+
* @returns The size.
27+
*/
28+
getSize(): number {
29+
return this.size;
30+
}
31+
32+
/**
33+
* Sets a key-value pair.
34+
*
35+
* @param key The key.
36+
* @param value The value.
37+
*/
38+
set(key: K, value: V): void {
39+
const loadFactor = this.size / this.buckets.length;
40+
if (loadFactor > this.loadFactor) {
41+
this.resize();
42+
}
43+
44+
const index = this.hash(key);
45+
const bucket = this.buckets[index];
46+
47+
if (bucket.length === 0) {
48+
bucket.push(new HashTableEntry(key, value));
49+
this.size++;
50+
return;
51+
}
52+
53+
for (const entry of bucket) {
54+
if (entry.key === key) {
55+
entry.value = value;
56+
return;
57+
}
58+
}
59+
60+
bucket.push(new HashTableEntry(key, value));
61+
this.size++;
62+
}
63+
64+
/**
65+
* Gets a value.
66+
*
67+
* @param key The key to get the value for.
68+
* @returns The value or null if the key does not exist.
69+
*/
70+
get(key: K): V | null {
71+
const index = this.hash(key);
72+
const bucket = this.buckets[index];
73+
74+
for (const entry of bucket) {
75+
if (entry.key === key) {
76+
return entry.value;
77+
}
78+
}
79+
80+
return null;
81+
}
82+
83+
/**
84+
* Deletes a key-value pair.
85+
*
86+
* @param key The key whose key-value pair to delete.
87+
*/
88+
delete(key: K): void {
89+
const index = this.hash(key);
90+
const bucket = this.buckets[index];
91+
92+
for (const entry of bucket) {
93+
if (entry.key === key) {
94+
bucket.splice(bucket.indexOf(entry), 1);
95+
this.size--;
96+
return;
97+
}
98+
}
99+
}
100+
101+
/**
102+
* Checks if a key exists.
103+
*
104+
* @param key The key.
105+
* @returns Whether the key exists.
106+
*/
107+
has(key: K): boolean {
108+
const index = this.hash(key);
109+
const bucket = this.buckets[index];
110+
111+
for (const entry of bucket) {
112+
if (entry.key === key) {
113+
return true;
114+
}
115+
}
116+
117+
return false;
118+
}
119+
120+
/**
121+
* Clears the hash table.
122+
*/
123+
clear(): void {
124+
this.size = 0;
125+
this.initializeBuckets(16);
126+
}
127+
128+
/**
129+
* Gets all keys.
130+
*
131+
* @returns The keys.
132+
*/
133+
keys(): K[] {
134+
const keys: K[] = [];
135+
for (const bucket of this.buckets) {
136+
for (const entry of bucket) {
137+
keys.push(entry.key);
138+
}
139+
}
140+
141+
return keys;
142+
}
143+
144+
/**
145+
* Gets all values.
146+
*
147+
* @returns The values.
148+
*/
149+
values(): V[] {
150+
const values: V[] = [];
151+
for (const bucket of this.buckets) {
152+
for (const entry of bucket) {
153+
values.push(entry.value);
154+
}
155+
}
156+
157+
return values;
158+
}
159+
160+
/**
161+
* Gets all entries.
162+
*
163+
* @returns The entries.
164+
*/
165+
entries(): HashTableEntry<K, V>[] {
166+
const entries: HashTableEntry<K, V>[] = [];
167+
for (const bucket of this.buckets) {
168+
for (const entry of bucket) {
169+
entries.push(entry);
170+
}
171+
}
172+
173+
return entries;
174+
}
175+
176+
/**
177+
* Initializes the buckets.
178+
*
179+
* @param amount The amount of buckets to initialize.
180+
*/
181+
private initializeBuckets(amount: number): void {
182+
this.buckets = [];
183+
for (let i = 0; i < amount; i++) {
184+
this.buckets.push([]);
185+
}
186+
}
187+
188+
/**
189+
* Hashes a key to an index.
190+
* This implementation uses the djb2 algorithm, which might not be the best.
191+
* Feel free to change it to something else.
192+
*
193+
* @param key The key.
194+
* @return The index.
195+
*/
196+
protected hash(key: K): number {
197+
let hash = 0;
198+
199+
for (let i = 0; i < String(key).length; i++) {
200+
hash = (hash << 5) - hash + String(key).charCodeAt(i);
201+
}
202+
203+
return hash % this.buckets.length;
204+
}
205+
206+
/**
207+
* Resizes the hash table by doubling the amount of buckets.
208+
*/
209+
private resize(): void {
210+
this.initializeBuckets(this.buckets.length * 2);
211+
this.size = 0;
212+
213+
for (const entry of this.entries()) {
214+
this.set(entry.key, entry.value);
215+
}
216+
}
217+
}
218+
219+
/**
220+
* Represents a key-value pair.
221+
*
222+
* @template K The type of the key.
223+
* @template V The type of the value.
224+
* @param key The key.
225+
* @param value The value.
226+
*/
227+
class HashTableEntry<K, V> {
228+
key: K;
229+
value: V;
230+
231+
constructor(key: K, value: V) {
232+
this.key = key;
233+
this.value = value;
234+
}
235+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { HashTable } from "../hash_table";
2+
3+
describe("Hash Table", () => {
4+
let hashTable: HashTable<string, number>;
5+
beforeEach(() => {
6+
hashTable = new HashTable();
7+
});
8+
9+
it("should set a value", () => {
10+
hashTable.set("a", 1);
11+
12+
expect(hashTable.values()).toEqual([1]);
13+
});
14+
15+
it("should override a value", () => {
16+
hashTable.set("a", 1);
17+
hashTable.set("a", 2);
18+
19+
expect(hashTable.values()).toEqual([2]);
20+
});
21+
22+
it("should get a value", () => {
23+
hashTable.set("a", 1);
24+
25+
expect(hashTable.get("a")).toBe(1);
26+
});
27+
28+
it("should get null if key does not exist", () => {
29+
expect(hashTable.get("a")).toBeNull();
30+
});
31+
32+
it("should delete a value", () => {
33+
hashTable.set("a", 1);
34+
hashTable.delete("a");
35+
36+
expect(hashTable.get("a")).toBeNull();
37+
});
38+
39+
it("should do nothing on delete if key does not exist", () => {
40+
hashTable.delete("a");
41+
42+
expect(hashTable.get("a")).toBeNull();
43+
});
44+
45+
it("should return true if key exists", () => {
46+
hashTable.set("a", 1);
47+
48+
expect(hashTable.has("a")).toBe(true);
49+
});
50+
51+
it("should return false if key does not exist", () => {
52+
expect(hashTable.has("a")).toBe(false);
53+
});
54+
55+
it("should clear the hash table", () => {
56+
hashTable.set("a", 1);
57+
hashTable.set("b", 2);
58+
hashTable.set("c", 3);
59+
hashTable.clear();
60+
61+
expect(hashTable.getSize()).toBe(0);
62+
});
63+
64+
it("should return all keys", () => {
65+
hashTable.set("a", 1);
66+
hashTable.set("b", 2);
67+
hashTable.set("c", 3);
68+
69+
expect(hashTable.keys()).toEqual(["a", "b", "c"]);
70+
});
71+
72+
it("should return all values", () => {
73+
hashTable.set("a", 1);
74+
hashTable.set("b", 2);
75+
hashTable.set("c", 3);
76+
77+
expect(hashTable.values()).toEqual([1, 2, 3]);
78+
});
79+
80+
it("should return all key-value pairs", () => {
81+
hashTable.set("a", 1);
82+
hashTable.set("b", 2);
83+
hashTable.set("c", 3);
84+
85+
expect(hashTable.entries()).toEqual([
86+
{ key: "a", value: 1 },
87+
{ key: "b", value: 2 },
88+
{ key: "c", value: 3 },
89+
]);
90+
});
91+
});

0 commit comments

Comments
 (0)