diff --git a/README.md b/README.md index 00f42a22..c4588e52 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ 3. [Stack](https://github.com/trekhleb/javascript-algorithms/tree/master/src/data-structures/stack) 4. [Hash Table](https://github.com/trekhleb/javascript-algorithms/tree/master/src/data-structures/hash-table) 5. [Heap](https://github.com/trekhleb/javascript-algorithms/tree/master/src/data-structures/heap) +5. [Trie](https://github.com/trekhleb/javascript-algorithms/tree/master/src/data-structures/trie) ## [Algorithms](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms) diff --git a/src/data-structures/trie/Trie.js b/src/data-structures/trie/Trie.js new file mode 100644 index 00000000..3e46eff4 --- /dev/null +++ b/src/data-structures/trie/Trie.js @@ -0,0 +1,45 @@ +import TrieNode from './TrieNode'; + +const HEAD_CHARACTER = '*'; + +export default class Trie { + constructor() { + this.head = new TrieNode(HEAD_CHARACTER); + } + + addWord(word) { + const characters = Array.from(word); + let currentNode = this.head; + for (let charIndex = 0; charIndex < characters.length; charIndex += 1) { + const isComplete = charIndex === characters.length - 1; + currentNode = currentNode.addChild(characters[charIndex], isComplete); + } + } + + suggestNextCharacters(word) { + const lastCharacter = this.getLastCharacterNode(word); + + if (!lastCharacter) { + return null; + } + + return lastCharacter.suggestChildren(); + } + + doesWordExists(word) { + return !!this.getLastCharacterNode(word); + } + + getLastCharacterNode(word) { + const characters = Array.from(word); + let currentNode = this.head; + for (let charIndex = 0; charIndex < characters.length; charIndex += 1) { + if (!currentNode.hasChild(characters[charIndex])) { + return null; + } + currentNode = currentNode.getChild(characters[charIndex]); + } + + return currentNode; + } +} diff --git a/src/data-structures/trie/TrieNode.js b/src/data-structures/trie/TrieNode.js new file mode 100644 index 00000000..4c267220 --- /dev/null +++ b/src/data-structures/trie/TrieNode.js @@ -0,0 +1,39 @@ +export default class TrieNode { + constructor(character, isCompleteWord = false) { + this.character = character; + this.isCompleteWord = isCompleteWord; + this.children = {}; + } + + getChild(character) { + if (!Object.prototype.hasOwnProperty.call(this.children, character)) { + return null; + } + + return this.children[character]; + } + + addChild(character, isCompleteWord = false) { + if (!this.children[character]) { + this.children[character] = new TrieNode(character, isCompleteWord); + } + + return this.children[character]; + } + + hasChild(character) { + return !!this.children[character]; + } + + suggestChildren() { + return Object.keys(this.children); + } + + toString() { + let childrenAsString = Object.keys(this.children).toString(); + childrenAsString = childrenAsString ? `:${childrenAsString}` : ''; + const isCompleteString = this.isCompleteWord ? '*' : ''; + + return `${this.character}${isCompleteString}${childrenAsString}`; + } +} diff --git a/src/data-structures/trie/__test__/Trie.test.js b/src/data-structures/trie/__test__/Trie.test.js new file mode 100644 index 00000000..e98616f4 --- /dev/null +++ b/src/data-structures/trie/__test__/Trie.test.js @@ -0,0 +1,51 @@ +import Trie from '../Trie'; + +describe('Trie', () => { + it('should create trie', () => { + const trie = new Trie(); + + expect(trie).toBeDefined(); + expect(trie.head.toString()).toBe('*'); + }); + + it('should add words to trie', () => { + const trie = new Trie(); + + trie.addWord('cat'); + + expect(trie.head.toString()).toBe('*:c'); + expect(trie.head.getChild('c').toString()).toBe('c:a'); + + trie.addWord('car'); + expect(trie.head.toString()).toBe('*:c'); + expect(trie.head.getChild('c').toString()).toBe('c:a'); + expect(trie.head.getChild('c').getChild('a').toString()).toBe('a:t,r'); + expect(trie.head.getChild('c').getChild('a').getChild('t').toString()).toBe('t*'); + }); + + it('should suggests next characters', () => { + const trie = new Trie(); + + trie.addWord('cat'); + trie.addWord('cats'); + trie.addWord('car'); + trie.addWord('caption'); + + expect(trie.suggestNextCharacters('ca')).toEqual(['t', 'r', 'p']); + expect(trie.suggestNextCharacters('cat')).toEqual(['s']); + expect(trie.suggestNextCharacters('cab')).toBeNull(); + }); + + it('should check if word exists', () => { + const trie = new Trie(); + + trie.addWord('cat'); + trie.addWord('cats'); + trie.addWord('car'); + trie.addWord('caption'); + + expect(trie.doesWordExists('cat')).toBeTruthy(); + expect(trie.doesWordExists('cap')).toBeTruthy(); + expect(trie.doesWordExists('call')).toBeFalsy(); + }); +}); diff --git a/src/data-structures/trie/__test__/TrieNode.test.js b/src/data-structures/trie/__test__/TrieNode.test.js new file mode 100644 index 00000000..d7570b57 --- /dev/null +++ b/src/data-structures/trie/__test__/TrieNode.test.js @@ -0,0 +1,51 @@ +import TrieNode from '../TrieNode'; + +describe('TrieNode', () => { + it('should create trie node', () => { + const trieNode = new TrieNode('c', true); + + expect(trieNode.character).toBe('c'); + expect(trieNode.isCompleteWord).toBeTruthy(); + expect(trieNode.toString()).toBe('c*'); + }); + + it('should add child nodes', () => { + const trieNode = new TrieNode('c'); + + trieNode.addChild('a', true); + trieNode.addChild('o'); + + expect(trieNode.toString()).toBe('c:a,o'); + }); + + it('should get child nodes', () => { + const trieNode = new TrieNode('c'); + + trieNode.addChild('a'); + trieNode.addChild('o'); + + expect(trieNode.getChild('a').toString()).toBe('a'); + expect(trieNode.getChild('o').toString()).toBe('o'); + expect(trieNode.getChild('b')).toBeNull(); + }); + + it('should check if node has specific child', () => { + const trieNode = new TrieNode('c'); + + trieNode.addChild('a'); + trieNode.addChild('o'); + + expect(trieNode.hasChild('a')).toBeTruthy(); + expect(trieNode.hasChild('o')).toBeTruthy(); + expect(trieNode.hasChild('b')).toBeFalsy(); + }); + + it('should suggest next children', () => { + const trieNode = new TrieNode('c'); + + trieNode.addChild('a'); + trieNode.addChild('o'); + + expect(trieNode.suggestChildren()).toEqual(['a', 'o']); + }); +});