Skip to content

Commit 965fbd1

Browse files
authoredMar 30, 2020
Merge pull request amejiarosario#43 from amejiarosario/feature/trie
feat(trie): implement trie data structure

File tree

5 files changed

+579
-0
lines changed

5 files changed

+579
-0
lines changed
 

‎src/data-structures/trees/trie-1.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
class Trie {
2+
constructor(val) {
3+
this.val = val;
4+
this.children = {};
5+
this.isWord = false;
6+
}
7+
8+
/**
9+
* Insert word into trie and mark last element as such.
10+
* @param {string} word
11+
* @return {undefined}
12+
*/
13+
insert(word) {
14+
let curr = this;
15+
16+
for (const char of word) {
17+
curr.children[char] = curr.children[char] || new Trie(char);
18+
curr = curr.children[char];
19+
}
20+
21+
curr.isWord = true;
22+
}
23+
24+
/**
25+
* Search for complete word (by default) or partial if flag is set.
26+
* @param {string} word - Word to search.
27+
* @param {boolean} options.partial - Whether or not match partial matches.
28+
* @return {boolean}
29+
*/
30+
search(word, { partial } = {}) {
31+
let curr = this;
32+
33+
for (const char of word) {
34+
if (!curr.children[char]) { return false; }
35+
curr = curr.children[char];
36+
}
37+
38+
return partial ? true : curr.isWord;
39+
}
40+
41+
/**
42+
* Return true if any word on the trie starts with the given prefix
43+
* @param {string} prefix - Partial word to search.
44+
* @return {boolean}
45+
*/
46+
startsWith(prefix) {
47+
return this.search(prefix, { partial: true });
48+
}
49+
50+
/**
51+
* Returns all the words from the current `node`.
52+
* Uses backtracking.
53+
*
54+
* @param {string} prefix - The prefix to append to each word.
55+
* @param {string} node - Current node to start backtracking.
56+
* @param {string[]} words - Accumulated words.
57+
* @param {string} string - Current string.
58+
*/
59+
getAllWords(prefix = '', node = this, words = [], string = '') {
60+
if (node.isWord) {
61+
words.push(`${prefix}${string}`);
62+
}
63+
64+
for (const char of Object.keys(node.children)) {
65+
this.getAllWords(prefix, node.children[char], words, `${string}${char}`);
66+
}
67+
68+
return words;
69+
}
70+
71+
/**
72+
* Return true if found the word to be removed, otherwise false.
73+
* Iterative approach
74+
* @param {string} word - The word to remove
75+
* @returns {boolean}
76+
*/
77+
remove(word) {
78+
const stack = [];
79+
let curr = this;
80+
81+
for (const char of word) {
82+
if (!curr.children[char]) { return false; }
83+
stack.push(curr);
84+
curr = curr.children[char];
85+
}
86+
87+
if (!curr.isWord) { return false; }
88+
let node = stack.pop();
89+
90+
do {
91+
node.children = {};
92+
node = stack.pop();
93+
} while (node && !node.isWord);
94+
95+
return true;
96+
}
97+
98+
/**
99+
* Return true if found the word to be removed, otherwise false.
100+
* recursive approach
101+
* @param {string} word - The word to remove
102+
* @returns {boolean}
103+
*/
104+
remove2(word, i = 0, parent = this) {
105+
if (i === word.length - 1) {
106+
return true;
107+
}
108+
const child = parent.children[word.charAt(i)];
109+
if (!child) return false;
110+
111+
const found = this.remove(word, i + 1, child);
112+
113+
if (found) {
114+
delete parent.children[word.charAt(i)];
115+
}
116+
return true;
117+
}
118+
}
119+
120+
// Aliases
121+
Trie.prototype.add = Trie.prototype.insert;
122+
123+
module.exports = Trie;

‎src/data-structures/trees/trie-2.js

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
class Trie {
2+
constructor(val) {
3+
this.val = val;
4+
this.children = {};
5+
this.isWord = false;
6+
}
7+
8+
/**
9+
* Insert word into trie and mark last element as such.
10+
* @param {string} word
11+
* @return {undefined}
12+
*/
13+
insert(word) {
14+
let curr = this;
15+
16+
for (const char of word) {
17+
curr.children[char] = curr.children[char] || new Trie(char);
18+
curr = curr.children[char];
19+
}
20+
21+
curr.isWord = true;
22+
}
23+
24+
/**
25+
* Return true if found the word to be removed, otherwise false.
26+
* @param {string} word - The word to remove
27+
* @returns {boolean}
28+
*/
29+
remove(word) {
30+
return this.removeHelper(word);
31+
}
32+
33+
/**
34+
* Remove word from trie, return true if found, otherwise false.
35+
* @param {string} word - The word to remove.
36+
* @param {Trie} parent - The parent node.
37+
* @param {number} index - The index.
38+
* @param {number} meta.stop - Keeps track of the last letter that won't be removed.
39+
* @returns {boolean}
40+
*/
41+
removeHelper(word, parent = this, index = 0, meta = { stop: 0 }) {
42+
if (index === word.length) {
43+
parent.isWord = false;
44+
if (Object.keys(parent.children)) { meta.stop = index; }
45+
return true;
46+
}
47+
const child = parent.children[word.charAt(index)];
48+
if (!child) { return false; }
49+
if (parent.isWord) { meta.stop = index; }
50+
const found = this.removeHelper(word, child, index + 1, meta);
51+
// deletes all the nodes beyond `meta.stop`.
52+
if (found && index >= meta.stop) {
53+
delete parent.children[word.charAt(index)];
54+
}
55+
return found;
56+
}
57+
58+
/**
59+
* Retun last node that matches word or prefix or false if not found.
60+
* @param {string} word - Word to search.
61+
* @param {boolean} options.partial - Whether or not match partial matches.
62+
* @return {Trie|false}
63+
*/
64+
searchNode(word) {
65+
let curr = this;
66+
67+
for (const char of word) {
68+
if (!curr.children[char]) { return false; }
69+
curr = curr.children[char];
70+
}
71+
72+
return curr;
73+
}
74+
75+
/**
76+
* Search for complete word (by default) or partial if flag is set.
77+
* @param {string} word - Word to search.
78+
* @param {boolean} options.partial - Whether or not match partial matches.
79+
* @return {boolean}
80+
*/
81+
search(word, { partial } = {}) {
82+
const curr = this.searchNode(word);
83+
if (!curr) { return false; }
84+
return partial ? true : curr.isWord;
85+
}
86+
87+
/**
88+
* Return true if any word on the trie starts with the given prefix
89+
* @param {string} prefix - Partial word to search.
90+
* @return {boolean}
91+
*/
92+
startsWith(prefix) {
93+
return this.search(prefix, { partial: true });
94+
}
95+
96+
/**
97+
* Returns all the words from the current `node`.
98+
* Uses backtracking.
99+
*
100+
* @param {string} prefix - The prefix to append to each word.
101+
* @param {string} node - Current node to start backtracking.
102+
*/
103+
getAllWords(prefix = '', node = this) {
104+
let words = [];
105+
106+
if (!node) { return words; }
107+
if (node.isWord) {
108+
words.push(prefix);
109+
}
110+
111+
for (const char of Object.keys(node.children)) {
112+
const newWords = this.getAllWords(`${prefix}${char}`, node.children[char]);
113+
words = words.concat(newWords);
114+
}
115+
116+
return words;
117+
}
118+
119+
/**
120+
* Return a list of words matching the prefix
121+
* @param {*} prefix - The prefix to match.
122+
* @returns {string[]}
123+
*/
124+
autocomplete(prefix = '') {
125+
const curr = this.searchNode(prefix);
126+
return this.getAllWords(prefix, curr);
127+
}
128+
}
129+
130+
// Aliases
131+
Trie.prototype.add = Trie.prototype.insert;
132+
133+
module.exports = Trie;

‎src/data-structures/trees/trie.js

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
class Trie {
2+
constructor(val) {
3+
this.val = val;
4+
this.children = {};
5+
this.isWord = false;
6+
}
7+
8+
/**
9+
* Insert word into trie and mark last element as such.
10+
* @param {string} word
11+
* @return {undefined}
12+
*/
13+
insert(word) {
14+
let curr = this;
15+
16+
for (const char of word) {
17+
curr.children[char] = curr.children[char] || new Trie(char);
18+
curr = curr.children[char];
19+
}
20+
21+
curr.isWord = true;
22+
}
23+
24+
/**
25+
* Return true if found the word to be removed, otherwise false.
26+
* @param {string} word - The word to remove
27+
* @returns {boolean}
28+
*/
29+
remove(word) {
30+
let curr = this;
31+
// let lastWordToKeep = 0;
32+
const stack = [curr];
33+
34+
// find word and stack path
35+
for (const char of word) {
36+
if (!curr.children[char]) { return false; }
37+
// lastWordToKeep += 1;
38+
curr = curr.children[char];
39+
stack.push(curr);
40+
}
41+
42+
let child = stack.pop();
43+
child.isWord = false;
44+
45+
// remove non words without children
46+
while (stack.length) {
47+
const parent = stack.pop();
48+
if (!child.isWord && !Object.keys(child.children).length) {
49+
delete parent.children[child.val];
50+
}
51+
child = parent;
52+
}
53+
54+
return true;
55+
}
56+
57+
/**
58+
* Retun last node that matches word or prefix or false if not found.
59+
* @param {string} word - Word to search.
60+
* @param {boolean} options.partial - Whether or not match partial matches.
61+
* @return {Trie|false}
62+
*/
63+
searchNode(word) {
64+
let curr = this;
65+
66+
for (const char of word) {
67+
if (!curr.children[char]) { return false; }
68+
curr = curr.children[char];
69+
}
70+
71+
return curr;
72+
}
73+
74+
/**
75+
* Search for complete word (by default) or partial if flag is set.
76+
* @param {string} word - Word to search.
77+
* @param {boolean} options.partial - Whether or not match partial matches.
78+
* @return {boolean}
79+
*/
80+
search(word, { partial } = {}) {
81+
const curr = this.searchNode(word);
82+
if (!curr) { return false; }
83+
return partial ? true : curr.isWord;
84+
}
85+
86+
/**
87+
* Return true if any word on the trie starts with the given prefix
88+
* @param {string} prefix - Partial word to search.
89+
* @return {boolean}
90+
*/
91+
startsWith(prefix) {
92+
return this.search(prefix, { partial: true });
93+
}
94+
95+
/**
96+
* Returns all the words from the current `node`.
97+
* Uses backtracking.
98+
*
99+
* @param {string} prefix - The prefix to append to each word.
100+
* @param {string} node - Current node to start backtracking.
101+
*/
102+
getAllWords(prefix = '', node = this) {
103+
let words = [];
104+
105+
if (!node) { return words; }
106+
if (node.isWord) {
107+
words.push(prefix);
108+
}
109+
110+
for (const char of Object.keys(node.children)) {
111+
const newWords = this.getAllWords(`${prefix}${char}`, node.children[char]);
112+
words = words.concat(newWords);
113+
}
114+
115+
return words;
116+
}
117+
118+
/**
119+
* Return a list of words matching the prefix
120+
* @param {*} prefix - The prefix to match.
121+
* @returns {string[]}
122+
*/
123+
autocomplete(prefix = '') {
124+
const curr = this.searchNode(prefix);
125+
return this.getAllWords(prefix, curr);
126+
}
127+
}
128+
129+
// Aliases
130+
Trie.prototype.add = Trie.prototype.insert;
131+
132+
module.exports = Trie;
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
const Trie = require('./trie');
2+
3+
describe('Trie', () => {
4+
let trie;
5+
6+
beforeEach(() => {
7+
trie = new Trie();
8+
});
9+
10+
describe('construtor', () => {
11+
it('should initialize trie', () => {
12+
expect(trie).toBeDefined();
13+
});
14+
15+
it('should set default value to undefined', () => {
16+
expect(trie.val).toEqual(undefined);
17+
});
18+
19+
it('should initialization value', () => {
20+
trie = new Trie(1);
21+
expect(trie.val).toEqual(1);
22+
});
23+
24+
it('should initialize children as empty map', () => {
25+
expect(trie.children).toEqual({});
26+
});
27+
28+
it('should not be a word by default', () => {
29+
expect(trie.isWord).toEqual(false);
30+
});
31+
});
32+
33+
describe('insert', () => {
34+
it('should insert a word', () => {
35+
trie.insert('ab');
36+
expect(trie.children.a).toBeDefined();
37+
expect(trie.children.a.children.b).toBeDefined();
38+
expect(trie.children.a.isWord).toEqual(false);
39+
expect(trie.children.a.children.b.isWord).toEqual(true);
40+
});
41+
42+
it('should insert multiple words with the same root', () => {
43+
trie.insert('a');
44+
trie.insert('ab');
45+
expect(trie.children.a.isWord).toEqual(true);
46+
expect(trie.children.a.children.b.isWord).toEqual(true);
47+
});
48+
});
49+
50+
describe('search & startsWith', () => {
51+
beforeEach(() => {
52+
trie.insert('dog');
53+
trie.insert('dogs');
54+
trie.insert('door');
55+
});
56+
57+
it('should search for words', () => {
58+
expect(trie.search('dog')).toEqual(true);
59+
});
60+
61+
it('should not match incomplete words by default', () => {
62+
expect(trie.search('do')).toEqual(false);
63+
});
64+
65+
it('should match partial words if partial is set', () => {
66+
expect(trie.search('do', {
67+
partial: true,
68+
})).toEqual(true);
69+
expect(trie.startsWith('do')).toEqual(true);
70+
});
71+
72+
it('should match full words if partial is set', () => {
73+
expect(trie.search('dogs', {
74+
partial: true,
75+
})).toEqual(true);
76+
expect(trie.startsWith('dogs')).toEqual(true);
77+
});
78+
79+
it('should not match non existing words', () => {
80+
expect(trie.search('doors')).toEqual(false);
81+
});
82+
83+
it('should not match non existing words with partials', () => {
84+
expect(trie.search('doors', {
85+
partial: true,
86+
})).toEqual(false);
87+
expect(trie.startsWith('doors')).toEqual(false);
88+
});
89+
});
90+
91+
describe('when multiple words are inserted', () => {
92+
beforeEach(() => {
93+
trie.insert('dog');
94+
trie.insert('dogs');
95+
trie.insert('door');
96+
trie.insert('day');
97+
trie.insert('cat');
98+
});
99+
100+
describe('getAllWords', () => {
101+
it('should get all words', () => {
102+
const words = trie.getAllWords();
103+
expect(words.length).toEqual(5);
104+
expect(words).toEqual(['dog', 'dogs', 'door', 'day', 'cat']);
105+
});
106+
107+
it('should use prefix', () => {
108+
const words = trie.getAllWords("Adrian's ");
109+
expect(words.length).toEqual(5);
110+
expect(words).toEqual([
111+
"Adrian's dog",
112+
"Adrian's dogs",
113+
"Adrian's door",
114+
"Adrian's day",
115+
"Adrian's cat",
116+
]);
117+
});
118+
});
119+
120+
describe('autocomplete', () => {
121+
it('should return all words if not prefix is given', () => {
122+
const words = trie.autocomplete();
123+
expect(words.length).toBe(5);
124+
expect(words).toEqual(['dog', 'dogs', 'door', 'day', 'cat']);
125+
});
126+
127+
it('should auto complete words given a prefix', () => {
128+
const words = trie.autocomplete('do');
129+
expect(words.length).toBe(3);
130+
expect(words).toEqual(['dog', 'dogs', 'door']);
131+
});
132+
133+
it('should handle non-existing words prefixes', () => {
134+
const words = trie.autocomplete('co');
135+
expect(words.length).toBe(0);
136+
expect(words).toEqual([]);
137+
});
138+
});
139+
140+
fdescribe('remove', () => {
141+
it('should remove a word', () => {
142+
trie = new Trie();
143+
trie.insert('a');
144+
expect(trie.remove('a')).toEqual(true);
145+
expect(trie.getAllWords()).toEqual([]);
146+
});
147+
148+
it('should remove word and keep other words', () => {
149+
trie = new Trie();
150+
trie.insert('a');
151+
trie.insert('ab');
152+
expect(trie.remove('a')).toEqual(true);
153+
expect(trie.getAllWords()).toEqual(['ab']);
154+
});
155+
156+
it('should remove surrounding word', () => {
157+
trie = new Trie();
158+
trie.insert('a');
159+
trie.insert('ab');
160+
expect(trie.remove('ab')).toEqual(true);
161+
expect(trie.getAllWords()).toEqual(['a']);
162+
});
163+
164+
it('should return false when word is not found', () => {
165+
expect(trie.remove('not there')).toBe(false);
166+
});
167+
168+
it('should remove words in between and still match', () => {
169+
expect(trie.remove('dog')).toBe(true);
170+
expect(trie.search('dogs')).toBe(true);
171+
expect(trie.startsWith('dog')).toBe(true);
172+
expect(trie.getAllWords()).toEqual([
173+
'dogs', 'door', 'day', 'cat',
174+
]);
175+
});
176+
177+
it('should remove word and no longer match partials', () => {
178+
expect(trie.remove('dogs')).toBe(true);
179+
expect(trie.search('dogs')).toBe(false);
180+
expect(trie.search('dog')).toBe(true);
181+
expect(trie.startsWith('dog')).toBe(true);
182+
expect(trie.getAllWords()).toEqual([
183+
'dog', 'door', 'day', 'cat',
184+
]);
185+
});
186+
});
187+
});
188+
});

‎src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const BinaryTreeNode = require('./data-structures/trees/binary-tree-node');
1414
const AvlTree = require('./data-structures/trees/avl-tree');
1515
const RedBlackTree = require('./data-structures/trees/red-black-tree');
1616
const LRUCache = require('./data-structures/custom/lru-cache');
17+
const Trie = require('./data-structures/trees/trie');
18+
1719
// algorithms
1820
const bubbleSort = require('./algorithms/sorting/bubble-sort');
1921
const insertionSort = require('./algorithms/sorting/insertion-sort');
@@ -37,6 +39,7 @@ module.exports = {
3739
AvlTree,
3840
RedBlackTree,
3941
LRUCache,
42+
Trie,
4043
bubbleSort,
4144
insertionSort,
4245
selectionSort,

0 commit comments

Comments
 (0)
Please sign in to comment.