C# generic binary search tree with IEnumerable
Constrain the element type with IComparable<T> and expose the sorted values with yield return.
using System;
using System.Collections.Generic;
using System.Linq;
class Tree<T> where T : IComparable<T> {
private class Node {
public T Value;
public Node Left;
public Node Right;
}
private Node root;
public void Insert(T value) {
root = Insert(root, value);
}
private Node Insert(Node node, T value) {
if (node == null) {
return new Node { Value = value };
}
int order = value.CompareTo(node.Value);
if (order < 0) {
node.Left = Insert(node.Left, value);
} else if (order > 0) {
node.Right = Insert(node.Right, value);
}
return node;
}
public IEnumerable<T> InOrder() {
return InOrder(root);
}
private IEnumerable<T> InOrder(Node node) {
if (node == null) {
yield break;
}
foreach (T value in InOrder(node.Left)) {
yield return value;
}
yield return node.Value;
foreach (T value in InOrder(node.Right)) {
yield return value;
}
}
}
class Program {
static void Main() {
var numbers = new Tree<int>();
foreach (int n in new[] { 50, 30, 70, 20, 40, 60, 80, 40 }) {
numbers.Insert(n);
}
Console.WriteLine(string.Join(" ", numbers.InOrder()));
Console.WriteLine(string.Join(" ", numbers.InOrder().Take(3)));
var words = new Tree<string>();
foreach (string w in new[] { "pear", "fig", "apple", "kiwi" }) {
words.Insert(w);
}
Console.WriteLine(string.Join(" ", words.InOrder()));
}
}20 30 40 50 60 70 80 20 30 40 apple fig kiwi pear
Explanation
A binary search tree needs to compare its elements. A generic constraint, where T : IComparable<T>, lets the tree call CompareTo on the elements and refuses to compile for a type that cannot be compared. Integers, strings, and DateTime already implement the interface, and your own classes can too.
yield return turns a method into an iterator: it produces values one at a time, and the caller can use foreach or LINQ on them. A recursive in-order traversal yields the left subtree, then the node, then the right subtree, so the values come out sorted. The traversal is lazy, so taking only the first few values does not visit the whole tree.
Explore the dry run
using System;using System.Collections.Generic;using System.Linq; class Tree<T> where T : IComparable<T> { private class Node { public T Value; public Node Left; public Node Right; } private Node root; public void Insert(T value) { root = Insert(root, value); } private Node Insert(Node node, T value) { if (node == null) { return new Node { Value = value }; } int order = value.CompareTo(node.Value); if (order < 0) { node.Left = Insert(node.Left, value); } else if (order > 0) { node.Right = Insert(node.Right, value); } return node; } public IEnumerable<T> InOrder() { return InOrder(root); } private IEnumerable<T> InOrder(Node node) { if (node == null) { yield break; } foreach (T value in InOrder(node.Left)) { yield return value; } yield return node.Value; foreach (T value in InOrder(node.Right)) { yield return value; } }} class Program { static void Main() { var numbers = new Tree<int>(); foreach (int n in new[] { 50, 30, 70, 20, 40, 60, 80, 40 }) { numbers.Insert(n); } Console.WriteLine(string.Join(" ", numbers.InOrder())); Console.WriteLine(string.Join(" ", numbers.InOrder().Take(3))); var words = new Tree<string>(); foreach (string w in new[] { "pear", "fig", "apple", "kiwi" }) { words.Insert(w); } Console.WriteLine(string.Join(" ", words.InOrder())); }}Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| The constraint guarantees that CompareTo is available on every element. | — | — | |
| Negative means go left, positive means go right, zero means a duplicate that is ignored. | — | — | |
| Yield every value of the left subtree first. | — | — | |
| Then the node itself, so the sequence is sorted. | — | — | |
| The sorted values; the duplicate 40 was ignored. | — | 20 30 40 50 60 70 80 | |
| Take(3) stops the iteration early, so only the left part of the tree is visited. | — | 20 30 40 50 60 70 80 20 30 40 | |
| Strings use their own CompareTo, so the same class works for text. | — | 20 30 40 50 60 70 80 20 30 40 apple fig kiwi pear |
Cost and common mistakes
Insert is O(h); the full traversal is O(n).
- Nested foreach with yield return makes each value pass through one iterator per level of depth. For a very deep tree an explicit stack is faster.
- A tree built from sorted input becomes a chain, and operations take O(n). SortedSet<T> in the standard library is a balanced alternative.
- CompareTo on null reference values throws. Decide how nulls should be handled before using reference types.
Try it yourself
How would you make the tree sort strings ignoring case?
Reveal answer and reasoning
Accept an IComparer<T> in the constructor and use it instead of CompareTo, for example StringComparer.OrdinalIgnoreCase.