From a0e0fd8dec28ccdf99765c98dd1a07dec7b3ff6c Mon Sep 17 00:00:00 2001 From: Adrian Mejia Date: Sun, 22 Mar 2020 12:04:11 -0400 Subject: [PATCH 1/4] feat(trie): implement trie data structure --- src/data-structures/trees/trie-1.js | 75 ++++++++++++++ src/data-structures/trees/trie.js | 98 ++++++++++++++++++ src/data-structures/trees/trie.spec.js | 133 +++++++++++++++++++++++++ 3 files changed, 306 insertions(+) create mode 100644 src/data-structures/trees/trie-1.js create mode 100644 src/data-structures/trees/trie.js create mode 100644 src/data-structures/trees/trie.spec.js diff --git a/src/data-structures/trees/trie-1.js b/src/data-structures/trees/trie-1.js new file mode 100644 index 00000000..80ede3b0 --- /dev/null +++ b/src/data-structures/trees/trie-1.js @@ -0,0 +1,75 @@ +class Trie { + constructor(val) { + this.val = val; + this.children = {}; + this.isWord = false; + } + + /** + * Insert word into trie and mark last element as such. + * @param {string} word + * @return {undefined} + */ + insert(word) { + let curr = this; + + for (const char of word) { + curr.children[char] = curr.children[char] || new Trie(char); + curr = curr.children[char]; + } + + curr.isWord = true; + } + + /** + * Search for complete word (by default) or partial if flag is set. + * @param {string} word - Word to search. + * @param {boolean} options.partial - Whether or not match partial matches. + * @return {boolean} + */ + search(word, { partial } = {}) { + let curr = this; + + for (const char of word) { + if (!curr.children[char]) { return false; } + curr = curr.children[char]; + } + + return partial ? true : curr.isWord; + } + + /** + * Return true if any word on the trie starts with the given prefix + * @param {string} prefix - Partial word to search. + * @return {boolean} + */ + startsWith(prefix) { + return this.search(prefix, { partial: true }); + } + + /** + * Returns all the words from the current `node`. + * Uses backtracking. + * + * @param {string} prefix - The prefix to append to each word. + * @param {string} node - Current node to start backtracking. + * @param {string[]} words - Accumulated words. + * @param {string} string - Current string. + */ + getAllWords(prefix = '', node = this, words = [], string = '') { + if (node.isWord) { + words.push(`${prefix}${string}`); + } + + for (const char of Object.keys(node.children)) { + this.getAllWords(prefix, node.children[char], words, `${string}${char}`); + } + + return words; + } +} + +// Aliases +Trie.prototype.add = Trie.prototype.insert; + +module.exports = Trie; diff --git a/src/data-structures/trees/trie.js b/src/data-structures/trees/trie.js new file mode 100644 index 00000000..4a7a7f41 --- /dev/null +++ b/src/data-structures/trees/trie.js @@ -0,0 +1,98 @@ +class Trie { + constructor(val) { + this.val = val; + this.children = {}; + this.isWord = false; + } + + /** + * Insert word into trie and mark last element as such. + * @param {string} word + * @return {undefined} + */ + insert(word) { + let curr = this; + + for (const char of word) { + curr.children[char] = curr.children[char] || new Trie(char); + curr = curr.children[char]; + } + + curr.isWord = true; + } + + /** + * Retun last node that matches word or prefix or false if not found. + * @param {string} word - Word to search. + * @param {boolean} options.partial - Whether or not match partial matches. + * @return {Trie|false} + */ + searchNode(word) { + let curr = this; + + for (const char of word) { + if (!curr.children[char]) { return false; } + curr = curr.children[char]; + } + + return curr; + } + + /** + * Search for complete word (by default) or partial if flag is set. + * @param {string} word - Word to search. + * @param {boolean} options.partial - Whether or not match partial matches. + * @return {boolean} + */ + search(word, { partial } = {}) { + const curr = this.searchNode(word); + if (!curr) { return false; } + return partial ? true : curr.isWord; + } + + /** + * Return true if any word on the trie starts with the given prefix + * @param {string} prefix - Partial word to search. + * @return {boolean} + */ + startsWith(prefix) { + return this.search(prefix, { partial: true }); + } + + /** + * Returns all the words from the current `node`. + * Uses backtracking. + * + * @param {string} prefix - The prefix to append to each word. + * @param {string} node - Current node to start backtracking. + * @param {string[]} words - Accumulated words. + * @param {string} string - Current string. + */ + getAllWords(prefix = '', node = this, words = [], string = '') { + if (!node) { return words; } + if (node.isWord) { + words.push(`${prefix}${string}`); + } + + for (const char of Object.keys(node.children)) { + this.getAllWords(prefix, node.children[char], words, `${string}${char}`); + } + + return words; + } + + /** + * Return a list of words matching the prefix + * @param {*} prefix - The prefix to match. + * @returns {string[]} + */ + autocomplete(prefix = '') { + const curr = this.searchNode(prefix); + return this.getAllWords(prefix, curr); + } +} + +// Aliases +Trie.prototype.add = Trie.prototype.insert; + +module.exports = Trie; diff --git a/src/data-structures/trees/trie.spec.js b/src/data-structures/trees/trie.spec.js new file mode 100644 index 00000000..f72f7adf --- /dev/null +++ b/src/data-structures/trees/trie.spec.js @@ -0,0 +1,133 @@ +const Trie = require('./trie'); + +describe('Trie', () => { + let trie; + + beforeEach(() => { + trie = new Trie(); + }); + + describe('construtor', () => { + it('should initialize trie', () => { + expect(trie).toBeDefined(); + }); + + it('should set default value to undefined', () => { + expect(trie.val).toEqual(undefined); + }); + + it('should initialization value', () => { + trie = new Trie(1); + expect(trie.val).toEqual(1); + }); + + it('should initialize children as empty map', () => { + expect(trie.children).toEqual({}); + }); + + it('should not be a word by default', () => { + expect(trie.isWord).toEqual(false); + }); + }); + + describe('insert', () => { + it('should insert a word', () => { + trie.insert('ab'); + expect(trie.children.a).toBeDefined(); + expect(trie.children.a.children.b).toBeDefined(); + expect(trie.children.a.isWord).toEqual(false); + expect(trie.children.a.children.b.isWord).toEqual(true); + }); + + it('should insert multiple words with the same root', () => { + trie.insert('a'); + trie.insert('ab'); + expect(trie.children.a.isWord).toEqual(true); + expect(trie.children.a.children.b.isWord).toEqual(true); + }); + }); + + describe('search & startsWith', () => { + beforeEach(() => { + trie.insert('dog'); + trie.insert('dogs'); + trie.insert('door'); + }); + + it('should search for words', () => { + expect(trie.search('dog')).toEqual(true); + }); + + it('should not match incomplete words by default', () => { + expect(trie.search('do')).toEqual(false); + }); + + it('should match partial words if partial is set', () => { + expect(trie.search('do', { + partial: true, + })).toEqual(true); + expect(trie.startsWith('do')).toEqual(true); + }); + + it('should not match non existing words', () => { + expect(trie.search('doors')).toEqual(false); + }); + + it('should not match non existing words with partials', () => { + expect(trie.search('doors', { + partial: true, + })).toEqual(false); + expect(trie.startsWith('doors')).toEqual(false); + }); + }); + + describe('when multiple words are inserted', () => { + beforeEach(() => { + trie.insert('dog'); + trie.insert('dogs'); + trie.insert('door'); + trie.insert('day'); + trie.insert('cat'); + }); + + describe('getAllWords', () => { + it('should get all words', () => { + const words = trie.getAllWords(); + expect(words.length).toEqual(5); + expect(words).toEqual(['dog', 'dogs', 'door', 'day', 'cat']); + }); + + it('should use prefix', () => { + const words = trie.getAllWords("Adrian's "); + expect(words.length).toEqual(5); + expect(words).toEqual([ + "Adrian's dog", + "Adrian's dogs", + "Adrian's door", + "Adrian's day", + "Adrian's cat", + ]); + }); + }); + + describe('autocomplete', () => { + it('should return all words if not prefix is given', () => { + const words = trie.autocomplete(); + expect(words.length).toBe(5); + expect(words).toEqual(['dog', 'dogs', 'door', 'day', 'cat']); + }); + + it('should auto complete words given a prefix', () => { + const words = trie.autocomplete('do'); + expect(words.length).toBe(3); + expect(words).toEqual(['dog', 'dogs', 'door']); + }); + + it('should handle non-existing words prefixes', () => { + const words = trie.autocomplete('co'); + expect(words.length).toBe(0); + expect(words).toEqual([]); + }); + }); + }); +}); From 524670ebfb5b02ce68a5e27201c826e473aa5454 Mon Sep 17 00:00:00 2001 From: Adrian Mejia Date: Sun, 22 Mar 2020 12:20:57 -0400 Subject: [PATCH 2/4] simplify backtracking algorithm --- src/data-structures/trees/trie.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/data-structures/trees/trie.js b/src/data-structures/trees/trie.js index 4a7a7f41..b634443b 100644 --- a/src/data-structures/trees/trie.js +++ b/src/data-structures/trees/trie.js @@ -65,17 +65,18 @@ class Trie { * * @param {string} prefix - The prefix to append to each word. * @param {string} node - Current node to start backtracking. - * @param {string[]} words - Accumulated words. - * @param {string} string - Current string. */ - getAllWords(prefix = '', node = this, words = [], string = '') { + getAllWords(prefix = '', node = this) { + let words = []; + if (!node) { return words; } if (node.isWord) { - words.push(`${prefix}${string}`); + words.push(prefix); } for (const char of Object.keys(node.children)) { - this.getAllWords(prefix, node.children[char], words, `${string}${char}`); + const newWords = this.getAllWords(`${prefix}${char}`, node.children[char]); + words = words.concat(newWords); } return words; From a81f6e1d57ee07d8ee1dc6b5abb2f6486cb15505 Mon Sep 17 00:00:00 2001 From: Adrian Mejia Date: Sun, 22 Mar 2020 13:55:10 -0400 Subject: [PATCH 3/4] feat(trie): remove method --- src/data-structures/trees/trie-1.js | 48 ++++++++++++++++++++++ src/data-structures/trees/trie.js | 34 ++++++++++++++++ src/data-structures/trees/trie.spec.js | 55 ++++++++++++++++++++++++++ 3 files changed, 137 insertions(+) diff --git a/src/data-structures/trees/trie-1.js b/src/data-structures/trees/trie-1.js index 80ede3b0..fb6ed1bf 100644 --- a/src/data-structures/trees/trie-1.js +++ b/src/data-structures/trees/trie-1.js @@ -67,6 +67,54 @@ class Trie { return words; } + + /** + * Return true if found the word to be removed, otherwise false. + * Iterative approach + * @param {string} word - The word to remove + * @returns {boolean} + */ + remove(word) { + const stack = []; + let curr = this; + + for (const char of word) { + if (!curr.children[char]) { return false; } + stack.push(curr); + curr = curr.children[char]; + } + + if (!curr.isWord) { return false; } + let node = stack.pop(); + + do { + node.children = {}; + node = stack.pop(); + } while (node && !node.isWord); + + return true; + } + + /** + * Return true if found the word to be removed, otherwise false. + * recursive approach + * @param {string} word - The word to remove + * @returns {boolean} + */ + remove2(word, i = 0, parent = this) { + if (i === word.length - 1) { + return true; + } + const child = parent.children[word.charAt(i)]; + if (!child) return false; + + const found = this.remove(word, i + 1, child); + + if (found) { + delete parent.children[word.charAt(i)]; + } + return true; + } } // Aliases diff --git a/src/data-structures/trees/trie.js b/src/data-structures/trees/trie.js index b634443b..1a03c241 100644 --- a/src/data-structures/trees/trie.js +++ b/src/data-structures/trees/trie.js @@ -21,6 +21,40 @@ class Trie { curr.isWord = true; } + /** + * Return true if found the word to be removed, otherwise false. + * @param {string} word - The word to remove + * @returns {boolean} + */ + remove(word) { + return this.removeHelper(word); + } + + /** + * Remove word from trie, return true if found, otherwise false. + * @param {string} word - The word to remove. + * @param {Trie} parent - The parent node. + * @param {number} index - The index. + * @param {number} meta.stop - Keeps track of the last letter that won't be removed. + * @returns {boolean} + */ + removeHelper(word, parent = this, index = 0, meta = { stop: 0 }) { + if (index === word.length) { + parent.isWord = false; + if (Object.keys(parent.children)) { meta.stop = index; } + return true; + } + const child = parent.children[word.charAt(index)]; + if (!child) { return false; } + if (parent.isWord) { meta.stop = index; } + const found = this.removeHelper(word, child, index + 1, meta); + // deletes all the nodes beyond `meta.stop`. + if (found && index >= meta.stop) { + delete parent.children[word.charAt(index)]; + } + return found; + } + /** * Retun last node that matches word or prefix or false if not found. * @param {string} word - Word to search. diff --git a/src/data-structures/trees/trie.spec.js b/src/data-structures/trees/trie.spec.js index f72f7adf..fca6588e 100644 --- a/src/data-structures/trees/trie.spec.js +++ b/src/data-structures/trees/trie.spec.js @@ -69,6 +69,13 @@ describe('Trie', () => { expect(trie.startsWith('do')).toEqual(true); }); + it('should match full words if partial is set', () => { + expect(trie.search('dogs', { + partial: true, + })).toEqual(true); + expect(trie.startsWith('dogs')).toEqual(true); + }); + it('should not match non existing words', () => { expect(trie.search('doors')).toEqual(false); }); @@ -129,5 +136,53 @@ describe('Trie', () => { expect(words).toEqual([]); }); }); + + describe('remove', () => { + it('should remove a word', () => { + trie = new Trie(); + trie.insert('a'); + expect(trie.remove('a')).toEqual(true); + expect(trie.getAllWords()).toEqual([]); + }); + + it('should remove word and keep other words', () => { + trie = new Trie(); + trie.insert('a'); + trie.insert('ab'); + expect(trie.remove('a')).toEqual(true); + expect(trie.getAllWords()).toEqual(['ab']); + }); + + it('should remove surrounding word', () => { + trie = new Trie(); + trie.insert('a'); + trie.insert('ab'); + expect(trie.remove('ab')).toEqual(true); + expect(trie.getAllWords()).toEqual(['a']); + }); + + it('should return false when word is not found', () => { + expect(trie.remove('not there')).toBe(false); + }); + + it('should remove words in between and still match', () => { + expect(trie.remove('dog')).toBe(true); + expect(trie.search('dogs')).toBe(true); + expect(trie.startsWith('dog')).toBe(true); + expect(trie.getAllWords()).toEqual([ + 'dogs', 'door', 'day', 'cat', + ]); + }); + + it('should remove word and no longer match partials', () => { + expect(trie.remove('dogs')).toBe(true); + expect(trie.search('dogs')).toBe(false); + expect(trie.search('dog')).toBe(true); + expect(trie.startsWith('dog')).toBe(true); + expect(trie.getAllWords()).toEqual([ + 'dog', 'door', 'day', 'cat', + ]); + }); + }); }); }); From e31cc62b0d075a7479b35876a75e3a58d43ecfdb Mon Sep 17 00:00:00 2001 From: Adrian Mejia Date: Thu, 26 Mar 2020 21:52:27 -0400 Subject: [PATCH 4/4] feat(trie): feature complete --- src/data-structures/trees/trie-2.js | 133 +++++++++++++++++++++++++ src/data-structures/trees/trie.js | 45 ++++----- src/data-structures/trees/trie.spec.js | 2 +- src/index.js | 3 + 4 files changed, 159 insertions(+), 24 deletions(-) create mode 100644 src/data-structures/trees/trie-2.js diff --git a/src/data-structures/trees/trie-2.js b/src/data-structures/trees/trie-2.js new file mode 100644 index 00000000..1a03c241 --- /dev/null +++ b/src/data-structures/trees/trie-2.js @@ -0,0 +1,133 @@ +class Trie { + constructor(val) { + this.val = val; + this.children = {}; + this.isWord = false; + } + + /** + * Insert word into trie and mark last element as such. + * @param {string} word + * @return {undefined} + */ + insert(word) { + let curr = this; + + for (const char of word) { + curr.children[char] = curr.children[char] || new Trie(char); + curr = curr.children[char]; + } + + curr.isWord = true; + } + + /** + * Return true if found the word to be removed, otherwise false. + * @param {string} word - The word to remove + * @returns {boolean} + */ + remove(word) { + return this.removeHelper(word); + } + + /** + * Remove word from trie, return true if found, otherwise false. + * @param {string} word - The word to remove. + * @param {Trie} parent - The parent node. + * @param {number} index - The index. + * @param {number} meta.stop - Keeps track of the last letter that won't be removed. + * @returns {boolean} + */ + removeHelper(word, parent = this, index = 0, meta = { stop: 0 }) { + if (index === word.length) { + parent.isWord = false; + if (Object.keys(parent.children)) { meta.stop = index; } + return true; + } + const child = parent.children[word.charAt(index)]; + if (!child) { return false; } + if (parent.isWord) { meta.stop = index; } + const found = this.removeHelper(word, child, index + 1, meta); + // deletes all the nodes beyond `meta.stop`. + if (found && index >= meta.stop) { + delete parent.children[word.charAt(index)]; + } + return found; + } + + /** + * Retun last node that matches word or prefix or false if not found. + * @param {string} word - Word to search. + * @param {boolean} options.partial - Whether or not match partial matches. + * @return {Trie|false} + */ + searchNode(word) { + let curr = this; + + for (const char of word) { + if (!curr.children[char]) { return false; } + curr = curr.children[char]; + } + + return curr; + } + + /** + * Search for complete word (by default) or partial if flag is set. + * @param {string} word - Word to search. + * @param {boolean} options.partial - Whether or not match partial matches. + * @return {boolean} + */ + search(word, { partial } = {}) { + const curr = this.searchNode(word); + if (!curr) { return false; } + return partial ? true : curr.isWord; + } + + /** + * Return true if any word on the trie starts with the given prefix + * @param {string} prefix - Partial word to search. + * @return {boolean} + */ + startsWith(prefix) { + return this.search(prefix, { partial: true }); + } + + /** + * Returns all the words from the current `node`. + * Uses backtracking. + * + * @param {string} prefix - The prefix to append to each word. + * @param {string} node - Current node to start backtracking. + */ + getAllWords(prefix = '', node = this) { + let words = []; + + if (!node) { return words; } + if (node.isWord) { + words.push(prefix); + } + + for (const char of Object.keys(node.children)) { + const newWords = this.getAllWords(`${prefix}${char}`, node.children[char]); + words = words.concat(newWords); + } + + return words; + } + + /** + * Return a list of words matching the prefix + * @param {*} prefix - The prefix to match. + * @returns {string[]} + */ + autocomplete(prefix = '') { + const curr = this.searchNode(prefix); + return this.getAllWords(prefix, curr); + } +} + +// Aliases +Trie.prototype.add = Trie.prototype.insert; + +module.exports = Trie; diff --git a/src/data-structures/trees/trie.js b/src/data-structures/trees/trie.js index 1a03c241..77fa5b57 100644 --- a/src/data-structures/trees/trie.js +++ b/src/data-structures/trees/trie.js @@ -27,32 +27,31 @@ class Trie { * @returns {boolean} */ remove(word) { - return this.removeHelper(word); - } + let curr = this; + // let lastWordToKeep = 0; + const stack = [curr]; - /** - * Remove word from trie, return true if found, otherwise false. - * @param {string} word - The word to remove. - * @param {Trie} parent - The parent node. - * @param {number} index - The index. - * @param {number} meta.stop - Keeps track of the last letter that won't be removed. - * @returns {boolean} - */ - removeHelper(word, parent = this, index = 0, meta = { stop: 0 }) { - if (index === word.length) { - parent.isWord = false; - if (Object.keys(parent.children)) { meta.stop = index; } - return true; + // find word and stack path + for (const char of word) { + if (!curr.children[char]) { return false; } + // lastWordToKeep += 1; + curr = curr.children[char]; + stack.push(curr); } - const child = parent.children[word.charAt(index)]; - if (!child) { return false; } - if (parent.isWord) { meta.stop = index; } - const found = this.removeHelper(word, child, index + 1, meta); - // deletes all the nodes beyond `meta.stop`. - if (found && index >= meta.stop) { - delete parent.children[word.charAt(index)]; + + let child = stack.pop(); + child.isWord = false; + + // remove non words without children + while (stack.length) { + const parent = stack.pop(); + if (!child.isWord && !Object.keys(child.children).length) { + delete parent.children[child.val]; + } + child = parent; } - return found; + + return true; } /** diff --git a/src/data-structures/trees/trie.spec.js b/src/data-structures/trees/trie.spec.js index fca6588e..84483763 100644 --- a/src/data-structures/trees/trie.spec.js +++ b/src/data-structures/trees/trie.spec.js @@ -137,7 +137,7 @@ describe('Trie', () => { }); }); - describe('remove', () => { + fdescribe('remove', () => { it('should remove a word', () => { trie = new Trie(); trie.insert('a'); diff --git a/src/index.js b/src/index.js index d575aaf0..2c29579d 100644 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,8 @@ const BinaryTreeNode = require('./data-structures/trees/binary-tree-node'); const AvlTree = require('./data-structures/trees/avl-tree'); const RedBlackTree = require('./data-structures/trees/red-black-tree'); const LRUCache = require('./data-structures/custom/lru-cache'); +const Trie = require('./data-structures/trees/trie'); + // algorithms const bubbleSort = require('./algorithms/sorting/bubble-sort'); const insertionSort = require('./algorithms/sorting/insertion-sort'); @@ -37,6 +39,7 @@ module.exports = { AvlTree, RedBlackTree, LRUCache, + Trie, bubbleSort, insertionSort, selectionSort,