Delete Word from Trie

Problem and examples

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.
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

Explore Delete Word from Trie

Load an example or enter your own input. Use the player controls to follow each change.

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.
  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.

Implementation and efficiency

C implementation

#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;
}

C++ implementation

#include <iostream>
#include <unordered_map>
using namespace std;

class TrieNode {
public:
    unordered_map<char, TrieNode*> children;
    bool isEnd = false;
};

class Trie {
    TrieNode* root;
public:
    Trie() { root = new TrieNode(); }

    void insert(const string& word) {
        TrieNode* node = root;
        for (char ch : word) {
            if (!node->children[ch]) node->children[ch] = new TrieNode();
            node = node->children[ch];
        }
        node->isEnd = true;
    }

    bool search(const string& word) {
        TrieNode* node = root;
        for (char ch : word) {
            if (!node->children.count(ch)) return false;
            node = node->children[ch];
        }
        return node->isEnd;
    }

    bool deleteWord(TrieNode* node, const string& word, int depth) {
        if (!node) return false;
        if (depth == word.length()) {
            if (!node->isEnd) return false;
            node->isEnd = false;
            return node->children.empty();
        }
        char ch = word[depth];
        TrieNode* child = node->children[ch];
        if (deleteWord(child, word, depth + 1)) {
            delete child;
            node->children.erase(ch);
            return !node->isEnd && node->children.empty();
        }
        return false;
    }

    void deleteWord(const string& word) {
        deleteWord(root, word, 0);
    }
};

int main() {
    Trie trie;
    trie.insert("apple");
    trie.insert("app");
    trie.deleteWord("apple");
    cout << trie.search("apple") << endl; // 0
    cout << trie.search("app") << endl;   // 1
    return 0;
}

Python implementation

class TrieNode:
    def __init__(self):
        self.children = {}
        self.is_end_of_word = False

class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word):
        node = self.root
        for char in word:
            if char not in node.children:
                node.children[char] = TrieNode()
            node = node.children[char]
        node.is_end_of_word = True

    def search(self, word):
        node = self.root
        for char in word:
            if char not in node.children:
                return False
            node = node.children[char]
        return node.is_end_of_word

    def delete(self, word):
        def _delete(node, word, index):
            if index == len(word):
                if not node.is_end_of_word:
                    return False
                node.is_end_of_word = False
                return len(node.children) == 0
            char = word[index]
            if char not in node.children:
                return False
            should_delete = _delete(node.children[char], word, index + 1)
            if should_delete:
                del node.children[char]
                return not node.is_end_of_word and len(node.children) == 0
            return False
        _delete(self.root, word, 0)

# Example usage
trie = Trie()
trie.insert("apple")
trie.insert("app")
trie.delete("apple")
print(trie.search("apple"))  # False
print(trie.search("app"))    # True

Java implementation

import java.util.HashMap;

class TrieNode {
    HashMap<Character, TrieNode> children = new HashMap<>();
    boolean isEndOfWord = false;
}

class Trie {
    private TrieNode root = new TrieNode();

    public void insert(String word) {
        TrieNode node = root;
        for (char ch : word.toCharArray()) {
            node.children.putIfAbsent(ch, new TrieNode());
            node = node.children.get(ch);
        }
        node.isEndOfWord = true;
    }

    public boolean search(String word) {
        TrieNode node = root;
        for (char ch : word.toCharArray()) {
            if (!node.children.containsKey(ch)) return false;
            node = node.children.get(ch);
        }
        return node.isEndOfWord;
    }

    public void delete(String word) {
        delete(root, word, 0);
    }

    private boolean delete(TrieNode node, String word, int index) {
        if (index == word.length()) {
            if (!node.isEndOfWord) return false;
            node.isEndOfWord = false;
            return node.children.isEmpty();
        }
        char ch = word.charAt(index);
        TrieNode child = node.children.get(ch);
        if (child == null) return false;
        boolean shouldDeleteChild = delete(child, word, index + 1);
        if (shouldDeleteChild) {
            node.children.remove(ch);
            return !node.isEndOfWord && node.children.isEmpty();
        }
        return false;
    }

    public static void main(String[] args) {
        Trie trie = new Trie();
        trie.insert("apple");
        trie.insert("app");
        trie.delete("apple");
        System.out.println(trie.search("apple")); // false
        System.out.println(trie.search("app"));   // true
    }
}

JavaScript implementation

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);
  }

  search(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;
  }
}

// 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;

Go implementation

package main

import "fmt"

const AlphabetSize = 26

type TrieNode struct {
    children [AlphabetSize]*TrieNode
    isEnd    bool
}

type Trie struct {
    root *TrieNode
}

func NewTrieNode() *TrieNode {
    return &TrieNode{}
}

func NewTrie() *Trie {
    return &Trie{root: NewTrieNode()}
}

func (t *Trie) Insert(word string) {
    node := t.root
    for _, ch := range word {
        index := ch - 'a'
        if node.children[index] == nil {
            node.children[index] = NewTrieNode()
        }
        node = node.children[index]
    }
    node.isEnd = true
}

func (t *Trie) Search(word string) bool {
    node := t.root
    for _, ch := range word {
        index := ch - 'a'
        if node.children[index] == nil {
            return false
        }
        node = node.children[index]
    }
    return node.isEnd
}

func (t *Trie) deleteHelper(node *TrieNode, word string, depth int) bool {
    if node == nil {
        return false
    }
    if depth == len(word) {
        if !node.isEnd {
            return false
        }
        node.isEnd = false
        for i := 0; i < AlphabetSize; i++ {
            if node.children[i] != nil {
                return false
            }
        }
        return true
    }
    index := word[depth] - 'a'
    if t.deleteHelper(node.children[index], word, depth+1) {
        node.children[index] = nil
        return !node.isEnd && noChildren(node)
    }
    return false
}

func noChildren(node *TrieNode) bool {
    for i := 0; i < AlphabetSize; i++ {
        if node.children[i] != nil {
            return false
        }
    }
    return true
}

func (t *Trie) Delete(word string) {
    t.deleteHelper(t.root, word, 0)
}

func main() {
    trie := NewTrie()
    trie.Insert("apple")
    trie.Insert("app")
    trie.Delete("apple")
    fmt.Println(trie.Search("apple")) // false
    fmt.Println(trie.Search("app"))   // true
}

Rust implementation

use std::collections::HashMap;

struct TrieNode {
    children: HashMap<char, TrieNode>,
    is_end: bool,
}

impl TrieNode {
    fn new() -> Self {
        TrieNode {
            children: HashMap::new(),
            is_end: false,
        }
    }
}

struct Trie {
    root: TrieNode,
}

impl Trie {
    fn new() -> Self {
        Trie { root: TrieNode::new() }
    }

    fn insert(&mut self, word: &str) {
        let mut node = &mut self.root;
        for ch in word.chars() {
            node = node.children.entry(ch).or_insert_with(TrieNode::new);
        }
        node.is_end = true;
    }

    fn search(&self, word: &str) -> bool {
        let mut node = &self.root;
        for ch in word.chars() {
            if let Some(next) = node.children.get(&ch) {
                node = next;
            } else {
                return false;
            }
        }
        node.is_end
    }

    fn delete_helper(node: &mut TrieNode, word: &str, depth: usize) -> bool {
        if depth == word.len() {
            if !node.is_end {
                return false;
            }
            node.is_end = false;
            return node.children.is_empty();
        }
        let ch = word.chars().nth(depth).unwrap();
        if let Some(child) = node.children.get_mut(&ch) {
            if Trie::delete_helper(child, word, depth + 1) {
                node.children.remove(&ch);
                return !node.is_end && node.children.is_empty();
            }
        } else {
            return false;
        }
        false
    }

    fn delete(&mut self, word: &str) {
        Trie::delete_helper(&mut self.root, word, 0);
    }
}

fn main() {
    let mut trie = Trie::new();
    trie.insert("apple");
    trie.insert("app");
    trie.delete("apple");
    println!("{}", trie.search("apple")); // false
    println!("{}", trie.search("app"));   // true
}

Kotlin implementation

class TrieNode {
    val children = mutableMapOf<Char, TrieNode>()
    var isEndOfWord = false
}

class Trie {
    private val root = TrieNode()

    fun insert(word: String) {
        var node = root
        for (ch in word) {
            node = node.children.getOrPut(ch) { TrieNode() }
        }
        node.isEndOfWord = true
    }

    fun search(word: String): Boolean {
        var node = root
        for (ch in word) {
            node = node.children[ch] ?: return false
        }
        return node.isEndOfWord
    }

    private fun delete(node: TrieNode?, word: String, index: Int): Boolean {
        if (node == null) return false
        if (index == word.length) {
            if (!node.isEndOfWord) return false
            node.isEndOfWord = false
            return node.children.isEmpty()
        }
        val ch = word[index]
        val child = node.children[ch] ?: return false
        val shouldDeleteChild = delete(child, word, index + 1)
        if (shouldDeleteChild) {
            node.children.remove(ch)
            return !node.isEndOfWord && node.children.isEmpty()
        }
        return false
    }

    fun delete(word: String) {
        delete(root, word, 0)
    }
}

fun main() {
    val trie = Trie()
    trie.insert("apple")
    trie.insert("app")
    trie.delete("apple")
    println(trie.search("apple")) // false
    println(trie.search("app"))   // true
}

Swift implementation

class TrieNode {
    var children: [Character: TrieNode] = [:]
    var isEndOfWord = false
}

class Trie {
    private let root = TrieNode()

    func insert(_ word: String) {
        var node = root
        for ch in word {
            if node.children[ch] == nil {
                node.children[ch] = TrieNode()
            }
            node = node.children[ch]!
        }
        node.isEndOfWord = true
    }

    func search(_ word: String) -> Bool {
        var node = root
        for ch in word {
            guard let next = node.children[ch] else { return false }
            node = next
        }
        return node.isEndOfWord
    }

    @discardableResult
    private func delete(_ node: TrieNode?, _ word: String, _ index: String.Index) -> Bool {
        guard let node = node else { return false }
        if index == word.endIndex {
            if !node.isEndOfWord { return false }
            node.isEndOfWord = false
            return node.children.isEmpty
        }
        let ch = word[index]
        let nextIndex = word.index(after: index)
        if delete(node.children[ch], word, nextIndex) {
            node.children[ch] = nil
            return !node.isEndOfWord && node.children.isEmpty
        }
        return false
    }

    func delete(_ word: String) {
        delete(root, word, word.startIndex)
    }
}

let trie = Trie()
trie.insert("apple")
trie.insert("app")
trie.delete("apple")
print(trie.search("apple")) // false
print(trie.search("app"))   // true

TypeScript implementation

class TrieNode {
  children: { [key: string]: TrieNode } = {};
  isEndOfWord: boolean = false;
}

class Trie {
  root: TrieNode = new TrieNode();

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

  search(word: string): boolean {
    let node = this.root;
    for (const char of word) {
      if (!node.children[char]) return false;
      node = node.children[char];
    }
    return node.isEndOfWord;
  }

  delete(word: string): void {
    const deleteHelper = (node: TrieNode | undefined, word: string, index: number): boolean => {
      if (!node) return false;
      if (index === word.length) {
        if (!node.isEndOfWord) return false;
        node.isEndOfWord = false;
        return Object.keys(node.children).length === 0;
      }

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

      const shouldDeleteChild = deleteHelper(node.children[char], word, index + 1);

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

      return false;
    };

    deleteHelper(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;

Time complexity

CaseTimeExplanation
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)

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