From e1822e71fc1d548488fcacaa8983031fefc7ec4e Mon Sep 17 00:00:00 2001 From: ethanhaines <122860615+ethanhaines@users.noreply.github.com> Date: Mon, 3 Nov 2025 17:27:07 -0500 Subject: [PATCH 1/3] Added BTree Data Structure and Tests --- DataStructures.Tests/BTreeTests.cs | 567 ++++++++++++++++++++++ DataStructures/BTree/BTree.cs | 742 +++++++++++++++++++++++++++++ DataStructures/BTree/BTreeNode.cs | 105 ++++ README.md | 3 + 4 files changed, 1417 insertions(+) create mode 100644 DataStructures.Tests/BTreeTests.cs create mode 100644 DataStructures/BTree/BTree.cs create mode 100644 DataStructures/BTree/BTreeNode.cs diff --git a/DataStructures.Tests/BTreeTests.cs b/DataStructures.Tests/BTreeTests.cs new file mode 100644 index 00000000..ab201bce --- /dev/null +++ b/DataStructures.Tests/BTreeTests.cs @@ -0,0 +1,567 @@ +using DataStructures.BTree; +using static FluentAssertions.FluentActions; + +namespace DataStructures.Tests; + +internal class BTreeTests +{ + private static readonly int[] Data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + + [Test] + public void Constructor_DefaultMinimumDegree_CreatesEmptyTree() + { + var tree = new BTree(); + + tree.Count.Should().Be(0); + tree.MinimumDegree.Should().Be(2); + } + + [Test] + public void Constructor_CustomMinimumDegree_SetsCorrectDegree() + { + var tree = new BTree(3); + + tree.MinimumDegree.Should().Be(3); + tree.Count.Should().Be(0); + } + + [Test] + public void Constructor_MinimumDegreeLessThan2_ThrowsException() + { + Invoking(() => new BTree(1)) + .Should() + .ThrowExactly() + .WithMessage("Minimum degree must be at least 2.*"); + } + + [Test] + public void Constructor_UseCustomComparer_FormsCorrectTree() + { + var tree = new BTree(2, Comparer.Create((x, y) => y.CompareTo(x))); + + tree.AddRange(new[] { 1, 2, 3, 4, 5 }); + + tree.GetMin().Should().Be(5); + tree.GetMax().Should().Be(1); + + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + new[] { 5, 4, 3, 2, 1 }, + config => config.WithStrictOrdering()); + } + + [Test] + public void Add_SingleKey_IncreasesCount() + { + var tree = new BTree(); + + tree.Add(5); + + tree.Count.Should().Be(1); + tree.Contains(5).Should().BeTrue(); + } + + [Test] + public void Add_MultipleKeys_FormsCorrectTree() + { + var tree = new BTree(2); + + for (var i = 0; i < Data.Length; i++) + { + tree.Add(Data[i]); + tree.Count.Should().Be(i + 1); + } + + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + Data, + config => config.WithStrictOrdering()); + } + + [Test] + public void Add_DuplicateKey_ThrowsException() + { + var tree = new BTree(); + tree.AddRange(new[] { 1, 2, 3, 4, 5 }); + + Invoking(() => tree.Add(3)) + .Should() + .ThrowExactly() + .WithMessage("""Key "3" already exists in B-Tree."""); + } + + [Test] + public void Add_CausesNodeSplit_TreeStillValid() + { + var tree = new BTree(2); + tree.AddRange(new[] { 1, 2, 3, 4, 5, 6, 7 }); + + tree.Count.Should().Be(7); + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + new[] { 1, 2, 3, 4, 5, 6, 7 }, + config => config.WithStrictOrdering()); + } + + [Test] + public void Add_LargeNumberOfKeys_TreeStillValid() + { + var tree = new BTree(3); + var keys = Enumerable.Range(1, 100).ToArray(); + + tree.AddRange(keys); + + tree.Count.Should().Be(100); + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + keys, + config => config.WithStrictOrdering()); + } + + [Test] + public void AddRange_MultipleKeys_FormsCorrectTree() + { + var tree = new BTree(2); + tree.AddRange(new[] { 'a', 'b', 'c', 'd', 'e', 'f', 'g' }); + + tree.Count.Should().Be(7); + + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + new[] { 'a', 'b', 'c', 'd', 'e', 'f', 'g' }, + config => config.WithStrictOrdering()); + } + + [Test] + public void AddRange_EmptyCollection_TreeRemainsEmpty() + { + var tree = new BTree(); + tree.AddRange(Array.Empty()); + + tree.Count.Should().Be(0); + tree.GetKeysInOrder().Should().BeEmpty(); + } + + [Test] + public void Contains_KeyExists_ReturnsTrue() + { + var tree = new BTree(); + tree.AddRange(Data); + + tree.Contains(5).Should().BeTrue(); + tree.Contains(1).Should().BeTrue(); + tree.Contains(15).Should().BeTrue(); + } + + [Test] + public void Contains_KeyDoesNotExist_ReturnsFalse() + { + var tree = new BTree(); + tree.AddRange(Data); + + tree.Contains(100).Should().BeFalse(); + tree.Contains(-5).Should().BeFalse(); + tree.Contains(0).Should().BeFalse(); + } + + [Test] + public void Contains_EmptyTree_ReturnsFalse() + { + var tree = new BTree(); + + tree.Contains(5).Should().BeFalse(); + tree.Contains(-12).Should().BeFalse(); + } + + [Test] + public void Remove_SingleKey_DecreasesCount() + { + var tree = new BTree(); + tree.AddRange(new[] { 1, 2, 3, 4, 5 }); + + tree.Remove(3); + + tree.Count.Should().Be(4); + tree.Contains(3).Should().BeFalse(); + } + + [Test] + public void Remove_FromLeafNode_TreeStillValid() + { + var tree = new BTree(2); + tree.AddRange(Data); + + tree.Remove(1); + + tree.Count.Should().Be(14); + tree.Contains(1).Should().BeFalse(); + + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + new[] { 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, + config => config.WithStrictOrdering()); + } + + [Test] + public void Remove_FromNonLeafNode_TreeStillValid() + { + var tree = new BTree(2); + tree.AddRange(Data); + + tree.Remove(8); + + tree.Count.Should().Be(14); + tree.Contains(8).Should().BeFalse(); + + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + new[] { 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15 }, + config => config.WithStrictOrdering()); + } + + [Test] + public void Remove_CausesMerge_TreeStillValid() + { + var tree = new BTree(2); + tree.AddRange(new[] { 1, 2, 3, 4, 5, 6, 7 }); + + tree.Remove(4); + + tree.Count.Should().Be(6); + tree.Contains(4).Should().BeFalse(); + + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + new[] { 1, 2, 3, 5, 6, 7 }, + config => config.WithStrictOrdering()); + } + + [Test] + public void Remove_MultipleKeys_TreeStillValid() + { + var tree = new BTree(2); + tree.AddRange(Data); + + tree.Remove(5); + tree.Remove(10); + tree.Remove(15); + + tree.Count.Should().Be(12); + tree.Contains(5).Should().BeFalse(); + tree.Contains(10).Should().BeFalse(); + tree.Contains(15).Should().BeFalse(); + + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + new[] { 1, 2, 3, 4, 6, 7, 8, 9, 11, 12, 13, 14 }, + config => config.WithStrictOrdering()); + } + + [Test] + public void Remove_AllKeys_TreeBecomesEmpty() + { + var tree = new BTree(2); + tree.AddRange(new[] { 1, 2, 3, 4, 5 }); + + tree.Remove(1); + tree.Remove(2); + tree.Remove(3); + tree.Remove(4); + tree.Remove(5); + + tree.Count.Should().Be(0); + tree.GetKeysInOrder().Should().BeEmpty(); + } + + [Test] + public void Remove_EmptyTree_ThrowsException() + { + var tree = new BTree(); + + Invoking(() => tree.Remove(1)) + .Should() + .ThrowExactly() + .WithMessage("""Key "1" is not in the B-Tree."""); + } + + [Test] + public void Remove_KeyNotInTree_ThrowsException() + { + var tree = new BTree(); + tree.AddRange(Data); + + Invoking(() => tree.Remove(100)) + .Should() + .ThrowExactly() + .WithMessage("""Key "100" is not in the B-Tree."""); + } + + [Test] + public void GetMin_NonEmptyTree_ReturnsMinimum() + { + var tree = new BTree(); + tree.AddRange(new[] { 5, 2, 8, 1, 9, 3 }); + + tree.GetMin().Should().Be(1); + } + + [Test] + public void GetMin_SingleElement_ReturnsElement() + { + var tree = new BTree(); + tree.Add(42); + + tree.GetMin().Should().Be(42); + } + + [Test] + public void GetMin_EmptyTree_ThrowsException() + { + var tree = new BTree(); + + Invoking(() => tree.GetMin()) + .Should() + .ThrowExactly() + .WithMessage("B-Tree is empty."); + } + + [Test] + public void GetMax_NonEmptyTree_ReturnsMaximum() + { + var tree = new BTree(); + tree.AddRange(new[] { 5, 2, 8, 1, 9, 3 }); + + tree.GetMax().Should().Be(9); + } + + [Test] + public void GetMax_SingleElement_ReturnsElement() + { + var tree = new BTree(); + tree.Add(42); + + tree.GetMax().Should().Be(42); + } + + [Test] + public void GetMax_EmptyTree_ThrowsException() + { + var tree = new BTree(); + + Invoking(() => tree.GetMax()) + .Should() + .ThrowExactly() + .WithMessage("B-Tree is empty."); + } + + [Test] + public void GetKeysInOrder_NonEmptyTree_ReturnsCorrectOrder() + { + var tree = new BTree(2); + tree.AddRange(new[] { 5, 2, 8, 1, 9, 3, 7, 4, 6 }); + + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, + config => config.WithStrictOrdering()); + } + + [Test] + public void GetKeysInOrder_EmptyTree_ReturnsEmpty() + { + var tree = new BTree(); + + tree.GetKeysInOrder().Should().BeEmpty(); + } + + [Test] + public void GetKeysInOrder_AfterRemoval_ReturnsCorrectOrder() + { + var tree = new BTree(2); + tree.AddRange(Data); + tree.Remove(5); + tree.Remove(10); + + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + new[] { 1, 2, 3, 4, 6, 7, 8, 9, 11, 12, 13, 14, 15 }, + config => config.WithStrictOrdering()); + } + + [Test] + public void GetKeysPreOrder_NonEmptyTree_ReturnsCorrectOrder() + { + var tree = new BTree(2); + tree.AddRange(new[] { 1, 2, 3, 4, 5, 6, 7 }); + + var preOrder = tree.GetKeysPreOrder().ToArray(); + + preOrder.Should().HaveCount(7); + preOrder.Should().Contain(new[] { 1, 2, 3, 4, 5, 6, 7 }); + } + + [Test] + public void GetKeysPreOrder_EmptyTree_ReturnsEmpty() + { + var tree = new BTree(); + + tree.GetKeysPreOrder().Should().BeEmpty(); + } + + [Test] + public void GetKeysPostOrder_NonEmptyTree_ReturnsCorrectOrder() + { + var tree = new BTree(2); + tree.AddRange(new[] { 1, 2, 3, 4, 5, 6, 7 }); + + var postOrder = tree.GetKeysPostOrder().ToArray(); + + postOrder.Should().HaveCount(7); + postOrder.Should().Contain(new[] { 1, 2, 3, 4, 5, 6, 7 }); + } + + [Test] + public void GetKeysPostOrder_EmptyTree_ReturnsEmpty() + { + var tree = new BTree(); + + tree.GetKeysPostOrder().Should().BeEmpty(); + } + + [Test] + public void BTree_LargeDataSet_MaintainsCorrectness() + { + var tree = new BTree(5); + var keys = Enumerable.Range(1, 1000).ToArray(); + + tree.AddRange(keys); + + tree.Count.Should().Be(1000); + tree.GetMin().Should().Be(1); + tree.GetMax().Should().Be(1000); + + var inOrder = tree.GetKeysInOrder().ToArray(); + inOrder.Should().BeEquivalentTo(keys, config => config.WithStrictOrdering()); + + for (var i = 1; i <= 1000; i++) + { + tree.Contains(i).Should().BeTrue(); + } + } + + [Test] + public void BTree_RandomInsertion_MaintainsCorrectness() + { + var tree = new BTree(3); + var random = new Random(42); + var keys = Enumerable.Range(1, 100) + .OrderBy(_ => random.Next()) + .ToArray(); + + tree.AddRange(keys); + + tree.Count.Should().Be(100); + + var inOrder = tree.GetKeysInOrder().ToArray(); + inOrder.Should().BeEquivalentTo( + Enumerable.Range(1, 100), + config => config.WithStrictOrdering()); + } + + [Test] + public void BTree_RandomDeletion_MaintainsCorrectness() + { + var tree = new BTree(3); + tree.AddRange(Enumerable.Range(1, 50)); + + var random = new Random(42); + var keysToRemove = Enumerable.Range(1, 50) + .OrderBy(_ => random.Next()) + .Take(25) + .ToArray(); + + foreach (var key in keysToRemove) + { + tree.Remove(key); + } + + tree.Count.Should().Be(25); + + var remainingKeys = Enumerable.Range(1, 50) + .Except(keysToRemove) + .OrderBy(x => x) + .ToArray(); + + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + remainingKeys, + config => config.WithStrictOrdering()); + } + + [Test] + public void BTree_StringKeys_WorksCorrectly() + { + var tree = new BTree(2); + var keys = new[] { "apple", "banana", "cherry", "date", "elderberry", "fig", "grape" }; + + tree.AddRange(keys); + + tree.Count.Should().Be(7); + tree.GetMin().Should().Be("apple"); + tree.GetMax().Should().Be("grape"); + + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + keys.OrderBy(x => x), + config => config.WithStrictOrdering()); + } + + [Test] + public void BTree_DifferentMinimumDegrees_AllWorkCorrectly() + { + for (var degree = 2; degree <= 10; degree++) + { + var tree = new BTree(degree); + tree.AddRange(Enumerable.Range(1, 50)); + + tree.Count.Should().Be(50); + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + Enumerable.Range(1, 50), + config => config.WithStrictOrdering()); + } + } + + [Test] + public void BTree_InsertRemoveInsert_WorksCorrectly() + { + var tree = new BTree(2); + + tree.AddRange(new[] { 1, 2, 3, 4, 5 }); + tree.Remove(3); + tree.Add(3); + + tree.Count.Should().Be(5); + tree.Contains(3).Should().BeTrue(); + + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + new[] { 1, 2, 3, 4, 5 }, + config => config.WithStrictOrdering()); + } +} diff --git a/DataStructures/BTree/BTree.cs b/DataStructures/BTree/BTree.cs new file mode 100644 index 00000000..5efdf227 --- /dev/null +++ b/DataStructures/BTree/BTree.cs @@ -0,0 +1,742 @@ +namespace DataStructures.BTree; + +/// +/// A self-balancing search tree data structure that maintains sorted data +/// and allows searches, sequential access, insertions, and deletions in +/// logarithmic time. +/// +/// +/// A B-Tree is a generalization of a binary search tree in which a node +/// can have more than two children. Unlike self-balancing binary search +/// trees, the B-Tree is optimized for systems that read and write large +/// blocks of data. It is commonly used in databases and file systems. +/// +/// Properties of a B-Tree of minimum degree t: +/// 1. Every node has at most 2t-1 keys. +/// 2. Every node (except root) has at least t-1 keys. +/// 3. The root has at least 1 key (if tree is not empty). +/// 4. All leaves are at the same level. +/// 5. A non-leaf node with k keys has k+1 children. +/// +/// Time Complexity: +/// - Search: O(log n) +/// - Insert: O(log n) +/// - Delete: O(log n) +/// +/// Space Complexity: O(n) +/// +/// See https://en.wikipedia.org/wiki/B-tree for more information. +/// Visualizer: https://www.cs.usfca.edu/~galles/visualization/BTree.html. +/// +/// Type of key for the tree. +public class BTree +{ + /// + /// Gets the number of keys in the tree. + /// + public int Count { get; private set; } + + /// + /// Gets the minimum degree (t) of the B-Tree. + /// Each node can contain at most 2t-1 keys and at least t-1 keys (except root). + /// + public int MinimumDegree { get; } + + /// + /// Comparer to use when comparing key values. + /// + private readonly Comparer comparer; + + /// + /// Reference to the root node. + /// + private BTreeNode? root; + + /// + /// Initializes a new instance of the + /// class with the specified minimum degree. + /// + /// + /// Minimum degree (t) of the B-Tree. Must be at least 2. + /// Each node can contain at most 2t-1 keys. + /// + /// + /// Thrown when minimumDegree is less than 2. + /// + public BTree(int minimumDegree = 2) + { + if (minimumDegree < 2) + { + throw new ArgumentException("Minimum degree must be at least 2.", nameof(minimumDegree)); + } + + MinimumDegree = minimumDegree; + comparer = Comparer.Default; + } + + /// + /// Initializes a new instance of the + /// class with the specified minimum degree and custom comparer. + /// + /// + /// Minimum degree (t) of the B-Tree. Must be at least 2. + /// + /// + /// Comparer to use when comparing keys. + /// + /// + /// Thrown when minimumDegree is less than 2. + /// + public BTree(int minimumDegree, Comparer customComparer) + { + if (minimumDegree < 2) + { + throw new ArgumentException("Minimum degree must be at least 2.", nameof(minimumDegree)); + } + + MinimumDegree = minimumDegree; + comparer = customComparer; + } + + /// + /// Add a single key to the tree. + /// + /// Key value to add. + public void Add(TKey key) + { + if (root is null) + { + root = new BTreeNode(MinimumDegree, true); + root.InsertKey(0, key); + Count++; + return; + } + + if (root.IsFull()) + { + var newRoot = new BTreeNode(MinimumDegree, false); + newRoot.InsertChild(0, root); + SplitChild(newRoot, 0); + root = newRoot; + } + + InsertNonFull(root, key); + Count++; + } + + /// + /// Add multiple keys to the tree. + /// + /// Key values to add. + public void AddRange(IEnumerable keys) + { + foreach (var key in keys) + { + Add(key); + } + } + + /// + /// Remove a key from the tree. + /// + /// Key value to remove. + /// + /// Thrown when the key is not found in the tree. + /// + public void Remove(TKey key) + { + if (root is null) + { + throw new KeyNotFoundException($"""Key "{key}" is not in the B-Tree."""); + } + + Remove(root, key); + + if (root.KeyCount == 0) + { + root = root.IsLeaf ? null : root.Children[0]; + } + + Count--; + } + + /// + /// Check if given key is in the tree. + /// + /// Key value to search for. + /// Whether or not the key is in the tree. + public bool Contains(TKey key) + { + return Search(root, key) is not null; + } + + /// + /// Get the minimum key in the tree. + /// + /// Minimum key in tree. + /// + /// Thrown when the tree is empty. + /// + public TKey GetMin() + { + if (root is null) + { + throw new InvalidOperationException("B-Tree is empty."); + } + + return GetMin(root); + } + + /// + /// Get the maximum key in the tree. + /// + /// Maximum key in tree. + /// + /// Thrown when the tree is empty. + /// + public TKey GetMax() + { + if (root is null) + { + throw new InvalidOperationException("B-Tree is empty."); + } + + return GetMax(root); + } + + /// + /// Get keys in order from smallest to largest as defined by the + /// comparer. + /// + /// Keys in tree in order from smallest to largest. + public IEnumerable GetKeysInOrder() + { + List result = []; + InOrderTraversal(root); + return result; + + void InOrderTraversal(BTreeNode? node) + { + if (node is null) + { + return; + } + + for (var i = 0; i < node.KeyCount; i++) + { + if (!node.IsLeaf) + { + InOrderTraversal(node.Children[i]); + } + + result.Add(node.Keys[i]); + } + + if (!node.IsLeaf) + { + InOrderTraversal(node.Children[node.KeyCount]); + } + } + } + + /// + /// Get keys in the pre-order order. + /// + /// Keys in pre-order order. + public IEnumerable GetKeysPreOrder() + { + var result = new List(); + PreOrderTraversal(root); + return result; + + void PreOrderTraversal(BTreeNode? node) + { + if (node is null) + { + return; + } + + for (var i = 0; i < node.KeyCount; i++) + { + result.Add(node.Keys[i]); + } + + if (!node.IsLeaf) + { + for (var i = 0; i <= node.KeyCount; i++) + { + PreOrderTraversal(node.Children[i]); + } + } + } + } + + /// + /// Get keys in the post-order order. + /// + /// Keys in the post-order order. + public IEnumerable GetKeysPostOrder() + { + var result = new List(); + PostOrderTraversal(root); + return result; + + void PostOrderTraversal(BTreeNode? node) + { + if (node is null) + { + return; + } + + if (!node.IsLeaf) + { + for (var i = 0; i <= node.KeyCount; i++) + { + PostOrderTraversal(node.Children[i]); + } + } + + for (var i = 0; i < node.KeyCount; i++) + { + result.Add(node.Keys[i]); + } + } + } + + /// + /// Search for a key in the subtree rooted at the given node. + /// + /// Root of the subtree to search. + /// Key to search for. + /// Node containing the key, or null if not found. + private BTreeNode? Search(BTreeNode? node, TKey key) + { + if (node is null) + { + return null; + } + + var i = 0; + while (i < node.KeyCount && comparer.Compare(key, node.Keys[i]) > 0) + { + i++; + } + + if (i < node.KeyCount && comparer.Compare(key, node.Keys[i]) == 0) + { + return node; + } + + if (node.IsLeaf) + { + return null; + } + + return Search(node.Children[i], key); + } + + /// + /// Insert a key into a non-full node. + /// + /// Node to insert the key into. + /// Key to insert. + /// + /// Thrown when the key already exists in the tree. + /// + private void InsertNonFull(BTreeNode node, TKey key) + { + var i = node.KeyCount - 1; + + if (node.IsLeaf) + { + while (i >= 0 && comparer.Compare(key, node.Keys[i]) < 0) + { + node.Keys[i + 1] = node.Keys[i]; + i--; + } + + if (i >= 0 && comparer.Compare(key, node.Keys[i]) == 0) + { + throw new ArgumentException($"""Key "{key}" already exists in B-Tree."""); + } + + node.Keys[i + 1] = key; + node.KeyCount++; + } + else + { + while (i >= 0 && comparer.Compare(key, node.Keys[i]) < 0) + { + i--; + } + + if (i >= 0 && comparer.Compare(key, node.Keys[i]) == 0) + { + throw new ArgumentException($"""Key "{key}" already exists in B-Tree."""); + } + + i++; + + if (node.Children[i]!.IsFull()) + { + SplitChild(node, i); + + if (comparer.Compare(key, node.Keys[i]) > 0) + { + i++; + } + } + + InsertNonFull(node.Children[i]!, key); + } + } + + /// + /// Split a full child of a node. + /// + /// Parent node. + /// Index of the child to split. + private void SplitChild(BTreeNode parent, int childIndex) + { + var fullChild = parent.Children[childIndex]!; + var newChild = new BTreeNode(MinimumDegree, fullChild.IsLeaf); + var midIndex = MinimumDegree - 1; + + for (var j = 0; j < MinimumDegree - 1; j++) + { + newChild.Keys[j] = fullChild.Keys[j + MinimumDegree]; + newChild.KeyCount++; + } + + if (!fullChild.IsLeaf) + { + for (var j = 0; j < MinimumDegree; j++) + { + newChild.Children[j] = fullChild.Children[j + MinimumDegree]; + } + } + + fullChild.KeyCount = MinimumDegree - 1; + + for (var j = parent.KeyCount; j > childIndex; j--) + { + parent.Children[j + 1] = parent.Children[j]; + } + + parent.Children[childIndex + 1] = newChild; + + for (var j = parent.KeyCount - 1; j >= childIndex; j--) + { + parent.Keys[j + 1] = parent.Keys[j]; + } + + parent.Keys[childIndex] = fullChild.Keys[midIndex]; + parent.KeyCount++; + } + + /// + /// Remove a key from the subtree rooted at the given node. + /// + /// Root of the subtree. + /// Key to remove. + /// + /// Thrown when the key is not found in the subtree. + /// + private void Remove(BTreeNode node, TKey key) + { + var idx = FindKey(node, key); + + if (idx < node.KeyCount && comparer.Compare(node.Keys[idx], key) == 0) + { + if (node.IsLeaf) + { + RemoveFromLeaf(node, idx); + } + else + { + RemoveFromNonLeaf(node, idx); + } + } + else + { + if (node.IsLeaf) + { + throw new KeyNotFoundException($"""Key "{key}" is not in the B-Tree."""); + } + + var isInSubtree = idx == node.KeyCount; + + if (node.Children[idx]!.KeyCount < MinimumDegree) + { + Fill(node, idx); + } + + if (isInSubtree && idx > node.KeyCount) + { + Remove(node.Children[idx - 1]!, key); + } + else + { + Remove(node.Children[idx]!, key); + } + } + } + + /// + /// Find the index of the first key greater than or equal to the given key. + /// + /// Node to search in. + /// Key to find. + /// Index of the first key >= key. + private int FindKey(BTreeNode node, TKey key) + { + var idx = 0; + while (idx < node.KeyCount && comparer.Compare(node.Keys[idx], key) < 0) + { + idx++; + } + + return idx; + } + + /// + /// Remove a key from a leaf node. + /// + /// Leaf node to remove from. + /// Index of the key to remove. + private void RemoveFromLeaf(BTreeNode node, int idx) + { + node.RemoveKey(idx); + } + + /// + /// Remove a key from a non-leaf node. + /// + /// Non-leaf node to remove from. + /// Index of the key to remove. + private void RemoveFromNonLeaf(BTreeNode node, int idx) + { + var key = node.Keys[idx]; + + if (node.Children[idx]!.KeyCount >= MinimumDegree) + { + var predecessor = GetPredecessor(node, idx); + node.Keys[idx] = predecessor; + Remove(node.Children[idx]!, predecessor); + } + else if (node.Children[idx + 1]!.KeyCount >= MinimumDegree) + { + var successor = GetSuccessor(node, idx); + node.Keys[idx] = successor; + Remove(node.Children[idx + 1]!, successor); + } + else + { + Merge(node, idx); + Remove(node.Children[idx]!, key); + } + } + + /// + /// Get the predecessor key of the key at the given index. + /// + /// Node containing the key. + /// Index of the key. + /// Predecessor key. + private TKey GetPredecessor(BTreeNode node, int idx) + { + var current = node.Children[idx]!; + while (!current.IsLeaf) + { + current = current.Children[current.KeyCount]!; + } + + return current.Keys[current.KeyCount - 1]; + } + + /// + /// Get the successor key of the key at the given index. + /// + /// Node containing the key. + /// Index of the key. + /// Successor key. + private TKey GetSuccessor(BTreeNode node, int idx) + { + var current = node.Children[idx + 1]!; + while (!current.IsLeaf) + { + current = current.Children[0]!; + } + + return current.Keys[0]; + } + + /// + /// Fill a child node that has fewer than minimum degree - 1 keys. + /// + /// Parent node. + /// Index of the child to fill. + private void Fill(BTreeNode node, int idx) + { + if (idx != 0 && node.Children[idx - 1]!.KeyCount >= MinimumDegree) + { + BorrowFromPrevious(node, idx); + } + else if (idx != node.KeyCount && node.Children[idx + 1]!.KeyCount >= MinimumDegree) + { + BorrowFromNext(node, idx); + } + else + { + if (idx != node.KeyCount) + { + Merge(node, idx); + } + else + { + Merge(node, idx - 1); + } + } + } + + /// + /// Borrow a key from the previous sibling. + /// + /// Parent node. + /// Index of the child that needs a key. + private void BorrowFromPrevious(BTreeNode node, int childIdx) + { + var child = node.Children[childIdx]!; + var sibling = node.Children[childIdx - 1]!; + + for (var i = child.KeyCount - 1; i >= 0; i--) + { + child.Keys[i + 1] = child.Keys[i]; + } + + if (!child.IsLeaf) + { + for (var i = child.KeyCount; i >= 0; i--) + { + child.Children[i + 1] = child.Children[i]; + } + } + + child.Keys[0] = node.Keys[childIdx - 1]; + + if (!child.IsLeaf) + { + child.Children[0] = sibling.Children[sibling.KeyCount]; + } + + node.Keys[childIdx - 1] = sibling.Keys[sibling.KeyCount - 1]; + + child.KeyCount++; + sibling.KeyCount--; + } + + /// + /// Borrow a key from the next sibling. + /// + /// Parent node. + /// Index of the child that needs a key. + private void BorrowFromNext(BTreeNode node, int childIdx) + { + var child = node.Children[childIdx]!; + var sibling = node.Children[childIdx + 1]!; + + child.Keys[child.KeyCount] = node.Keys[childIdx]; + + if (!child.IsLeaf) + { + child.Children[child.KeyCount + 1] = sibling.Children[0]; + } + + node.Keys[childIdx] = sibling.Keys[0]; + + for (var i = 1; i < sibling.KeyCount; i++) + { + sibling.Keys[i - 1] = sibling.Keys[i]; + } + + if (!sibling.IsLeaf) + { + for (var i = 1; i <= sibling.KeyCount; i++) + { + sibling.Children[i - 1] = sibling.Children[i]; + } + } + + child.KeyCount++; + sibling.KeyCount--; + } + + /// + /// Merge a child with its sibling. + /// + /// Parent node. + /// Index of the child to merge. + private void Merge(BTreeNode node, int idx) + { + var child = node.Children[idx]!; + var sibling = node.Children[idx + 1]!; + + child.Keys[MinimumDegree - 1] = node.Keys[idx]; + + for (var i = 0; i < sibling.KeyCount; i++) + { + child.Keys[i + MinimumDegree] = sibling.Keys[i]; + } + + if (!child.IsLeaf) + { + for (var i = 0; i <= sibling.KeyCount; i++) + { + child.Children[i + MinimumDegree] = sibling.Children[i]; + } + } + + for (var i = idx + 1; i < node.KeyCount; i++) + { + node.Keys[i - 1] = node.Keys[i]; + } + + for (var i = idx + 2; i <= node.KeyCount; i++) + { + node.Children[i - 1] = node.Children[i]; + } + + child.KeyCount += sibling.KeyCount + 1; + node.KeyCount--; + } + + /// + /// Get the minimum key in the subtree rooted at the given node. + /// + /// Root of the subtree. + /// Minimum key in the subtree. + private TKey GetMin(BTreeNode node) + { + while (!node.IsLeaf) + { + node = node.Children[0]!; + } + + return node.Keys[0]; + } + + /// + /// Get the maximum key in the subtree rooted at the given node. + /// + /// Root of the subtree. + /// Maximum key in the subtree. + private TKey GetMax(BTreeNode node) + { + while (!node.IsLeaf) + { + node = node.Children[node.KeyCount]!; + } + + return node.Keys[node.KeyCount - 1]; + } +} diff --git a/DataStructures/BTree/BTreeNode.cs b/DataStructures/BTree/BTreeNode.cs new file mode 100644 index 00000000..382a341c --- /dev/null +++ b/DataStructures/BTree/BTreeNode.cs @@ -0,0 +1,105 @@ +namespace DataStructures.BTree; + +/// +/// Generic class to represent nodes in a +/// instance. +/// +/// The type of key for the node. +/// +/// Initializes a new instance of the +/// class. +/// +/// Minimum degree of the B-Tree. +/// Whether this node is a leaf node. +internal class BTreeNode(int minDegree, bool isLeaf) +{ + /// + /// Gets the minimum degree (t) of the B-Tree. + /// A node can contain at most 2t-1 keys and at least t-1 keys (except root). + /// + public int MinDegree { get; } = minDegree; + + /// + /// Gets or sets a value indicating whether this node is a leaf node. + /// + public bool IsLeaf { get; set; } = isLeaf; + + /// + /// Gets or sets the current number of keys stored in this node. + /// + public int KeyCount { get; internal set; } + + /// + /// Gets the array of keys stored in this node. + /// Maximum size is 2*MinDegree - 1. + /// + public TKey[] Keys { get; } = new TKey[2 * minDegree - 1]; + + /// + /// Gets the array of child pointers. + /// Maximum size is 2*MinDegree. + /// + public BTreeNode?[] Children { get; } = new BTreeNode[2 * minDegree]; + + /// + /// Inserts a key at the specified position in the keys array. + /// + /// Position to insert the key. + /// Key to insert. + public void InsertKey(int index, TKey key) + { + Keys[index] = key; + KeyCount++; + } + + /// + /// Removes the key at the specified position in the keys array. + /// + /// Position of the key to remove. + public void RemoveKey(int index) + { + for (var i = index; i < KeyCount - 1; i++) + { + Keys[i] = Keys[i + 1]; + } + + Keys[KeyCount - 1] = default!; + KeyCount--; + } + + /// + /// Inserts a child pointer at the specified position. + /// + /// Position to insert the child. + /// Child node to insert. + public void InsertChild(int index, BTreeNode child) + { + Children[index] = child; + } + + /// + /// Removes the child pointer at the specified position. + /// + /// Position of the child to remove. + public void RemoveChild(int index) + { + for (var i = index; i < KeyCount; i++) + { + Children[i] = Children[i + 1]; + } + + Children[KeyCount] = null; + } + + /// + /// Checks if the node is full (contains maximum number of keys). + /// + /// True if the node is full, false otherwise. + public bool IsFull() => KeyCount == 2 * MinDegree - 1; + + /// + /// Checks if the node has minimum number of keys. + /// + /// True if the node has minimum keys, false otherwise. + public bool HasMinimumKeys() => KeyCount >= MinDegree - 1; +} diff --git a/README.md b/README.md index 1b5de66b..0773c264 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,8 @@ find more than one implementation for the same objective but using different alg * [Backtracking](./Algorithms/Problems/NQueens/BacktrackingNQueensSolver.cs) * [Knight Tour](./Algorithms/Problems/KnightTour/) * [Open Knight Tour](./Algorithms/Problems/KnightTour/OpenKnightTour.cs) + * [Graph Coloring](./Algorithms/Problems/GraphColoring) + * [Backtracking Graph Coloring Solver](./Algorithms/Problems/GraphColoring/GraphColoringSolver.cs) * [Dynamic Programming](./Algorithms/Problems/DynamicProgramming) * [Coin Change](./Algorithms/Problems/DynamicProgramming/CoinChange/DynamicCoinChangeSolver.cs) * [Levenshtein Distance](./Algorithms/Problems/DynamicProgramming/LevenshteinDistance/LevenshteinDistance.cs) @@ -268,6 +270,7 @@ find more than one implementation for the same objective but using different alg * [Fenwick tree (or Binary Indexed Tree)](./DataStructures/Fenwick/BinaryIndexedTree.cs) * [AA Tree](./DataStructures/AATree) * [AVL Tree](./DataStructures/AVLTree) + * [B-Tree](./DataStructures/BTree) * [Red-Black Tree](./DataStructures/RedBlackTree) * [Stack](./DataStructures/Stack) * [Array-based Stack](./DataStructures/Stack/ArrayBasedStack.cs) From cae9a4f49dcbc0f58a4c44c7173f013edf0b8ac8 Mon Sep 17 00:00:00 2001 From: ethanhaines <122860615+ethanhaines@users.noreply.github.com> Date: Mon, 3 Nov 2025 17:40:48 -0500 Subject: [PATCH 2/3] Added more tests for full coverage --- DataStructures.Tests/BTreeTests.cs | 239 +++++++++++++++++++++++++++++ DataStructures/BTree/BTreeNode.cs | 6 - 2 files changed, 239 insertions(+), 6 deletions(-) diff --git a/DataStructures.Tests/BTreeTests.cs b/DataStructures.Tests/BTreeTests.cs index ab201bce..12eebd07 100644 --- a/DataStructures.Tests/BTreeTests.cs +++ b/DataStructures.Tests/BTreeTests.cs @@ -564,4 +564,243 @@ public void BTree_InsertRemoveInsert_WorksCorrectly() new[] { 1, 2, 3, 4, 5 }, config => config.WithStrictOrdering()); } + + [Test] + public void Remove_BorrowFromPreviousSibling_TreeStillValid() + { + var tree = new BTree(3); + + tree.AddRange(new[] { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }); + + tree.Remove(100); + + tree.Count.Should().Be(9); + tree.Contains(100).Should().BeFalse(); + + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + new[] { 10, 20, 30, 40, 50, 60, 70, 80, 90 }, + config => config.WithStrictOrdering()); + } + + [Test] + public void Remove_BorrowFromNextSibling_TreeStillValid() + { + var tree = new BTree(3); + + tree.AddRange(new[] { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }); + + tree.Remove(10); + + tree.Count.Should().Be(9); + tree.Contains(10).Should().BeFalse(); + + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + new[] { 20, 30, 40, 50, 60, 70, 80, 90, 100 }, + config => config.WithStrictOrdering()); + } + + [Test] + public void Remove_UsesPredecessor_TreeStillValid() + { + var tree = new BTree(3); + + tree.AddRange(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); + + var rootKeys = tree.GetKeysPreOrder().Take(3).ToArray(); + var keyToRemove = rootKeys[0]; + + tree.Remove(keyToRemove); + + tree.Contains(keyToRemove).Should().BeFalse(); + tree.Count.Should().Be(14); + + var inOrder = tree.GetKeysInOrder().ToArray(); + inOrder.Should().HaveCount(14); + inOrder.Should().BeInAscendingOrder(); + } + + [Test] + public void Remove_UsesSuccessor_TreeStillValid() + { + var tree = new BTree(2); + + tree.AddRange(new[] { 10, 20, 30, 40, 50, 60, 70 }); + + tree.Remove(40); + + tree.Contains(40).Should().BeFalse(); + tree.Count.Should().Be(6); + + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + new[] { 10, 20, 30, 50, 60, 70 }, + config => config.WithStrictOrdering()); + } + + [Test] + public void Remove_MergeWithSibling_TreeStillValid() + { + var tree = new BTree(2); + + tree.AddRange(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }); + + tree.Remove(9); + tree.Remove(8); + tree.Remove(7); + + tree.Count.Should().Be(6); + + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + new[] { 1, 2, 3, 4, 5, 6 }, + config => config.WithStrictOrdering()); + } + + [Test] + public void Remove_ComplexRebalancing_TreeStillValid() + { + var tree = new BTree(3); + + tree.AddRange(Enumerable.Range(1, 30)); + + var keysToRemove = new[] { 5, 15, 25, 10, 20, 30, 1, 29 }; + + foreach (var key in keysToRemove) + { + tree.Remove(key); + tree.Contains(key).Should().BeFalse(); + } + + tree.Count.Should().Be(22); + + var expected = Enumerable.Range(1, 30).Except(keysToRemove).OrderBy(x => x); + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + expected, + config => config.WithStrictOrdering()); + } + + [Test] + public void Remove_FromMinimumDegree3_AllRebalancingPaths() + { + var tree = new BTree(3); + + tree.AddRange(Enumerable.Range(1, 50)); + var keysToRemove = new[] { 25, 12, 37, 6, 44, 18, 31, 3, 47, 15 }; + + foreach (var key in keysToRemove) + { + var countBefore = tree.Count; + tree.Remove(key); + + tree.Count.Should().Be(countBefore - 1); + tree.Contains(key).Should().BeFalse(); + + var inOrder = tree.GetKeysInOrder().ToArray(); + inOrder.Should().BeInAscendingOrder(); + } + + tree.Count.Should().Be(40); + } + + [Test] + public void Remove_SequentialFromLargeTree_MaintainsStructure() + { + var tree = new BTree(4); + + tree.AddRange(Enumerable.Range(1, 100)); + for (var i = 3; i <= 99; i += 3) + { + tree.Remove(i); + } + + tree.Count.Should().Be(67); + + var inOrder = tree.GetKeysInOrder().ToArray(); + inOrder.Should().HaveCount(67); + inOrder.Should().BeInAscendingOrder(); + + tree.Contains(3).Should().BeFalse(); + tree.Contains(6).Should().BeFalse(); + tree.Contains(99).Should().BeFalse(); + tree.Contains(1).Should().BeTrue(); + tree.Contains(2).Should().BeTrue(); + tree.Contains(100).Should().BeTrue(); + } + + [Test] + public void BTree_DegreeThree_CompleteInsertDeleteCycle() + { + var tree = new BTree(3); + + var keys = Enumerable.Range(1, 40).ToArray(); + tree.AddRange(keys); + tree.Count.Should().Be(40); + + for (var i = 2; i <= 40; i += 2) + { + tree.Remove(i); + } + + tree.Count.Should().Be(20); + + var remaining = tree.GetKeysInOrder().ToArray(); + remaining.Should().BeEquivalentTo( + Enumerable.Range(1, 40).Where(x => x % 2 == 1), + config => config.WithStrictOrdering()); + + tree.Add(2); + tree.Add(4); + tree.Count.Should().Be(22); + + tree.Contains(2).Should().BeTrue(); + tree.Contains(4).Should().BeTrue(); + } + + [Test] + public void Remove_RootWithMultipleChildren_HandledCorrectly() + { + var tree = new BTree(2); + + tree.AddRange(new[] { 1, 2, 3, 4, 5 }); + tree.Remove(3); + tree.Count.Should().Be(4); + + tree.GetKeysInOrder() + .Should() + .BeEquivalentTo( + new[] { 1, 2, 4, 5 }, + config => config.WithStrictOrdering()); + } + + [Test] + public void BTree_HighDegree_StressTest() + { + var tree = new BTree(10); + + tree.AddRange(Enumerable.Range(1, 200)); + tree.Count.Should().Be(200); + + for (var i = 10; i <= 200; i += 10) + { + tree.Remove(i); + } + + tree.Count.Should().Be(180); + + var inOrder = tree.GetKeysInOrder().ToArray(); + inOrder.Should().HaveCount(180); + inOrder.Should().BeInAscendingOrder(); + + tree.Contains(10).Should().BeFalse(); + tree.Contains(100).Should().BeFalse(); + tree.Contains(200).Should().BeFalse(); + } } diff --git a/DataStructures/BTree/BTreeNode.cs b/DataStructures/BTree/BTreeNode.cs index 382a341c..11c7d86f 100644 --- a/DataStructures/BTree/BTreeNode.cs +++ b/DataStructures/BTree/BTreeNode.cs @@ -96,10 +96,4 @@ public void RemoveChild(int index) /// /// True if the node is full, false otherwise. public bool IsFull() => KeyCount == 2 * MinDegree - 1; - - /// - /// Checks if the node has minimum number of keys. - /// - /// True if the node has minimum keys, false otherwise. - public bool HasMinimumKeys() => KeyCount >= MinDegree - 1; } From 7a51c5310927f927db84017b3d938462064325e8 Mon Sep 17 00:00:00 2001 From: ethanhaines <122860615+ethanhaines@users.noreply.github.com> Date: Mon, 3 Nov 2025 17:53:58 -0500 Subject: [PATCH 3/3] Fixed static code issues and increased test coverage --- DataStructures.Tests/BTreeTests.cs | 25 ++++++- DataStructures/BTree/BTree.cs | 102 ++++++++++++++++++----------- DataStructures/BTree/BTreeNode.cs | 14 ---- 3 files changed, 88 insertions(+), 53 deletions(-) diff --git a/DataStructures.Tests/BTreeTests.cs b/DataStructures.Tests/BTreeTests.cs index 12eebd07..5517ce32 100644 --- a/DataStructures.Tests/BTreeTests.cs +++ b/DataStructures.Tests/BTreeTests.cs @@ -34,6 +34,17 @@ public void Constructor_MinimumDegreeLessThan2_ThrowsException() .WithMessage("Minimum degree must be at least 2.*"); } + [Test] + public void Constructor_CustomComparerMinimumDegreeLessThan2_ThrowsException() + { + var comparer = Comparer.Create((x, y) => y.CompareTo(x)); + + Invoking(() => new BTree(1, comparer)) + .Should() + .ThrowExactly() + .WithMessage("Minimum degree must be at least 2.*"); + } + [Test] public void Constructor_UseCustomComparer_FormsCorrectTree() { @@ -92,6 +103,18 @@ public void Add_DuplicateKey_ThrowsException() .WithMessage("""Key "3" already exists in B-Tree."""); } + [Test] + public void Add_DuplicateKeyInLeaf_ThrowsException() + { + var tree = new BTree(3); + tree.AddRange(new[] { 10, 20 }); + + Invoking(() => tree.Add(10)) + .Should() + .ThrowExactly() + .WithMessage("""Key "10" already exists in B-Tree."""); + } + [Test] public void Add_CausesNodeSplit_TreeStillValid() { @@ -753,7 +776,7 @@ public void BTree_DegreeThree_CompleteInsertDeleteCycle() var remaining = tree.GetKeysInOrder().ToArray(); remaining.Should().BeEquivalentTo( - Enumerable.Range(1, 40).Where(x => x % 2 == 1), + Enumerable.Range(1, 40).Where(x => x % 2 != 0), config => config.WithStrictOrdering()); tree.Add(2); diff --git a/DataStructures/BTree/BTree.cs b/DataStructures/BTree/BTree.cs index 5efdf227..7ff58e50 100644 --- a/DataStructures/BTree/BTree.cs +++ b/DataStructures/BTree/BTree.cs @@ -345,50 +345,83 @@ void PostOrderTraversal(BTreeNode? node) /// private void InsertNonFull(BTreeNode node, TKey key) { - var i = node.KeyCount - 1; - if (node.IsLeaf) { - while (i >= 0 && comparer.Compare(key, node.Keys[i]) < 0) - { - node.Keys[i + 1] = node.Keys[i]; - i--; - } + InsertIntoLeaf(node, key); + } + else + { + InsertIntoNonLeaf(node, key); + } + } - if (i >= 0 && comparer.Compare(key, node.Keys[i]) == 0) - { - throw new ArgumentException($"""Key "{key}" already exists in B-Tree."""); - } + /// + /// Insert a key into a leaf node. + /// + /// Leaf node to insert into. + /// Key to insert. + private void InsertIntoLeaf(BTreeNode node, TKey key) + { + var i = node.KeyCount - 1; - node.Keys[i + 1] = key; - node.KeyCount++; + while (i >= 0 && comparer.Compare(key, node.Keys[i]) < 0) + { + node.Keys[i + 1] = node.Keys[i]; + i--; } - else + + if (i >= 0 && comparer.Compare(key, node.Keys[i]) == 0) { - while (i >= 0 && comparer.Compare(key, node.Keys[i]) < 0) - { - i--; - } + throw new ArgumentException($"""Key "{key}" already exists in B-Tree."""); + } + + node.Keys[i + 1] = key; + node.KeyCount++; + } - if (i >= 0 && comparer.Compare(key, node.Keys[i]) == 0) + /// + /// Insert a key into a non-leaf node. + /// + /// Non-leaf node to insert into. + /// Key to insert. + private void InsertIntoNonLeaf(BTreeNode node, TKey key) + { + var i = FindInsertionIndex(node, key); + + if (node.Children[i]!.IsFull()) + { + SplitChild(node, i); + + if (comparer.Compare(key, node.Keys[i]) > 0) { - throw new ArgumentException($"""Key "{key}" already exists in B-Tree."""); + i++; } + } - i++; + InsertNonFull(node.Children[i]!, key); + } - if (node.Children[i]!.IsFull()) - { - SplitChild(node, i); + /// + /// Find the index where a key should be inserted in a non-leaf node. + /// + /// Node to search in. + /// Key to insert. + /// Index where the key should be inserted. + private int FindInsertionIndex(BTreeNode node, TKey key) + { + var i = node.KeyCount - 1; - if (comparer.Compare(key, node.Keys[i]) > 0) - { - i++; - } - } + while (i >= 0 && comparer.Compare(key, node.Keys[i]) < 0) + { + i--; + } - InsertNonFull(node.Children[i]!, key); + if (i >= 0 && comparer.Compare(key, node.Keys[i]) == 0) + { + throw new ArgumentException($"""Key "{key}" already exists in B-Tree."""); } + + return i + 1; } /// @@ -588,14 +621,7 @@ private void Fill(BTreeNode node, int idx) } else { - if (idx != node.KeyCount) - { - Merge(node, idx); - } - else - { - Merge(node, idx - 1); - } + Merge(node, idx != node.KeyCount ? idx : idx - 1); } } diff --git a/DataStructures/BTree/BTreeNode.cs b/DataStructures/BTree/BTreeNode.cs index 11c7d86f..e104d64b 100644 --- a/DataStructures/BTree/BTreeNode.cs +++ b/DataStructures/BTree/BTreeNode.cs @@ -77,20 +77,6 @@ public void InsertChild(int index, BTreeNode child) Children[index] = child; } - /// - /// Removes the child pointer at the specified position. - /// - /// Position of the child to remove. - public void RemoveChild(int index) - { - for (var i = index; i < KeyCount; i++) - { - Children[i] = Children[i + 1]; - } - - Children[KeyCount] = null; - } - /// /// Checks if the node is full (contains maximum number of keys). ///