Master prefix trees and specialized data structures for optimal performance. Learn Tries for string problems, Segment Trees for range queries, Fenwick Trees for efficient updates, Union-Find for connectivity, and more. Build production-grade implementations with complete C# code for 10+ interview problems.
A Trie (prefix tree) is a tree-based data structure optimized for string search and prefix matching. Each node represents a character, enabling O(m) search where m is the word length, independent of dictionary size.
A Trie stores strings in a tree where:
public class TrieNode { public Dictionary<char, TrieNode> children = new(); public bool isWord = false; public int count = 0; // For frequency tracking }
public void Insert(string word) { TrieNode node = root; foreach (char c in word) { if (!node.children.ContainsKey(c)) node.children[c] = new TrieNode(); node = node.children[c]; } node.isWord = true; node.count++; }
Time: O(m) where m = word length | Space: O(1) amortized
public bool Search(string word) { TrieNode node = root; foreach (char c in word) { if (!node.children.ContainsKey(c)) return false; node = node.children[c]; } return node.isWord; }
Time: O(m) | Space: O(1)
public bool StartsWith(string prefix) { TrieNode node = root; foreach (char c in prefix) { if (!node.children.ContainsKey(c)) return false; node = node.children[c]; } return true; }
Time: O(m) | Space: O(1)
public bool Delete(string word) { return DeleteHelper(root, word, 0); } private bool DeleteHelper(TrieNode node, string word, int idx) { if (idx == word.Length) { if (!node.isWord) return false; node.isWord = false; return node.children.Count == 0; } char c = word[idx]; if (!node.children.ContainsKey(c)) return false; TrieNode child = node.children[c]; bool shouldDeleteChild = DeleteHelper(child, word, idx + 1); if (shouldDeleteChild) node.children.Remove(c); return !node.isWord && node.children.Count == 0; }
Time: O(m) | Space: O(m) recursive depth
Full Trie class with all operations, supporting word insertion, exact search, prefix matching, deletion, and autocomplete.
public class Trie { private TrieNode root = new TrieNode(); public void Insert(string word) { TrieNode node = root; foreach (char c in word) { if (!node.children.ContainsKey(c)) node.children[c] = new TrieNode(); node = node.children[c]; } node.isWord = true; } public bool Search(string word) { TrieNode node = FindNode(word); return node != null && node.isWord; } public bool StartsWith(string prefix) { return FindNode(prefix) != null; } private TrieNode FindNode(string s) { TrieNode node = root; foreach (char c in s) { if (!node.children.ContainsKey(c)) return null; node = node.children[c]; } return node; } public List<string> GetWordsWithPrefix(string prefix) { List<string> result = new(); TrieNode node = FindNode(prefix); if (node == null) return result; DFS(node, prefix, result); return result; } private void DFS(TrieNode node, string path, List<string> result) { if (node.isWord) result.Add(path); foreach (var (c, child) in node.children) DFS(child, path + c, result); } public bool Delete(string word) { return DeleteHelper(root, word, 0); } private bool DeleteHelper(TrieNode node, string word, int idx) { if (idx == word.Length) { if (!node.isWord) return false; node.isWord = false; return node.children.Count == 0; } char c = word[idx]; if (!node.children.ContainsKey(c)) return false; TrieNode child = node.children[c]; bool shouldDelete = DeleteHelper(child, word, idx + 1); if (shouldDelete) node.children.Remove(c); return !node.isWord && node.children.Count == 0; } }
Design and implement a trie with insert, search, and startsWith methods.
public class Trie { private TrieNode root; public Trie() { root = new TrieNode(); } public void Insert(string word) { TrieNode node = root; foreach (char c in word) { if (!node.children.ContainsKey(c)) node.children[c] = new TrieNode(); node = node.children[c]; } node.isWord = true; } public bool Search(string word) { TrieNode node = root; foreach (char c in word) { if (!node.children.ContainsKey(c)) return false; node = node.children[c]; } return node.isWord; } public bool StartsWith(string prefix) { TrieNode node = root; foreach (char c in prefix) { if (!node.children.ContainsKey(c)) return false; node = node.children[c]; } return true; } }
Find all words from a dictionary that exist in an m×n board. Words can be constructed by sequentially adjacent cells (horizontally or vertically).
public IList<string> FindWords(char[][] board, string[] words) { HashSet<string> result = new(); TrieNode root = new TrieNode(); foreach (var word in words) { TrieNode node = root; foreach (char c in word) { if (!node.children.ContainsKey(c)) node.children[c] = new TrieNode(); node = node.children[c]; } node.isWord = true; } int rows = board.Length, cols = board[0].Length; bool[][] visited = new bool[rows][]; for (int i = 0; i < rows; i++) { visited[i] = new bool[cols]; for (int j = 0; j < cols; j++) { DFS(board, visited, i, j, root, "", result); } } return new List<string>(result); } private void DFS(char[][] board, bool[][] visited, int i, int j, TrieNode node, string path, HashSet<string> result) { if (i < 0 || i >= board.Length || j < 0 || j >= board[0].Length || visited[i][j]) return; char c = board[i][j]; if (!node.children.ContainsKey(c)) return; visited[i][j] = true; node = node.children[c]; if (node.isWord) result.Add(path + c); DFS(board, visited, i + 1, j, node, path + c, result); DFS(board, visited, i - 1, j, node, path + c, result); DFS(board, visited, i, j + 1, node, path + c, result); DFS(board, visited, i, j - 1, node, path + c, result); visited[i][j] = false; }
Design a data structure that supports adding words and searching for words with '.' as wildcard matching any single letter.
public class WordDictionary { private TrieNode root = new TrieNode(); public void AddWord(string word) { TrieNode node = root; foreach (char c in word) { if (!node.children.ContainsKey(c)) node.children[c] = new TrieNode(); node = node.children[c]; } node.isWord = true; } public bool Search(string word) { return SearchDFS(root, word, 0); } private bool SearchDFS(TrieNode node, string word, int idx) { if (idx == word.Length) return node.isWord; char c = word[idx]; if (c == '.') { foreach (var child in node.children.Values) if (SearchDFS(child, word, idx + 1)) return true; } else { if (!node.children.ContainsKey(c)) return false; return SearchDFS(node.children[c], word, idx + 1); } return false; } }
Replace words in a sentence with their shortest dictionary root if the root exists.
public string ReplaceWords(IList<string> dictionary, string sentence) { TrieNode root = new TrieNode(); foreach (var word in dictionary) { TrieNode node = root; foreach (char c in word) { if (!node.children.ContainsKey(c)) node.children[c] = new TrieNode(); node = node.children[c]; } node.isWord = true; } string[] words = sentence.Split(' '); StringBuilder result = new(); foreach (var word in words) { TrieNode node = root; string root_word = ""; foreach (char c in word) { if (!node.children.ContainsKey(c) || node.isWord) break; root_word += c; node = node.children[c]; } if (node.isWord) result.Append(root_word); else result.Append(word); result.Append(' '); } return result.ToString().TrimEnd(); }
Find the longest word in a dictionary that can be built by iteratively adding one letter at a time, starting from a single letter word.
public string LongestWord(string[] words) { TrieNode root = new TrieNode(); foreach (var word in words) { TrieNode node = root; foreach (char c in word) { if (!node.children.ContainsKey(c)) node.children[c] = new TrieNode(); node = node.children[c]; } node.isWord = true; } Array.Sort(words, (a, b) => b.Length != a.Length ? b.Length.CompareTo(a.Length) : a.CompareTo(b)); foreach (var word in words) { TrieNode node = root; bool valid = true; foreach (char c in word) { node = node.children[c]; if (!node.isWord) { valid = false; break; } } if (valid) return word; } return ""; }
A Segment Tree is a binary tree where each node stores an aggregate function (sum, min, max) over a range. Enables O(log n) range queries and O(log n) point updates.
public class SegmentTree { private int[] tree; private int n; public SegmentTree(int[] nums) { n = nums.Length; tree = new int[4 * n]; if (n > 0) Build(nums, 0, 0, n - 1); } private void Build(int[] nums, int node, int start, int end) { if (start == end) { tree[node] = nums[start]; } else { int mid = start + (end - start) / 2; Build(nums, 2 * node + 1, start, mid); Build(nums, 2 * node + 2, mid + 1, end); tree[node] = tree[2 * node + 1] + tree[2 * node + 2]; } } public void Update(int index, int val) { UpdateHelper(0, 0, n - 1, index, val); } private void UpdateHelper(int node, int start, int end, int idx, int val) { if (start == end) { tree[node] = val; } else { int mid = start + (end - start) / 2; if (idx <= mid) UpdateHelper(2 * node + 1, start, mid, idx, val); else UpdateHelper(2 * node + 2, mid + 1, end, idx, val); tree[node] = tree[2 * node + 1] + tree[2 * node + 2]; } } public int QuerySum(int left, int right) { return QueryHelper(0, 0, n - 1, left, right); } private int QueryHelper(int node, int start, int end, int L, int R) { if (R < start || end < L) return 0; if (L <= start && end <= R) return tree[node]; int mid = start + (end - start) / 2; int p1 = QueryHelper(2 * node + 1, start, mid, L, R); int p2 = QueryHelper(2 * node + 2, mid + 1, end, L, R); return p1 + p2; } // Query minimum in range public int QueryMin(int left, int right) { return QueryMinHelper(0, 0, n - 1, left, right); } private int QueryMinHelper(int node, int start, int end, int L, int R) { if (R < start || end < L) return int.MaxValue; if (L <= start && end <= R) return tree[node]; int mid = start + (end - start) / 2; int p1 = QueryMinHelper(2 * node + 1, start, mid, L, R); int p2 = QueryMinHelper(2 * node + 2, mid + 1, end, L, R); return Math.Min(p1, p2); } }
Implement a class for range sum queries with element updates using segment tree.
public class NumArray { private SegmentTree st; public NumArray(int[] nums) { st = new SegmentTree(nums); } public void Update(int index, int val) { st.Update(index, val); } public int SumRange(int left, int right) { return st.QuerySum(left, right); } }
A Fenwick Tree is a space-efficient data structure for prefix sum queries and point updates in O(log n) time, using only O(n) space.
public class FenwickTree { private int[] tree; private int n; public FenwickTree(int size) { n = size; tree = new int[n + 1]; } // Point update: add delta to index public void Update(int index, int delta) { index++; // 1-indexed while (index <= n) { tree[index] += delta; index += index & (-index); // Add lowest set bit } } // Prefix sum query [0, index] public int Query(int index) { index++; // 1-indexed int sum = 0; while (index > 0) { sum += tree[index]; index -= index & (-index); } return sum; } // Range sum [left, right] public int RangeQuery(int left, int right) { if (left == 0) return Query(right); return Query(right) - Query(left - 1); } // Build from array in O(n log n) public static FenwickTree Build(int[] nums) { FenwickTree ft = new FenwickTree(nums.Length); for (int i = 0; i < nums.Length; i++) ft.Update(i, nums[i]); return ft; } }
index & (-index) isolates the lowest set bit. This enables O(log n) navigation of the tree structure without explicit pointers.
| Feature | Segment Tree | Fenwick Tree |
|---|---|---|
| Query Time | O(log n) | O(log n) |
| Update Time | O(log n) | O(log n) |
| Space | O(4n) | O(n) |
| Build Time | O(n) | O(n log n) |
| Range Min/Max | Yes | No (sum only) |
| Range Update | With lazy prop | No |
| Implementation | Recursive | Iterative |
| Use When | Multiple functions, complex queries | Sum queries, space-efficient |
Union-Find efficiently manages a collection of disjoint sets with two operations: Find (with path compression) and Union (with rank optimization). Both approach O(1) amortized time.
public class UnionFind { private int[] parent; private int[] rank; private int count; public UnionFind(int n) { parent = new int[n]; rank = new int[n]; count = n; for (int i = 0; i < n; i++) parent[i] = i; } // Find with path compression public int Find(int x) { if (parent[x] != x) parent[x] = Find(parent[x]); // Path compression return parent[x]; } // Union by rank public bool Union(int x, int y) { int rootX = Find(x); int rootY = Find(y); if (rootX == rootY) return false; // Already connected if (rank[rootX] < rank[rootY]) parent[rootX] = rootY; else if (rank[rootX] > rank[rootY]) parent[rootY] = rootX; else { parent[rootY] = rootX; rank[rootX]++; } count--; return true; // Successfully merged } // Check if two elements are in same set public bool Connected(int x, int y) { return Find(x) == Find(y); } // Number of connected components public int GetCount() { return count; } }
Find the number of connected components in an undirected graph.
public int CountComponents(int n, int[][] edges) { UnionFind uf = new UnionFind(n); foreach (var edge in edges) uf.Union(edge[0], edge[1]); return uf.GetCount(); }
Find an edge that creates a cycle in an undirected tree (n nodes, n edges). Return any redundant edge.
public int[] FindRedundantConnection(int[][] edges) { UnionFind uf = new UnionFind(edges.Length + 1); foreach (var edge in edges) { if (!uf.Union(edge[0], edge[1])) return edge; // Found cycle } return new int[] { }; }
Merge accounts with shared email addresses. Return accounts with all merged emails in sorted order.
public IList<IList<string>> AccountsMerge(IList<IList<string>> accounts) { Dictionary<string, int> emailToId = new(); Dictionary<int, List<string>> idToEmails = new(); UnionFind uf = new UnionFind(accounts.Count); int idx = 0; foreach (var account in accounts) { List<string> emails = new(account); string name = emails[0]; emails.RemoveAt(0); int firstEmailId = -1; foreach (var email in emails) { if (!emailToId.ContainsKey(email)) { emailToId[email] = idx; firstEmailId = idx; } else { firstEmailId = emailToId[email]; } uf.Union(idx, firstEmailId); } idx++; } foreach (var (email, accountId) in emailToId) { int root = uf.Find(accountId); if (!idToEmails.ContainsKey(root)) idToEmails[root] = new List<string>(); idToEmails[root].Add(email); } IList<IList<string>> result = new List<IList<string>>(); idx = 0; foreach (var account in accounts) { int root = uf.Find(idx); if (idToEmails.ContainsKey(root)) { List<string> merged = new(); merged.Add(account[0]); idToEmails[root].Sort(); merged.AddRange(idToEmails[root]); result.Add(merged); idToEmails.Remove(root); } idx++; } return result; }
Find the number of provinces given an isConnected matrix (0=not connected, 1=connected).
public int FindCircleNum(int[][] isConnected) { int n = isConnected.Length; UnionFind uf = new UnionFind(n); for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { if (isConnected[i][j] == 1) uf.Union(i, j); } } return uf.GetCount(); }
A Skip List is a probabilistic data structure that maintains sorted sequences with O(log n) search, insertion, and deletion in expectation. Alternative to balanced trees.
Design a cache that evicts the least recently used item when capacity is exceeded. O(1) get and put operations using HashMap + Doubly Linked List.
public class LRUCache { private class Node { public int key, val; public Node prev, next; public Node(int k, int v) { key = k; val = v; } } private int capacity; private Dictionary<int, Node> cache = new(); private Node head = new Node(0, 0); private Node tail = new Node(0, 0); public LRUCache(int capacity) { this.capacity = capacity; head.next = tail; tail.prev = head; } public int Get(int key) { if (!cache.ContainsKey(key)) return -1; Node node = cache[key]; MoveToHead(node); return node.val; } public void Put(int key, int value) { if (cache.ContainsKey(key)) { Node node = cache[key]; node.val = value; MoveToHead(node); } else { Node newNode = new Node(key, value); cache[key] = newNode; AddToHead(newNode); if (cache.Count > capacity) { Node oldest = tail.prev; RemoveNode(oldest); cache.Remove(oldest.key); } } } private void AddToHead(Node node) { node.next = head.next; node.prev = head; head.next.prev = node; head.next = node; } private void RemoveNode(Node node) { node.prev.next = node.next; node.next.prev = node.prev; } private void MoveToHead(Node node) { RemoveNode(node); AddToHead(node); } }
Evicts the item with the lowest frequency. If tied, evict the least recently used among those with the lowest frequency.
A Bloom Filter is a space-efficient probabilistic data structure for membership testing. Uses multiple hash functions and bit array with O(1) insert and lookup, but allows false positives.
public class BloomFilter { private BitArray bits; private int k; // number of hash functions public BloomFilter(int size, int hashFunctions) { bits = new BitArray(size, false); k = hashFunctions; } public void Add(string element) { for (int i = 0; i < k; i++) { int hash = Hash(element, i) % bits.Length; bits[hash] = true; } } public bool MightContain(string element) { for (int i = 0; i < k; i++) { int hash = Hash(element, i) % bits.Length; if (!bits[hash]) return false; // Definitely not present } return true; // Probably present } private int Hash(string s, int seed) { int hash = seed; foreach (char c in s) hash = hash * 31 + c; return Math.Abs(hash); } }
| DS | Insert | Search | Delete | Space | Best For |
|---|---|---|---|---|---|
| Trie | O(m) | O(m) | O(m) | O(N*M) | Prefix search |
| SegTree | O(log n) | O(log n) | N/A | O(4n) | Range queries |
| FenwickTree | O(log n) | O(log n) | N/A | O(n) | Sum queries |
| UnionFind | O(α(n)) | O(α(n)) | O(α(n)) | O(n) | Connectivity |
| SkipList | O(log n) | O(log n) | O(log n) | O(n) | Ordered data |
| LRUCache | O(1) | O(1) | O(1) | O(capacity) | Caching |