mirror of
https://github.moeyy.xyz/https://github.com/trekhleb/javascript-algorithms.git
synced 2024-12-26 07:01:18 +08:00
Add HashTable.
This commit is contained in:
parent
97b8765a7d
commit
ce40b52e09
@ -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
|
||||
|
||||
|
36
src/data-structures/hash-table/HashTable.js
Normal file
36
src/data-structures/hash-table/HashTable.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
9
src/data-structures/hash-table/README.md
Normal file
9
src/data-structures/hash-table/README.md
Normal file
@ -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.
|
49
src/data-structures/hash-table/__test__/HashTable.test.js
Normal file
49
src/data-structures/hash-table/__test__/HashTable.test.js
Normal file
@ -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');
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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}`;
|
||||
}
|
||||
}
|
||||
|
@ -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 |
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user