Implement Trie (Prefix Tree)
Implementing Trie alias as Prefix Tree
A prefix tree (or trie) is a special tree structure that helps store and quickly find strings, making it useful for tasks like auto-complete or spell checking.
Here’s what the PrefixTree class does:
- PrefixTree(): Creates an empty prefix tree.
- insert(String word): Adds a word to the prefix tree.
- search(String word): Checks if a word exists in the tree. Returns true if the word is found, otherwise false.
- startsWith(String prefix): Checks if any word in the tree starts with a given prefix. Returns true if such a word exists, otherwise false.
["Trie", "insert", "dog", "search", "dog", "search", "do", "startsWith", "do", "insert", "do", "search", "do"]
Output:
[null, null, true, false, true, null, true]
Explanation:
- PrefixTree prefixTree = new PrefixTree();
- prefixTree.insert(“dog”);
- prefixTree.search(“dog”); // return true
- prefixTree.search(“do”); // return false
- prefixTree.startsWith(“do”); // return true
- prefixTree.insert(“do”);
- prefixTree.search(“do”); // return true
Constraints:
- 1 <= word.length, prefix.length <= 1000
- word and prefix are made up of lowercase English letters.
Implementing Trie(Prefix Tree) Solution
Recommendation for Time and Space Complexity – Aim for O(n) time for each function call and O(t) space, where n is the length of the string and t is the total number of nodes in the Trie.
Hints for solving problems
Hint 1 :
A Trie is a tree-like structure where each node represents a character and has child nodes for the next characters. Each node has a flag to indicate if it’s the end of a word. The root node is the starting point, and words are stored by linking nodes based on their prefixes.
Hint 2 :
To insert a word, start from the root and iterate through each character. If a character is missing, create a new node for it. Move to the next character until the end, and then mark the last node as the end of the word.
Hint 3 :
To search for a word, follow the characters through the Trie. If a character is missing or the end-of-word flag isn’t set at the last node, return false.
There are mainly 2 approach to solve this problem-
- Prefix Tree(Array) Method
- Prefix Tree(Hash Map) Method
1. Prefix Tree(Array) Method
This approach uses an array to represent child nodes of each Trie node, typically of size 26 for lowercase English letters.
- Each node maintains an array to store references to its child nodes and a flag to mark the end of a word.
- This method is space-efficient for fixed character sets but less flexible for larger or dynamic character ranges.
– Time complexity: O(n) for each function call.
– Space complexity: O(t)
where n is the length of the string and t is the total number of TrieNodes created in the Trie.
Code
class TrieNode {
public:
TrieNode* children[26];
bool endOfWord;
TrieNode() {
for (int i = 0; i < 26; i++) { children[i] = nullptr; } endOfWord = false; } }; class PrefixTree { TrieNode* root; public: PrefixTree() { root = new TrieNode(); } void insert(string word) { TrieNode* cur = root; for (char c : word) { int i = c - 'a'; if (cur->children[i] == nullptr) {
cur->children[i] = new TrieNode();
}
cur = cur->children[i];
}
cur->endOfWord = true;
}
bool search(string word) {
TrieNode* cur = root;
for (char c : word) {
int i = c - 'a';
if (cur->children[i] == nullptr) {
return false;
}
cur = cur->children[i];
}
return cur->endOfWord;
}
bool startsWith(string prefix) {
TrieNode* cur = root;
for (char c : prefix) {
int i = c - 'a';
if (cur->children[i] == nullptr) {
return false;
}
cur = cur->children[i];
}
return true;
}
};
public class TrieNode {
TrieNode[] children = new TrieNode[26];
boolean endOfWord = false;
}
public class PrefixTree {
private TrieNode root;
public PrefixTree() {
root = new TrieNode();
}
public void insert(String word) {
TrieNode cur = root;
for (char c : word.toCharArray()) {
int i = c - 'a';
if (cur.children[i] == null) {
cur.children[i] = new TrieNode();
}
cur = cur.children[i];
}
cur.endOfWord = true;
}
public boolean search(String word) {
TrieNode cur = root;
for (char c : word.toCharArray()) {
int i = c - 'a';
if (cur.children[i] == null) {
return false;
}
cur = cur.children[i];
}
return cur.endOfWord;
}
public boolean startsWith(String prefix) {
TrieNode cur = root;
for (char c : prefix.toCharArray()) {
int i = c - 'a';
if (cur.children[i] == null) {
return false;
}
cur = cur.children[i];
}
return true;
}
}
class TrieNode:
def __init__(self):
self.children = [None] * 26
self.endOfWord = False
class PrefixTree:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str) -> None:
cur = self.root
for c in word:
i = ord(c) - ord("a")
if cur.children[i] == None:
cur.children[i] = TrieNode()
cur = cur.children[i]
cur.endOfWord = True
def search(self, word: str) -> bool:
cur = self.root
for c in word:
i = ord(c) - ord("a")
if cur.children[i] == None:
return False
cur = cur.children[i]
return cur.endOfWord
def startsWith(self, prefix: str) -> bool:
cur = self.root
for c in prefix:
i = ord(c) - ord("a")
if cur.children[i] == None:
return False
cur = cur.children[i]
return True
class TrieNode {
constructor() {
this.children = new Array(26).fill(null);
this.endOfWord = false;
}
}
class PrefixTree {
constructor() {
this.root = new TrieNode();
}
/**
* @param {string} word
* @return {void}
*/
insert(word) {
let cur = this.root;
for (let c of word) {
let i = c.charCodeAt(0) - 97;
if (cur.children[i] === null) {
cur.children[i] = new TrieNode();
}
cur = cur.children[i];
}
cur.endOfWord = true;
}
/**
* @param {string} word
* @return {boolean}
*/
search(word) {
let cur = this.root;
for (let c of word) {
let i = c.charCodeAt(0) - 97;
if (cur.children[i] === null) {
return false;
}
cur = cur.children[i];
}
return cur.endOfWord;
}
/**
* @param {string} prefix
* @return {boolean}
*/
startsWith(prefix) {
let cur = this.root;
for (let c of prefix) {
let i = c.charCodeAt(0) - 97;
if (cur.children[i] === null) {
return false;
}
cur = cur.children[i];
}
return true;
}
}
2. Prefix Tree(Hash Map) Method
This approach uses a hash map in each Trie node to store child nodes, where keys represent characters and values point to the corresponding child nodes. It is more flexible and memory-efficient for dynamic or sparse character sets, as it only stores nodes for existing characters.
- Time complexity: O(n) for each function call.
- Space complexity: O(t)
where n is the length of the string and t is the total number of TrieNodes created in the Trie.
Code
class TrieNode {
public:
unordered_map<char, TrieNode*> children;
bool endOfWord = false;
};
class PrefixTree {
TrieNode* root;
public:
PrefixTree() {
root = new TrieNode();
}
void insert(string word) {
TrieNode* cur = root;
for (char c : word) {
if (cur->children.find(c) == cur->children.end()) {
cur->children[c] = new TrieNode();
}
cur = cur->children[c];
}
cur->endOfWord = true;
}
bool search(string word) {
TrieNode* cur = root;
for (char c : word) {
if (cur->children.find(c) == cur->children.end()) {
return false;
}
cur = cur->children[c];
}
return cur->endOfWord;
}
bool startsWith(string prefix) {
TrieNode* cur = root;
for (char c : prefix) {
if (cur->children.find(c) == cur->children.end()) {
return false;
}
cur = cur->children[c];
}
return true;
}
};
public class TrieNode {
HashMap<Character, TrieNode> children = new HashMap<>();
boolean endOfWord = false;
}
public class PrefixTree {
private TrieNode root;
public PrefixTree() {
root = new TrieNode();
}
public void insert(String word) {
TrieNode cur = root;
for (char c : word.toCharArray()) {
cur.children.putIfAbsent(c, new TrieNode());
cur = cur.children.get(c);
}
cur.endOfWord = true;
}
public boolean search(String word) {
TrieNode cur = root;
for (char c : word.toCharArray()) {
if (!cur.children.containsKey(c)) {
return false;
}
cur = cur.children.get(c);
}
return cur.endOfWord;
}
public boolean startsWith(String prefix) {
TrieNode cur = root;
for (char c : prefix.toCharArray()) {
if (!cur.children.containsKey(c)) {
return false;
}
cur = cur.children.get(c);
}
return true;
}
}
class TrieNode:
def __init__(self):
self.children = {}
self.endOfWord = False
class PrefixTree:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str) -> None:
cur = self.root
for c in word:
if c not in cur.children:
cur.children[c] = TrieNode()
cur = cur.children[c]
cur.endOfWord = True
def search(self, word: str) -> bool:
cur = self.root
for c in word:
if c not in cur.children:
return False
cur = cur.children[c]
return cur.endOfWord
def startsWith(self, prefix: str) -> bool:
cur = self.root
for c in prefix:
if c not in cur.children:
return False
cur = cur.children[c]
return True
class TrieNode {
constructor() {
this.children = new Map();
this.endOfWord = false;
}
}
class PrefixTree {
constructor() {
this.root = new TrieNode();
}
/**
* @param {string} word
* @return {void}
*/
insert(word) {
let cur = this.root;
for (let c of word) {
if (!cur.children.has(c)) {
cur.children.set(c, new TrieNode());
}
cur = cur.children.get(c);
}
cur.endOfWord = true;
}
/**
* @param {string} word
* @return {boolean}
*/
search(word) {
let cur = this.root;
for (let c of word) {
if (!cur.children.has(c)) {
return false;
}
cur = cur.children.get(c);
}
return cur.endOfWord;
}
/**
* @param {string} prefix
* @return {boolean}
*/
startsWith(prefix) {
let cur = this.root;
for (let c of prefix) {
if (!cur.children.has(c)) {
return false;
}
cur = cur.children.get(c);
}
return true;
}
}
