diff --git a/src/main/java/com/thealgorithms/datastructures/trees/BTree.java b/src/main/java/com/thealgorithms/datastructures/trees/BTree.java new file mode 100644 index 000000000000..2c19253b45e7 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/trees/BTree.java @@ -0,0 +1,323 @@ +package com.thealgorithms.datastructures.trees; + +import java.util.ArrayList; + +/** + * Implementation of a B-Tree, a self-balancing tree data structure that maintains sorted data + * and allows searches, sequential access, insertions, and deletions in logarithmic time. + * + * B-Trees are generalizations of binary search trees in that a node can have more than two children. + * They're widely used in databases and file systems. + * + * For more information: https://en.wikipedia.org/wiki/B-tree + */ + +public class BTree { + static class BTreeNode { + int[] keys; + int t; // Minimum degree (defines range for number of keys) + BTreeNode[] children; + int n; // Current number of keys + boolean leaf; + + BTreeNode(int t, boolean leaf) { + this.t = t; + this.leaf = leaf; + this.keys = new int[2 * t - 1]; + this.children = new BTreeNode[2 * t]; + this.n = 0; + } + + void traverse(ArrayList result) { + for (int i = 0; i < n; i++) { + if (!leaf) { + children[i].traverse(result); + } + result.add(keys[i]); + } + if (!leaf) { + children[n].traverse(result); + } + } + + BTreeNode search(int key) { + int i = 0; + while (i < n && key > keys[i]) { + i++; + } + if (i < n && keys[i] == key) { + return this; + } + if (leaf) { + return null; + } + return children[i].search(key); + } + + void insertNonFull(int key) { + int i = n - 1; + if (leaf) { + while (i >= 0 && keys[i] > key) { + keys[i + 1] = keys[i]; + i--; + } + keys[i + 1] = key; + n++; + } else { + while (i >= 0 && keys[i] > key) { + i--; + } + if (children[i + 1].n == 2 * t - 1) { + splitChild(i + 1, children[i + 1]); + if (keys[i + 1] < key) { + i++; + } + } + children[i + 1].insertNonFull(key); + } + } + + void splitChild(int i, BTreeNode y) { + BTreeNode z = new BTreeNode(y.t, y.leaf); + z.n = t - 1; + + System.arraycopy(y.keys, t, z.keys, 0, t - 1); + if (!y.leaf) { + System.arraycopy(y.children, t, z.children, 0, t); + } + y.n = t - 1; + + for (int j = n; j >= i + 1; j--) { + children[j + 1] = children[j]; + } + children[i + 1] = z; + + for (int j = n - 1; j >= i; j--) { + keys[j + 1] = keys[j]; + } + keys[i] = y.keys[t - 1]; + n++; + } + + void remove(int key) { + int idx = findKey(key); + + if (idx < n && keys[idx] == key) { + if (leaf) { + removeFromLeaf(idx); + } else { + removeFromNonLeaf(idx); + } + } else { + if (leaf) { + return; // Key not found + } + + boolean flag = idx == n; + if (children[idx].n < t) { + fill(idx); + } + + if (flag && idx > n) { + children[idx - 1].remove(key); + } else { + children[idx].remove(key); + } + } + } + + private int findKey(int key) { + int idx = 0; + while (idx < n && keys[idx] < key) { + ++idx; + } + return idx; + } + + private void removeFromLeaf(int idx) { + for (int i = idx + 1; i < n; ++i) { + keys[i - 1] = keys[i]; + } + n--; + } + + private void removeFromNonLeaf(int idx) { + int key = keys[idx]; + if (children[idx].n >= t) { + int pred = getPredecessor(idx); + keys[idx] = pred; + children[idx].remove(pred); + } else if (children[idx + 1].n >= t) { + int succ = getSuccessor(idx); + keys[idx] = succ; + children[idx + 1].remove(succ); + } else { + merge(idx); + children[idx].remove(key); + } + } + + private int getPredecessor(int idx) { + BTreeNode cur = children[idx]; + while (!cur.leaf) { + cur = cur.children[cur.n]; + } + return cur.keys[cur.n - 1]; + } + + private int getSuccessor(int idx) { + BTreeNode cur = children[idx + 1]; + while (!cur.leaf) { + cur = cur.children[0]; + } + return cur.keys[0]; + } + + private void fill(int idx) { + if (idx != 0 && children[idx - 1].n >= t) { + borrowFromPrev(idx); + } else if (idx != n && children[idx + 1].n >= t) { + borrowFromNext(idx); + } else { + if (idx != n) { + merge(idx); + } else { + merge(idx - 1); + } + } + } + + private void borrowFromPrev(int idx) { + BTreeNode child = children[idx]; + BTreeNode sibling = children[idx - 1]; + + for (int i = child.n - 1; i >= 0; --i) { + child.keys[i + 1] = child.keys[i]; + } + + if (!child.leaf) { + for (int i = child.n; i >= 0; --i) { + child.children[i + 1] = child.children[i]; + } + } + + child.keys[0] = keys[idx - 1]; + + if (!child.leaf) { + child.children[0] = sibling.children[sibling.n]; + } + + keys[idx - 1] = sibling.keys[sibling.n - 1]; + + child.n += 1; + sibling.n -= 1; + } + + private void borrowFromNext(int idx) { + BTreeNode child = children[idx]; + BTreeNode sibling = children[idx + 1]; + + child.keys[child.n] = keys[idx]; + + if (!child.leaf) { + child.children[child.n + 1] = sibling.children[0]; + } + + keys[idx] = sibling.keys[0]; + + for (int i = 1; i < sibling.n; ++i) { + sibling.keys[i - 1] = sibling.keys[i]; + } + + if (!sibling.leaf) { + for (int i = 1; i <= sibling.n; ++i) { + sibling.children[i - 1] = sibling.children[i]; + } + } + + child.n += 1; + sibling.n -= 1; + } + + private void merge(int idx) { + BTreeNode child = children[idx]; + BTreeNode sibling = children[idx + 1]; + + child.keys[t - 1] = keys[idx]; + + for (int i = 0; i < sibling.n; ++i) { + child.keys[i + t] = sibling.keys[i]; + } + + if (!child.leaf) { + for (int i = 0; i <= sibling.n; ++i) { + child.children[i + t] = sibling.children[i]; + } + } + + for (int i = idx + 1; i < n; ++i) { + keys[i - 1] = keys[i]; + } + + for (int i = idx + 2; i <= n; ++i) { + children[i - 1] = children[i]; + } + + child.n += sibling.n + 1; + n--; + } + } + + private BTreeNode root; + private final int t; + + public BTree(int t) { + this.root = null; + this.t = t; + } + + public void traverse(ArrayList result) { + if (root != null) { + root.traverse(result); + } + } + + public boolean search(int key) { + return root != null && root.search(key) != null; + } + + public void insert(int key) { + if (search(key)) { + return; + } + if (root == null) { + root = new BTreeNode(t, true); + root.keys[0] = key; + root.n = 1; + } else { + if (root.n == 2 * t - 1) { + BTreeNode s = new BTreeNode(t, false); + s.children[0] = root; + s.splitChild(0, root); + int i = 0; + if (s.keys[0] < key) { + i++; + } + s.children[i].insertNonFull(key); + root = s; + } else { + root.insertNonFull(key); + } + } + } + + public void delete(int key) { + if (root == null) { + return; + } + root.remove(key); + if (root.n == 0) { + root = root.leaf ? null : root.children[0]; + } + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/trees/BTreeTest.java b/src/test/java/com/thealgorithms/datastructures/trees/BTreeTest.java new file mode 100644 index 000000000000..da29e117f25d --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/trees/BTreeTest.java @@ -0,0 +1,94 @@ +package com.thealgorithms.datastructures.trees; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +public class BTreeTest { + + @Test + public void testInsertSearchDelete() { + BTree bTree = new BTree(3); // Minimum degree t = 3 + + int[] values = {10, 20, 5, 6, 12, 30, 7, 17}; + for (int val : values) { + bTree.insert(val); + } + + for (int val : values) { + assertTrue(bTree.search(val), "Should find inserted value: " + val); + } + + ArrayList traversal = new ArrayList<>(); + bTree.traverse(traversal); + assertEquals(Arrays.asList(5, 6, 7, 10, 12, 17, 20, 30), traversal); + + bTree.delete(6); + assertFalse(bTree.search(6)); + traversal.clear(); + bTree.traverse(traversal); + assertEquals(Arrays.asList(5, 7, 10, 12, 17, 20, 30), traversal); + } + + @Test + public void testEmptyTreeSearch() { + BTree bTree = new BTree(3); + assertFalse(bTree.search(42), "Search in empty tree should return false."); + } + + @Test + public void testDuplicateInsertions() { + BTree bTree = new BTree(3); + bTree.insert(15); + bTree.insert(15); // Attempt duplicate + bTree.insert(15); // Another duplicate + + ArrayList traversal = new ArrayList<>(); + bTree.traverse(traversal); + + // Should contain only one 15 + long count = traversal.stream().filter(x -> x == 15).count(); + assertEquals(1, count, "Duplicate keys should not be inserted."); + } + + @Test + public void testDeleteNonExistentKey() { + BTree bTree = new BTree(3); + bTree.insert(10); + bTree.insert(20); + bTree.delete(99); // Doesn't exist + assertTrue(bTree.search(10)); + assertTrue(bTree.search(20)); + } + + @Test + public void testComplexInsertDelete() { + BTree bTree = new BTree(2); // Smaller degree to trigger splits more easily + int[] values = {1, 3, 7, 10, 11, 13, 14, 15, 18, 16, 19, 24, 25, 26, 21, 4, 5, 20, 22, 2, 17, 12, 6}; + + for (int val : values) { + bTree.insert(val); + } + + for (int val : values) { + assertTrue(bTree.search(val)); + } + + int[] toDelete = {6, 13, 7, 4, 2, 16}; + for (int val : toDelete) { + bTree.delete(val); + assertFalse(bTree.search(val)); + } + + ArrayList remaining = new ArrayList<>(); + bTree.traverse(remaining); + + for (int val : toDelete) { + assertFalse(remaining.contains(val)); + } + } +}