Advanced Tree Structures
Tries (Prefix Trees)
Why Tries Exist
Tries excel at string operations like autocomplete, spell checking, and prefix matching. They provide O(m) search time where m is the string length, regardless of how many strings are stored. Essential for text processing, search engines, and any application dealing with string prefixes.
Real impact: Tries power autocomplete features, spell checkers, IP routing tables, and efficient string search in large datasets.
Trie Implementation
public class TrieNode
{
public Dictionary<char, TrieNode> Children { get; set; }
public bool IsEndOfWord { get; set; }
public string Word { get; set; } // Optional: store the complete word
public TrieNode()
{
Children = new Dictionary<char, TrieNode>();
IsEndOfWord = false;
Word = null;
}
}
public class Trie
{
private TrieNode root;
public Trie()
{
root = new TrieNode();
}
public void Insert(string word)
{
TrieNode current = root;
foreach (char c in word.ToLower())
{
if (!current.Children.ContainsKey(c))
{
current.Children[c] = new TrieNode();
}
current = current.Children[c];
}
current.IsEndOfWord = true;
current.Word = word;
}
public bool Search(string word)
{
TrieNode node = FindNode(word.ToLower());
return node != null && node.IsEndOfWord;
}
public bool StartsWith(string prefix)
{
return FindNode(prefix.ToLower()) != null;
}
private TrieNode FindNode(string prefix)
{
TrieNode current = root;
foreach (char c in prefix)
{
if (!current.Children.ContainsKey(c))
{
return null;
}
current = current.Children[c];
}
return current;
}
// Get all words with given prefix (autocomplete)
public List<string> GetWordsWithPrefix(string prefix)
{
var result = new List<string>();
TrieNode prefixNode = FindNode(prefix.ToLower());
if (prefixNode != null)
{
CollectWords(prefixNode, prefix.ToLower(), result);
}
return result;
}
private void CollectWords(TrieNode node, string prefix, List<string> result)
{
if (node.IsEndOfWord)
{
result.Add(node.Word ?? prefix);
}
foreach (var kvp in node.Children)
{
CollectWords(kvp.Value, prefix + kvp.Key, result);
}
}
// Delete a word from trie
public void Delete(string word)
{
DeleteHelper(root, word.ToLower(), 0);
}
private bool DeleteHelper(TrieNode node, string word, int index)
{
if (index == word.Length)
{
if (!node.IsEndOfWord) return false;
node.IsEndOfWord = false;
node.Word = null;
// If node has no children, it can be deleted
return node.Children.Count == 0;
}
char c = word[index];
if (!node.Children.ContainsKey(c)) return false;
bool shouldDeleteChild = DeleteHelper(node.Children[c], word, index + 1);
if (shouldDeleteChild)
{
node.Children.Remove(c);
// Return true if current node has no children and is not end of word
return node.Children.Count == 0 && !node.IsEndOfWord;
}
return false;
}
}
Advanced Trie Applications
public static class TrieApplications
{
// Word suggestion with maximum count
public static List<string> GetTopSuggestions(Trie trie, string prefix, int maxSuggestions)
{
var suggestions = trie.GetWordsWithPrefix(prefix);
return suggestions.Take(maxSuggestions).ToList();
}
// Find longest common prefix of all strings
public static string LongestCommonPrefix(string[] words)
{
if (words.Length == 0) return "";
var trie = new Trie();
foreach (string word in words)
{
trie.Insert(word);
}
var current = trie.root;
var prefix = new StringBuilder();
while (current.Children.Count == 1 && !current.IsEndOfWord)
{
var kvp = current.Children.First();
prefix.Append(kvp.Key);
current = kvp.Value;
}
return prefix.ToString();
}
// Count words with given prefix
public static int CountWordsWithPrefix(Trie trie, string prefix)
{
return trie.GetWordsWithPrefix(prefix).Count;
}
}
Balanced Binary Search Trees
AVL Trees (Self-Balancing BST)
Invented by Soviet mathematicians Georgy Adelson-Velsky and Evgenii Landis in 1962. Named from their initials: AVL
Key properties:
- Strictly balanced: height difference between left and right subtrees ≤ 1
- Guarantees O(log n) for search, insert, delete
- More strictly balanced than Red-Black trees (faster lookups, slower insertions)
- Uses rotations (single and double) to maintain balance
-
Trade-off: Faster reads, slower writes compared to Red-Black trees ```csharp public class AVLNode
where T : IComparable { public T Value { get; set; } public AVLNode Left { get; set; } public AVLNode Right { get; set; } public int Height { get; set; } public AVLNode(T value) { Value = value; Height = 1; } }
public class AVLTree
private int GetHeight(AVLNode<T> node)
{
return node?.Height ?? 0;
}
private int GetBalance(AVLNode<T> node)
{
return node == null ? 0 : GetHeight(node.Left) - GetHeight(node.Right);
}
private void UpdateHeight(AVLNode<T> node)
{
if (node != null)
{
node.Height = 1 + Math.Max(GetHeight(node.Left), GetHeight(node.Right));
}
}
private AVLNode<T> RotateRight(AVLNode<T> y)
{
AVLNode<T> x = y.Left;
AVLNode<T> T2 = x.Right;
// Perform rotation
x.Right = y;
y.Left = T2;
// Update heights
UpdateHeight(y);
UpdateHeight(x);
return x;
}
private AVLNode<T> RotateLeft(AVLNode<T> x)
{
AVLNode<T> y = x.Right;
AVLNode<T> T2 = y.Left;
// Perform rotation
y.Left = x;
x.Right = T2;
// Update heights
UpdateHeight(x);
UpdateHeight(y);
return y;
}
public void Insert(T value)
{
root = InsertHelper(root, value);
}
private AVLNode<T> InsertHelper(AVLNode<T> node, T value)
{
// 1. Perform normal BST insertion
if (node == null)
{
return new AVLNode<T>(value);
}
int comparison = value.CompareTo(node.Value);
if (comparison < 0)
{
node.Left = InsertHelper(node.Left, value);
}
else if (comparison > 0)
{
node.Right = InsertHelper(node.Right, value);
}
else
{
return node; // Duplicate values not allowed
}
// 2. Update height of current node
UpdateHeight(node);
// 3. Get the balance factor
int balance = GetBalance(node);
// 4. If unbalanced, there are 4 cases
// Left Left Case
if (balance > 1 && value.CompareTo(node.Left.Value) < 0)
{
return RotateRight(node);
}
// Right Right Case
if (balance < -1 && value.CompareTo(node.Right.Value) > 0)
{
return RotateLeft(node);
}
// Left Right Case
if (balance > 1 && value.CompareTo(node.Left.Value) > 0)
{
node.Left = RotateLeft(node.Left);
return RotateRight(node);
}
// Right Left Case
if (balance < -1 && value.CompareTo(node.Right.Value) < 0)
{
node.Right = RotateRight(node.Right);
return RotateLeft(node);
}
return node;
}
public bool Search(T value)
{
return SearchHelper(root, value);
}
private bool SearchHelper(AVLNode<T> node, T value)
{
if (node == null) return false;
int comparison = value.CompareTo(node.Value);
if (comparison == 0) return true;
else if (comparison < 0) return SearchHelper(node.Left, value);
else return SearchHelper(node.Right, value);
} } ```
Segment Trees
Basic Segment Tree (Range Sum Queries)
public class SegmentTree
{
private int[] tree;
private int n;
public SegmentTree(int[] arr)
{
n = arr.Length;
tree = new int[4 * n];
Build(arr, 0, 0, n - 1);
}
private void Build(int[] arr, int node, int start, int end)
{
if (start == end)
{
tree[node] = arr[start];
}
else
{
int mid = (start + end) / 2;
Build(arr, 2 * node + 1, start, mid);
Build(arr, 2 * node + 2, mid + 1, end);
tree[node] = tree[2 * node + 1] + tree[2 * node + 2];
}
}
public void Update(int index, int value)
{
UpdateHelper(0, 0, n - 1, index, value);
}
private void UpdateHelper(int node, int start, int end, int index, int value)
{
if (start == end)
{
tree[node] = value;
}
else
{
int mid = (start + end) / 2;
if (index <= mid)
{
UpdateHelper(2 * node + 1, start, mid, index, value);
}
else
{
UpdateHelper(2 * node + 2, mid + 1, end, index, value);
}
tree[node] = tree[2 * node + 1] + tree[2 * node + 2];
}
}
public int Query(int left, int right)
{
return QueryHelper(0, 0, n - 1, left, right);
}
private int QueryHelper(int node, int start, int end, int left, int right)
{
if (right < start || end < left)
{
return 0; // Out of range
}
if (left <= start && end <= right)
{
return tree[node]; // Segment completely inside range
}
int mid = (start + end) / 2;
int leftSum = QueryHelper(2 * node + 1, start, mid, left, right);
int rightSum = QueryHelper(2 * node + 2, mid + 1, end, left, right);
return leftSum + rightSum;
}
}
Binary Indexed Tree (Fenwick Tree)
BIT Implementation
public class BinaryIndexedTree
{
private int[] tree;
private int n;
public BinaryIndexedTree(int size)
{
n = size;
tree = new int[n + 1];
}
public BinaryIndexedTree(int[] arr) : this(arr.Length)
{
for (int i = 0; i < arr.Length; i++)
{
Update(i, arr[i]);
}
}
public void Update(int index, int delta)
{
index++; // BIT is 1-indexed
while (index <= n)
{
tree[index] += delta;
index += index & (-index); // Add last set bit
}
}
public int Query(int index)
{
index++; // BIT is 1-indexed
int sum = 0;
while (index > 0)
{
sum += tree[index];
index -= index & (-index); // Remove last set bit
}
return sum;
}
public int RangeQuery(int left, int right)
{
return Query(right) - (left > 0 ? Query(left - 1) : 0);
}
}
Tree Problems and Patterns
Lowest Common Ancestor (LCA)
public static class TreeLCA
{
public static BinaryTreeNode<T> FindLCA<T>(BinaryTreeNode<T> root,
BinaryTreeNode<T> p,
BinaryTreeNode<T> q) where T : IEquatable<T>
{
if (root == null || root == p || root == q)
{
return root;
}
var left = FindLCA(root.Left, p, q);
var right = FindLCA(root.Right, p, q);
if (left != null && right != null)
{
return root; // Current node is LCA
}
return left ?? right;
}
// LCA in BST (more efficient)
public static BinaryTreeNode<T> FindLCAInBST<T>(BinaryTreeNode<T> root,
T p, T q) where T : IComparable<T>
{
if (root == null) return null;
// Both p and q are in left subtree
if (p.CompareTo(root.Value) < 0 && q.CompareTo(root.Value) < 0)
{
return FindLCAInBST(root.Left, p, q);
}
// Both p and q are in right subtree
if (p.CompareTo(root.Value) > 0 && q.CompareTo(root.Value) > 0)
{
return FindLCAInBST(root.Right, p, q);
}
// p and q are on different sides or one is the root
return root;
}
}
Tree Diameter
public static class TreeDiameter
{
private static int maxDiameter = 0;
public static int GetDiameter<T>(BinaryTreeNode<T> root)
{
maxDiameter = 0;
GetHeight(root);
return maxDiameter;
}
private static int GetHeight<T>(BinaryTreeNode<T> node)
{
if (node == null) return 0;
int leftHeight = GetHeight(node.Left);
int rightHeight = GetHeight(node.Right);
// Update diameter if current path is longer
maxDiameter = Math.Max(maxDiameter, leftHeight + rightHeight);
return 1 + Math.Max(leftHeight, rightHeight);
}
}
Serialize and Deserialize Binary Tree
public static class TreeSerialization
{
public static string Serialize<T>(BinaryTreeNode<T> root)
{
var result = new List<string>();
SerializeHelper(root, result);
return string.Join(",", result);
}
private static void SerializeHelper<T>(BinaryTreeNode<T> node, List<string> result)
{
if (node == null)
{
result.Add("null");
return;
}
result.Add(node.Value.ToString());
SerializeHelper(node.Left, result);
SerializeHelper(node.Right, result);
}
public static BinaryTreeNode<int> Deserialize(string data)
{
var queue = new Queue<string>(data.Split(','));
return DeserializeHelper(queue);
}
private static BinaryTreeNode<int> DeserializeHelper(Queue<string> queue)
{
if (queue.Count == 0) return null;
string val = queue.Dequeue();
if (val == "null") return null;
var node = new BinaryTreeNode<int>(int.Parse(val));
node.Left = DeserializeHelper(queue);
node.Right = DeserializeHelper(queue);
return node;
}
}
Performance Comparison
Structure | Search | Insert | Delete | Space | Best Use Case |
---|---|---|---|---|---|
BST | O(log n) avg, O(n) worst | O(log n) avg, O(n) worst | O(log n) avg, O(n) worst | O(n) | Simple ordered data |
AVL Tree | O(log n) | O(log n) | O(log n) | O(n) | Frequent searches |
Trie | O(m) | O(m) | O(m) | O(ALPHABET_SIZE × N × M) | String operations |
Segment Tree | O(log n) range | O(log n) | O(log n) | O(4n) | Range queries |
BIT | O(log n) | O(log n) | O(log n) | O(n) | Prefix sums, updates |
m = string length, n = number of elements, N = number of strings, M = average string length
Advanced Applications
Database Indexing
- B-Trees: Used in databases for efficient disk-based storage
- B+ Trees: Optimized for range queries and sequential access
- Hash indices: O(1) average lookup time
Computational Geometry
- KD-Trees: Multi-dimensional search and nearest neighbor queries
- Range Trees: Multi-dimensional range queries
- Quad Trees: 2D spatial partitioning
Game Development
- Decision Trees: AI behavior trees
- Scene Graphs: 3D rendering hierarchies
- Spatial Trees: Collision detection and culling
When to Use Advanced Trees
Tries: String processing, autocomplete, IP routing, pattern matching
Balanced Trees: When you need guaranteed O(log n) operations and can’t afford worst-case O(n)
Segment Trees: Range queries with updates (sum, min, max over ranges)
Binary Indexed Trees: Prefix sum queries with frequent updates
Avoid advanced trees when:
- Built-in collections (
SortedDictionary
,List
) meet performance needs - Implementation complexity outweighs benefits
- Memory usage is a critical constraint
Quick Reference
Advanced Tree Comparison
| Structure | Best For | Time (Insert/Search) | Space | |———–|———-|———————|——-| | Trie | String prefix operations | O(m) where m = key length | O(ALPHABET_SIZE × n × m) | | Segment Tree | Range queries, updates | O(log n) | O(4n) | | Fenwick Tree | Range sum queries | O(log n) | O(n) |
When to Use
- Trie: Autocomplete, spell check, IP routing, dictionary operations
- Segment Tree: Range min/max/sum with updates
- Fenwick (BIT): Simple range sums, frequency queries
C# Patterns
// Trie for autocomplete
var trie = new Trie();
trie.Insert("apple");
trie.GetWordsWithPrefix("app"); // ["apple", "application", ...]
// Use SortedDictionary for most needs
var sorted = new SortedDictionary<string, int>();
Found this guide helpful? Share it with your team:
Share on LinkedIn