Trie
Data Structure · Core
Step through it: Trie / Prefix Search pattern
Step 1 of 5
Goal: store many words so we can quickly ask "does any word start with these letters?" A trie is a tree where each step down is one letter, so words that share a beginning share a path. Add "cat", starting from an empty root.
Step 2 of 5
c, a, t: each letter hangs below the previous one.
Step 3 of 5
Add "car". c and a already exist, so only r is new. Shared beginnings are stored once.
Step 4 of 5
Does anything start with "ca"? Walk c, then a. The path exists: yes, in 2 steps, however many words are stored. The cost depends on the length of the word, not the number of words. This is a trie.
Step 5 of 5
Your turn.
In computer science, a trie, also called digital tree and sometimes radix tree or prefix tree (as they can be searched by prefixes), is a kind of search tree—an ordered tree data structure that is used to store a dynamic set or associative array where the keys are usually strings. Unlike a binary search tree, no node in the tree stores the key associated with that node; instead, its position in the tree defines the key with which it is associated. All the descendants of a node have a common prefix of the string associated with that node, and the root is associated with the empty string. Values are not necessarily associated with every node. Rather, values tend only to be associated with leaves, and with some inner nodes that correspond to keys of interest. For the space-optimized presentation of prefix tree, see compact prefix tree.

Made with okso.app
Code
import TrieNode from './TrieNode';
// Character that we will use for trie tree root.
const HEAD_CHARACTER = '*';
export default class Trie {
constructor() {
this.head = new TrieNode(HEAD_CHARACTER);
}
/**
* @param {string} word
* @return {Trie}
*/
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);
}
return this;
}
/**
* @param {string} word
* @return {Trie}
*/
deleteWord(word) {
const depthFirstDelete = (currentNode, charIndex = 0) => {
if (charIndex >= word.length) {
// Return if we're trying to delete the character that is out of word's scope.
return;
}
const character = word[charIndex];
const nextNode = currentNode.getChild(character);
if (nextNode == null) {
// Return if we're trying to delete a word that has not been added to the Trie.
return;
}
// Go deeper.
depthFirstDelete(nextNode, charIndex + 1);
// Since we're going to delete a word let's un-mark its last character isCompleteWord flag.
if (charIndex === (word.length - 1)) {
nextNode.isCompleteWord = false;
}
// childNode is deleted only if:
// - childNode has NO children
// - childNode.isCompleteWord === false
currentNode.removeChild(character);
};
// Start depth-first deletion from the head node.
depthFirstDelete(this.head);
return this;
}
/**
* @param {string} word
* @return {string[]}
*/
suggestNextCharacters(word) {
const lastCharacter = this.getLastCharacterNode(word);
if (!lastCharacter) {
return null;
}
return lastCharacter.suggestChildren();
}
/**
* Check if complete word exists in Trie.
*
* @param {string} word
* @return {boolean}
*/
doesWordExist(word) {
const lastCharacter = this.getLastCharacterNode(word);
return !!lastCharacter && lastCharacter.isCompleteWord;
}
/**
* @param {string} word
* @return {TrieNode}
*/
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;
}
}import HashTable from '../hash-table/HashTable';
export default class TrieNode {
/**
* @param {string} character
* @param {boolean} isCompleteWord
*/
constructor(character, isCompleteWord = false) {
this.character = character;
this.isCompleteWord = isCompleteWord;
this.children = new HashTable();
}
/**
* @param {string} character
* @return {TrieNode}
*/
getChild(character) {
return this.children.get(character);
}
/**
* @param {string} character
* @param {boolean} isCompleteWord
* @return {TrieNode}
*/
addChild(character, isCompleteWord = false) {
if (!this.children.has(character)) {
this.children.set(character, new TrieNode(character, isCompleteWord));
}
const childNode = this.children.get(character);
// In cases similar to adding "car" after "carpet" we need to mark "r" character as complete.
childNode.isCompleteWord = childNode.isCompleteWord || isCompleteWord;
return childNode;
}
/**
* @param {string} character
* @return {TrieNode}
*/
removeChild(character) {
const childNode = this.getChild(character);
// Delete childNode only if:
// - childNode has NO children,
// - childNode.isCompleteWord === false.
if (
childNode
&& !childNode.isCompleteWord
&& !childNode.hasChildren()
) {
this.children.delete(character);
}
return this;
}
/**
* @param {string} character
* @return {boolean}
*/
hasChild(character) {
return this.children.has(character);
}
/**
* Check whether current TrieNode has children or not.
* @return {boolean}
*/
hasChildren() {
return this.children.getKeys().length !== 0;
}
/**
* @return {string[]}
*/
suggestChildren() {
return [...this.children.getKeys()];
}
/**
* @return {string}
*/
toString() {
let childrenAsString = this.suggestChildren().toString();
childrenAsString = childrenAsString ? `:${childrenAsString}` : '';
const isCompleteString = this.isCompleteWord ? '*' : '';
return `${this.character}${isCompleteString}${childrenAsString}`;
}
}References
Practice
Spotted a mistake or something unclear? Report it on GitHub.