Fading Coder

An Old Coder’s Final Dance

Home > Tech > Content

Implementing a B-Tree in Java with Top-Down Insertion and Deletion

Tech 2

B-trees are height-balanced search trees dseigned to minimize I/O by packing many ordered keys into a single node sized to a disk block or cache line. Instead of assuming all data lives in main memory (as typical for AVL or Red-Black trees), B-trees optimize for large datasets by reducing the number of node visits (hence disk reads). If the height of the tree is h, search, insert, delete, min/max, and predecessor/successor operations require O(h) node accesses, typically O(log n).

Key properties (minimum degree t):

  • Every node except the root stores between t−1 and 2t−1 keys; the root stores between 1 and 2t−1 keys (unless the tree is empty).
  • Internal nodes with k keys have k+1 children.
  • All leaves reside at the same depth.
  • Keys within a node are strictly increasing; subtrees partition the key ranges accordingly.
  • The tree grows and shrinks at the root. Node splits and merges maintain the invariants.

This implementation uses the standard top-down (proactive) insertion: on the way down, we split full children before descending, guaranteeing insertion always lands in a non-full leaf. Deletion follows the classic cases (borrow from siblings or merge) while ensuring we never descend into a child with fewer than t keys.

Java implementation (integer keys)

// B-Tree of integers with top-down insertion and full delete support
// Minimum degree t >= 2

class BTNode {
    final int t;                  // minimum degree
    int[] keys;                   // sorted keys
    BTNode[] kids;                // child pointers
    int keyCount;                 // number of valid keys
    boolean leaf;                 // is this a leaf

    BTNode(int t, boolean leaf) {
        this.t = t;
        this.leaf = leaf;
        this.keys = new int[2 * t - 1];
        this.kids = new BTNode[2 * t];
        this.keyCount = 0;
    }

    // Return the first index i such that keys[i] >= k (or keyCount if all < k)
    int findIndex(int k) {
        int i = 0;
        while (i < keyCount && keys[i] < k) i++;
        return i;
    }

    BTNode find(int k) {
        int i = 0;
        while (i < keyCount && k > keys[i]) i++;
        if (i < keyCount && keys[i] == k) return this;
        if (leaf) return null;
        return kids[i].find(k);
    }

    void traverse() {
        int i;
        for (i = 0; i < keyCount; i++) {
            if (!leaf) kids[i].traverse();
            System.out.print(" " + keys[i]);
        }
        if (!leaf) kids[i].traverse();
    }

    // Insert assuming this node is non-full
    void insertIntoNonFull(int k) {
        int i = keyCount - 1;
        if (leaf) {
            while (i >= 0 && keys[i] > k) {
                keys[i + 1] = keys[i];
                i--;
            }
            keys[i + 1] = k;
            keyCount++;
        } else {
            while (i >= 0 && keys[i] > k) i--;
            int childIdx = i + 1;
            if (kids[childIdx].keyCount == 2 * t - 1) {
                splitAt(childIdx, kids[childIdx]);
                if (keys[childIdx] < k) childIdx++;
            }
            kids[childIdx].insertIntoNonFull(k);
        }
    }

    // Split full child y at index i into y and z; promote median into this node
    void splitAt(int i, BTNode y) {
        BTNode z = new BTNode(y.t, y.leaf);
        z.keyCount = t - 1;

        // move top (t-1) keys from y to z
        for (int j = 0; j < t - 1; j++) z.keys[j] = y.keys[j + t];
        if (!y.leaf) {
            for (int j = 0; j < t; j++) z.kids[j] = y.kids[j + t];
        }
        y.keyCount = t - 1;

        // make room for new child
        for (int j = keyCount; j >= i + 1; j--) kids[j + 1] = kids[j];
        kids[i + 1] = z;

        // move median up
        for (int j = keyCount - 1; j >= i; j--) keys[j + 1] = keys[j];
        keys[i] = y.keys[t - 1];
        keyCount++;
    }

    void delete(int k) {
        int idx = findIndex(k);
        if (idx < keyCount && keys[idx] == k) {
            if (leaf) deleteFromLeaf(idx);
            else deleteFromInternal(idx);
        } else {
            if (leaf) {
                // key not present
                return;
            }
            boolean wasLast = (idx == keyCount);
            if (kids[idx].keyCount < t) fixChildSize(idx);
            // After fix, if we originally aimed past the last child and the node shrank,
            // move to the left sibling; otherwise continue with the same index.
            if (wasLast && idx > keyCount) {
                kids[idx - 1].delete(k);
            } else {
                kids[idx].delete(k);
            }
        }
    }

    private void deleteFromLeaf(int idx) {
        for (int i = idx + 1; i < keyCount; i++) keys[i - 1] = keys[i];
        keyCount--;
    }

    private void deleteFromInternal(int idx) {
        int k = keys[idx];
        if (kids[idx].keyCount >= t) {
            int pred = predecessor(idx);
            keys[idx] = pred;
            kids[idx].delete(pred);
        } else if (kids[idx + 1].keyCount >= t) {
            int succ = successor(idx);
            keys[idx] = succ;
            kids[idx + 1].delete(succ);
        } else {
            fuseChildren(idx);
            kids[idx].delete(k);
        }
    }

    private int predecessor(int idx) {
        BTNode cur = kids[idx];
        while (!cur.leaf) cur = cur.kids[cur.keyCount];
        return cur.keys[cur.keyCount - 1];
    }

    private int successor(int idx) {
        BTNode cur = kids[idx + 1];
        while (!cur.leaf) cur = cur.kids[0];
        return cur.keys[0];
    }

    private void fixChildSize(int idx) {
        if (idx > 0 && kids[idx - 1].keyCount >= t) {
            rotateFromLeft(idx);
        } else if (idx < keyCount && kids[idx + 1].keyCount >= t) {
            rotateFromRight(idx);
        } else {
            if (idx < keyCount) {
                fuseChildren(idx);
            } else {
                fuseChildren(idx - 1);
            }
        }
    }

    // Borrow one key from the left sibling
    private void rotateFromLeft(int idx) {
        BTNode child = kids[idx];
        BTNode left = kids[idx - 1];

        // shift child keys/children right by 1
        for (int i = child.keyCount - 1; i >= 0; i--) child.keys[i + 1] = child.keys[i];
        if (!child.leaf) {
            for (int i = child.keyCount; i >= 0; i--) child.kids[i + 1] = child.kids[i];
        }

        // move separator down to child
        child.keys[0] = keys[idx - 1];
        if (!child.leaf) child.kids[0] = left.kids[left.keyCount];

        // move left's last key up as new separator
        keys[idx - 1] = left.keys[left.keyCount - 1];

        child.keyCount++;
        left.keyCount--;
    }

    // Borrow one key from the right sibling
    private void rotateFromRight(int idx) {
        BTNode child = kids[idx];
        BTNode right = kids[idx + 1];

        // bring separator down
        child.keys[child.keyCount] = keys[idx];
        if (!child.leaf) child.kids[child.keyCount + 1] = right.kids[0];

        // new separator is right's first key
        keys[idx] = right.keys[0];

        // shift right sibling left by 1
        for (int i = 1; i < right.keyCount; i++) right.keys[i - 1] = right.keys[i];
        if (!right.leaf) {
            for (int i = 1; i <= right.keyCount; i++) right.kids[i - 1] = right.kids[i];
        }

        child.keyCount++;
        right.keyCount--;
    }

    // Merge kids[idx] and kids[idx+1] into a single node
    private void fuseChildren(int idx) {
        BTNode left = kids[idx];
        BTNode right = kids[idx + 1];

        // pull separator key into left
        left.keys[t - 1] = keys[idx];

        // copy right's keys and children into left
        for (int i = 0; i < right.keyCount; i++) left.keys[i + t] = right.keys[i];
        if (!left.leaf) {
            for (int i = 0; i <= right.keyCount; i++) left.kids[i + t] = right.kids[i];
        }

        // shift keys and children of current node left
        for (int i = idx + 1; i < keyCount; i++) keys[i - 1] = keys[i];
        for (int i = idx + 2; i <= keyCount; i++) kids[i - 1] = kids[i];

        left.keyCount += right.keyCount + 1;
        keyCount--;
    }
}

class BTreeInt {
    private BTNode root;
    private final int t;

    BTreeInt(int t) {
        if (t < 2) throw new IllegalArgumentException("Minimum degree must be >= 2");
        this.t = t;
        this.root = null;
    }

    void insert(int k) {
        if (root == null) {
            root = new BTNode(t, true);
            root.keys[0] = k;
            root.keyCount = 1;
            return;
        }
        if (root.keyCount == 2 * t - 1) {
            BTNode s = new BTNode(t, false);
            s.kids[0] = root;
            s.splitAt(0, root);
            int i = (s.keys[0] < k) ? 1 : 0;
            s.kids[i].insertIntoNonFull(k);
            root = s;
        } else {
            root.insertIntoNonFull(k);
        }
    }

    void delete(int k) {
        if (root == null) return;
        root.delete(k);
        if (root.keyCount == 0) {
            root = root.leaf ? null : root.kids[0];
        }
    }

    boolean contains(int k) {
        return root != null && root.find(k) != null;
    }

    void traverse() {
        if (root != null) root.traverse();
    }
}

public class Demo {
    public static void main(String[] args) {
        BTreeInt tree = new BTreeInt(2); // t=2 (2-3-4 tree)
        int[] inserts = {
            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 v : inserts) tree.insert(v);

        System.out.println("Traversal of constructed tree:");
        tree.traverse();
        System.out.println();

        int[] deletes = {6,13,7,4,2,16};
        for (int d : deletes) {
            tree.delete(d);
            System.out.println("Traversal after removing " + d + ":");
            tree.traverse();
            System.out.println();
        }
    }
}

Notes on operations

  • Search: at most O(h) node visits by binary-like scan within a node followed by one child descent.
  • Insertion: split full children on the descent (top-down) so the final inesrtion goes into a node with fewer than 2t−1 keys.
  • Deletion:
    • If the key is in a leaf, remove it directly.
    • If the key is in an internal node, replace it with its predecessor or successor if the corresponding child has ≥ t keys; otherwise merge the two children and continue.
    • Before descending, ensure the child has at least t keys by rotating from siblings or merging.

Height is O(log_t n), and each operation touches O(h) nodes, which is efficient when nodes are sized to fit a block or cache line.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.