Problem Title: Trie - Prefix Tree Implementation
Problem Description:
A Trie (pronounced "try") or prefix tree is a tree-like data structure that stores a dynamic set of strings. It is particularly useful for operations like searching, prefix matching, and autocomplete. Each node in the Trie represents a character of a string, and the path from the root to a node represents a prefix of one or more strings.
Your task is to implement a Trie class that supports the following operations:
- Insert a String: Insert a word into the Trie.
- Search for a String: Check if a word exists in the Trie.
- Search for a Prefix: Check if any word in the Trie starts with a given prefix.
- Delete a String: Remove a word from the Trie.
- Count Words Starting with a Prefix: Return the number of words in the Trie that start with a given prefix.
Operations:
-
insert(word: str) -> None
- Input: A string
word
to be inserted into the Trie. - Output: None.
- Input: A string
-
search(word: str) -> bool
- Input: A string
word
to search for in the Trie. - Output:
True
if the word exists in the Trie, otherwiseFalse
.
- Input: A string
-
startsWith(prefix: str) -> bool
- Input: A string
prefix
to check if any word in the Trie starts with it. - Output:
True
if there is any word in the Trie that starts with the prefix, otherwiseFalse
.
- Input: A string
-
delete(word: str) -> bool
- Input: A string
word
to delete from the Trie. - Output:
True
if the word was successfully deleted, otherwiseFalse
.
- Input: A string
-
countWordsStartingWith(prefix: str) -> int
- Input: A string
prefix
. - Output: The number of words in the Trie that start with the given prefix.
- Input: A string
Constraints:
- The input words will consist of lowercase English letters.
- The length of each word will be at most 1000 characters.
- The number of operations (insert, search, startsWith, delete, countWordsStartingWith) will be at most 10^4.
Example Test Cases:
Test Case 1:
-
Input:
- Operations:
insert("apple")
search("apple")
search("app")
startsWith("app")
insert("app")
search("app")
delete("apple")
search("apple")
search("app")
- Operations:
-
Expected Output:
- After operation 2:
True
- After operation 3:
False
- After operation 4:
True
- After operation 6:
True
- After operation 8:
False
- After operation 9:
True
- After operation 2:
Test Case 2:
-
Input:
- Operations:
insert("hello")
insert("world")
insert("hell")
countWordsStartingWith("he")
delete("hell")
countWordsStartingWith("he")
delete("hello")
countWordsStartingWith("he")
- Operations:
-
Expected Output:
- After operation 4:
2
- After operation 6:
1
- After operation 8:
0
- After operation 4:
Test Case 3:
-
Input:
- Operations:
insert("a")
insert("aa")
insert("aaa")
countWordsStartingWith("a")
countWordsStartingWith("aa")
countWordsStartingWith("aaa")
delete("aa")
countWordsStartingWith("a")
countWordsStartingWith("aa")
- Operations:
-
Expected Output:
- After operation 4:
3
- After operation 5:
2
- After operation 6:
1
- After operation 8:
2
- After operation 9:
1
- After operation 4:
Test Case 4:
-
Input:
- Operations:
insert("cat")
insert("car")
insert("cart")
insert("caterpillar")
countWordsStartingWith("ca")
delete("cart")
countWordsStartingWith("ca")
delete("caterpillar")
countWordsStartingWith("ca")
- Operations:
-
Expected Output:
- After operation 5:
4
- After operation 7:
3
- After operation 9:
2
- After operation 5:
Test Case 5:
-
Input:
- Operations:
insert("xyz")
insert("xy")
insert("x")
countWordsStartingWith("x")
delete("xy")
countWordsStartingWith("x")
delete("xyz")
countWordsStartingWith("x")
- Operations:
-
Expected Output:
- After operation 4:
3
- After operation 6:
2
- After operation 8:
1
- After operation 4:
Challenge:
- Implement the Trie class with the above operations.
- Ensure that the operations are efficient, especially for large datasets.
- Handle edge cases, such as inserting an empty string, deleting a non-existent word, or counting words with an empty prefix.
Note: Do not provide the code, just the problem statement and test cases. The challenge is to implement the class based on the given operations and constraints.
class TrieNode:
def __init__(self):
# Each node holds a map from character → child node, and a flag for word‐end.
self.children = {}
self.is_end = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str) -> None:
"""
Insert `word` into the trie, iteratively.
"""
node = self.root
for ch in word:
if ch not in node.children:
node.children[ch] = TrieNode()
node = node.children[ch]
node.is_end = True
def search(self, word: str) -> bool:
"""
Return True if `word` is in the trie (i.e. was inserted before).
"""
node = self.root
for ch in word:
if ch not in node.children:
return False
node = node.children[ch]
return node.is_end
def starts_with(self, prefix: str) -> bool:
"""
Return True if there is any word in the trie that starts with `prefix`.
"""
node = self.root
for ch in prefix:
if ch not in node.children:
return False
node = node.children[ch]
return True
def delete(self, word: str) -> bool:
"""
Delete `word` from the trie, if it exists. Returns True if deleted.
This is done iteratively by first walking down and recording the path,
then unmarking the end‐of‐word flag and pruning unused nodes bottom‐up.
"""
node = self.root
stack = [] # to remember (parent_node, char) on the path
# 1) Walk down, storing the path
for ch in word:
if ch not in node.children:
return False # word not found
stack.append((node, ch))
node = node.children[ch]
# 2) If this node wasn't marking end of a word, nothing to delete
if not node.is_end:
return False
# 3) Unmark the word
node.is_end = False
# 4) Prune: walk the stored path in reverse
# and delete child nodes that are now useless
for parent, ch in reversed(stack):
child = parent.children[ch]
# if child has no children and isn't end of another word → delete it
if not child.children and not child.is_end:
del parent.children[ch]
else:
break
return True
# —— Example Usage ——
if __name__ == "__main__":
trie = Trie()
for word in ["apple", "app", "apt", "bat"]:
trie.insert(word)
print(trie.search("app")) # True
print(trie.search("apple")) # True
print(trie.search("ap")) # False
print(trie.starts_with("ap")) # True
print(trie.delete("app")) # True
print(trie.search("app")) # False
print(trie.search("apple")) # True
-
Node Structure
- Each
TrieNode
has:children
: a dictionary mapping characters → child nodes.is_end
: a boolean flag indicating if a complete word ends at this node.
- Each
-
Insertion (
insert
)- Start at the root and for each character:
- If the child for that character doesn’t exist, create it.
- Move to that child.
- After consuming all characters, mark the last node’s
is_end = True
.
- Start at the root and for each character:
-
Search (
search
)- Walk down from the root following each character in the query word.
- If at any step the character isn’t found, return
False
. - After the loop, return the
is_end
status of the last node—onlyTrue
if the exact word was inserted.
-
Prefix Check (
starts_with
)- Very similar to
search
, but you don’t requireis_end = True
at the end. - As long as you can follow the prefix all the way through, return
True
.
- Very similar to
-
Deletion (
delete
)- Iterative walk: traverse the word while pushing
(parent_node, ch)
onto a stack. - If the word isn’t present or doesn’t terminate a word, you abort with
False
. - Unmark
is_end
on the terminal node. - Pruning: pop back up the stack, and at each step delete the child reference from its parent if:
- That child has no other children, and
- It isn’t marked as the end of another word.
- Stop pruning once you hit a node that’s still needed (either has other children or is an
is_end
).
- Iterative walk: traverse the word while pushing
All methods avoid recursion by using simple loops and, for deletion, an explicit stack to backtrack. This keeps everything iterative while still maintaining full Trie functionality.