Skip to content

Commit ce40b52

Browse files
committedMar 28, 2018
Add HashTable.
1 parent 97b8765 commit ce40b52

File tree

9 files changed

+250
-24
lines changed

9 files changed

+250
-24
lines changed
 

‎README.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Data Structures
44

55
- [Linked List](https://github.com/trekhleb/javascript-algorithms/tree/master/src/data-structures/linked-list)
6+
- [Hash Table](https://github.com/trekhleb/javascript-algorithms/tree/master/src/data-structures/hash-table)
67

78
## Running Tests
89

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import LinkedList from '../linked-list/LinkedList';
2+
3+
const defaultHashTableSize = 32;
4+
5+
export default class HashTable {
6+
constructor(hashTableSize = defaultHashTableSize) {
7+
// Create hash table of certain size and fill each bucket with empty linked list.
8+
this.buckets = Array(hashTableSize).fill(null).map(() => new LinkedList());
9+
}
10+
11+
// Converts key string to hash number.
12+
hash(key) {
13+
const hash = Array.from(key).reduce(
14+
(hashAccumulator, keySymbol) => (hashAccumulator + keySymbol.charCodeAt(0)),
15+
0,
16+
);
17+
18+
// Reduce hash number so it would fit hash table size.
19+
return hash % this.buckets.length;
20+
}
21+
22+
insert(key, value) {
23+
const bucketLinkedList = this.buckets[this.hash(key)];
24+
bucketLinkedList.appendUnique({ key, value });
25+
}
26+
27+
delete(key) {
28+
const bucketLinkedList = this.buckets[this.hash(key)];
29+
return bucketLinkedList.deleteByKey(key);
30+
}
31+
32+
get(key) {
33+
const bucketLinkedList = this.buckets[this.hash(key)];
34+
return bucketLinkedList.findByKey(key);
35+
}
36+
}
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Hashed Table
2+
3+
|Operation |Complexity |
4+
|---------------------------|-------------------|
5+
|Find |O(1)* |
6+
|Insert |O(1)* |
7+
|Delete |O(1)* |
8+
9+
* - assuming that we have "good" hash function and big enough hash table size so that collisions are rare.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import HashTable from '../HashTable';
2+
3+
describe('HashTable', () => {
4+
it('should create hash table of certain size', () => {
5+
const defaultHashTable = new HashTable();
6+
expect(defaultHashTable.buckets.length).toBe(32);
7+
8+
const biggerHashTable = new HashTable(64);
9+
expect(biggerHashTable.buckets.length).toBe(64);
10+
});
11+
12+
it('should generate proper hash for specified keys', () => {
13+
const hashTable = new HashTable();
14+
15+
expect(hashTable.hash('a')).toBe(1);
16+
expect(hashTable.hash('b')).toBe(2);
17+
expect(hashTable.hash('abc')).toBe(6);
18+
});
19+
20+
it('should insert, read and delete data with collisions', () => {
21+
const hashTable = new HashTable(3);
22+
23+
expect(hashTable.hash('a')).toBe(1);
24+
expect(hashTable.hash('b')).toBe(2);
25+
expect(hashTable.hash('c')).toBe(0);
26+
expect(hashTable.hash('d')).toBe(1);
27+
28+
hashTable.insert('a', 'sky-old');
29+
hashTable.insert('a', 'sky');
30+
hashTable.insert('b', 'sea');
31+
hashTable.insert('c', 'earth');
32+
hashTable.insert('d', 'ocean');
33+
34+
expect(hashTable.buckets[0].toString()).toBe('c:earth');
35+
expect(hashTable.buckets[1].toString()).toBe('a:sky,d:ocean');
36+
expect(hashTable.buckets[2].toString()).toBe('b:sea');
37+
38+
expect(hashTable.get('a').value).toBe('sky');
39+
expect(hashTable.get('d').value).toBe('ocean');
40+
41+
hashTable.delete('a');
42+
43+
expect(hashTable.get('a')).toBeNull();
44+
expect(hashTable.get('d').value).toBe('ocean');
45+
46+
hashTable.insert('d', 'ocean-new');
47+
expect(hashTable.get('d').value).toBe('ocean-new');
48+
});
49+
});

‎src/data-structures/linked-list/LinkedList.js

+84-6
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ export default class LinkedList {
55
this.head = null;
66
}
77

8-
append(value) {
9-
const newNode = new LinkedListNode(value);
8+
append({ value, key = null }) {
9+
const newNode = new LinkedListNode({ value, key });
1010

1111
// If there is no head yet let's make new node a head.
1212
if (!this.head) {
@@ -27,27 +27,65 @@ export default class LinkedList {
2727
return newNode;
2828
}
2929

30-
prepend(value) {
31-
const newNode = new LinkedListNode(value, this.head);
30+
prepend({ value, key = null }) {
31+
const newNode = new LinkedListNode({ value, key, next: this.head });
32+
33+
// Make new node to be a head.
3234
this.head = newNode;
3335

3436
return newNode;
3537
}
3638

37-
delete(value) {
39+
appendUnique({ value, key = null }) {
40+
const newNode = new LinkedListNode({ value, key });
41+
42+
// If there is no head yet let's make new node a head.
43+
if (!this.head) {
44+
this.head = newNode;
45+
46+
return newNode;
47+
}
48+
49+
// Rewind to last node.
50+
let currentNode = this.head;
51+
while (currentNode.next !== null) {
52+
// If there is a node with specified key exists then update it instead of adding new one.
53+
if (key && currentNode.key === key) {
54+
currentNode.value = value;
55+
return currentNode;
56+
}
57+
58+
currentNode = currentNode.next;
59+
}
60+
61+
// If there is a node with specified key exists then update it instead of adding new one.
62+
if (key && currentNode.key === key) {
63+
currentNode.value = value;
64+
return currentNode;
65+
}
66+
67+
// Attach new node to the end of linked list.
68+
currentNode.next = newNode;
69+
70+
return newNode;
71+
}
72+
73+
deleteByValue(value) {
3874
if (!this.head) {
3975
return null;
4076
}
4177

4278
let deletedNode = null;
4379

80+
// If the head must be deleted then make 2nd node to be a head.
4481
if (this.head.value === value) {
4582
deletedNode = this.head;
4683
this.head = this.head.next;
4784
}
4885

4986
let currentNode = this.head;
5087

88+
// If next node must be deleted then make next node to be a next next one.
5189
while (currentNode.next) {
5290
if (currentNode.next.value === value) {
5391
deletedNode = currentNode.next;
@@ -59,12 +97,52 @@ export default class LinkedList {
5997
return deletedNode;
6098
}
6199

100+
deleteByKey(key) {
101+
if (!this.head) {
102+
return null;
103+
}
104+
105+
let deletedNode = null;
106+
107+
// If the head must be deleted then make 2nd node to be a head.
108+
if (this.head.key === key) {
109+
deletedNode = this.head;
110+
this.head = this.head.next;
111+
}
112+
113+
let currentNode = this.head;
114+
115+
// If next node must be deleted then make next node to be a next next one.
116+
while (currentNode.next) {
117+
if (currentNode.next.key === key) {
118+
deletedNode = currentNode.next;
119+
currentNode.next = currentNode.next.next;
120+
}
121+
currentNode = currentNode.next;
122+
}
123+
124+
return deletedNode;
125+
}
126+
127+
findByKey(key) {
128+
let currentNode = this.head;
129+
130+
while (currentNode) {
131+
if (currentNode.key === key) {
132+
return currentNode;
133+
}
134+
currentNode = currentNode.next;
135+
}
136+
137+
return null;
138+
}
139+
62140
toArray() {
63141
const listArray = [];
64142
let currentNode = this.head;
65143

66144
while (currentNode) {
67-
listArray.push(currentNode.value);
145+
listArray.push(currentNode.toString());
68146
currentNode = currentNode.next;
69147
}
70148

Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
export default class LinkedListNode {
2-
constructor(value, next = null) {
2+
constructor({ value, next = null, key = null }) {
33
this.value = value;
44
this.next = next;
5+
6+
// Key is added to make this linked list nodes to be reusable in hash tables.
7+
this.key = key;
8+
}
9+
10+
toString() {
11+
if (this.key) {
12+
return `${this.key}:${this.value}`;
13+
}
14+
15+
return `${this.value}`;
516
}
617
}

‎src/data-structures/linked-list/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
|Operation |Complexity |
44
|---------------------------|-------------------|
5-
|Indexing |O(n) |
5+
|Find |O(n) |
66
|Insert/delete at beginning |O(1) |
77
|Insert/delete in middle |O(1) + search time |
88
|Insert/delete at end |O(1) + search time |

‎src/data-structures/linked-list/__test__/LinkedList.test.js

+47-14
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,21 @@ describe('LinkedList', () => {
99
it('should append node to linked list', () => {
1010
const linkedList = new LinkedList();
1111

12-
const node1 = linkedList.append(1);
13-
const node2 = linkedList.append(2);
12+
const node1 = linkedList.append({ value: 1 });
13+
const node2 = linkedList.append({ value: 2, key: 'test' });
1414

1515
expect(node1.value).toBe(1);
1616
expect(node2.value).toBe(2);
17+
expect(node2.key).toBe('test');
1718

18-
expect(linkedList.toString()).toBe('1,2');
19+
expect(linkedList.toString()).toBe('1,test:2');
1920
});
2021

2122
it('should prepend node to linked list', () => {
2223
const linkedList = new LinkedList();
2324

24-
const node1 = linkedList.append(1);
25-
const node2 = linkedList.prepend(2);
25+
const node1 = linkedList.append({ value: 1 });
26+
const node2 = linkedList.prepend({ value: 2 });
2627

2728
expect(node1.value).toBe(1);
2829
expect(node2.value).toBe(2);
@@ -33,21 +34,53 @@ describe('LinkedList', () => {
3334
it('should delete node by value from linked list', () => {
3435
const linkedList = new LinkedList();
3536

36-
linkedList.append(1);
37-
linkedList.append(2);
38-
linkedList.append(3);
39-
linkedList.append(3);
40-
linkedList.append(4);
41-
linkedList.append(5);
37+
linkedList.append({ value: 1 });
38+
linkedList.append({ value: 2 });
39+
linkedList.append({ value: 3 });
40+
linkedList.append({ value: 3 });
41+
linkedList.append({ value: 4 });
42+
linkedList.append({ value: 5 });
4243

43-
const deletedNode = linkedList.delete(3);
44+
const deletedNode = linkedList.deleteByValue(3);
4445
expect(deletedNode.value).toBe(3);
4546
expect(linkedList.toString()).toBe('1,2,3,4,5');
4647

47-
linkedList.delete(3);
48+
linkedList.deleteByValue(3);
4849
expect(linkedList.toString()).toBe('1,2,4,5');
4950

50-
linkedList.delete(1);
51+
linkedList.deleteByValue(1);
5152
expect(linkedList.toString()).toBe('2,4,5');
5253
});
54+
55+
it('should delete node by key from linked list', () => {
56+
const linkedList = new LinkedList();
57+
58+
linkedList.append({ value: 1, key: 'test1' });
59+
linkedList.append({ value: 2, key: 'test2' });
60+
linkedList.append({ value: 3, key: 'test3' });
61+
62+
const deletedNode = linkedList.deleteByKey('test2');
63+
expect(deletedNode.key).toBe('test2');
64+
expect(linkedList.toString()).toBe('test1:1,test3:3');
65+
});
66+
67+
it('should append unique nodes', () => {
68+
const linkedList = new LinkedList();
69+
70+
linkedList.appendUnique({ value: 1, key: 'test1' });
71+
linkedList.appendUnique({ value: 2, key: 'test2' });
72+
linkedList.appendUnique({ value: 3, key: 'test2' });
73+
74+
expect(linkedList.toString()).toBe('test1:1,test2:3');
75+
});
76+
77+
it('should find node by its key', () => {
78+
const linkedList = new LinkedList();
79+
80+
linkedList.appendUnique({ value: 1, key: 'test1' });
81+
linkedList.appendUnique({ value: 2, key: 'test2' });
82+
linkedList.appendUnique({ value: 3, key: 'test3' });
83+
84+
expect(linkedList.findByKey('test3').toString()).toBe('test3:3');
85+
});
5386
});
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import LinkedListNode from '../LinkedListNode';
22

33
describe('LinkedListNode', () => {
4-
it('should create list node with value', () => {
5-
const node = new LinkedListNode(1);
4+
it('should create list node with kay and value', () => {
5+
const node = new LinkedListNode({ value: 1, key: 'test' });
66
expect(node.value).toBe(1);
7+
expect(node.key).toBe('test');
78
expect(node.next).toBeNull();
89
});
10+
11+
it('should convert node to string', () => {
12+
const node = new LinkedListNode({ value: 1 });
13+
const nodeWithKey = new LinkedListNode({ value: 1, key: 'test' });
14+
15+
expect(node.toString()).toBe('1');
16+
expect(nodeWithKey.toString()).toBe('test:1');
17+
});
918
});

0 commit comments

Comments
 (0)
Please sign in to comment.