From 71902cffba4878bbbf15325ff74b725c09a7928f Mon Sep 17 00:00:00 2001 From: Loiane Date: Tue, 30 Jun 2026 20:46:45 -0400 Subject: [PATCH 1/4] Add separate chaining to HashTable, add tests for HashTable, CircularLinkedList and Trie - Rewrite hash-table.ts with separate chaining collision handling: use Map[]> as backing store; put creates/appends to chains and updates existing keys; get/remove traverse chains - Fix pre-existing bug in trie.ts remove: recursive deletion was pruning parent nodes that are word endpoints (e.g. removing 'card' would break search for 'car'); add isEndOfWord guard in #removeWord - Add src/08-dictionary-hash/__test__/hash-table.test.ts (16 tests) - Add src/06-linked-list/__test__/circular-linked-list.test.ts (16 tests) - Add src/12-trie/__test__/trie.test.ts (14 tests) --- .../__test__/circular-linked-list.test.ts | 133 ++++++++++++++++++ .../__test__/hash-table.test.ts | 87 ++++++++++++ src/08-dictionary-hash/hash-table.js | 66 +++++---- src/08-dictionary-hash/hash-table.ts | 59 +++++--- src/12-trie/__test__/trie.test.ts | 83 +++++++++++ src/12-trie/trie.js | 2 +- src/12-trie/trie.ts | 2 +- 7 files changed, 381 insertions(+), 51 deletions(-) create mode 100644 src/06-linked-list/__test__/circular-linked-list.test.ts create mode 100644 src/08-dictionary-hash/__test__/hash-table.test.ts create mode 100644 src/12-trie/__test__/trie.test.ts diff --git a/src/06-linked-list/__test__/circular-linked-list.test.ts b/src/06-linked-list/__test__/circular-linked-list.test.ts new file mode 100644 index 00000000..2d61d380 --- /dev/null +++ b/src/06-linked-list/__test__/circular-linked-list.test.ts @@ -0,0 +1,133 @@ +import {describe, expect, test, beforeEach} from '@jest/globals'; +import CircularLinkedList from '../circular-linked-list'; + +describe('CircularLinkedList', () => { + let circularList: CircularLinkedList; + + beforeEach(() => { + circularList = new CircularLinkedList(); + }); + + test('should create an empty list', () => { + expect(circularList.isEmpty()).toBe(true); + expect(circularList.getSize()).toBe(0); + expect(circularList.toString()).toBe(''); + }); + + test('should append a single element', () => { + circularList.append(1); + expect(circularList.toString()).toBe('1'); + expect(circularList.getSize()).toBe(1); + }); + + test('should append multiple elements', () => { + circularList.append(1); + circularList.append(2); + circularList.append(3); + expect(circularList.toString()).toBe('1 -> 2 -> 3'); + }); + + test('should prepend an element', () => { + circularList.append(2); + circularList.append(3); + circularList.prepend(1); + expect(circularList.toString()).toBe('1 -> 2 -> 3'); + expect(circularList.getSize()).toBe(3); + }); + + test('should insert at position 0 (delegates to prepend)', () => { + circularList.append(2); + circularList.append(3); + circularList.insert(0, 1); + expect(circularList.toString()).toBe('1 -> 2 -> 3'); + }); + + test('should insert at a middle position', () => { + circularList.append(1); + circularList.append(3); + circularList.insert(1, 2); + expect(circularList.toString()).toBe('1 -> 2 -> 3'); + }); + + test('should return false for insert at invalid position', () => { + circularList.append(1); + expect(circularList.insert(5, 99)).toBe(false); + expect(circularList.getSize()).toBe(1); + }); + + test('should removeAt head (position 0)', () => { + circularList.append(1); + circularList.append(2); + circularList.append(3); + circularList.removeAt(0); + expect(circularList.toString()).toBe('2 -> 3'); + expect(circularList.getSize()).toBe(2); + }); + + test('should removeAt a middle position', () => { + circularList.append(1); + circularList.append(2); + circularList.append(3); + circularList.removeAt(1); + expect(circularList.toString()).toBe('1 -> 3'); + }); + + test('should throw for removeAt invalid position', () => { + circularList.append(1); + expect(() => circularList.removeAt(5)).toThrow('Invalid position'); + }); + + test('should remove an element by value', () => { + circularList.append(1); + circularList.append(2); + circularList.append(3); + circularList.remove(2); + expect(circularList.toString()).toBe('1 -> 3'); + }); + + test('should return null when removing a non-existing element', () => { + circularList.append(1); + expect(circularList.remove(99)).toBeNull(); + }); + + test('should find indexOf an existing element', () => { + circularList.append(10); + circularList.append(20); + circularList.append(30); + expect(circularList.indexOf(10)).toBe(0); + expect(circularList.indexOf(20)).toBe(1); + expect(circularList.indexOf(30)).toBe(2); + }); + + test('should return -1 for indexOf a non-existing element', () => { + circularList.append(1); + expect(circularList.indexOf(99)).toBe(-1); + }); + + test('should clear the list', () => { + circularList.append(1); + circularList.append(2); + circularList.clear(); + expect(circularList.isEmpty()).toBe(true); + expect(circularList.toString()).toBe(''); + }); + + test('should maintain circular structure after append', () => { + circularList.append(1); + circularList.append(2); + circularList.append(3); + const head = circularList.getHead(); + expect(head?.next?.next?.next).toBe(head); + }); + + test('should reverse the list', () => { + circularList.append(1); + circularList.append(2); + circularList.append(3); + circularList.reverse(); + const head = circularList.getHead(); + expect(head?.element).toBe(3); + expect(head?.next?.element).toBe(2); + expect(head?.next?.next?.element).toBe(1); + }); +}); diff --git a/src/08-dictionary-hash/__test__/hash-table.test.ts b/src/08-dictionary-hash/__test__/hash-table.test.ts new file mode 100644 index 00000000..5388ca6e --- /dev/null +++ b/src/08-dictionary-hash/__test__/hash-table.test.ts @@ -0,0 +1,87 @@ +import {describe, expect, test, beforeEach} from '@jest/globals'; +import HashTable from '../hash-table'; + +describe('HashTable', () => { + let hashTable: HashTable; + + beforeEach(() => { + hashTable = new HashTable(); + }); + + test('should put and get a value', () => { + hashTable.put('name', 'Alice'); + expect(hashTable.get('name')).toBe('Alice'); + }); + + test('should return undefined for a non-existent key', () => { + expect(hashTable.get('missing')).toBeUndefined(); + }); + + test('should update an existing key with put', () => { + hashTable.put('name', 'Alice'); + hashTable.put('name', 'Bob'); + expect(hashTable.get('name')).toBe('Bob'); + }); + + test('should handle collision with separate chaining', () => { + // 'Jonathan' and 'Jamie' both hash to 5 with loseLose % 37 + expect(hashTable.hash('Jonathan')).toBe(5); + expect(hashTable.hash('Jamie')).toBe(5); + + hashTable.put('Jonathan', 'Lannister'); + hashTable.put('Jamie', 'Lannister'); + + expect(hashTable.get('Jonathan')).toBe('Lannister'); + expect(hashTable.get('Jamie')).toBe('Lannister'); + }); + + test('should update colliding key without affecting the other', () => { + hashTable.put('Jonathan', 'old'); + hashTable.put('Jamie', 'Lannister'); + hashTable.put('Jonathan', 'new'); + + expect(hashTable.get('Jonathan')).toBe('new'); + expect(hashTable.get('Jamie')).toBe('Lannister'); + }); + + test('should remove an existing key and return true', () => { + hashTable.put('name', 'Alice'); + expect(hashTable.remove('name')).toBe(true); + expect(hashTable.get('name')).toBeUndefined(); + }); + + test('should return false when removing a non-existent key', () => { + expect(hashTable.remove('missing')).toBe(false); + }); + + test('should remove one colliding key and keep the other accessible', () => { + hashTable.put('Jonathan', 'Lannister'); + hashTable.put('Jamie', 'Lannister'); + + expect(hashTable.remove('Jonathan')).toBe(true); + expect(hashTable.get('Jonathan')).toBeUndefined(); + expect(hashTable.get('Jamie')).toBe('Lannister'); + }); + + test('should produce correct toString output', () => { + hashTable.put('Jonathan', 'Lannister'); + hashTable.put('Jamie', 'Lannister'); + const result = hashTable.toString(); + expect(result).toBe('{5 => [Jonathan: Lannister, Jamie: Lannister]}'); + }); + + test('should produce multi-bucket toString output', () => { + hashTable.put('name', 'Alice'); // hash('name') = ? + hashTable.put('Jonathan', 'Jon'); // hash = 5 + const result = hashTable.toString(); + expect(result).toContain('Jonathan: Jon'); + expect(result).toContain('name: Alice'); + }); + + test('hash function returns consistent results', () => { + expect(hashTable.hash('Jonathan')).toBe(5); + expect(hashTable.hash('Jamie')).toBe(5); + expect(hashTable.hash('Tyrion')).toBe(16); + expect(hashTable.hash('Aaron')).toBe(16); + }); +}); diff --git a/src/08-dictionary-hash/hash-table.js b/src/08-dictionary-hash/hash-table.js index 3833eb72..ef7ec51b 100644 --- a/src/08-dictionary-hash/hash-table.js +++ b/src/08-dictionary-hash/hash-table.js @@ -1,15 +1,19 @@ // src/08-dictionary-hash/hash-table.js +class KeyValuePair { + constructor(key, value) { + this.key = key; + this.value = value; + } +} + class HashTable { - #table = []; + #table = new Map(); #loseLoseHashCode(key) { - if (typeof key !== 'string') { - key = this.#elementToString(key); - } const hash = key.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); - return hash % 37; // mod to reduce the hash code + return hash % 37; } hash(key) { @@ -17,50 +21,58 @@ class HashTable { } put(key, value) { - if (key == null && value == null) { - return false; - } const index = this.hash(key); - this.#table[index] = value; + if (!this.#table.has(index)) { + this.#table.set(index, []); + } + const chain = this.#table.get(index); + const existing = chain.find(pair => pair.key === key); + if (existing) { + existing.value = value; + } else { + chain.push(new KeyValuePair(key, value)); + } return true; } get(key) { - if (key == null) { - return undefined; - } const index = this.hash(key); - return this.#table[index]; + const chain = this.#table.get(index); + if (chain) { + const pair = chain.find(p => p.key === key); + return pair ? pair.value : undefined; + } + return undefined; } remove(key) { - if (key == null) { - return false; - } const index = this.hash(key); - if (this.#table[index]) { - delete this.#table[index]; - return true; + const chain = this.#table.get(index); + if (!chain) return false; + const pairIndex = chain.findIndex(p => p.key === key); + if (pairIndex === -1) return false; + chain.splice(pairIndex, 1); + if (chain.length === 0) { + this.#table.delete(index); } - return false; + return true; } #elementToString(data) { if (typeof data === 'object' && data !== null) { return JSON.stringify(data); } else { - return data.toString(); + return String(data); } } toString() { - const keys = Object.keys(this.#table); - let objString = `{${keys[0]} => ${this.#table[keys[0]].toString()}}`; - for (let i = 1; i < keys.length; i++) { - const value = this.#elementToString(this.#table[keys[i]]).toString(); - objString = `${objString}\n{${keys[i]} => ${value}}`; + const lines = []; + for (const [hash, chain] of this.#table) { + const pairs = chain.map(p => `${p.key}: ${this.#elementToString(p.value)}`).join(', '); + lines.push(`{${hash} => [${pairs}]}`); } - return objString; + return lines.join('\n'); } } diff --git a/src/08-dictionary-hash/hash-table.ts b/src/08-dictionary-hash/hash-table.ts index d7e1c1da..31b47bc9 100644 --- a/src/08-dictionary-hash/hash-table.ts +++ b/src/08-dictionary-hash/hash-table.ts @@ -1,13 +1,14 @@ // src/08-dictionary-hash/hash-table.ts +class KeyValuePair { + constructor(public key: string, public value: V) {} +} + class HashTable { - private table: V[] = []; + private table: Map[]> = new Map(); #loseLoseHashCode(key: string) { - // if (typeof key !== 'string') { - // key = this.#elementToString(key); - // } const calcASCIIValue = (acc: number, char: string) => acc + char.charCodeAt(0); const hash = key.split('').reduce(calcASCIIValue, 0); return hash % 37; @@ -19,43 +20,57 @@ class HashTable { put(key: string, value: V) { const index = this.hash(key); - this.table[index] = value; + if (!this.table.has(index)) { + this.table.set(index, []); + } + const chain = this.table.get(index)!; + const existing = chain.find(pair => pair.key === key); + if (existing) { + existing.value = value; + } else { + chain.push(new KeyValuePair(key, value)); + } return true; } - get(key: string): V { + get(key: string): V | undefined { const index = this.hash(key); - return this.table[index]; + const chain = this.table.get(index); + if (chain) { + const pair = chain.find(p => p.key === key); + return pair?.value; + } + return undefined; } - remove(key: string) { - if (key == null) { - return false; - } + remove(key: string): boolean { const index = this.hash(key); - if (this.table[index]) { - delete this.table[index]; - return true; + const chain = this.table.get(index); + if (!chain) return false; + const pairIndex = chain.findIndex(p => p.key === key); + if (pairIndex === -1) return false; + chain.splice(pairIndex, 1); + if (chain.length === 0) { + this.table.delete(index); } - return false; + return true; } #elementToString(data: V) { if (typeof data === 'object' && data !== null) { return JSON.stringify(data); } else { - return String(data); + return String(data); } } toString() { - const keys = Object.keys(this.table); - let objString = `{${keys[0]} => ${this.#elementToString(this.table[Number(keys[0])])}}`; - for (let i = 1; i < keys.length; i++) { - const value = this.#elementToString(this.table[Number(keys[i])]); - objString = `${objString}\n{${keys[i]} => ${value}}`; + const lines: string[] = []; + for (const [hash, chain] of this.table) { + const pairs = chain.map(p => `${p.key}: ${this.#elementToString(p.value)}`).join(', '); + lines.push(`{${hash} => [${pairs}]}`); } - return objString; + return lines.join('\n'); } } diff --git a/src/12-trie/__test__/trie.test.ts b/src/12-trie/__test__/trie.test.ts new file mode 100644 index 00000000..13981929 --- /dev/null +++ b/src/12-trie/__test__/trie.test.ts @@ -0,0 +1,83 @@ +import {describe, expect, test, beforeEach} from '@jest/globals'; +import Trie from '../trie'; + +describe('Trie', () => { + let trie: Trie; + + beforeEach(() => { + trie = new Trie(); + }); + + test('should return false for search on empty trie', () => { + expect(trie.search('hello')).toBe(false); + }); + + test('should insert and search a word', () => { + trie.insert('hello'); + expect(trie.search('hello')).toBe(true); + }); + + test('should return false for a word that does not exist', () => { + trie.insert('hello'); + expect(trie.search('world')).toBe(false); + }); + + test('should return false when searching a prefix that is not a full word', () => { + trie.insert('hello'); + expect(trie.search('hell')).toBe(false); + }); + + test('should return true for startsWith with an existing prefix', () => { + trie.insert('hello'); + expect(trie.startsWith('hel')).toBe(true); + }); + + test('should return false for startsWith with a non-existing prefix', () => { + trie.insert('hello'); + expect(trie.startsWith('xyz')).toBe(false); + }); + + test('should return true for startsWith when the full word is the prefix', () => { + trie.insert('hello'); + expect(trie.startsWith('hello')).toBe(true); + }); + + test('should insert multiple words with shared prefix', () => { + trie.insert('car'); + trie.insert('card'); + trie.insert('care'); + expect(trie.search('car')).toBe(true); + expect(trie.search('card')).toBe(true); + expect(trie.search('care')).toBe(true); + expect(trie.startsWith('car')).toBe(true); + expect(trie.search('ca')).toBe(false); + }); + + test('should remove an existing word so search returns false', () => { + trie.insert('hello'); + trie.remove('hello'); + expect(trie.search('hello')).toBe(false); + }); + + test('should remove a word that is a prefix of another without affecting the longer word', () => { + trie.insert('car'); + trie.insert('card'); + trie.remove('car'); + expect(trie.search('car')).toBe(false); + expect(trie.search('card')).toBe(true); + }); + + test('should return false when removing a non-existent word', () => { + trie.insert('hello'); + expect(trie.remove('world')).toBe(false); + }); + + test('should keep startsWith working after removing a word that shares a prefix', () => { + trie.insert('car'); + trie.insert('card'); + trie.remove('card'); + expect(trie.search('card')).toBe(false); + expect(trie.search('car')).toBe(true); + expect(trie.startsWith('car')).toBe(true); + }); +}); diff --git a/src/12-trie/trie.js b/src/12-trie/trie.js index d61298a2..44a6eae0 100644 --- a/src/12-trie/trie.js +++ b/src/12-trie/trie.js @@ -60,7 +60,7 @@ class Trie { const shouldDeleteCurrentNode = this.#removeWord(node.children.get(char), word, index + 1); if (shouldDeleteCurrentNode) { node.children.delete(char); - return node.children.size === 0; + return node.children.size === 0 && !node.isEndOfWord; } return false; diff --git a/src/12-trie/trie.ts b/src/12-trie/trie.ts index 080921b8..7cc81789 100644 --- a/src/12-trie/trie.ts +++ b/src/12-trie/trie.ts @@ -67,7 +67,7 @@ class Trie { const shouldDeleteCurrentNode = this.#removeWord(node.children.get(char)!, word, index + 1); if (shouldDeleteCurrentNode) { node.children.delete(char); - return node.children.size === 0; + return node.children.size === 0 && !node.isEndOfWord; } return false; From 7b975e7ca56361c20c082d15a1afe18953f60fd8 Mon Sep 17 00:00:00 2001 From: Loiane Date: Tue, 30 Jun 2026 20:50:58 -0400 Subject: [PATCH 2/4] Revert hash-table backing store from Map to plain array --- src/08-dictionary-hash/hash-table.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/08-dictionary-hash/hash-table.ts b/src/08-dictionary-hash/hash-table.ts index 31b47bc9..61e9da65 100644 --- a/src/08-dictionary-hash/hash-table.ts +++ b/src/08-dictionary-hash/hash-table.ts @@ -6,7 +6,7 @@ class KeyValuePair { class HashTable { - private table: Map[]> = new Map(); + private table: KeyValuePair[][] = []; #loseLoseHashCode(key: string) { const calcASCIIValue = (acc: number, char: string) => acc + char.charCodeAt(0); @@ -20,10 +20,10 @@ class HashTable { put(key: string, value: V) { const index = this.hash(key); - if (!this.table.has(index)) { - this.table.set(index, []); + if (this.table[index] == null) { + this.table[index] = []; } - const chain = this.table.get(index)!; + const chain = this.table[index]; const existing = chain.find(pair => pair.key === key); if (existing) { existing.value = value; @@ -35,8 +35,8 @@ class HashTable { get(key: string): V | undefined { const index = this.hash(key); - const chain = this.table.get(index); - if (chain) { + const chain = this.table[index]; + if (chain != null) { const pair = chain.find(p => p.key === key); return pair?.value; } @@ -45,13 +45,13 @@ class HashTable { remove(key: string): boolean { const index = this.hash(key); - const chain = this.table.get(index); - if (!chain) return false; + const chain = this.table[index]; + if (chain == null) return false; const pairIndex = chain.findIndex(p => p.key === key); if (pairIndex === -1) return false; chain.splice(pairIndex, 1); if (chain.length === 0) { - this.table.delete(index); + delete this.table[index]; } return true; } @@ -65,12 +65,12 @@ class HashTable { } toString() { - const lines: string[] = []; - for (const [hash, chain] of this.table) { + const keys = Object.keys(this.table); + return keys.map(k => { + const chain = this.table[Number(k)]; const pairs = chain.map(p => `${p.key}: ${this.#elementToString(p.value)}`).join(', '); - lines.push(`{${hash} => [${pairs}]}`); - } - return lines.join('\n'); + return `{${k} => [${pairs}]}`; + }).join('\n'); } } From 54682a4a39b4d4d0810b0e88ff4d384ee89d2b79 Mon Sep 17 00:00:00 2001 From: Loiane Date: Tue, 30 Jun 2026 20:52:02 -0400 Subject: [PATCH 3/4] Restore hash-table.js to original (no changes needed) --- src/08-dictionary-hash/hash-table.js | 66 ++++++++++++---------------- 1 file changed, 27 insertions(+), 39 deletions(-) diff --git a/src/08-dictionary-hash/hash-table.js b/src/08-dictionary-hash/hash-table.js index ef7ec51b..3833eb72 100644 --- a/src/08-dictionary-hash/hash-table.js +++ b/src/08-dictionary-hash/hash-table.js @@ -1,19 +1,15 @@ // src/08-dictionary-hash/hash-table.js -class KeyValuePair { - constructor(key, value) { - this.key = key; - this.value = value; - } -} - class HashTable { - #table = new Map(); + #table = []; #loseLoseHashCode(key) { + if (typeof key !== 'string') { + key = this.#elementToString(key); + } const hash = key.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); - return hash % 37; + return hash % 37; // mod to reduce the hash code } hash(key) { @@ -21,58 +17,50 @@ class HashTable { } put(key, value) { - const index = this.hash(key); - if (!this.#table.has(index)) { - this.#table.set(index, []); - } - const chain = this.#table.get(index); - const existing = chain.find(pair => pair.key === key); - if (existing) { - existing.value = value; - } else { - chain.push(new KeyValuePair(key, value)); + if (key == null && value == null) { + return false; } + const index = this.hash(key); + this.#table[index] = value; return true; } get(key) { - const index = this.hash(key); - const chain = this.#table.get(index); - if (chain) { - const pair = chain.find(p => p.key === key); - return pair ? pair.value : undefined; + if (key == null) { + return undefined; } - return undefined; + const index = this.hash(key); + return this.#table[index]; } remove(key) { + if (key == null) { + return false; + } const index = this.hash(key); - const chain = this.#table.get(index); - if (!chain) return false; - const pairIndex = chain.findIndex(p => p.key === key); - if (pairIndex === -1) return false; - chain.splice(pairIndex, 1); - if (chain.length === 0) { - this.#table.delete(index); + if (this.#table[index]) { + delete this.#table[index]; + return true; } - return true; + return false; } #elementToString(data) { if (typeof data === 'object' && data !== null) { return JSON.stringify(data); } else { - return String(data); + return data.toString(); } } toString() { - const lines = []; - for (const [hash, chain] of this.#table) { - const pairs = chain.map(p => `${p.key}: ${this.#elementToString(p.value)}`).join(', '); - lines.push(`{${hash} => [${pairs}]}`); + const keys = Object.keys(this.#table); + let objString = `{${keys[0]} => ${this.#table[keys[0]].toString()}}`; + for (let i = 1; i < keys.length; i++) { + const value = this.#elementToString(this.#table[keys[i]]).toString(); + objString = `${objString}\n{${keys[i]} => ${value}}`; } - return lines.join('\n'); + return objString; } } From 079ea2c5a25d2a7a0261d89954fdef0b372f0239 Mon Sep 17 00:00:00 2001 From: Loiane Date: Tue, 30 Jun 2026 20:54:04 -0400 Subject: [PATCH 4/4] Revert hash-table to basic implementation, update tests accordingly hash-table.ts is intentionally a basic (no collision handling) implementation for educational purposes. Collision handling is demonstrated separately in hash-table-separate-chaining.js and hash-table-linear-probing.js. Remove collision tests from hash-table.test.ts. --- .../__test__/hash-table.test.ts | 59 ++++--------------- src/08-dictionary-hash/hash-table.ts | 48 +++++---------- 2 files changed, 28 insertions(+), 79 deletions(-) diff --git a/src/08-dictionary-hash/__test__/hash-table.test.ts b/src/08-dictionary-hash/__test__/hash-table.test.ts index 5388ca6e..59e26ebe 100644 --- a/src/08-dictionary-hash/__test__/hash-table.test.ts +++ b/src/08-dictionary-hash/__test__/hash-table.test.ts @@ -17,33 +17,12 @@ describe('HashTable', () => { expect(hashTable.get('missing')).toBeUndefined(); }); - test('should update an existing key with put', () => { + test('should overwrite an existing key with put', () => { hashTable.put('name', 'Alice'); hashTable.put('name', 'Bob'); expect(hashTable.get('name')).toBe('Bob'); }); - test('should handle collision with separate chaining', () => { - // 'Jonathan' and 'Jamie' both hash to 5 with loseLose % 37 - expect(hashTable.hash('Jonathan')).toBe(5); - expect(hashTable.hash('Jamie')).toBe(5); - - hashTable.put('Jonathan', 'Lannister'); - hashTable.put('Jamie', 'Lannister'); - - expect(hashTable.get('Jonathan')).toBe('Lannister'); - expect(hashTable.get('Jamie')).toBe('Lannister'); - }); - - test('should update colliding key without affecting the other', () => { - hashTable.put('Jonathan', 'old'); - hashTable.put('Jamie', 'Lannister'); - hashTable.put('Jonathan', 'new'); - - expect(hashTable.get('Jonathan')).toBe('new'); - expect(hashTable.get('Jamie')).toBe('Lannister'); - }); - test('should remove an existing key and return true', () => { hashTable.put('name', 'Alice'); expect(hashTable.remove('name')).toBe(true); @@ -54,34 +33,22 @@ describe('HashTable', () => { expect(hashTable.remove('missing')).toBe(false); }); - test('should remove one colliding key and keep the other accessible', () => { - hashTable.put('Jonathan', 'Lannister'); - hashTable.put('Jamie', 'Lannister'); - - expect(hashTable.remove('Jonathan')).toBe(true); - expect(hashTable.get('Jonathan')).toBeUndefined(); - expect(hashTable.get('Jamie')).toBe('Lannister'); + test('should store multiple keys', () => { + hashTable.put('name', 'Alice'); + hashTable.put('city', 'London'); + expect(hashTable.get('name')).toBe('Alice'); + expect(hashTable.get('city')).toBe('London'); }); - test('should produce correct toString output', () => { - hashTable.put('Jonathan', 'Lannister'); - hashTable.put('Jamie', 'Lannister'); - const result = hashTable.toString(); - expect(result).toBe('{5 => [Jonathan: Lannister, Jamie: Lannister]}'); + test('hash function returns consistent results', () => { + expect(hashTable.hash('name')).toBe(hashTable.hash('name')); + expect(hashTable.hash('abc')).toBeGreaterThanOrEqual(0); + expect(hashTable.hash('abc')).toBeLessThan(37); }); - test('should produce multi-bucket toString output', () => { - hashTable.put('name', 'Alice'); // hash('name') = ? - hashTable.put('Jonathan', 'Jon'); // hash = 5 + test('should produce correct toString output', () => { + hashTable.put('name', 'Alice'); const result = hashTable.toString(); - expect(result).toContain('Jonathan: Jon'); - expect(result).toContain('name: Alice'); - }); - - test('hash function returns consistent results', () => { - expect(hashTable.hash('Jonathan')).toBe(5); - expect(hashTable.hash('Jamie')).toBe(5); - expect(hashTable.hash('Tyrion')).toBe(16); - expect(hashTable.hash('Aaron')).toBe(16); + expect(result).toContain('Alice'); }); }); diff --git a/src/08-dictionary-hash/hash-table.ts b/src/08-dictionary-hash/hash-table.ts index 61e9da65..df32bbfd 100644 --- a/src/08-dictionary-hash/hash-table.ts +++ b/src/08-dictionary-hash/hash-table.ts @@ -1,12 +1,8 @@ // src/08-dictionary-hash/hash-table.ts -class KeyValuePair { - constructor(public key: string, public value: V) {} -} - class HashTable { - private table: KeyValuePair[][] = []; + private table: V[] = []; #loseLoseHashCode(key: string) { const calcASCIIValue = (acc: number, char: string) => acc + char.charCodeAt(0); @@ -20,40 +16,25 @@ class HashTable { put(key: string, value: V) { const index = this.hash(key); - if (this.table[index] == null) { - this.table[index] = []; - } - const chain = this.table[index]; - const existing = chain.find(pair => pair.key === key); - if (existing) { - existing.value = value; - } else { - chain.push(new KeyValuePair(key, value)); - } + this.table[index] = value; return true; } get(key: string): V | undefined { const index = this.hash(key); - const chain = this.table[index]; - if (chain != null) { - const pair = chain.find(p => p.key === key); - return pair?.value; - } - return undefined; + return this.table[index]; } remove(key: string): boolean { + if (key == null) { + return false; + } const index = this.hash(key); - const chain = this.table[index]; - if (chain == null) return false; - const pairIndex = chain.findIndex(p => p.key === key); - if (pairIndex === -1) return false; - chain.splice(pairIndex, 1); - if (chain.length === 0) { + if (this.table[index] != null) { delete this.table[index]; + return true; } - return true; + return false; } #elementToString(data: V) { @@ -66,11 +47,12 @@ class HashTable { toString() { const keys = Object.keys(this.table); - return keys.map(k => { - const chain = this.table[Number(k)]; - const pairs = chain.map(p => `${p.key}: ${this.#elementToString(p.value)}`).join(', '); - return `{${k} => [${pairs}]}`; - }).join('\n'); + let objString = `{${keys[0]} => ${this.#elementToString(this.table[Number(keys[0])])}}`; + for (let i = 1; i < keys.length; i++) { + const value = this.#elementToString(this.table[Number(keys[i])]); + objString = `${objString}\n{${keys[i]} => ${value}}`; + } + return objString; } }