Add HashTable.

This commit is contained in:
Oleksii Trekhleb 2018-03-28 09:29:47 +03:00
parent 97b8765a7d
commit ce40b52e09
9 changed files with 250 additions and 24 deletions

View File

@ -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

View 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);
}
}

View 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.

View 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');
});
});

View File

@ -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;
}

View File

@ -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}`;
}
}

View File

@ -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 |

View File

@ -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');
});
});

View File

@ -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');
});
});