Bonus Topics
Trie | Union-Find (DSU) | Monotonic Stack | Sliding Window | Two Pointers
Topic A β Trie (Prefix Tree)
A.1 β What Is a Trie?
A Trie (pronounced 'try', from retrieval) is a tree-shaped data structure for storing strings where each node represents a single character. Strings that share a common prefix share the same path from the root. This gives O(L) insert, search, and prefix-search operations where L is the string length β independent of the number of stored strings.
Trie Structure: Character-by-Character Branching Insert: ['apple', 'app', 'apt', 'bat', 'bad']root / \ a b | | p a / \ / \ p t t d | | * * (* = isEnd marker) l * | e | *βappβ -> root->a->p->p* (isEnd=true at second p) βappleβ -> root->a->p->p->l->e* (extends βappβ branch) βaptβ -> root->a->p->t* (shares βapβ with above)
Search βappβ: traverse a->p->p, check isEnd -> true. StartsWith βapβ: traverse a->p, node exists -> true. Search βapβ: traverse a->p, isEnd=false -> false.
A.2 β Trie Implementation
class Trie { TrieNode* root; public: Trie() { root = new TrieNode(); }
void insert(const string& word) {
TrieNode* cur = root;
for (char c : word) {
int idx = c - 'a';
if (!cur->children[idx])
cur->children[idx] = new TrieNode();
cur = cur->children[idx];
}
cur->isEnd = true;
}
bool search(const string& word) {
TrieNode* cur = root;
for (char c : word) {
int idx = c - 'a';
if (!cur->children[idx]) return false;
cur = cur->children[idx];
}
return cur->isEnd; // must be a complete word
}
bool startsWith(const string& prefix) {
TrieNode* cur = root;
for (char c : prefix) {
int idx = c - 'a';
if (!cur->children[idx]) return false;
cur = cur->children[idx];
}
return true; // prefix exists, word completeness irrelevant
}
};
</div>
<h3>A.3 β Word Search II (Trie + Backtracking)</h3>
<div class="ch-code-wrap">
```cpp
// LeetCode 212 β Word Search II
// Find all words from a dictionary that exist in a 2D grid.
// Time: O(M*N * 4^L) L=longest word Space: O(total chars in dict)
struct TrieNode {
TrieNode* ch[26] = {};
string word; // non-empty = this node is end of a word
};
class Solution {
void dfs(vector<vector<char>>& g, TrieNode* node,
int r, int c, vector<string>& res) {
if (r<0||r>=(int)g.size()||c<0||c>=(int)g[0].size()) return;
char ch = g[r][c];
if (ch=='#' || !node->ch[ch-'a']) return; // visited or no prefix
node = node->ch[ch-'a'];
if (!node->word.empty()) { // found a complete word
res.push_back(node->word);
node->word = ""; // de-duplicate
}
g[r][c] = '#'; // mark visited
dfs(g,node,r+1,c,res); dfs(g,node,r-1,c,res);
dfs(g,node,r,c+1,res); dfs(g,node,r,c-1,res);
g[r][c] = ch; // restore
}
public:
vector<string> findWords(vector<vector<char>>& board,
vector<string>& words) {
TrieNode* root = new TrieNode();
for (auto& w : words) { // build trie
TrieNode* cur = root;
for (char c : w) {
if (!cur->ch[c-'a']) cur->ch[c-'a'] = new TrieNode();
cur = cur->ch[c-'a'];
}
cur->word = w;
}
vector<string> res;
for (int r=0;r<(int)board.size();r++)
for (int c=0;c<(int)board[0].size();c++)
dfs(board, root, r, c, res);
return res;
}
};
A.4 β When to Use a Trie
| Operation | Trie | Hash Set | Sorted Array (binary search) |
|---|---|---|---|
| Insert word | O(L) | O(L) | O(L + log n) |
| Exact search | O(L) | O(L) | O(L log n) |
| Prefix search | O(L) | O(n*L) scan | O(L log n) |
| All words with prefix | O(L + output) | O(n*L) scan | O(L log n + output) |
| Space | O(total chars * 26) | O(total chars) | O(total chars) |
Trie Complexity
- Insert / Search / StartsWith (length L): O(L) time
- Build Trie from n words avg length L: O(n*L) time, O(n*L*26) space worst case
- Autocomplete (all words with prefix p): O(|p| + k) time where k = output size
- Trie vs Hash Set: Hash set gives O(L) exact lookup. Trie gives O(L) exact lookup PLUS O(L) prefix queries. Use Trie when prefix operations are needed.
Topic B β Union-Find (Disjoint Set Union)
B.1 β What Is Union-Find?
Union-Find (also called Disjoint Set Union, DSU) is a data structure that tracks a partition of elements into disjoint sets. It supports two operations in near-constant amortised time: union(a, b) β merge the sets containing a and b β and find(a) β identify which set a belongs to (by its representative).
Union-Find: Union by Rank + Path Compression Elements: {0, 1, 2, 3, 4, 5} Initially each element is its own set. parent = [0, 1, 2, 3, 4, 5] rank = [0, 0, 0, 0, 0, 0]union(0, 1): find(0)=0, find(1)=1. rank equal -> parent[1]=0, rank[0]++. union(2, 3): find(2)=2, find(3)=3. parent[3]=2, rank[2]++. union(0, 2): find(0)=0, find(2)=2. parent[2]=0, rank[0]++. parent = [0, 0, 0, 2, 4, 5]
find(3) with PATH COMPRESSION: find(3) -> parent[3]=2 -> parent[2]=0 -> root=0. Compress: parent[3] = 0, parent[2] = 0. parent = [0, 0, 0, 0, 4, 5] (3 now directly points to root 0)
Sets: Set A: {0, 1, 2, 3} Set B: {4} Set C: {5}
B.2 β Union-Find Implementation
// Path compression: make every node on the find-path point to root
int find(int x) {
if (parent[x] != x)
parent[x] = find(parent[x]); // recursive compression
return parent[x];
}
// Union by rank: attach smaller tree under larger tree
bool unite(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) return false; // already in same set
if (rank_[ra] < rank_[rb]) swap(ra, rb);
parent[rb] = ra; // attach rb under ra
if (rank_[ra] == rank_[rb]) rank_[ra]++;
components--;
return true;
}
bool connected(int a, int b) { return find(a) == find(b); }
int count() { return components; }
};
</div>
<h3>B.3 β Number of Islands Using Union-Find</h3>
<div class="ch-code-wrap">
```cpp
// LeetCode 200 β Number of Islands (Union-Find approach)
// Time: O(M*N * alpha(M*N)) Space: O(M*N)
int numIslands(vector<vector<char>>& grid) {
int m = grid.size(), n = grid[0].size();
UnionFind uf(m * n);
int water = 0;
int dr[] = {0, 1, 0, -1};
int dc[] = {1, 0, -1, 0};
for (int r = 0; r < m; r++) {
for (int c = 0; c < n; c++) {
if (grid[r][c] == '0') { water++; continue; }
for (int d = 0; d < 4; d++) {
int nr = r+dr[d], nc = c+dc[d];
if (nr>=0&&nr<m&&nc>=0&&nc<n&&grid[nr][nc]=='1')
uf.unite(r*n+c, nr*n+nc); // merge adjacent land
}
}
}
return uf.count() - water; // subtract water cells
}
| Operation | Time | Notes |
|---|---|---|
| find(x) β no optimisation | O(n) worst | Linear chain without path compression |
| find(x) β path compression only | O(log n) amortised | Better but not optimal |
| find(x) β union by rank only | O(log n) worst | Tree height bounded by log n |
| find(x) β both optimisations | O(alpha(n)) amortised | Alpha = inverse Ackermann, < 5 for all practical n |
| n operations total | O(n * alpha(n)) ~ O(n) | Entire DSU effectively linear |
Topic C β Monotonic Stack
C.1 β What Is a Monotonic Stack?
A monotonic stack is a stack that maintains its elements in either strictly increasing or strictly decreasing order from bottom to top. When a new element is pushed, elements that violate the monotone property are popped first. This gives O(n) solutions to problems that naively require O(nΒ²) nested loops.
Monotonic Stack: Next Greater Element Trace Array: [2, 1, 5, 6, 2, 3] Goal: find Next Greater Element (NGE) for each index.Use a DECREASING monotonic stack. Stack stores INDICES. When we pop index j because nums[i] > nums[j], nums[i] is the NGE for j.
i=0: nums[0]=2. Stack empty. Push 0. stack=[0] (values: [2]) i=1: nums[1]=1. 1 < nums[top]=2 -> push 1. stack=[0,1] (values: [2,1]) i=2: nums[2]=5. 5 > nums[1]=1 -> pop 1, NGE[1]=5. 5 > nums[0]=2 -> pop 0, NGE[0]=5. Stack empty. Push 2. stack=[2] (values: [5]) i=3: nums[3]=6. 6 > nums[2]=5 -> pop 2, NGE[2]=6. Push 3. i=4: nums[4]=2. 2 < nums[3]=6 -> push 4. stack=[3,4] i=5: nums[5]=3. 3 > nums[4]=2 -> pop 4, NGE[4]=3. Push 5. End: remaining {3,5} have no NGE -> NGE[3]=NGE[5]=-1.
Result: NGE = [5, 5, 6, -1, 3, -1] Each element pushed and popped at most once -> O(n) total.
C.2 β Next Greater Element
C.3 β Daily Temperatures
| Problem | Stack Type | Pop trigger | Answer at pop time |
|---|---|---|---|
| Next Greater Element | Decreasing | new > top |
NGE = current element |
| Next Smaller Element | Increasing | new < top |
NSE = current element |
| Daily Temperatures | Decreasing | new > top |
wait = current_idx - popped_idx |
| Largest Rectangle | Increasing | new < top |
area = h * calculated_width |
| Trapping Rain Water | Decreasing | new >= top |
trapped = (min(L,R)-h)*width |
| Remove K Digits | Increasing | new < top AND k > 0 |
remove top |
Key Insight
- DECREASING stack (top is smallest): use to find NEXT GREATER ELEMENT. Pop when new element > top.
- INCREASING stack (top is largest): use to find NEXT SMALLER ELEMENT. Pop when new element < top.
- Store INDICES not values when you need width/distance calculations (Histogram, Trapping Rain Water).
- Each element is pushed once and popped at most once β O(n) total operations.
Topic D β Sliding Window
D.1 β Fixed vs Variable Window
The sliding window technique maintains a contiguous subarray or substring by moving two pointers. It reduces O(nΒ²) brute-force to O(n) by avoiding recomputation β instead of restarting from scratch, we slide forward: add the new element on the right, remove the old element on the left.
| Type | When to use | Template shape | Classic problems |
|---|---|---|---|
| Fixed window (size k) | Window size is given | Advance both ends together | Max sum subarray of size k, sliding window max |
| Variable window (shrink) | Optimise window size under constraint | Expand right, shrink left when violated | Longest substring without repeat, min window substring |
// ββ VARIABLE WINDOW: Longest substring without repeating chars β // Time: O(n) Space: O(min(n, charset)) int lengthOfLongestSubstring(string s) { unordered_map<char,int> freq; int lo = 0, best = 0; for (int hi = 0; hi < (int)s.size(); hi++) { freq[s[hi]]++; while (freq[s[hi]] > 1) { // constraint violated: shrink left freq[s[lo]]β; lo++; } best = max(best, hi - lo + 1); } return best; }
// ββ VARIABLE WINDOW: Minimum Window Substring (LC 76) ββββββββ // Time: O(n) Space: O(charset) string minWindow(string s, string t) { unordered_map<char,int> need, have; for (char c : t) need[c]++; int formed = 0, required = need.size(); int lo = 0, minLen = INT_MAX, start = 0; for (int hi = 0; hi < (int)s.size(); hi++) { char c = s[hi]; have[c]++; if (need.count(c) && have[c] == need[c]) formed++; while (formed == required) { // valid window: try to shrink if (hi - lo + 1 < minLen) { minLen = hi-lo+1; start = lo; } have[s[lo]]β; if (need.count(s[lo]) && have[s[lo]] < need[s[lo]]) formedβ; lo++; } } return minLen == INT_MAX ? "" : s.substr(start, minLen); }
</div>
</section>
<!-- TOPIC E: TWO POINTERS -->
<section id="topic-e" class="chapter-section">
<h2> Topic E β Two Pointers</h2>
<h3>E.1 β Two Pointer Patterns</h3>
<p><strong>Two pointers</strong> uses two indices that move through a data structure, typically towards each other or in the same direction. This eliminates the inner loop of an O(nΒ²) solution when the data has monotone properties (sorted array, or a constraint that improves/worsens as pointers move).</p>
<div class="table-responsive">
<table class="insight-table">
<thead>
<tr>
<th>Pattern</th>
<th>Pointer motion</th>
<th>Classic problems</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Opposite ends (converge)</strong></td>
<td>lo starts left, hi starts right; move toward each other</td>
<td>Two Sum (sorted), Container With Most Water, 3Sum</td>
</tr>
<tr>
<td><strong>Same direction (fast/slow)</strong></td>
<td>Both move left-to-right at different speeds</td>
<td>Remove duplicates, find middle of linked list, cycle detection</td>
</tr>
<tr>
<td><strong>Partition</strong></td>
<td>lo tracks write position, hi reads</td>
<td>Dutch National Flag, partition for QuickSort</td>
</tr>
</tbody>
</table>
</div>
<div class="ch-code-wrap">
```cpp
// ββ OPPOSITE ENDS: Two Sum on sorted array ββββββββββββββββββ
// Time: O(n) Space: O(1)
vector<int> twoSumSorted(vector<int>& nums, int target) {
int lo = 0, hi = (int)nums.size()-1;
while (lo < hi) {
int sum = nums[lo] + nums[hi];
if (sum == target) return {lo+1, hi+1}; // 1-indexed
else if (sum < target) lo++; // need larger sum
else hi--; // need smaller sum
}
return {};
}
// ββ SAME DIRECTION: Remove duplicates from sorted array ββββββ
// Time: O(n) Space: O(1)
int removeDuplicates(vector<int>& nums) {
int lo = 0; // write pointer
for (int hi = 1; hi < (int)nums.size(); hi++) {
if (nums[hi] != nums[lo]) { // new unique element
lo++;
nums[lo] = nums[hi];
}
}
return lo + 1; // new length
}
// ββ FAST/SLOW: Floyd's cycle detection βββββββββββββββββββββββ
// Time: O(n) Space: O(1)
bool hasCycle(ListNode* head) {
ListNode *slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) return true; // cycle detected
}
return false;
}
Solved Problem 1: Implement Trie (Prefix Tree)
Implement a Trie supporting insert(word), search(word), and startsWith(prefix). (LeetCode 208 β Medium)
OBSERVATIONS
- Each node has 26 children (one per lowercase letter) and a boolean
isEndflag. Traversal always takes O(L) steps. search()requiresisEnd=trueat the final node.startsWith()only requires the path to exist.- Common mistake: returning true in
search()as soon as the path exists without checkingisEnd. 'app' exists as a path even if only 'apple' was inserted.
Node* traverse(const string& s) { // returns node after last char or null
Node* cur = root;
for (char c : s) {
if (!cur->ch[c-'a']) return nullptr;
cur = cur->ch[c-'a'];
}
return cur;
}
public: void insert(string word) { Node* cur = root; for (char c : word) { if (!cur->ch[c-βaβ]) cur->ch[c-βaβ] = new Node(); cur = cur->ch[c-βaβ]; } cur->isEnd = true; } bool search(string word) { Node* end = traverse(word); return end && end->isEnd; // path exist AND complete word } bool startsWith(string prefix) { return traverse(prefix) != nullptr; // path existence is sufficient } };
</div>
<p><strong>Complexity:</strong> Time O(L) per operation, Space O(total_chars * 26)</p>
</section>
<!-- SOLVED PROBLEM 2 -->
<section id="solved-2" class="chapter-section">
<h2> Solved Problem 2: Number of Connected Components</h2>
<p>Given n nodes (0 to n-1) and a list of undirected edges, return the number of connected components. (LeetCode 323 β Medium)</p>
<div class="insight-box">
<h4>OBSERVATIONS</h4>
<ul>
<li>Classic Union-Find application: initialise n components (each node is its own set), then for each edge union the two endpoints. Final component count = answer.</li>
<li>Alternative: BFS/DFS counting visited components. Both O(V+E). Union-Find is cleaner for dynamic edge-addition scenarios.</li>
<li>Every time <code>unite(a,b)</code> successfully merges two different sets, the component count decreases by 1. Start from n, apply all edges.</li>
</ul>
</div>
<div class="ch-code-wrap">
```cpp
class Solution {
public:
int countComponents(int n, vector<vector<int>>& edges) {
vector<int> parent(n), rank_(n, 0);
iota(parent.begin(), parent.end(), 0);
function<int(int)> find = [&](int x) -> int {
if (parent[x] != x) parent[x] = find(parent[x]); // path compress
return parent[x];
};
int components = n;
for (auto& e : edges) {
int ra = find(e[0]), rb = find(e[1]);
if (ra == rb) continue; // already connected: skip
if (rank_[ra] < rank_[rb]) swap(ra, rb);
parent[rb] = ra;
if (rank_[ra] == rank_[rb]) rank_[ra]++;
components--;
}
return components;
}
};
Complexity: Time O((V+E) * alpha(V)), Space O(V)
Solved Problem 3: Largest Rectangle in Histogram
Given an array of bar heights representing a histogram, return the area of the largest rectangle. (LeetCode 84 β Hard)
OBSERVATIONS
- For each bar, the largest rectangle using that bar as the shortest has height = heights[i] and extends left and right as far as all bars are >= heights[i].
- Brute force: For each pair (i,j), compute min height Γ width. O(nΒ²). TLE for large n.
- Monotonic stack insight: Maintain an increasing stack of indices. When we encounter a bar shorter than the stack top, the top bar is the shortest in its maximal range β compute its area immediately.
- Width calculation: when popping index j at trigger i, width = i - stack.top() - 1 (or just i if stack is empty).
- Sentinel: append 0 to heights to force all bars to pop at the end.
Complexity: Time O(n), Space O(n)
Complexity Reference β All Bonus Topics
| Data Structure / Algorithm | Operation | Time | Space |
|---|---|---|---|
| Trie | Insert / Search / StartsWith | O(L) | O(n*L*26) |
| Trie | Build from n words | O(n*L) | O(n*L*26) |
| Union-Find (both optimisations) | find / unite | O(alpha(n)) ~ O(1) | O(n) |
| Monotonic Stack | Next Greater/Smaller Element | O(n) | O(n) |
| Monotonic Stack | Largest Rectangle in Histogram | O(n) | O(n) |
| Sliding Window (fixed) | Max/min over window of size k | O(n) | O(1) |
| Sliding Window (variable) | Longest/shortest window under constraint | O(n) | O(charset) |
| Two Pointers (converge) | Two Sum on sorted array | O(n) | O(1) |
| Two Pointers (fast/slow) | Cycle detection, find middle | O(n) | O(1) |
Common Interview Questions
| Topic | Problem | Key Detail |
|---|---|---|
| Trie | Implement Trie (LC 208) | insert, search, startsWith |
| Design Add and Search Words (LC 211) | Trie with wildcard '.' DFS | |
| Word Search II (LC 212) | Trie + DFS backtracking on grid | |
| Replace Words (LC 648) | Trie prefix replacement in sentence | |
| Maximum XOR of Two Numbers (LC 421) | Binary Trie (30 levels) | |
| Union-Find | Number of Connected Components (LC 323) | Classic DSU application |
| Redundant Connection (LC 684) | First edge creating a cycle | |
| Graph Valid Tree (LC 261) | n-1 edges + 1 component | |
| Accounts Merge (LC 721) | Union on shared emails | |
| Number of Islands (LC 200) | BFS or Union-Find on grid | |
| Monotonic Stack | Daily Temperatures (LC 739) | Days until warmer, decreasing stack |
| Next Greater Element I (LC 496) | NGE with hash map + stack | |
| Largest Rectangle in Histogram (LC 84) | Increasing stack, O(n) | |
| Maximal Rectangle (LC 85) | Histogram per row + LC 84 | |
| Trapping Rain Water (LC 42) | Monotonic stack or two pointers | |
| Sliding Window | Maximum Average Subarray (LC 643) | Fixed window of size k |
| Longest Substring Without Repeating (LC 3) | Variable window, freq map | |
| Minimum Window Substring (LC 76) | Variable window, formed count | |
| Permutation in String (LC 567) | Fixed window, freq comparison | |
| Sliding Window Maximum (LC 239) | Fixed window, monotonic deque | |
| Two Pointers | Two Sum II β sorted array (LC 167) | Converging pointers |
| 3Sum (LC 15) | Sort + two pointers for each fixed element | |
| Container With Most Water (LC 11) | Always move the shorter pointer | |
| Linked List Cycle II (LC 142) | Floyd's algorithm, find cycle entry | |
| Sort Colors / Dutch Flag (LC 75) | 3-way partition, lo/mid/hi pointers |
Chapter 11 β Key Takeaways
- TRIE: O(L) insert/search/prefix-check regardless of dictionary size. Children array [26] + isEnd flag. Use when prefix queries are needed.
- UNION-FIND: near-O(1) amortised connectivity queries. Path compression + union by rank together give O(alpha(n)). Use for dynamic connectivity, cycle detection, and component counting.
- MONOTONIC STACK: O(n) Next Greater/Smaller Element and histogram problems. Store indices (not values) when width/distance is needed.
- SLIDING WINDOW: O(n) for subarray/substring optimisation. Fixed window: slide both ends. Variable window: expand right, shrink left when constraint violated.
- TWO POINTERS: O(n) on sorted arrays or linked lists. Converging pointers for sum problems; fast/slow for cycle detection; partition for in-place array reorganisation.
- Pattern selection: prefix queries β Trie. Dynamic connectivity β Union-Find. Next greater in O(n) β Monotonic Stack. Optimal contiguous window β Sliding Window. O(n) on sorted structure β Two Pointers.