Delete Word from Trie

Problem Statement

Given a set of words inserted into a Trie, implement a function to delete a specific word from it. Ensure the function properly handles edge cases such as shared prefixes or non-existent words. The function should not delete nodes that are part of other valid words.

Examples

Inserted Words Word to Delete Expected Trie Behavior Explanation
["apple", "banana", "grape"]
"banana" "banana" removed, others unchanged
"banana" is fully deleted, and since no other word shares its path, the whole branch is removed. Visualization
["car", "cart", "carbon"]
"car" "car" end marker removed, prefix retained
"car" shares prefix with "cart" and "carbon", so only the end-of-word marker is removed for "car". Visualization
["hello", "world"]
"hell" No change
"hell" was never inserted as a word, so nothing changes in the Trie. Visualization
[] "apple" No change The Trie is empty, so there's nothing to delete. Visualization

Visualization Player

Solution

Deleting a word from a Trie is a bit more complex than inserting or searching. This is because we must ensure that we only remove the nodes that are no longer required by other words. To do this, we use a recursive approach that checks whether a node is part of any other word before removing it.

Approach and Steps

We define a recursive function deleteWord(node, word, index) that traverses the Trie from the root node.

  1. If we reach the end of the word (i.e., index === word.length), we check if the word actually exists in the Trie.
  2. If it exists, we unset the is_end_of_word flag. If this node has no children, we tell the parent it can safely remove this node.
  3. As we return back up the recursive call stack, we check each node to see if its child (just deleted) can be removed, and if so, we remove it from the current node’s children.
  4. At each level, we check if the current node is still useful: it must either have children or be marked as an end of another word. If not, it can also be deleted.

Example: Deleting "ant" from a Trie

Let’s assume we have already inserted the words: ["app", "apple", "and", "ant", "danger", "dance"] into the Trie.

  1. We call deleteWord(root, "ant", 0).
  2. The function navigates through each character: 'a' → 'n' → 't'.
  3. At node 't', we unmark is_end_of_word = false.
  4. We check if 't' has any children — it doesn't, so it can be deleted from its parent 'n'.
  5. Next, we check 'n' — since it's still used by the word "and", we do not delete 'n'.
  6. The word "ant" is deleted, but the shared nodes needed by "and" are preserved.

Case 1 – Deleting a word that exists and has unique path

Example: Trie contains ["cat", "dog"], and we delete "dog".

  • Traversal goes 'd' → 'o' → 'g'.
  • 'g' is end of word and has no children → deleted.
  • 'o' now has no children → deleted.
  • 'd' now has no children → deleted.
  • The entire branch "dog" is removed from the Trie.

Case 2 – Deleting a word that is a prefix for another word

Example: Trie contains ["bat", "batch"], and we delete "bat".

  • Traversal reaches 't' and unmarks it as end of word.
  • 't' has a child ('c' from "batch") → node must not be deleted.
  • Only the is_end_of_word flag is updated, structure remains.

Case 3 – Trying to delete a word that does not exist

Example: Trie contains ["cat", "car"], and we try to delete "can".

  • Traversal fails at node 'n', which doesn't exist under 'a'.
  • Function returns false, and Trie remains unchanged.

Case 4 – Deleting a word from an empty Trie

Example: Trie is empty, and we try to delete the word "apple".

  • Since the root has no children, traversal stops immediately.
  • Return false, no changes made.

Case 5 – Deleting the only word in the Trie

Example: Trie contains ["run"], and we delete "run".

  • Each node is unmarked and deleted from leaf to root.
  • After deletion, the Trie becomes empty again.

Algorithm Steps

  1. Start from the root node and define a recursive function deleteWord(node, word, index).
  2. If index equals the word's length:
    1. If node.is_end_of_word is false, the word does not exist — return false.
    2. Unset node.is_end_of_word.
    3. Return true if node.children is empty (to delete this node in parent).
  3. Get the current character and its corresponding child node.
  4. If the child node does not exist, return false.
  5. Recursively call deleteWord on the child node and next index.
  6. After recursion, if deletion is needed, remove the child from current node's children map.
  7. Return true if the current node is not end of another word and has no children left.

Code

JavaScript
class TrieNode {
  constructor() {
    this.children = {};
    this.is_end_of_word = false;
  }
}

class Trie {
  constructor() {
    this.root = new TrieNode();
  }

  insert(word) {
    let node = this.root;
    for (let char of word) {
      if (!node.children[char]) {
        node.children[char] = new TrieNode();
      }
      node = node.children[char];
    }
    node.is_end_of_word = true;
  }

  delete(word) {
    const deleteWord = (node, word, index) => {
      if (index === word.length) {
        if (!node.is_end_of_word) return false;
        node.is_end_of_word = false;
        return Object.keys(node.children).length === 0;
      }

      const char = word[index];
      const child = node.children[char];
      if (!child) return false;

      const shouldDeleteChild = deleteWord(child, word, index + 1);

      if (shouldDeleteChild) {
        delete node.children[char];
        return !node.is_end_of_word && Object.keys(node.children).length === 0;
      }

      return false;
    };

    deleteWord(this.root, word, 0);
  }
}

// Example usage:
const trie = new Trie();
trie.insert("apple");
trie.insert("app");
trie.delete("apple");
console.log(trie.search("apple")); // false
console.log(trie.search("app"));   // true

Trie.prototype.search = function(word) {
  let node = this.root;
  for (let char of word) {
    if (!node.children[char]) return false;
    node = node.children[char];
  }
  return node.is_end_of_word;
};