Visualize B-Trees and B+ Trees in C#
Build a C# console app that implements and visualizes B-Tree and B+ Tree structures.
Introduction
30 Second Summary
Every time you search for a record in a database or open a file on your computer, a tree structure is working behind the scenes to find it in milliseconds. B-Trees and B+ Trees are the data structures that make this speed possible.
In this project, you will build both a B-Tree and a B+ Tree from scratch in C#, visualizing exactly how nodes split and keys get promoted as you insert data.
What You'll Build
An interactive console application that implements both tree structures and prints their shape after every insertion, making the self-balancing mechanics visible in real time.
By the end of this project, you'll have:
- A working B-Tree implementation with insert, search, and tree printing that shows proactive splitting and key promotions.
- A working B+ Tree implementation with separate internal and leaf node types, a linked leaf chain, and efficient range queries.
- A side-by-side comparison that makes the structural and performance differences between both trees visually obvious.
- Secret Mission: implement delete with rebalancing, handling borrow-from-sibling and merge operations.
Are there any prerequisites?
You should be comfortable with C# generics and classes. You also need the .NET SDK installed on your machine.
Set Up the Project
Before building the tree implementations, you need a C# project to hold your code. The .NET SDK provides a scaffolding command that creates a ready-to-run console application in seconds.
In this step, you'll create a new console project, confirm it compiles, then add the empty files where your B-Tree and B+ Tree classes will live.
In this step, get ready to:
- Create a .NET console project named BTreeVisualizer.
- Verify the project compiles and runs.
- Create empty files for the B-Tree and B+ Tree implementations.
Create the .NET console project
You need a terminal to run .NET commands and create your project files.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create the console project by running this command:
dotnet new console -n BTreeVisualizer
What does this command do?
dotnet new console scaffolds a minimal C# console application with a Program.cs file and a .csproj project file.
-n BTreeVisualizer names the project and creates a matching folder on your Desktop.
- Confirm the project folder was created by running:
ls
- You should see a BTreeVisualizer folder in the output.
Don't see the folder?
Make sure you ran cd ~/Desktop before creating the project. If ls shows no BTreeVisualizer folder, run the dotnet new console command again.
Help me create my .NET console project
Verify the project runs
The scaffolded project comes with a simple program that prints text to the console. Running it confirms your .NET SDK is configured correctly.
- Move into the project folder and run the application by entering these commands:
cd BTreeVisualizer
dotnet run
- You should see Hello, World! printed in your terminal.
✔️ I see Hello, World!
Your project is set up correctly. The .NET SDK compiled and ran your app without errors.
ⓧ I see an error
Check the following:
- Make sure you are inside the BTreeVisualizer folder. Run pwd to check your current directory.
- Verify your .NET SDK is installed by running dotnet --version in your terminal. You should see a version number.
- If you see a version below 6.0, update your .NET SDK from the official .NET downloads page.
Help me fix my dotnet run error
Create the tree implementation files
You need two additional files for the B-Tree and B+ Tree classes. Creating them now gives you a clean project structure before you start coding.
- Create the two empty files by running the command for your operating system:
macOS
touch BTree.cs BPlusTree.cs
Windows
New-Item -ItemType "File" -Path "BTree.cs", "BPlusTree.cs"
- Confirm the files exist by running:
ls
- You should see BTree.cs, BPlusTree.cs, Program.cs, and BTreeVisualizer.csproj in the output.
Don't see both files?
Make sure you are inside the BTreeVisualizer folder (not on the Desktop). Run pwd to confirm your location ends with BTreeVisualizer.
Help me create my BTree.cs and BPlusTree.cs files
What is your project structure?
Your BTreeVisualizer folder now contains four files. Program.cs is the entry point that .NET runs first. BTree.cs and BPlusTree.cs are empty for now. You'll fill them with tree implementations in the next steps.
Your project is ready to go. Next up, you'll build the B-Tree implementation and watch nodes split in real time as you insert values.
Build the B-Tree
Your project structure is ready. Now it is time to build the core data structure that powers database indexes and file systems.
A B-Tree keeps itself balanced by splitting nodes when they get too full. In this step, you will implement a B-Tree from scratch with insert, search, and a visual print method so you can watch exactly how splits and key promotions happen.
In this step, get ready to:
- Create a BTreeNode class that holds keys, children, and metadata.
- Implement insertion with proactive splitting so nodes never overflow.
- Add search and print methods that visualize traversal paths and tree structure.
Implement the BTreeNode class
Every B-Tree is made of nodes. Each node stores an array of keys (sorted integers), an array of child pointers, a count of how many keys it currently holds, and a flag indicating whether it is a leaf.
- Open BTree.cs in Cursor and paste the following code:
namespace BTreeVisualizer;
public class BTreeNode
{
public int[] Keys;
public BTreeNode[] Children;
public int KeyCount;
public bool IsLeaf;
public BTreeNode(int minDegree, bool isLeaf)
{
IsLeaf = isLeaf;
// Max keys = 2t - 1 (for t=2, that's 3 keys)
Keys = new int[2 * minDegree - 1];
// Max children = 2t (for t=2, that's 4 children)
Children = new BTreeNode[2 * minDegree];
KeyCount = 0;
}
}
What does this code do?
The BTreeNode class defines the building block of the tree. With minimum degree t=2, each node can hold a maximum of 3 keys and point to up to 4 children.
This small capacity means splits will happen frequently even with a handful of values. That is exactly what makes the mechanics visible.
You will not see a change yet. The next chunks add the BTree class that uses this node.
Implement the BTree Insert method
The B-Tree uses proactive splitting. Before descending into a full child, it splits that child first. This guarantees every node you visit has room for a new key, so you never need to backtrack up the tree.
- Add the following BTree class below the BTreeNode class in BTree.cs:
public class BTree
{
private BTreeNode? _root;
private readonly int _minDegree;
public BTree(int minDegree)
{
_minDegree = minDegree;
_root = null;
}
public void Insert(int key)
{
if (_root == null)
{
// First insertion creates the root
_root = new BTreeNode(_minDegree, true);
_root.Keys[0] = key;
_root.KeyCount = 1;
Console.WriteLine($" Inserted {key} as root.");
return;
}
if (_root.KeyCount == 2 * _minDegree - 1)
{
// Root is full - split it by creating a new root
var newRoot = new BTreeNode(_minDegree, false);
newRoot.Children[0] = _root;
SplitChild(newRoot, 0, _root);
Console.WriteLine($" Root was full. Split root. New root key: [{string.Join(", ", GetNodeKeys(newRoot))}]");
int i = 0;
if (newRoot.Keys[0] < key)
i++;
InsertNonFull(newRoot.Children[i], key);
_root = newRoot;
}
else
{
InsertNonFull(_root, key);
}
}
}
What does this code do?
The Insert method handles two cases. If the tree is empty, it creates a root node. If the root is full (3 keys), it creates a new root and splits the old root first.
This is proactive splitting in action. The root split happens before trying to insert, so the tree grows upward while staying balanced.
You will not see a change yet. The next chunk adds the InsertNonFull method that Insert depends on.
- Add the InsertNonFull method inside the BTree class, just before the closing }:
private void InsertNonFull(BTreeNode node, int key)
{
int i = node.KeyCount - 1;
if (node.IsLeaf)
{
// Shift keys right to make room for the new key
while (i >= 0 && node.Keys[i] > key)
{
node.Keys[i + 1] = node.Keys[i];
i--;
}
node.Keys[i + 1] = key;
node.KeyCount++;
Console.WriteLine($" Inserted {key} into leaf: [{string.Join(", ", GetNodeKeys(node))}]");
}
else
{
// Find the child to descend into
while (i >= 0 && node.Keys[i] > key)
i--;
i++;
// Proactive split: if the child is full, split it before descending
if (node.Children[i].KeyCount == 2 * _minDegree - 1)
{
SplitChild(node, i, node.Children[i]);
Console.WriteLine($" Child was full. Split child. Parent keys: [{string.Join(", ", GetNodeKeys(node))}]");
if (node.Keys[i] < key)
i++;
}
InsertNonFull(node.Children[i], key);
}
}
What does this code do?
InsertNonFull handles the recursive descent. If the current node is a leaf, it inserts the key in sorted order. If it is an internal node, it finds the correct child to descend into.
The key insight is the proactive check: before descending into any child, it checks if that child is full. If so, it splits the child first. This is what guarantees you never reach a full leaf with no room.
You will not see a change yet. The next chunk adds SplitChild and a helper method to make everything compile.
- Add the SplitChild and GetNodeKeys methods inside the BTree class, after InsertNonFull:
private void SplitChild(BTreeNode parent, int index, BTreeNode fullChild)
{
// Create a new node to hold the right half of the full child
var newNode = new BTreeNode(_minDegree, fullChild.IsLeaf);
newNode.KeyCount = _minDegree - 1;
// Copy the right half of keys to the new node
for (int j = 0; j < _minDegree - 1; j++)
newNode.Keys[j] = fullChild.Keys[j + _minDegree];
// Copy the right half of children if not a leaf
if (!fullChild.IsLeaf)
{
for (int j = 0; j < _minDegree; j++)
newNode.Children[j] = fullChild.Children[j + _minDegree];
}
fullChild.KeyCount = _minDegree - 1;
// Make room in parent for the new child pointer
for (int j = parent.KeyCount; j > index; j--)
parent.Children[j + 1] = parent.Children[j];
parent.Children[index + 1] = newNode;
// Promote the middle key up to the parent
for (int j = parent.KeyCount - 1; j >= index; j--)
parent.Keys[j + 1] = parent.Keys[j];
parent.Keys[index] = fullChild.Keys[_minDegree - 1];
parent.KeyCount++;
}
private int[] GetNodeKeys(BTreeNode node)
{
int[] keys = new int[node.KeyCount];
Array.Copy(node.Keys, keys, node.KeyCount);
return keys;
}
What does this code do?
SplitChild is the heart of B-Tree balancing. When a child node is full (3 keys), it splits it into two nodes and promotes the middle key up to the parent.
For example, if a node holds [5, 10, 20] and needs to split, the middle key 10 gets pushed up to the parent. The left child keeps [5] and the right child gets [20].
GetNodeKeys is a small helper that extracts only the active keys from a node's array for display purposes.
- Save BTree.cs.
- Verify the insertion logic compiles by running:
dotnet build
You should see Build succeeded. in the terminal output.
Seeing a build error?
Check that InsertNonFull and SplitChild are inside the BTree class braces, not outside them. Each method must sit between the opening { after public class BTree and the final }.
Also confirm that the namespace BTreeVisualizer; line is at the very top of the file.
help me fix a build error in my BTree.cs file
Implement Search and Print methods
Now that insertion works, you need a way to find keys and visualize the tree structure. The Search method will print which nodes it visits at each level. The Print method will display the full tree with indentation.
- Add the Search and SearchNode methods inside the BTree class, after SplitChild:
public bool Search(int key)
{
if (_root == null)
{
Console.WriteLine(" Tree is empty.");
return false;
}
return SearchNode(_root, key, 0);
}
private bool SearchNode(BTreeNode node, int key, int depth)
{
int i = 0;
// Find the first key greater than or equal to the search key
while (i < node.KeyCount && key > node.Keys[i])
i++;
string indent = new string(' ', depth * 2);
Console.WriteLine($"{indent} Level {depth}: Checking node [{string.Join(", ", GetNodeKeys(node))}]");
if (i < node.KeyCount && node.Keys[i] == key)
{
Console.WriteLine($"{indent} Found {key} at level {depth}, position {i}!");
return true;
}
if (node.IsLeaf)
{
Console.WriteLine($"{indent} Reached leaf. Key {key} not found.");
return false;
}
// Descend into the appropriate child
Console.WriteLine($"{indent} Key {key} falls between position {i}, going to child {i}");
return SearchNode(node.Children[i], key, depth + 1);
}
What does this code do?
Search starts at the root and descends level by level. At each node, it compares the search key against the node's keys to decide which child to visit next.
Unlike a B+ Tree (which you will build next), a B-Tree can find data at ANY level. If the key matches a key in an internal node, the search stops there.
You will not see a change yet. The next chunk adds the tree printing methods.
- Add the Print and PrintNode methods inside the BTree class, after SearchNode:
public void Print()
{
if (_root == null)
{
Console.WriteLine(" (empty tree)");
return;
}
PrintNode(_root, "", true);
}
private void PrintNode(BTreeNode node, string indent, bool isLast)
{
// Use tree-drawing characters for visual structure
string connector = isLast ? "└── " : "├── ";
Console.WriteLine($"{indent}{connector}[{string.Join(", ", GetNodeKeys(node))}]");
string childIndent = indent + (isLast ? " " : "│ ");
if (!node.IsLeaf)
{
// Recursively print each child with proper indentation
for (int i = 0; i <= node.KeyCount; i++)
{
if (node.Children[i] != null)
PrintNode(node.Children[i], childIndent, i == node.KeyCount);
}
}
}
What does this code do?
Print renders the tree using box-drawing characters (└──, ├──, │) so you can see parent-child relationships at a glance.
Each node displays its keys in brackets. Indentation increases with depth, making splits visually obvious when you insert values that cause the tree to grow taller.
- Make sure GetNodeKeys (which you added earlier) is the last method before the closing } of the BTree class.
- Save BTree.cs.
- Verify everything compiles by running:
dotnet build
You should see Build succeeded. with no errors.
Build still failing?
Make sure all methods are inside the BTree class. The file should have exactly two classes: BTreeNode and BTree. Count your curly braces to make sure each opening { has a matching }.
If you see an error about GetNodeKeys, confirm it is defined inside the BTree class and not accidentally placed outside or inside BTreeNode.
help me fix build errors in my BTree.cs file with Search and Print methods
✔️ Awesome, I've got everything!
Great. Double check you have saved BTree.cs and that dotnet build succeeds with no errors.
ⓧ I'd like to double check the full code
Here is the complete BTree.cs file. Compare yours against it:
namespace BTreeVisualizer;
public class BTreeNode
{
public int[] Keys;
public BTreeNode[] Children;
public int KeyCount;
public bool IsLeaf;
public BTreeNode(int minDegree, bool isLeaf)
{
IsLeaf = isLeaf;
Keys = new int[2 * minDegree - 1];
Children = new BTreeNode[2 * minDegree];
KeyCount = 0;
}
}
public class BTree
{
private BTreeNode? _root;
private readonly int _minDegree;
public BTree(int minDegree)
{
_minDegree = minDegree;
_root = null;
}
public void Insert(int key)
{
if (_root == null)
{
_root = new BTreeNode(_minDegree, true);
_root.Keys[0] = key;
_root.KeyCount = 1;
Console.WriteLine($" Inserted {key} as root.");
return;
}
if (_root.KeyCount == 2 * _minDegree - 1)
{
var newRoot = new BTreeNode(_minDegree, false);
newRoot.Children[0] = _root;
SplitChild(newRoot, 0, _root);
Console.WriteLine($" Root was full. Split root. New root key: [{string.Join(", ", GetNodeKeys(newRoot))}]");
int i = 0;
if (newRoot.Keys[0] < key)
i++;
InsertNonFull(newRoot.Children[i], key);
_root = newRoot;
}
else
{
InsertNonFull(_root, key);
}
}
private void InsertNonFull(BTreeNode node, int key)
{
int i = node.KeyCount - 1;
if (node.IsLeaf)
{
while (i >= 0 && node.Keys[i] > key)
{
node.Keys[i + 1] = node.Keys[i];
i--;
}
node.Keys[i + 1] = key;
node.KeyCount++;
Console.WriteLine($" Inserted {key} into leaf: [{string.Join(", ", GetNodeKeys(node))}]");
}
else
{
while (i >= 0 && node.Keys[i] > key)
i--;
i++;
if (node.Children[i].KeyCount == 2 * _minDegree - 1)
{
SplitChild(node, i, node.Children[i]);
Console.WriteLine($" Child was full. Split child. Parent keys: [{string.Join(", ", GetNodeKeys(node))}]");
if (node.Keys[i] < key)
i++;
}
InsertNonFull(node.Children[i], key);
}
}
private void SplitChild(BTreeNode parent, int index, BTreeNode fullChild)
{
var newNode = new BTreeNode(_minDegree, fullChild.IsLeaf);
newNode.KeyCount = _minDegree - 1;
for (int j = 0; j < _minDegree - 1; j++)
newNode.Keys[j] = fullChild.Keys[j + _minDegree];
if (!fullChild.IsLeaf)
{
for (int j = 0; j < _minDegree; j++)
newNode.Children[j] = fullChild.Children[j + _minDegree];
}
fullChild.KeyCount = _minDegree - 1;
for (int j = parent.KeyCount; j > index; j--)
parent.Children[j + 1] = parent.Children[j];
parent.Children[index + 1] = newNode;
for (int j = parent.KeyCount - 1; j >= index; j--)
parent.Keys[j + 1] = parent.Keys[j];
parent.Keys[index] = fullChild.Keys[_minDegree - 1];
parent.KeyCount++;
}
public bool Search(int key)
{
if (_root == null)
{
Console.WriteLine(" Tree is empty.");
return false;
}
return SearchNode(_root, key, 0);
}
private bool SearchNode(BTreeNode node, int key, int depth)
{
int i = 0;
while (i < node.KeyCount && key > node.Keys[i])
i++;
string indent = new string(' ', depth * 2);
Console.WriteLine($"{indent} Level {depth}: Checking node [{string.Join(", ", GetNodeKeys(node))}]");
if (i < node.KeyCount && node.Keys[i] == key)
{
Console.WriteLine($"{indent} Found {key} at level {depth}, position {i}!");
return true;
}
if (node.IsLeaf)
{
Console.WriteLine($"{indent} Reached leaf. Key {key} not found.");
return false;
}
Console.WriteLine($"{indent} Key {key} falls between position {i}, going to child {i}");
return SearchNode(node.Children[i], key, depth + 1);
}
public void Print()
{
if (_root == null)
{
Console.WriteLine(" (empty tree)");
return;
}
PrintNode(_root, "", true);
}
private void PrintNode(BTreeNode node, string indent, bool isLast)
{
string connector = isLast ? "└── " : "├── ";
Console.WriteLine($"{indent}{connector}[{string.Join(", ", GetNodeKeys(node))}]");
string childIndent = indent + (isLast ? " " : "│ ");
if (!node.IsLeaf)
{
for (int i = 0; i <= node.KeyCount; i++)
{
if (node.Children[i] != null)
PrintNode(node.Children[i], childIndent, i == node.KeyCount);
}
}
}
private int[] GetNodeKeys(BTreeNode node)
{
int[] keys = new int[node.KeyCount];
Array.Copy(node.Keys, keys, node.KeyCount);
return keys;
}
}
Your B-Tree is fully implemented. Next up, you will build a B+ Tree and see how its linked leaf nodes and copy-up splitting strategy create a fundamentally different structure.
Build the B+ Tree
Your B-Tree is working, but it stores data in every node. Databases need something more efficient for scanning ranges of data, like finding all orders between two dates.
A B+ Tree solves this by pushing all data down to the leaf level and linking leaves together. In this step, you will build a complete B+ Tree implementation with insert, search, range queries, and tree printing.
In this step, get ready to:
- Implement the B+ Tree node hierarchy with separate internal and leaf node types.
- Build the insert method with leaf splits (copy up) and internal splits (push up).
- Add search, range query, and tree printing methods.
Implement the B+ Tree node classes
Unlike the B-Tree which uses a single node class, a B+ Tree needs two distinct node types. Internal nodes hold only keys and child pointers for routing. Leaf nodes hold keys, associated values, and a pointer to the next leaf.
- Open BPlusTree.cs in your editor and add the following node class hierarchy:
namespace BTreeVisualizer;
// Abstract base class shared by both node types
public abstract class BPlusNode
{
public int[] Keys;
public int KeyCount;
protected BPlusNode(int maxKeys)
{
Keys = new int[maxKeys + 1];
KeyCount = 0;
}
}
// Internal nodes store keys for routing and pointers to children
public class BPlusInternalNode : BPlusNode
{
public BPlusNode[] Children;
public BPlusInternalNode(int maxKeys) : base(maxKeys)
{
Children = new BPlusNode[maxKeys + 2];
}
}
// Leaf nodes store keys + values and link to the next leaf
public class BPlusLeafNode : BPlusNode
{
public string[] Values;
public BPlusLeafNode? Next;
public BPlusLeafNode(int maxKeys) : base(maxKeys)
{
Values = new string[maxKeys + 1];
Next = null;
}
}
What does this code do?
BPlusNode is an abstract base class that holds the Keys array and KeyCount shared by both node types. Arrays are sized to maxKeys + 1 to hold one extra key during a split before dividing.
BPlusInternalNode has only Children pointers (no data values). BPlusLeafNode stores the actual Values and has a Next pointer linking it to the next leaf in sequence.
- Save BPlusTree.cs.
- Confirm the project still builds by running:
dotnet build
- You should see Build succeeded with 0 errors. The node classes compile on their own since they do not reference anything undefined.
Seeing build errors?
- Make sure the first line is namespace BTreeVisualizer; (with a semicolon). This must match the namespace in BTree.cs.
- Check that BPlusNode uses protected on its constructor (not private). Derived classes need to call it.
- help me fix a build error in my BPlusTree node classes
Why separate node types instead of a single class?
In a B-Tree, every node stores data. In a B+ Tree, internal nodes exist purely for navigation. Separating the types means internal nodes waste no memory on value storage. Leaf nodes gain a Next pointer that internal nodes never need.
Implement insert with split logic
The B+ Tree insert works recursively. It descends to the correct leaf, inserts the key-value pair, then checks if the leaf overflowed. If a split happens, the promoted key bubbles back up to the parent.
- Below the BPlusLeafNode class in BPlusTree.cs, add the BPlusTree class with its constructor, Insert, and InsertRecursive methods:
public class BPlusTree
{
private BPlusNode? _root;
private readonly int _maxKeys;
public BPlusTree(int order)
{
_maxKeys = order - 1;
_root = null;
}
public void Insert(int key, string value)
{
// First insert creates the root leaf
if (_root == null)
{
var leaf = new BPlusLeafNode(_maxKeys);
leaf.Keys[0] = key;
leaf.Values[0] = value;
leaf.KeyCount = 1;
_root = leaf;
Console.WriteLine($" Inserted ({key}, \"{value}\") as first leaf.");
return;
}
// Recursively insert and check if root needs splitting
var result = InsertRecursive(_root, key, value);
if (result != null)
{
var newRoot = new BPlusInternalNode(_maxKeys);
newRoot.Keys[0] = result.Value.Key;
newRoot.Children[0] = _root;
newRoot.Children[1] = result.Value.Node;
newRoot.KeyCount = 1;
_root = newRoot;
Console.WriteLine($" Root split! New root key: [{result.Value.Key}]");
}
}
private (int Key, BPlusNode Node)? InsertRecursive(BPlusNode node, int key, string value)
{
if (node is BPlusLeafNode leaf)
{
// Shift keys and values to make room
int i = leaf.KeyCount - 1;
while (i >= 0 && leaf.Keys[i] > key)
{
leaf.Keys[i + 1] = leaf.Keys[i];
leaf.Values[i + 1] = leaf.Values[i];
i--;
}
leaf.Keys[i + 1] = key;
leaf.Values[i + 1] = value;
leaf.KeyCount++;
// Split if leaf is overfull
if (leaf.KeyCount > _maxKeys)
{
return SplitLeaf(leaf);
}
Console.WriteLine($" Inserted ({key}, \"{value}\") into leaf: [{string.Join(", ", GetNodeKeys(leaf))}]");
return null;
}
else if (node is BPlusInternalNode internalNode)
{
// Find the correct child to descend into
int i = 0;
while (i < internalNode.KeyCount && key >= internalNode.Keys[i])
i++;
var result = InsertRecursive(internalNode.Children[i], key, value);
// If child split, insert the promoted key here
if (result != null)
{
int j = internalNode.KeyCount - 1;
while (j >= 0 && internalNode.Keys[j] > result.Value.Key)
{
internalNode.Keys[j + 1] = internalNode.Keys[j];
internalNode.Children[j + 2] = internalNode.Children[j + 1];
j--;
}
internalNode.Keys[j + 1] = result.Value.Key;
internalNode.Children[j + 2] = result.Value.Node;
internalNode.KeyCount++;
Console.WriteLine($" Promoted key {result.Value.Key} to internal node: [{string.Join(", ", GetNodeKeys(internalNode))}]");
// Split internal node if overfull
if (internalNode.KeyCount > _maxKeys)
{
return SplitInternal(internalNode);
}
return null;
}
return null;
}
return null;
}
What does this code do?
- Insert handles the top-level logic: create the root if empty, otherwise recurse down and create a new root if the old root splits.
- InsertRecursive returns a nullable tuple. If a child split, the tuple carries the promoted key and the new node. If no split occurred, it returns null.
- When the node is a leaf, it inserts in sorted order then checks for overflow. When it is internal, it finds the right child, recurses, then handles any promoted key coming back up.
- You won't see a change yet. The code references SplitLeaf, SplitInternal, and GetNodeKeys which you will add next.
- Still inside the BPlusTree class (below InsertRecursive), add the split methods and the GetNodeKeys utility:
private (int Key, BPlusNode Node) SplitLeaf(BPlusLeafNode leaf)
{
int mid = leaf.KeyCount / 2;
var newLeaf = new BPlusLeafNode(_maxKeys);
// Move the right half of keys and values to the new leaf
for (int i = mid; i < leaf.KeyCount; i++)
{
newLeaf.Keys[i - mid] = leaf.Keys[i];
newLeaf.Values[i - mid] = leaf.Values[i];
}
newLeaf.KeyCount = leaf.KeyCount - mid;
leaf.KeyCount = mid;
// Maintain the linked list between leaves
newLeaf.Next = leaf.Next;
leaf.Next = newLeaf;
// COPY UP the first key of the new leaf to the parent
int promotedKey = newLeaf.Keys[0];
Console.WriteLine($" Leaf split! Left: [{string.Join(", ", GetNodeKeys(leaf))}] Right: [{string.Join(", ", GetNodeKeys(newLeaf))}] Promoted: {promotedKey}");
return (promotedKey, newLeaf);
}
private (int Key, BPlusNode Node) SplitInternal(BPlusInternalNode node)
{
int mid = node.KeyCount / 2;
// PUSH UP the middle key (it is removed from both children)
int promotedKey = node.Keys[mid];
var newNode = new BPlusInternalNode(_maxKeys);
// Move keys after the middle to the new node
for (int i = mid + 1; i < node.KeyCount; i++)
{
newNode.Keys[i - mid - 1] = node.Keys[i];
}
for (int i = mid + 1; i <= node.KeyCount; i++)
{
newNode.Children[i - mid - 1] = node.Children[i];
}
newNode.KeyCount = node.KeyCount - mid - 1;
node.KeyCount = mid;
Console.WriteLine($" Internal node split! Pushed up: {promotedKey}");
return (promotedKey, newNode);
}
private int[] GetNodeKeys(BPlusNode node)
{
int[] keys = new int[node.KeyCount];
Array.Copy(node.Keys, keys, node.KeyCount);
return keys;
}
What's the difference between copy up and push up?
When a leaf splits, the first key of the new leaf is COPIED UP to the parent. The key still exists in the leaf because leaf nodes hold the actual data.
When an internal node splits, the middle key is PUSHED UP (removed from both halves). Internal nodes only route, so a key in the parent is enough. This is the fundamental structural difference from the B-Tree's split.
- Save BPlusTree.cs and confirm the insert logic compiles by running:
dotnet build
- You should see Build succeeded with 0 errors. The insert pipeline is now self-contained.
Build failing with missing method errors?
- Make sure SplitLeaf, SplitInternal, and GetNodeKeys are all inside the BPlusTree class braces. They are private methods of the tree, not standalone functions.
- Check that the class opening brace for BPlusTree has no closing brace until after all methods. You will add the closing brace at the end of the file.
- help me fix my BPlusTree insert compilation errors
Add search, range query, and tree printing
With insert working, you need methods to query the tree and visualize its structure. The B+ Tree search always descends to a leaf (unlike the B-Tree which can stop at any level). The range query leverages the linked leaf chain.
- Below GetNodeKeys in the BPlusTree class, add the Search and SearchNode methods:
public string? Search(int key)
{
if (_root == null)
{
Console.WriteLine(" Tree is empty.");
return null;
}
return SearchNode(_root, key, 0);
}
private string? SearchNode(BPlusNode node, int key, int depth)
{
string indent = new string(' ', depth * 2);
if (node is BPlusLeafNode leaf)
{
// Always reach a leaf before checking for the key
Console.WriteLine($"{indent} Level {depth} (Leaf): [{string.Join(", ", GetNodeKeys(leaf))}]");
for (int i = 0; i < leaf.KeyCount; i++)
{
if (leaf.Keys[i] == key)
{
Console.WriteLine($"{indent} Found key {key} -> \"{leaf.Values[i]}\"");
return leaf.Values[i];
}
}
Console.WriteLine($"{indent} Key {key} not found in leaf.");
return null;
}
else if (node is BPlusInternalNode internalNode)
{
// Print which internal node we are visiting
Console.WriteLine($"{indent} Level {depth} (Internal): [{string.Join(", ", GetNodeKeys(internalNode))}]");
int i = 0;
while (i < internalNode.KeyCount && key >= internalNode.Keys[i])
i++;
Console.WriteLine($"{indent} Going to child {i}");
return SearchNode(internalNode.Children[i], key, depth + 1);
}
return null;
}
What does this code do?
Search always traverses down to a leaf node before looking for the key. This gives predictable performance because every search does the same number of hops regardless of which key you look for.
At each level, the console output shows which node is being visited and which child the search descends into. This makes the traversal path visible when you run the program later.
- You won't see a change yet. The next chunk adds the range query and print methods to complete the file.
- Below SearchNode, add the RangeQuery and FindLeaf methods:
public List<(int Key, string Value)> RangeQuery(int from, int to)
{
var results = new List<(int, string)>();
if (_root == null) return results;
// Find the leaf where the range starts
var leaf = FindLeaf(_root, from);
Console.WriteLine($" Range query [{from} to {to}]:");
Console.WriteLine($" Following leaf linked list...");
// Walk the linked list collecting values in range
while (leaf != null)
{
for (int i = 0; i < leaf.KeyCount; i++)
{
if (leaf.Keys[i] >= from && leaf.Keys[i] <= to)
{
results.Add((leaf.Keys[i], leaf.Values[i]));
Console.WriteLine($" Found: ({leaf.Keys[i]}, \"{leaf.Values[i]}\")");
}
if (leaf.Keys[i] > to)
return results;
}
leaf = leaf.Next;
}
return results;
}
private BPlusLeafNode FindLeaf(BPlusNode node, int key)
{
if (node is BPlusLeafNode leaf)
return leaf;
var internalNode = (BPlusInternalNode)node;
int i = 0;
while (i < internalNode.KeyCount && key >= internalNode.Keys[i])
i++;
return FindLeaf(internalNode.Children[i], key);
}
Why is the range query so efficient?
FindLeaf descends the tree once to locate the starting leaf. From there, RangeQuery simply follows the Next pointers collecting values until it passes the upper bound. No backtracking up the tree is needed.
This is exactly how database indexes scan ranges of rows. One tree descent plus a sequential leaf walk.
- You won't see a change yet. The final chunk adds tree printing to complete the file.
- Below FindLeaf, add the printing methods and close the class:
public void Print()
{
if (_root == null)
{
Console.WriteLine(" (empty tree)");
return;
}
PrintNode(_root, "", true);
}
private void PrintNode(BPlusNode node, string indent, bool isLast)
{
string connector = isLast ? "└── " : "├── ";
string nodeType = node is BPlusLeafNode ? "Leaf" : "Internal";
Console.WriteLine($"{indent}{connector}[{string.Join(", ", GetNodeKeys(node))}] ({nodeType})");
if (node is BPlusInternalNode internalNode)
{
string childIndent = indent + (isLast ? " " : "│ ");
for (int i = 0; i <= internalNode.KeyCount; i++)
{
if (internalNode.Children[i] != null)
PrintNode(internalNode.Children[i], childIndent, i == internalNode.KeyCount);
}
}
}
public void PrintLeafChain()
{
if (_root == null)
{
Console.WriteLine(" (empty tree)");
return;
}
// Walk from leftmost leaf to rightmost using Next pointers
var leaf = GetLeftmostLeaf(_root);
Console.Write(" Leaf chain: ");
while (leaf != null)
{
Console.Write($"[{string.Join(", ", GetNodeKeys(leaf))}]");
if (leaf.Next != null) Console.Write(" -> ");
leaf = leaf.Next;
}
Console.WriteLine();
}
private BPlusLeafNode GetLeftmostLeaf(BPlusNode node)
{
if (node is BPlusLeafNode leaf) return leaf;
return GetLeftmostLeaf(((BPlusInternalNode)node).Children[0]);
}
}
What does this code do?
- Print displays the tree structure with indentation. Each node is labeled as Internal or Leaf so you can see the separation.
- PrintLeafChain walks the linked list from the leftmost leaf to the rightmost, printing each leaf's keys connected by arrows. This proves the sequential access path works.
- GetLeftmostLeaf always takes the first child pointer until it reaches a leaf. This is the starting point for any left-to-right leaf scan.
- Save BPlusTree.cs and confirm the complete file compiles by running:
dotnet build
- You should see Build succeeded with 0 errors and 0 warnings. Your entire B+ Tree implementation is now ready to use.
Still seeing compile errors?
- Make sure the file ends with a closing } that closes the BPlusTree class. You should have one } at the very end of the file.
- Verify that GetNodeKeys appears exactly once in the file (above the Search method). If it is missing, the print and search methods cannot resolve it.
- Check that PrintLeafChain uses Console.Write (not Console.WriteLine) for the leaf entries, with a single Console.WriteLine() at the end to finish the line.
- help me fix my BPlusTree.cs compile errors
✔️ Awesome, I've got everything!
Great. Double check you have saved BPlusTree.cs and that dotnet build shows no errors.
ⓧ I'd like to double check the full code
Here is the complete BPlusTree.cs file for comparison:
namespace BTreeVisualizer;
public abstract class BPlusNode
{
public int[] Keys;
public int KeyCount;
protected BPlusNode(int maxKeys)
{
Keys = new int[maxKeys + 1];
KeyCount = 0;
}
}
public class BPlusInternalNode : BPlusNode
{
public BPlusNode[] Children;
public BPlusInternalNode(int maxKeys) : base(maxKeys)
{
Children = new BPlusNode[maxKeys + 2];
}
}
public class BPlusLeafNode : BPlusNode
{
public string[] Values;
public BPlusLeafNode? Next;
public BPlusLeafNode(int maxKeys) : base(maxKeys)
{
Values = new string[maxKeys + 1];
Next = null;
}
}
public class BPlusTree
{
private BPlusNode? _root;
private readonly int _maxKeys;
public BPlusTree(int order)
{
_maxKeys = order - 1;
_root = null;
}
public void Insert(int key, string value)
{
if (_root == null)
{
var leaf = new BPlusLeafNode(_maxKeys);
leaf.Keys[0] = key;
leaf.Values[0] = value;
leaf.KeyCount = 1;
_root = leaf;
Console.WriteLine($" Inserted ({key}, \"{value}\") as first leaf.");
return;
}
var result = InsertRecursive(_root, key, value);
if (result != null)
{
var newRoot = new BPlusInternalNode(_maxKeys);
newRoot.Keys[0] = result.Value.Key;
newRoot.Children[0] = _root;
newRoot.Children[1] = result.Value.Node;
newRoot.KeyCount = 1;
_root = newRoot;
Console.WriteLine($" Root split! New root key: [{result.Value.Key}]");
}
}
private (int Key, BPlusNode Node)? InsertRecursive(BPlusNode node, int key, string value)
{
if (node is BPlusLeafNode leaf)
{
int i = leaf.KeyCount - 1;
while (i >= 0 && leaf.Keys[i] > key)
{
leaf.Keys[i + 1] = leaf.Keys[i];
leaf.Values[i + 1] = leaf.Values[i];
i--;
}
leaf.Keys[i + 1] = key;
leaf.Values[i + 1] = value;
leaf.KeyCount++;
if (leaf.KeyCount > _maxKeys)
{
return SplitLeaf(leaf);
}
Console.WriteLine($" Inserted ({key}, \"{value}\") into leaf: [{string.Join(", ", GetNodeKeys(leaf))}]");
return null;
}
else if (node is BPlusInternalNode internalNode)
{
int i = 0;
while (i < internalNode.KeyCount && key >= internalNode.Keys[i])
i++;
var result = InsertRecursive(internalNode.Children[i], key, value);
if (result != null)
{
int j = internalNode.KeyCount - 1;
while (j >= 0 && internalNode.Keys[j] > result.Value.Key)
{
internalNode.Keys[j + 1] = internalNode.Keys[j];
internalNode.Children[j + 2] = internalNode.Children[j + 1];
j--;
}
internalNode.Keys[j + 1] = result.Value.Key;
internalNode.Children[j + 2] = result.Value.Node;
internalNode.KeyCount++;
Console.WriteLine($" Promoted key {result.Value.Key} to internal node: [{string.Join(", ", GetNodeKeys(internalNode))}]");
if (internalNode.KeyCount > _maxKeys)
{
return SplitInternal(internalNode);
}
return null;
}
return null;
}
return null;
}
private (int Key, BPlusNode Node) SplitLeaf(BPlusLeafNode leaf)
{
int mid = leaf.KeyCount / 2;
var newLeaf = new BPlusLeafNode(_maxKeys);
for (int i = mid; i < leaf.KeyCount; i++)
{
newLeaf.Keys[i - mid] = leaf.Keys[i];
newLeaf.Values[i - mid] = leaf.Values[i];
}
newLeaf.KeyCount = leaf.KeyCount - mid;
leaf.KeyCount = mid;
newLeaf.Next = leaf.Next;
leaf.Next = newLeaf;
int promotedKey = newLeaf.Keys[0];
Console.WriteLine($" Leaf split! Left: [{string.Join(", ", GetNodeKeys(leaf))}] Right: [{string.Join(", ", GetNodeKeys(newLeaf))}] Promoted: {promotedKey}");
return (promotedKey, newLeaf);
}
private (int Key, BPlusNode Node) SplitInternal(BPlusInternalNode node)
{
int mid = node.KeyCount / 2;
int promotedKey = node.Keys[mid];
var newNode = new BPlusInternalNode(_maxKeys);
for (int i = mid + 1; i < node.KeyCount; i++)
{
newNode.Keys[i - mid - 1] = node.Keys[i];
}
for (int i = mid + 1; i <= node.KeyCount; i++)
{
newNode.Children[i - mid - 1] = node.Children[i];
}
newNode.KeyCount = node.KeyCount - mid - 1;
node.KeyCount = mid;
Console.WriteLine($" Internal node split! Pushed up: {promotedKey}");
return (promotedKey, newNode);
}
public string? Search(int key)
{
if (_root == null)
{
Console.WriteLine(" Tree is empty.");
return null;
}
return SearchNode(_root, key, 0);
}
private string? SearchNode(BPlusNode node, int key, int depth)
{
string indent = new string(' ', depth * 2);
if (node is BPlusLeafNode leaf)
{
Console.WriteLine($"{indent} Level {depth} (Leaf): [{string.Join(", ", GetNodeKeys(leaf))}]");
for (int i = 0; i < leaf.KeyCount; i++)
{
if (leaf.Keys[i] == key)
{
Console.WriteLine($"{indent} Found key {key} -> \"{leaf.Values[i]}\"");
return leaf.Values[i];
}
}
Console.WriteLine($"{indent} Key {key} not found in leaf.");
return null;
}
else if (node is BPlusInternalNode internalNode)
{
Console.WriteLine($"{indent} Level {depth} (Internal): [{string.Join(", ", GetNodeKeys(internalNode))}]");
int i = 0;
while (i < internalNode.KeyCount && key >= internalNode.Keys[i])
i++;
Console.WriteLine($"{indent} Going to child {i}");
return SearchNode(internalNode.Children[i], key, depth + 1);
}
return null;
}
public List<(int Key, string Value)> RangeQuery(int from, int to)
{
var results = new List<(int, string)>();
if (_root == null) return results;
var leaf = FindLeaf(_root, from);
Console.WriteLine($" Range query [{from} to {to}]:");
Console.WriteLine($" Following leaf linked list...");
while (leaf != null)
{
for (int i = 0; i < leaf.KeyCount; i++)
{
if (leaf.Keys[i] >= from && leaf.Keys[i] <= to)
{
results.Add((leaf.Keys[i], leaf.Values[i]));
Console.WriteLine($" Found: ({leaf.Keys[i]}, \"{leaf.Values[i]}\")");
}
if (leaf.Keys[i] > to)
return results;
}
leaf = leaf.Next;
}
return results;
}
private BPlusLeafNode FindLeaf(BPlusNode node, int key)
{
if (node is BPlusLeafNode leaf)
return leaf;
var internalNode = (BPlusInternalNode)node;
int i = 0;
while (i < internalNode.KeyCount && key >= internalNode.Keys[i])
i++;
return FindLeaf(internalNode.Children[i], key);
}
public void Print()
{
if (_root == null)
{
Console.WriteLine(" (empty tree)");
return;
}
PrintNode(_root, "", true);
}
private void PrintNode(BPlusNode node, string indent, bool isLast)
{
string connector = isLast ? "└── " : "├── ";
string nodeType = node is BPlusLeafNode ? "Leaf" : "Internal";
Console.WriteLine($"{indent}{connector}[{string.Join(", ", GetNodeKeys(node))}] ({nodeType})");
if (node is BPlusInternalNode internalNode)
{
string childIndent = indent + (isLast ? " " : "│ ");
for (int i = 0; i <= internalNode.KeyCount; i++)
{
if (internalNode.Children[i] != null)
PrintNode(internalNode.Children[i], childIndent, i == internalNode.KeyCount);
}
}
}
public void PrintLeafChain()
{
if (_root == null)
{
Console.WriteLine(" (empty tree)");
return;
}
var leaf = GetLeftmostLeaf(_root);
Console.Write(" Leaf chain: ");
while (leaf != null)
{
Console.Write($"[{string.Join(", ", GetNodeKeys(leaf))}]");
if (leaf.Next != null) Console.Write(" -> ");
leaf = leaf.Next;
}
Console.WriteLine();
}
private BPlusLeafNode GetLeftmostLeaf(BPlusNode node)
{
if (node is BPlusLeafNode leaf) return leaf;
return GetLeftmostLeaf(((BPlusInternalNode)node).Children[0]);
}
private int[] GetNodeKeys(BPlusNode node)
{
int[] keys = new int[node.KeyCount];
Array.Copy(node.Keys, keys, node.KeyCount);
return keys;
}
}
Your B+ Tree is fully implemented. Next up, you will wire both trees into Program.cs and see them run side by side with the same data set.
Wire Up Program.cs and Run the Comparison
You now have two complete tree implementations sitting in your project, but the app still prints "Hello, World!" when you run it.
In this step, you will replace that placeholder with a demonstration that feeds the same data set into both trees and shows exactly how they differ.
In this step, get ready to:
- Write the B-Tree demonstration that inserts keys and searches the tree.
- Write the B+ Tree demonstration with insert, search, and range query.
- Add a side-by-side comparison summary and run the full app.
Replace Hello World with the B-Tree demonstration
The Program.cs file is the entry point that orchestrates both tree demos. You will replace the Hello World content with code that creates a BTree, inserts eight keys, prints the tree after each insert, and then searches for two values.
- Open Program.cs in your editor.
- Delete all existing content and replace it with the following code:
using BTreeVisualizer;
Console.WriteLine("================================================");
Console.WriteLine(" B-Tree and B+ Tree Visualizer");
Console.WriteLine(" Minimum Degree t=2 (Max 3 keys per node)");
Console.WriteLine("================================================");
Console.WriteLine();
// PART 1: B-Tree Demonstration
Console.WriteLine("================================================");
Console.WriteLine(" PART 1: B-TREE (t=2, max 3 keys)");
Console.WriteLine("================================================");
Console.WriteLine();
var btree = new BTree(2);
int[] btreeKeys = { 10, 20, 5, 6, 12, 30, 7, 17 };
foreach (int key in btreeKeys)
{
Console.WriteLine($">> Inserting {key}:");
btree.Insert(key);
Console.WriteLine(" Tree structure:");
btree.Print();
Console.WriteLine();
}
Console.WriteLine("-- B-Tree Search --");
Console.WriteLine();
Console.WriteLine(">> Searching for 12:");
btree.Search(12);
Console.WriteLine();
Console.WriteLine(">> Searching for 25 (not in tree):");
btree.Search(25);
Console.WriteLine();
What does this code do?
using BTreeVisualizer; imports the namespace where your BTree and BPlusTree classes live.
The code creates a B-Tree with minimum degree 2 (max 3 keys per node), inserts 8 values, and prints the tree structure after each insert so you can watch splits happen in real time. It then searches for key 12 (present) and key 25 (absent) to show the traversal path at each level.
- Save Program.cs.
- Run the app to confirm the B-Tree demo works by running:
dotnet run
You should see the B-Tree banner, then each insertion printing the tree structure with indentation showing parent-child relationships. Watch for "Split root" and "Split child" messages as the tree fills up.
Seeing a compilation error?
Make sure the first line is using BTreeVisualizer; and that your BTree.cs file uses namespace BTreeVisualizer; at the top. The namespace must match exactly.
help me debug a namespace error in my BTreeVisualizer project
Add the B+ Tree demonstration
The B+ Tree demo uses the same keys but pairs each one with a string value. This mirrors how real databases store key-value pairs in leaf nodes.
- In Program.cs, add the following code at the bottom of the file (after the last Console.WriteLine(); from Part 1):
// PART 2: B+ Tree Demonstration
Console.WriteLine("================================================");
Console.WriteLine(" PART 2: B+ TREE (order 4, max 3 keys)");
Console.WriteLine("================================================");
Console.WriteLine();
var bplusTree = new BPlusTree(4);
(int key, string value)[] bplusData =
{
(10, "Alice"), (20, "Bob"), (5, "Carol"),
(6, "Dave"), (12, "Eve"), (30, "Frank"),
(7, "Grace"), (17, "Heidi")
};
foreach (var (key, value) in bplusData)
{
Console.WriteLine($">> Inserting ({key}, \"{value}\"):");
bplusTree.Insert(key, value);
Console.WriteLine(" Tree structure:");
bplusTree.Print();
bplusTree.PrintLeafChain();
Console.WriteLine();
}
Console.WriteLine("-- B+ Tree Search --");
Console.WriteLine();
Console.WriteLine(">> Searching for 12:");
bplusTree.Search(12);
Console.WriteLine();
Console.WriteLine(">> Searching for 25 (not in tree):");
bplusTree.Search(25);
Console.WriteLine();
Console.WriteLine("-- B+ Tree Range Query --");
Console.WriteLine();
Console.WriteLine(">> Range query [6 to 17]:");
bplusTree.RangeQuery(6, 17);
Console.WriteLine();
What does this code do?
This creates a B+ Tree of order 4 (max 3 keys per node) and inserts the same 8 numeric keys paired with names. After each insert, it prints both the tree structure AND the leaf chain, so you can watch the linked list of leaves grow.
The search section shows how B+ Tree search always descends to a leaf node. The RangeQuery(6, 17) call demonstrates the key advantage of B+ Trees: once you find the starting leaf, you follow the Next pointers to collect all values in range without ever going back up the tree.
- Save Program.cs.
You won't run the app yet. The next chunk adds the comparison summary, then you will run everything together.
Add the comparison summary and run the full app
The final section prints a structured summary so you can see the key architectural differences at a glance, without scrolling back through all the output.
- Add the following code at the bottom of Program.cs:
// PART 3: Key Differences
Console.WriteLine("================================================");
Console.WriteLine(" KEY DIFFERENCES: B-Tree vs B+ Tree");
Console.WriteLine("================================================");
Console.WriteLine();
Console.WriteLine(" B-Tree:");
Console.WriteLine(" - Data stored in ALL nodes (internal + leaf)");
Console.WriteLine(" - No linked list between leaves");
Console.WriteLine(" - Search may stop at any level");
Console.WriteLine(" - Less predictable search time");
Console.WriteLine();
Console.WriteLine(" B+ Tree:");
Console.WriteLine(" - Data stored ONLY in leaf nodes");
Console.WriteLine(" - Leaves form a linked list (great for range queries)");
Console.WriteLine(" - Search always goes to a leaf");
Console.WriteLine(" - More predictable, often faster for scans");
Console.WriteLine();
Console.WriteLine(" Both:");
Console.WriteLine(" - Self-balancing (all leaves at same depth)");
Console.WriteLine(" - Nodes split when full, promoting keys up");
Console.WriteLine(" - O(log n) search time");
Console.WriteLine(" - Used in databases and file systems");
What does this code do?
This prints a clean reference table summarizing the structural differences you just observed. It covers where data lives, how search behaves, whether leaves are linked, and what both trees have in common.
- Save Program.cs.
- Run the full app by running:
dotnet run
You should see all three parts in sequence: the B-Tree inserts with splits, the B+ Tree inserts with leaf chains, and the final comparison summary at the bottom.
What should I look for in the output?
In Part 1, watch for "Root was full. Split root" appearing after the 4th insert. This is proactive splitting in action. The tree stays balanced because full nodes are split before you descend into them.
In Part 2, notice the "Leaf chain" line after each insert. It shows all leaf nodes connected as a linked list. The range query at the end walks this chain to find all values between 6 and 17 without backtracking.
Seeing unexpected output or errors?
If you see a compilation error about BPlusTree not being found, check that BPlusTree.cs is saved and uses namespace BTreeVisualizer; at the top.
If the range query returns no results, verify the call is bplusTree.RangeQuery(6, 17) with the variable name bplusTree (lowercase b).
help me debug my BTreeVisualizer Program.cs output
✔️ Awesome, I've got everything!
Great. Double check you have saved Program.cs and that dotnet run produces all three parts of output.
ⓧ I'd like to double check the full code
Here is the complete Program.cs file. Make sure yours matches exactly:
using BTreeVisualizer;
Console.WriteLine("================================================");
Console.WriteLine(" B-Tree and B+ Tree Visualizer");
Console.WriteLine(" Minimum Degree t=2 (Max 3 keys per node)");
Console.WriteLine("================================================");
Console.WriteLine();
// PART 1: B-Tree Demonstration
Console.WriteLine("================================================");
Console.WriteLine(" PART 1: B-TREE (t=2, max 3 keys)");
Console.WriteLine("================================================");
Console.WriteLine();
var btree = new BTree(2);
int[] btreeKeys = { 10, 20, 5, 6, 12, 30, 7, 17 };
foreach (int key in btreeKeys)
{
Console.WriteLine($">> Inserting {key}:");
btree.Insert(key);
Console.WriteLine(" Tree structure:");
btree.Print();
Console.WriteLine();
}
Console.WriteLine("-- B-Tree Search --");
Console.WriteLine();
Console.WriteLine(">> Searching for 12:");
btree.Search(12);
Console.WriteLine();
Console.WriteLine(">> Searching for 25 (not in tree):");
btree.Search(25);
Console.WriteLine();
// PART 2: B+ Tree Demonstration
Console.WriteLine("================================================");
Console.WriteLine(" PART 2: B+ TREE (order 4, max 3 keys)");
Console.WriteLine("================================================");
Console.WriteLine();
var bplusTree = new BPlusTree(4);
(int key, string value)[] bplusData =
{
(10, "Alice"), (20, "Bob"), (5, "Carol"),
(6, "Dave"), (12, "Eve"), (30, "Frank"),
(7, "Grace"), (17, "Heidi")
};
foreach (var (key, value) in bplusData)
{
Console.WriteLine($">> Inserting ({key}, \"{value}\"):");
bplusTree.Insert(key, value);
Console.WriteLine(" Tree structure:");
bplusTree.Print();
bplusTree.PrintLeafChain();
Console.WriteLine();
}
Console.WriteLine("-- B+ Tree Search --");
Console.WriteLine();
Console.WriteLine(">> Searching for 12:");
bplusTree.Search(12);
Console.WriteLine();
Console.WriteLine(">> Searching for 25 (not in tree):");
bplusTree.Search(25);
Console.WriteLine();
Console.WriteLine("-- B+ Tree Range Query --");
Console.WriteLine();
Console.WriteLine(">> Range query [6 to 17]:");
bplusTree.RangeQuery(6, 17);
Console.WriteLine();
// PART 3: Key Differences
Console.WriteLine("================================================");
Console.WriteLine(" KEY DIFFERENCES: B-Tree vs B+ Tree");
Console.WriteLine("================================================");
Console.WriteLine();
Console.WriteLine(" B-Tree:");
Console.WriteLine(" - Data stored in ALL nodes (internal + leaf)");
Console.WriteLine(" - No linked list between leaves");
Console.WriteLine(" - Search may stop at any level");
Console.WriteLine(" - Less predictable search time");
Console.WriteLine();
Console.WriteLine(" B+ Tree:");
Console.WriteLine(" - Data stored ONLY in leaf nodes");
Console.WriteLine(" - Leaves form a linked list (great for range queries)");
Console.WriteLine(" - Search always goes to a leaf");
Console.WriteLine(" - More predictable, often faster for scans");
Console.WriteLine();
Console.WriteLine(" Both:");
Console.WriteLine(" - Self-balancing (all leaves at same depth)");
Console.WriteLine(" - Nodes split when full, promoting keys up");
Console.WriteLine(" - O(log n) search time");
Console.WriteLine(" - Used in databases and file systems");
Your visualizer is complete. You can now see exactly how node splits propagate, how the B+ Tree leaf chain grows, and why databases prefer B+ Trees for range scans. Next up: the Secret Mission challenges you to implement the hardest operation of all.
Secret mission
Implement Delete with Rebalancing
Insertion is only half the story. Real databases need to remove keys too, and that means handling underflow when a node drops below minimum occupancy. Can you implement delete with borrow-or-merge rebalancing?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files or delete them.
Resources you used:
- The BTreeVisualizer project folder on your Desktop (contains Program.cs, BTree.cs, BPlusTree.cs, and the .csproj file).
Keep everything running
No action needed. Choose this if you want to keep experimenting with your B-Tree and B+ Tree implementations.
- Your BTreeVisualizer folder stays on your Desktop.
- You can re-run the app any time with dotnet run from inside the project folder.
Pause - I'll come back to this later
Since this is a local project with no running processes, there is nothing to pause. Your files will be waiting for you whenever you return.
- No background processes are running from this project.
- Your BTreeVisualizer folder remains on your Desktop until you decide to delete it.
Delete - I don't want to use this again
Remove all project files from your machine.
- Delete the project folder by running this command in your terminal:
macOS
rm -rf ~/Desktop/BTreeVisualizer
Windows
rmdir /s /q "$HOME\Desktop\BTreeVisualizer"
- Confirm the folder is gone by checking your Desktop. You should no longer see a BTreeVisualizer folder.
Nice Work!
Nice Work!
You just built both a B-Tree and a B+ Tree from scratch in C#, complete with real-time console visualization that makes every split, promotion, and traversal visible.
You've learned how to:
- Build a B-Tree from scratch with proactive splitting, search with path visualization, and tree printing that shows parent-child relationships.
- Build a B+ Tree with separate internal and leaf node types, a linked leaf list, and range query support that follows the chain.
- Demonstrate the structural differences between both trees: B-Trees store data at every level while B+ Trees store data only in leaves, making sequential scans and predictable search times possible.
- Secret Mission: Implement delete with rebalancing on the B-Tree, handling predecessor/successor replacement, borrowing from siblings, and merging nodes.
Ready to quiz yourself?