diff --git a/README.md b/README.md index b310af6b..9ad51701 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ ## Data Structures - [Linked List](https://github.com/trekhleb/javascript-algorithms/tree/master/src/data-structures/linked-list) +- [Hash Table](https://github.com/trekhleb/javascript-algorithms/tree/master/src/data-structures/hash-table) ## Running Tests diff --git a/src/data-structures/hash-table/HashTable.js b/src/data-structures/hash-table/HashTable.js new file mode 100644 index 00000000..f8fec1cd --- /dev/null +++ b/src/data-structures/hash-table/HashTable.js @@ -0,0 +1,36 @@ +import LinkedList from '../linked-list/LinkedList'; + +const defaultHashTableSize = 32; + +export default class HashTable { + constructor(hashTableSize = defaultHashTableSize) { + // Create hash table of certain size and fill each bucket with empty linked list. + this.buckets = Array(hashTableSize).fill(null).map(() => new LinkedList()); + } + + // Converts key string to hash number. + hash(key) { + const hash = Array.from(key).reduce( + (hashAccumulator, keySymbol) => (hashAccumulator + keySymbol.charCodeAt(0)), + 0, + ); + + // Reduce hash number so it would fit hash table size. + return hash % this.buckets.length; + } + + insert(key, value) { + const bucketLinkedList = this.buckets[this.hash(key)]; + bucketLinkedList.appendUnique({ key, value }); + } + + delete(key) { + const bucketLinkedList = this.buckets[this.hash(key)]; + return bucketLinkedList.deleteByKey(key); + } + + get(key) { + const bucketLinkedList = this.buckets[this.hash(key)]; + return bucketLinkedList.findByKey(key); + } +} diff --git a/src/data-structures/hash-table/README.md b/src/data-structures/hash-table/README.md new file mode 100644 index 00000000..16837c3c --- /dev/null +++ b/src/data-structures/hash-table/README.md @@ -0,0 +1,9 @@ +# Hashed Table + +|Operation |Complexity | +|---------------------------|-------------------| +|Find |O(1)* | +|Insert |O(1)* | +|Delete |O(1)* | + +* - assuming that we have "good" hash function and big enough hash table size so that collisions are rare. diff --git a/src/data-structures/hash-table/__test__/HashTable.test.js b/src/data-structures/hash-table/__test__/HashTable.test.js new file mode 100644 index 00000000..71e18cb8 --- /dev/null +++ b/src/data-structures/hash-table/__test__/HashTable.test.js @@ -0,0 +1,49 @@ +import HashTable from '../HashTable'; + +describe('HashTable', () => { + it('should create hash table of certain size', () => { + const defaultHashTable = new HashTable(); + expect(defaultHashTable.buckets.length).toBe(32); + + const biggerHashTable = new HashTable(64); + expect(biggerHashTable.buckets.length).toBe(64); + }); + + it('should generate proper hash for specified keys', () => { + const hashTable = new HashTable(); + + expect(hashTable.hash('a')).toBe(1); + expect(hashTable.hash('b')).toBe(2); + expect(hashTable.hash('abc')).toBe(6); + }); + + it('should insert, read and delete data with collisions', () => { + const hashTable = new HashTable(3); + + expect(hashTable.hash('a')).toBe(1); + expect(hashTable.hash('b')).toBe(2); + expect(hashTable.hash('c')).toBe(0); + expect(hashTable.hash('d')).toBe(1); + + hashTable.insert('a', 'sky-old'); + hashTable.insert('a', 'sky'); + hashTable.insert('b', 'sea'); + hashTable.insert('c', 'earth'); + hashTable.insert('d', 'ocean'); + + expect(hashTable.buckets[0].toString()).toBe('c:earth'); + expect(hashTable.buckets[1].toString()).toBe('a:sky,d:ocean'); + expect(hashTable.buckets[2].toString()).toBe('b:sea'); + + expect(hashTable.get('a').value).toBe('sky'); + expect(hashTable.get('d').value).toBe('ocean'); + + hashTable.delete('a'); + + expect(hashTable.get('a')).toBeNull(); + expect(hashTable.get('d').value).toBe('ocean'); + + hashTable.insert('d', 'ocean-new'); + expect(hashTable.get('d').value).toBe('ocean-new'); + }); +}); diff --git a/src/data-structures/linked-list/LinkedList.js b/src/data-structures/linked-list/LinkedList.js index 8ec15358..82937301 100644 --- a/src/data-structures/linked-list/LinkedList.js +++ b/src/data-structures/linked-list/LinkedList.js @@ -5,8 +5,8 @@ export default class LinkedList { this.head = null; } - append(value) { - const newNode = new LinkedListNode(value); + append({ value, key = null }) { + const newNode = new LinkedListNode({ value, key }); // If there is no head yet let's make new node a head. if (!this.head) { @@ -27,20 +27,57 @@ export default class LinkedList { return newNode; } - prepend(value) { - const newNode = new LinkedListNode(value, this.head); + prepend({ value, key = null }) { + const newNode = new LinkedListNode({ value, key, next: this.head }); + + // Make new node to be a head. this.head = newNode; return newNode; } - delete(value) { + appendUnique({ value, key = null }) { + const newNode = new LinkedListNode({ value, key }); + + // If there is no head yet let's make new node a head. + if (!this.head) { + this.head = newNode; + + return newNode; + } + + // Rewind to last node. + let currentNode = this.head; + while (currentNode.next !== null) { + // If there is a node with specified key exists then update it instead of adding new one. + if (key && currentNode.key === key) { + currentNode.value = value; + return currentNode; + } + + currentNode = currentNode.next; + } + + // If there is a node with specified key exists then update it instead of adding new one. + if (key && currentNode.key === key) { + currentNode.value = value; + return currentNode; + } + + // Attach new node to the end of linked list. + currentNode.next = newNode; + + return newNode; + } + + deleteByValue(value) { if (!this.head) { return null; } let deletedNode = null; + // If the head must be deleted then make 2nd node to be a head. if (this.head.value === value) { deletedNode = this.head; this.head = this.head.next; @@ -48,6 +85,7 @@ export default class LinkedList { let currentNode = this.head; + // If next node must be deleted then make next node to be a next next one. while (currentNode.next) { if (currentNode.next.value === value) { deletedNode = currentNode.next; @@ -59,12 +97,52 @@ export default class LinkedList { return deletedNode; } + deleteByKey(key) { + if (!this.head) { + return null; + } + + let deletedNode = null; + + // If the head must be deleted then make 2nd node to be a head. + if (this.head.key === key) { + deletedNode = this.head; + this.head = this.head.next; + } + + let currentNode = this.head; + + // If next node must be deleted then make next node to be a next next one. + while (currentNode.next) { + if (currentNode.next.key === key) { + deletedNode = currentNode.next; + currentNode.next = currentNode.next.next; + } + currentNode = currentNode.next; + } + + return deletedNode; + } + + findByKey(key) { + let currentNode = this.head; + + while (currentNode) { + if (currentNode.key === key) { + return currentNode; + } + currentNode = currentNode.next; + } + + return null; + } + toArray() { const listArray = []; let currentNode = this.head; while (currentNode) { - listArray.push(currentNode.value); + listArray.push(currentNode.toString()); currentNode = currentNode.next; } diff --git a/src/data-structures/linked-list/LinkedListNode.js b/src/data-structures/linked-list/LinkedListNode.js index f45ef1ac..58d900db 100644 --- a/src/data-structures/linked-list/LinkedListNode.js +++ b/src/data-structures/linked-list/LinkedListNode.js @@ -1,6 +1,17 @@ export default class LinkedListNode { - constructor(value, next = null) { + constructor({ value, next = null, key = null }) { this.value = value; this.next = next; + + // Key is added to make this linked list nodes to be reusable in hash tables. + this.key = key; + } + + toString() { + if (this.key) { + return `${this.key}:${this.value}`; + } + + return `${this.value}`; } } diff --git a/src/data-structures/linked-list/README.md b/src/data-structures/linked-list/README.md index 9b541d17..c7e662f7 100644 --- a/src/data-structures/linked-list/README.md +++ b/src/data-structures/linked-list/README.md @@ -2,7 +2,7 @@ |Operation |Complexity | |---------------------------|-------------------| -|Indexing |O(n) | +|Find |O(n) | |Insert/delete at beginning |O(1) | |Insert/delete in middle |O(1) + search time | |Insert/delete at end |O(1) + search time | diff --git a/src/data-structures/linked-list/__test__/LinkedList.test.js b/src/data-structures/linked-list/__test__/LinkedList.test.js index 0731ca5f..ff6ddefb 100644 --- a/src/data-structures/linked-list/__test__/LinkedList.test.js +++ b/src/data-structures/linked-list/__test__/LinkedList.test.js @@ -9,20 +9,21 @@ describe('LinkedList', () => { it('should append node to linked list', () => { const linkedList = new LinkedList(); - const node1 = linkedList.append(1); - const node2 = linkedList.append(2); + const node1 = linkedList.append({ value: 1 }); + const node2 = linkedList.append({ value: 2, key: 'test' }); expect(node1.value).toBe(1); expect(node2.value).toBe(2); + expect(node2.key).toBe('test'); - expect(linkedList.toString()).toBe('1,2'); + expect(linkedList.toString()).toBe('1,test:2'); }); it('should prepend node to linked list', () => { const linkedList = new LinkedList(); - const node1 = linkedList.append(1); - const node2 = linkedList.prepend(2); + const node1 = linkedList.append({ value: 1 }); + const node2 = linkedList.prepend({ value: 2 }); expect(node1.value).toBe(1); expect(node2.value).toBe(2); @@ -33,21 +34,53 @@ describe('LinkedList', () => { it('should delete node by value from linked list', () => { const linkedList = new LinkedList(); - linkedList.append(1); - linkedList.append(2); - linkedList.append(3); - linkedList.append(3); - linkedList.append(4); - linkedList.append(5); + linkedList.append({ value: 1 }); + linkedList.append({ value: 2 }); + linkedList.append({ value: 3 }); + linkedList.append({ value: 3 }); + linkedList.append({ value: 4 }); + linkedList.append({ value: 5 }); - const deletedNode = linkedList.delete(3); + const deletedNode = linkedList.deleteByValue(3); expect(deletedNode.value).toBe(3); expect(linkedList.toString()).toBe('1,2,3,4,5'); - linkedList.delete(3); + linkedList.deleteByValue(3); expect(linkedList.toString()).toBe('1,2,4,5'); - linkedList.delete(1); + linkedList.deleteByValue(1); expect(linkedList.toString()).toBe('2,4,5'); }); + + it('should delete node by key from linked list', () => { + const linkedList = new LinkedList(); + + linkedList.append({ value: 1, key: 'test1' }); + linkedList.append({ value: 2, key: 'test2' }); + linkedList.append({ value: 3, key: 'test3' }); + + const deletedNode = linkedList.deleteByKey('test2'); + expect(deletedNode.key).toBe('test2'); + expect(linkedList.toString()).toBe('test1:1,test3:3'); + }); + + it('should append unique nodes', () => { + const linkedList = new LinkedList(); + + linkedList.appendUnique({ value: 1, key: 'test1' }); + linkedList.appendUnique({ value: 2, key: 'test2' }); + linkedList.appendUnique({ value: 3, key: 'test2' }); + + expect(linkedList.toString()).toBe('test1:1,test2:3'); + }); + + it('should find node by its key', () => { + const linkedList = new LinkedList(); + + linkedList.appendUnique({ value: 1, key: 'test1' }); + linkedList.appendUnique({ value: 2, key: 'test2' }); + linkedList.appendUnique({ value: 3, key: 'test3' }); + + expect(linkedList.findByKey('test3').toString()).toBe('test3:3'); + }); }); diff --git a/src/data-structures/linked-list/__test__/LinkedListNode.test.js b/src/data-structures/linked-list/__test__/LinkedListNode.test.js index db5b4d7a..19bf3c9b 100644 --- a/src/data-structures/linked-list/__test__/LinkedListNode.test.js +++ b/src/data-structures/linked-list/__test__/LinkedListNode.test.js @@ -1,9 +1,18 @@ import LinkedListNode from '../LinkedListNode'; describe('LinkedListNode', () => { - it('should create list node with value', () => { - const node = new LinkedListNode(1); + it('should create list node with kay and value', () => { + const node = new LinkedListNode({ value: 1, key: 'test' }); expect(node.value).toBe(1); + expect(node.key).toBe('test'); expect(node.next).toBeNull(); }); + + it('should convert node to string', () => { + const node = new LinkedListNode({ value: 1 }); + const nodeWithKey = new LinkedListNode({ value: 1, key: 'test' }); + + expect(node.toString()).toBe('1'); + expect(nodeWithKey.toString()).toBe('test:1'); + }); });