diff --git a/src/data-structures/trees/trie-1.js b/src/data-structures/trees/trie-1.js new file mode 100644 index 00000000..fb6ed1bf --- /dev/null +++ b/src/data-structures/trees/trie-1.js @@ -0,0 +1,123 @@ +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; + } + + /** + * 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 +Trie.prototype.add = Trie.prototype.insert; + +module.exports = Trie; 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 new file mode 100644 index 00000000..77fa5b57 --- /dev/null +++ b/src/data-structures/trees/trie.js @@ -0,0 +1,132 @@ +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) { + let curr = this; + // let lastWordToKeep = 0; + const stack = [curr]; + + // 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); + } + + 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 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. + */ + 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.spec.js b/src/data-structures/trees/trie.spec.js new file mode 100644 index 00000000..84483763 --- /dev/null +++ b/src/data-structures/trees/trie.spec.js @@ -0,0 +1,188 @@ +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 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); + }); + + 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([]); + }); + }); + + fdescribe('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', + ]); + }); + }); + }); +}); 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,