Delete Word from Trie - Visualization

Visualization Player

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

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

C
C++
Python
Java
JS
Go
Rust
Kotlin
Swift
TS
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define CHAR_SIZE 26

struct TrieNode {
    struct TrieNode* children[CHAR_SIZE];
    bool isEndOfWord;
};

struct TrieNode* createNode() {
    struct TrieNode* node = (struct TrieNode*)malloc(sizeof(struct TrieNode));
    node->isEndOfWord = false;
    for (int i = 0; i < CHAR_SIZE; i++) node->children[i] = NULL;
    return node;
}

void insert(struct TrieNode* root, const char* word) {
    while (*word) {
        int index = *word - 'a';
        if (!root->children[index]) root->children[index] = createNode();
        root = root->children[index];
        word++;
    }
    root->isEndOfWord = true;
}

bool search(struct TrieNode* root, const char* word) {
    while (*word) {
        int index = *word - 'a';
        if (!root->children[index]) return false;
        root = root->children[index];
        word++;
    }
    return root != NULL && root->isEndOfWord;
}

bool deleteWord(struct TrieNode* root, const char* word, int depth) {
    if (!root) return false;
    if (word[depth] == '\0') {
        if (!root->isEndOfWord) return false;
        root->isEndOfWord = false;
        for (int i = 0; i < CHAR_SIZE; i++)
            if (root->children[i]) return false;
        return true;
    }
    int index = word[depth] - 'a';
    if (deleteWord(root->children[index], word, depth + 1)) {
        free(root->children[index]);
        root->children[index] = NULL;
        return !root->isEndOfWord && !search(root, word);
    }
    return false;
}

int main() {
    struct TrieNode* root = createNode();
    insert(root, "apple");
    insert(root, "app");
    deleteWord(root, "apple", 0);
    printf("apple: %d\n", search(root, "apple")); // 0 (false)
    printf("app: %d\n", search(root, "app"));   // 1 (true)
    return 0;
}

Time Complexity

CaseTime ComplexityExplanation
Best CaseO(n)In the best case, the algorithm still traverses each character of the word once, resulting in linear time complexity relative to the word length.
Average CaseO(n)On average, the algorithm traverses n nodes (one for each character of the word) and performs constant-time operations at each node, leading to O(n) time.
Worst CaseO(n)In the worst case, the algorithm traverses down the full length of the word and may also backtrack up to remove unnecessary nodes, still bounded by O(n).

Space Complexity

O(n)

Explanation: The space complexity is O(n) due to the recursion stack, which can go as deep as the length of the word being deleted.

Comments

💬 Please keep your comment relevant and respectful. Avoid spamming, offensive language, or posting promotional/backlink content.
All comments are subject to moderation before being published.


Loading comments...