Trie data structure in Javascript

A Trie, also known as a digital tree or prefix tree, is a kind of search tree — an ordered tree data structure used to store a dynamic set or associative array where the keys are usually strings.

In this tutorial, we will learn about the Trie data structure and how to implement a Trie in javascript.

What is Trie data structure?

Trie data structure was described by René de la Briandais in 1959 solely to solve the very problem of representing a set of words.

The term “trie” comes from the word retrieval and is usually pronounced “try”, to separate it from other “tree” structures.

However, it is basically a tree data structure with certain rules to follow in terms of how it is created and used. It is a tree-like data structure wherein the nodes of the tree store the entire alphabet and strings/words can be retrieved by traversing down a branch path.

Trie in Javascript

According to Donald Knuth’s research in The Art of Computer Programming:

Trie memory for computer searching was first recommended by René de la Briandais. He pointed out that we can save memory space at the expense of running time if we use a linked list for each node vector since most of the entries in the vectors tend to be empty.

The main idea behind using tries as a data structure was that they could be a nice compromise between running time and memory.

List of operations performed on Trie.

  • insert(word): Adds a new word.
  • remove(word): Removes the given word.
  • contains(word): Checks if Trie has the given word.
  • find(prefix): Returns all the words with given prefix.

Implementing Trie data structure in Javascript.

Each trie has an empty root node, with links (or references) to other nodes — one for each possible alphabetic value. The shape and the structure of a trie is always a set of linked nodes, connecting back to an empty root node.

Thus, the size of a trie is directly correlated to the size of all the possible values that the trie could represent.

Base structure

We will need a TrieNode object to insert a new word in the Trie, which would represent an entirely new Trie.

// we start with the TrieNode
const TrieNode = function (key) {
  // the "key" value will be the character in sequence
  this.key = key;
  
  // we keep a reference to parent
  this.parent = null;
  
  // we have hash of children
  this.children = {};
  
  // check to see if the node is at the end
  this.end = false;
  
  this.getWord = function() {
    let output = [];
    let node = this;

    while (node !== null) {
      output.unshift(node.key);
      node = node.parent;
    }

    return output.join('');
  };
}

const Trie = function() {
  this.root = new TrieNode(null);
 
  //Other methods will go here...
}

Inserting a word in Trie

To insert a new word in the Trie we have to check for two things.

  • Check that the word that needs to be added doesn’t already exist in this trie.
  • Next, if we’ve traversed down the branch where this word ought to live and the words don’t exist yet, we’d insert a value into the node’s reference where the word should go.

Inserting in trie

  // inserts a word into the trie.
  this.insert = function(word) {
    let node = this.root; // we start at the root

    // for every character in the word
    for(let i = 0; i < word.length; i++) {
      // check to see if character node exists in children.
      if (!node.children[word[i]]) {
        // if it doesn't exist, we then create it.
        node.children[word[i]] = new TrieNode(word[i]);

        // we also assign the parent to the child node.
        node.children[word[i]].parent = node;
      }

      // proceed to the next depth in the trie.
      node = node.children[word[i]];

      // finally, we check to see if it's the last word.
      if (i == word.length-1) {
        // if it is, we set the end flag to true.
        node.end = true;
      }
    }
  };

Searching a word in the Trie

To check if the trie contains the given word or not.

  • For every character in the word. Check to see if character node exists in children.
  • If it exists, proceed to the next depth of the trie.
  • Else return false, since its a not a valid word.
  • At the end return the word.

Searching in trie

 // check if it contains a whole word.
  this.contains = function(word) {
    let node = this.root;

    // for every character in the word
    for(let i = 0; i < word.length; i++) {
      // check to see if character node exists in children.
      if (node.children[word[i]]) {
        // if it exists, proceed to the next depth of the trie.
        node = node.children[word[i]];
      } else {
        // doesn't exist, return false since it's not a valid word.
        return false;
      }
    }

    // we finished going through all the words, but is it a whole word?
    return node.end;
  };

Find the word starting with given prefix in the Trie.

To find all the words with given prefix, we need to perform two operations.

  • First, make sure prefix actually has words.
  • Second, find all the words with given prefix.

Finding a word in trie

  // returns every word with given prefix
  this.find = function(prefix) {
    let node = this.root;
    let output = [];

    // for every character in the prefix
    for(let i = 0; i < prefix.length; i++) {
      // make sure prefix actually has words
      if (node.children[prefix[i]]) {
        node = node.children[prefix[i]];
      } else {
        // there's none. just return it.
        return output;
      }
    }

    // recursively find all words in the node
    findAllWords(node, output);

    return output;
  };
  
  // recursive function to find all words in the given node.
  const findAllWords = (node, arr) => {
    // base case, if node is at a word, push to output
    if (node.end) {
      arr.unshift(node.getWord());
    }

    // iterate through each children, call recursive findAllWords
    for (let child in node.children) {
      findAllWords(node.children[child], arr);
    }
  }

Removing a word from the Trie

To delete a key, we do not delete the node corresponding to the key as it might have some children which still contain a key. Instead, we simply have to search for it and set its value to null.

However, to improve efficiency, if the node corresponding to the key has no children or all its children have null values, we might also delete the entire node.

Removing a word from Trie

// removes the given word
this.remove = function (word) {
      let root = this.root;

      if(!word) return;

      // recursively finds and removes a word
      const removeWord = (node, word) => {

          // check if current node contains the word
          if (node.end && node.getWord() === word) {

              // check and see if node has children
              let hasChildren = Object.keys(node.children).length > 0;

              // if has children we only want to un-flag the end node that marks end of a word.
              // this way we do not remove words that contain/include supplied word
              if (hasChildren) {
                  node.end = false;
              } else {
                  // remove word by getting parent and setting children to empty dictionary
                  node.parent.children = {};
              }

              return true;
          }

          // recursively remove word from all children
          for (let key in node.children) {
              removeWord(node.children[key], word)
          }

          return false
      };

      // call remove word on root node
      removeWord(root, word);
  };

Complete code of Trie data structure in Javascript.

// we start with the TrieNode
const TrieNode = function (key) {
  // the "key" value will be the character in sequence
  this.key = key;
  
  // we keep a reference to parent
  this.parent = null;
  
  // we have hash of children
  this.children = {};
  
  // check to see if the node is at the end
  this.end = false;
  
  this.getWord = function() {
    let output = [];
    let node = this;

    while (node !== null) {
      output.unshift(node.key);
      node = node.parent;
    }

    return output.join('');
  };
}

const Trie = function() {
  this.root = new TrieNode(null);
  
  // inserts a word into the trie.
  this.insert = function(word) {
    let node = this.root; // we start at the root

    // for every character in the word
    for(let i = 0; i < word.length; i++) {
      // check to see if character node exists in children.
      if (!node.children[word[i]]) {
        // if it doesn't exist, we then create it.
        node.children[word[i]] = new TrieNode(word[i]);

        // we also assign the parent to the child node.
        node.children[word[i]].parent = node;
      }

      // proceed to the next depth in the trie.
      node = node.children[word[i]];

      // finally, we check to see if it's the last word.
      if (i == word.length-1) {
        // if it is, we set the end flag to true.
        node.end = true;
      }
    }
  };
  
  // check if it contains a whole word.
  this.contains = function(word) {
    let node = this.root;

    // for every character in the word
    for(let i = 0; i < word.length; i++) {
      // check to see if character node exists in children.
      if (node.children[word[i]]) {
        // if it exists, proceed to the next depth of the trie.
        node = node.children[word[i]];
      } else {
        // doesn't exist, return false since it's not a valid word.
        return false;
      }
    }

    // we finished going through all the words, but is it a whole word?
    return node.end;
  };
  
  // returns every word with given prefix
  this.find = function(prefix) {
    let node = this.root;
    let output = [];

    // for every character in the prefix
    for(let i = 0; i < prefix.length; i++) {
      // make sure prefix actually has words
      if (node.children[prefix[i]]) {
        node = node.children[prefix[i]];
      } else {
        // there's none. just return it.
        return output;
      }
    }

    // recursively find all words in the node
    findAllWords(node, output);

    return output;
  };
  
  // recursive function to find all words in the given node.
  const findAllWords = (node, arr) => {
    // base case, if node is at a word, push to output
    if (node.end) {
      arr.unshift(node.getWord());
    }

    // iterate through each children, call recursive findAllWords
    for (let child in node.children) {
      findAllWords(node.children[child], arr);
    }
  }

  // removes a word from the trie.
  this.remove = function (word) {
      let root = this.root;

      if(!word) return;

      // recursively finds and removes a word
      const removeWord = (node, word) => {

          // check if current node contains the word
          if (node.end && node.getWord() === word) {

              // check and see if node has children
              let hasChildren = Object.keys(node.children).length > 0;

              // if has children we only want to un-flag the end node that marks the end of a word.
              // this way we do not remove words that contain/include supplied word
              if (hasChildren) {
                  node.end = false;
              } else {
                  // remove word by getting parent and setting children to empty dictionary
                  node.parent.children = {};
              }

              return true;
          }

          // recursively remove word from all children
          for (let key in node.children) {
              removeWord(node.children[key], word)
          }

          return false
      };

      // call remove word on root node
      removeWord(root, word);
  };
}
Input:
const trie = new Trie();

// insert few values
trie.insert("peter");
trie.insert("piper");
trie.insert("picked");
trie.insert("pickled");
trie.insert("pepper");

// check contains method
console.log(trie.contains("picked"));  
console.log(trie.contains("pepper")); 
trie.remove("pepper");
// check find method
console.log(trie.find("pi"));  
console.log(trie.find("pe")); 

Output:
true
true
["pickled", "picked", "piper"]
["peter"]

Time complexity of Trie data structure

The time complexity of searching, inserting, and deleting from a trie depends on the length of the word that’s being searched for, inserted, or deleted, and the number of total words, n, making the runtime of these operations O(a * n).

While finding the words with the given prefix, the time complexity is O(p + n), where p is the length of the prefix and n is the number of words.

# Insert Search Find Delete
Average Θ(a * n) Θ(a * n) Θ(p + n) Θ(a * n)
Worst O(a * n) O(a * n) O(p + n) O(a * n)

Space complexity

The space complexity is O(n * k) where k is the number of references each node is storing (26 for english alphabets) and n is the number of nodes.

# Space
Worst O(n * k)

Applications of Trie data structure

It is rarely used, however, if required it used in combination with other data structures, the most prominent example of its use is the autocomplete feature of search, where we type alphabets, and all other words starting with given alphabets are suggested like in search engine.