A C# implementation of a dynamic tree data structure using Euler Tour Trees backed by a Treap (randomized binary search tree). This structure enables efficient tree operations while maintaining the ability to quickly compare nodes by their DFS order.
- O(1) DFS index comparison between any two nodes in the same tree
- O(1) parent lookup (stored reference)
- O(log n) insertion and removal of subtrees
- O(log n) navigation to siblings, first/last child
- O(k) non-recursive iteration over children or entire subtrees
- No global tree manager - trees are self-contained; subtrees become independent trees when removed
- Generic -
TreeNode<T>supports any payload type
Traditional tree representations (parent/child pointers) make certain operations expensive:
| Operation | Traditional Tree | EttTree |
|---|---|---|
| "Is A before B in DFS?" | O(n) | O(log n) |
| Move subtree to new parent | O(1) pointer swap | O(log n) |
| Non-recursive DFS iteration | Requires stack | O(1) per node |
| Get DFS index of node | O(n) | O(log n) |
| Get parent | O(1) | O(1) |
An Euler Tour linearizes a tree into a sequence by recording each node twice: once when entering (DFS down) and once when exiting (DFS up).
Tree: Euler Tour Sequence:
A [A_enter, B_enter, B_exit, C_enter, D_enter, D_exit, C_exit, A_exit]
/ \
B C
|
D
This sequence is stored in a Treap - a randomized BST that maintains balance through random priorities. The Treap uses implicit keys (subtree sizes) rather than explicit keys, treating the Euler Tour as an ordered sequence.
Key insight: A node's position in the Euler Tour directly corresponds to its DFS order. Comparing two nodes' positions tells you their relative DFS order in O(log n) time.
The main API for working with trees:
public class TreeNode<T>
{
public T Value { get; set; }
// Navigation
public TreeNode<T>? Parent { get; } // O(1) - stored reference
public TreeNode<T>? FirstChild { get; } // O(log n)
public TreeNode<T>? LastChild { get; } // O(log n)
public TreeNode<T>? NextSibling { get; } // O(log n)
public TreeNode<T>? PreviousSibling { get; } // O(log n)
// DFS index (O(log n))
public int GetDFSIndex();
// Tree manipulation (all O(log n))
public void Remove();
public void InsertFirstChild(TreeNode<T> newNode);
public void InsertLastChild(TreeNode<T> newNode);
public void InsertSiblingBefore(TreeNode<T> newNode);
public void InsertSiblingAfter(TreeNode<T> newNode);
public void RemoveAllChildren();
// Iteration (O(1) per element)
public ChildrenEnumerable Children { get; }
public SubtreeEnumerable Subtree { get; }
}Each TreeNode<T> contains two EttNode instances:
EttEnter- marks the start of this node's range in the Euler TourEttExit- marks the end of this node's range
The Euler Tour sequence for a subtree is: [Enter, ...descendants..., Exit]
public class EttNode
{
public int Priority { get; } // Random priority for Treap balancing
public int Size { get; } // Subtree size for implicit indexing
public EttNode? Left, Right, Parent;
public TreeNode<T> Owner { get; }
// Instance methods
public EttNode GetRoot(); // O(log n) - walk up to root
public int GetIndex(); // O(log n) - calculate position
public EttNode? GetNext(); // O(log n) - next in sequence
public EttNode? GetPrevious(); // O(log n) - previous in sequence
public int GetHeight(); // O(n) - for testing
public EttNode? GetNodeAtIndex(int index); // O(log n)
// Static Treap operations
public static EttNode? Merge(EttNode? l, EttNode? r);
public static (EttNode? L, EttNode? R) Split(EttNode? root, int k);
}var root = new TreeNode<string>("Root");
var child1 = new TreeNode<string>("Child1");
var child2 = new TreeNode<string>("Child2");
var grandchild = new TreeNode<string>("Grandchild");
root.InsertLastChild(child1);
root.InsertLastChild(child2);
child1.InsertFirstChild(grandchild);// O(log n) comparison - no traversal needed!
if (nodeA.GetDFSIndex() < nodeB.GetDFSIndex())
{
Console.WriteLine("A comes before B in DFS order");
}// Non-recursive, allocation-free iteration
foreach (var child in parent.Children)
{
Console.WriteLine(child.Value);
}// Non-recursive DFS traversal
foreach (var node in root.Subtree)
{
Console.WriteLine(node.Value);
}// Remove from current location and insert elsewhere - O(log n)
child1.Remove(); // child1 is now an independent tree
newParent.InsertFirstChild(child1); // child1 is now under newParentvar parent = node.Parent; // O(1) - stored reference
var firstChild = node.FirstChild; // O(log n)
var next = node.NextSibling; // O(log n)
// Check if node is a leaf
if (node.FirstChild == null)
{
Console.WriteLine("This is a leaf node");
}| Operation | Time Complexity | Notes |
|---|---|---|
Parent |
O(1) | Stored reference |
GetDFSIndex() |
O(log n) | Walk up to root, summing sizes |
FirstChild, LastChild |
O(log n) | GetNext()/GetPrevious() + parent check |
NextSibling, PreviousSibling |
O(log n) | GetNext()/GetPrevious() + parent check |
InsertFirstChild, InsertLastChild |
O(log n) | Split + Merge |
InsertSiblingBefore, InsertSiblingAfter |
O(log n) | Split + Merge |
Remove |
O(log n) | Split + Split + Merge |
RemoveAllChildren |
O(k log n) | k = number of direct children |
Iterate Children |
O(k log n) | k = number of children |
Iterate Subtree |
O(m log n) | m = subtree size |
Space Complexity: O(n) where n is the number of nodes. Each TreeNode creates 2 EttNode instances.
Each EttNode is assigned a random priority at creation. The Treap maintains the heap property on priorities while preserving sequence order through implicit keys. This ensures O(log n) expected height regardless of insertion order.
The TreeNode<T>.Parent property is a stored reference that is maintained by all tree manipulation methods:
Remove()sets_parent = nullInsertFirstChild()/InsertLastChild()sets_parent = thisInsertSiblingBefore()/InsertSiblingAfter()sets_parent = this._parent
This provides O(1) parent access while keeping the tree structure consistent.
The EttNode class provides GetNext() and GetPrevious() methods for efficient O(log n) navigation through the Euler Tour sequence:
- If there's a right/left subtree, go to its leftmost/rightmost node
- Otherwise, walk up until finding an ancestor where we came from the left/right
These methods are used by FirstChild, LastChild, NextSibling, and PreviousSibling combined with the stored parent reference for O(log n) navigation.
The fundamental Treap operations:
- Split(root, k): Divides the sequence at index k into two Treaps
- Merge(left, right): Combines two Treaps, maintaining sequence order
All tree manipulations are compositions of these operations:
InsertFirstChild(parent, new):
Split at parent.Enter + 1
Merge(left, new.tree, right)
Remove(node):
Split at node.Enter
Split at node.Exit + 1
Merge(before, after) // middle is now independent
When a subtree is removed, it becomes a completely independent tree. There is no global tree registry - the tree structure is defined entirely by the Treap connectivity of its EttNode instances.
The implementation includes comprehensive tests covering:
- Basic connectivity and DFS ordering
- Subtree extraction and re-parenting
- Sibling insertion and index shifting
- Navigation properties
- Edge cases (lone nodes, root operations, deep trees)
- Stress testing for Treap balance (1000+ nodes)
Run tests with:
dotnet testMIT
- Henzinger, M. R., & King, V. (1999). Randomized fully dynamic graph algorithms with polylogarithmic time per operation.
- Tarjan, R. E. (1979). A class of algorithms which require nonlinear time to maintain disjoint sets.