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
|
## Data Structures
|
||||||
|
|
||||||
- [Linked List](https://github.com/trekhleb/javascript-algorithms/tree/master/src/data-structures/linked-list)
|
- [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
|
## 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;
|
this.head = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
append(value) {
|
append({ value, key = null }) {
|
||||||
const newNode = new LinkedListNode(value);
|
const newNode = new LinkedListNode({ value, key });
|
||||||
|
|
||||||
// If there is no head yet let's make new node a head.
|
// If there is no head yet let's make new node a head.
|
||||||
if (!this.head) {
|
if (!this.head) {
|
||||||
@ -27,20 +27,57 @@ export default class LinkedList {
|
|||||||
return newNode;
|
return newNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
prepend(value) {
|
prepend({ value, key = null }) {
|
||||||
const newNode = new LinkedListNode(value, this.head);
|
const newNode = new LinkedListNode({ value, key, next: this.head });
|
||||||
|
|
||||||
|
// Make new node to be a head.
|
||||||
this.head = newNode;
|
this.head = newNode;
|
||||||
|
|
||||||
return 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) {
|
if (!this.head) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let deletedNode = null;
|
let deletedNode = null;
|
||||||
|
|
||||||
|
// If the head must be deleted then make 2nd node to be a head.
|
||||||
if (this.head.value === value) {
|
if (this.head.value === value) {
|
||||||
deletedNode = this.head;
|
deletedNode = this.head;
|
||||||
this.head = this.head.next;
|
this.head = this.head.next;
|
||||||
@ -48,6 +85,7 @@ export default class LinkedList {
|
|||||||
|
|
||||||
let currentNode = this.head;
|
let currentNode = this.head;
|
||||||
|
|
||||||
|
// If next node must be deleted then make next node to be a next next one.
|
||||||
while (currentNode.next) {
|
while (currentNode.next) {
|
||||||
if (currentNode.next.value === value) {
|
if (currentNode.next.value === value) {
|
||||||
deletedNode = currentNode.next;
|
deletedNode = currentNode.next;
|
||||||
@ -59,12 +97,52 @@ export default class LinkedList {
|
|||||||
return deletedNode;
|
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() {
|
toArray() {
|
||||||
const listArray = [];
|
const listArray = [];
|
||||||
let currentNode = this.head;
|
let currentNode = this.head;
|
||||||
|
|
||||||
while (currentNode) {
|
while (currentNode) {
|
||||||
listArray.push(currentNode.value);
|
listArray.push(currentNode.toString());
|
||||||
currentNode = currentNode.next;
|
currentNode = currentNode.next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,17 @@
|
|||||||
export default class LinkedListNode {
|
export default class LinkedListNode {
|
||||||
constructor(value, next = null) {
|
constructor({ value, next = null, key = null }) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.next = next;
|
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 |
|
|Operation |Complexity |
|
||||||
|---------------------------|-------------------|
|
|---------------------------|-------------------|
|
||||||
|Indexing |O(n) |
|
|Find |O(n) |
|
||||||
|Insert/delete at beginning |O(1) |
|
|Insert/delete at beginning |O(1) |
|
||||||
|Insert/delete in middle |O(1) + search time |
|
|Insert/delete in middle |O(1) + search time |
|
||||||
|Insert/delete at end |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', () => {
|
it('should append node to linked list', () => {
|
||||||
const linkedList = new LinkedList();
|
const linkedList = new LinkedList();
|
||||||
|
|
||||||
const node1 = linkedList.append(1);
|
const node1 = linkedList.append({ value: 1 });
|
||||||
const node2 = linkedList.append(2);
|
const node2 = linkedList.append({ value: 2, key: 'test' });
|
||||||
|
|
||||||
expect(node1.value).toBe(1);
|
expect(node1.value).toBe(1);
|
||||||
expect(node2.value).toBe(2);
|
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', () => {
|
it('should prepend node to linked list', () => {
|
||||||
const linkedList = new LinkedList();
|
const linkedList = new LinkedList();
|
||||||
|
|
||||||
const node1 = linkedList.append(1);
|
const node1 = linkedList.append({ value: 1 });
|
||||||
const node2 = linkedList.prepend(2);
|
const node2 = linkedList.prepend({ value: 2 });
|
||||||
|
|
||||||
expect(node1.value).toBe(1);
|
expect(node1.value).toBe(1);
|
||||||
expect(node2.value).toBe(2);
|
expect(node2.value).toBe(2);
|
||||||
@ -33,21 +34,53 @@ describe('LinkedList', () => {
|
|||||||
it('should delete node by value from linked list', () => {
|
it('should delete node by value from linked list', () => {
|
||||||
const linkedList = new LinkedList();
|
const linkedList = new LinkedList();
|
||||||
|
|
||||||
linkedList.append(1);
|
linkedList.append({ value: 1 });
|
||||||
linkedList.append(2);
|
linkedList.append({ value: 2 });
|
||||||
linkedList.append(3);
|
linkedList.append({ value: 3 });
|
||||||
linkedList.append(3);
|
linkedList.append({ value: 3 });
|
||||||
linkedList.append(4);
|
linkedList.append({ value: 4 });
|
||||||
linkedList.append(5);
|
linkedList.append({ value: 5 });
|
||||||
|
|
||||||
const deletedNode = linkedList.delete(3);
|
const deletedNode = linkedList.deleteByValue(3);
|
||||||
expect(deletedNode.value).toBe(3);
|
expect(deletedNode.value).toBe(3);
|
||||||
expect(linkedList.toString()).toBe('1,2,3,4,5');
|
expect(linkedList.toString()).toBe('1,2,3,4,5');
|
||||||
|
|
||||||
linkedList.delete(3);
|
linkedList.deleteByValue(3);
|
||||||
expect(linkedList.toString()).toBe('1,2,4,5');
|
expect(linkedList.toString()).toBe('1,2,4,5');
|
||||||
|
|
||||||
linkedList.delete(1);
|
linkedList.deleteByValue(1);
|
||||||
expect(linkedList.toString()).toBe('2,4,5');
|
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';
|
import LinkedListNode from '../LinkedListNode';
|
||||||
|
|
||||||
describe('LinkedListNode', () => {
|
describe('LinkedListNode', () => {
|
||||||
it('should create list node with value', () => {
|
it('should create list node with kay and value', () => {
|
||||||
const node = new LinkedListNode(1);
|
const node = new LinkedListNode({ value: 1, key: 'test' });
|
||||||
expect(node.value).toBe(1);
|
expect(node.value).toBe(1);
|
||||||
|
expect(node.key).toBe('test');
|
||||||
expect(node.next).toBeNull();
|
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