TRIES › INTRODUCTORY QUESTIONS
Open — the attempt gate is not wired up yet
This editorial is meant to unlock after you have run the problem at least once, with the worked solution behind one further deliberate click. That needs per-learner unlock state nothing stores today, so for now the whole article is open.
Try it yourself first →A trie represents a word in two separate ways: its characters select a path from the root, and a terminal marker says that the path itself is a complete word. This distinction is the key to deletion. If you remove the marker for the, the path t -> h -> e must remain when there, their, or another longer word still uses it.
After clearing the marker, inspect the key's path from the end toward the root. A node can be freed only when it is not the end of another word and has no children. The first node that fails either test protects every earlier prefix, because that node is still needed by some word.
| MEASURE | BOUND | WHY |
|---|---|---|
| Time | O(S + L) | Insertion and the final membership scan each process every input character a constant number of times, for O(S) total. Deletion visits at most L path nodes and checks 26 child slots per node; 26 is constant, so that work is O(L). |
| Space | O(S) extra | The trie has at most one node per inserted character plus the root, so it uses O(S) working memory; the recorded deletion path adds O(L). The returned vector is required output and is excluded. A trie-shaped input with no shared prefixes reaches the worst bound of O(S), while a highly shared input uses fewer nodes. |
#include <string>
#include <vector>
using namespace std;
class Solution {
struct Node {
Node* child[26];
bool terminal;
Node() : terminal(false) {
for (int i = 0; i < 26; ++i) {
child[i] = nullptr;
}
}
};
Node* root;
bool hasChild(Node* node) {
for (int i = 0; i < 26; ++i) {
if (node->child[i] != nullptr) return true;
}
return false;
}
void insertWord(const string& word) {
Node* current = root;
for (char letter : word) {
int index = letter - 'a';
if (current->child[index] == nullptr) {
current->child[index] = new Node();
}
current = current->child[index];
}
current->terminal = true;
}
bool searchWord(const string& word) const {
Node* current = root;
for (char letter : word) {
int index = letter - 'a';
if (current->child[index] == nullptr) return false;
current = current->child[index];
}
return current->terminal;
}
void eraseWord(const string& key) {
Node* current = root;
vector<Node*> path(key.size() + 1);
path[0] = root;
for (int i = 0; i < static_cast<int>(key.size()); ++i) {
int index = key[i] - 'a';
if (current->child[index] == nullptr) return;
current = current->child[index];
path[i + 1] = current;
}
if (!current->terminal) return;
current->terminal = false;
for (int i = static_cast<int>(key.size()) - 1; i >= 0; --i) {
Node* parent = path[i];
int index = key[i] - 'a';
Node* child = parent->child[index];
if (child->terminal || hasChild(child)) break;
delete child;
parent->child[index] = nullptr;
}
}
public:
vector<string> deleteKey(vector<string> words, string key) {
root = new Node();
for (const string& word : words) {
insertWord(word);
}
eraseWord(key);
vector<string> remaining;
for (const string& word : words) {
if (searchWord(word)) {
remaining.push_back(word);
}
}
return remaining;
}
};The terminal check must happen before pruning, and the pruning loop must move from the key's last character toward the root. Clearing the marker handles the logical deletion; the backward loop is only memory cleanup. Its break condition is deliberately conservative: once a node is terminal or has any child, every earlier node on the path is also still needed by a surviving word.