Delete Word from Trie
Problem and 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 |
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.
- If we reach the end of the word (i.e.,
index === word.length), we check if the word actually exists in the Trie. - If it exists, we unset the
is_end_of_wordflag. If this node has no children, we tell the parent it can safely remove this node. - 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.
- 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.
- We call
deleteWord(root, "ant", 0). - The function navigates through each character:
'a' → 'n' → 't'. - At node
't', we unmarkis_end_of_word = false. - We check if
't'has any children — it doesn't, so it can be deleted from its parent'n'. - Next, we check
'n'— since it's still used by the word"and", we do not delete'n'. - 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_wordflag 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.
- Start from the root node and define a recursive function
deleteWord(node, word, index). - If
indexequals the word's length:- If
node.is_end_of_wordis false, the word does not exist — returnfalse. - Unset
node.is_end_of_word. - Return
trueifnode.childrenis empty (to delete this node in parent).
- If
- Get the current character and its corresponding child node.
- If the child node does not exist, return
false. - Recursively call
deleteWordon the child node and next index. - After recursion, if deletion is needed, remove the child from current node's children map.
- Return
trueif 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")) # TrueJava 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")) // trueTypeScript 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
| Case | Time | Explanation |
|---|---|---|
| best case | O(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 case | O(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 case | O(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.