Skip to content

feat(trie): implement trie data structure #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 30, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(trie): implement trie data structure
  • Loading branch information
amejiarosario committed Mar 22, 2020
commit a0e0fd8dec28ccdf99765c98dd1a07dec7b3ff6c
75 changes: 75 additions & 0 deletions src/data-structures/trees/trie-1.js
Original file line number Diff line number Diff line change
@@ -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;
98 changes: 98 additions & 0 deletions src/data-structures/trees/trie.js
Original file line number Diff line number Diff line change
@@ -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;
133 changes: 133 additions & 0 deletions src/data-structures/trees/trie.spec.js
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
});
});