Skip to lab content

WEEK 7 · ADVANCED DATA STRUCTURES LAB

2–3 tree — insertion and node splitting

B.Tech CSE & Allied Branches · II Year, I Semester · AR25C173 tested sample runs

THE SYLLABUS TASK

What you need to solve

Implement B Trees (2,3).
Input format & conventions

Commands: 1 key = insert; 2 key = search; 3 = inorder; 4 = show nodes with depth; 0 = exit. Up to 100 distinct signed keys. This follows the manual’s 2–3 interpretation and implements insertion, search and display; deletion is outside this experiment.

UNDERSTAND THE IDEA

Explanation

A stable 2-node holds one key and, if internal, two children. A stable 3-node holds two ordered keys and, if internal, three children. All leaves have the same depth.

Insertion descends to a leaf. A temporary third key overflows that node, so its middle key is promoted and its two outer keys remain in separate nodes.

Promotion can overflow a parent and propagate to the root. A root split creates a new root containing the promoted key and links both surviving children. Temporary arrays allow three keys and four children safely.

TURN THE IDEA INTO STEPS

Step-by-step algorithm

  1. Ignore duplicates before insertion; enforce the 100-key teaching limit.
  2. Descend through the child interval containing the new key.
  3. Insert a leaf key or a promoted child key into ordered node slots.
  4. If three keys result, retain the smallest on the left, move the largest to a new right node and return the middle key upward.
  5. When the root splits, create a new root linking the old root and the new right node.

SEE THE CONTROL FLOW

Flowcharts

Diamonds show decisions; labeled arrows select a branch. Returning arrows repeat work. Helper-function steps group the operations explained above and in the code. These charts focus on the named core operation. On a phone, scroll within the diagram or open the full-size version.

2–3 insertion and split propagation

2–3 insertion and split propagation

Open full-size flowchart ↗

C17

Complete C program

Download .c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>

/* Read one entire token; reject malformed, oversized and out-of-range input. */
int readToken(char text[], size_t capacity) {
    int ch; size_t used = 0; int fits = 1;
    while ((ch = getchar()) != EOF && isspace((unsigned char)ch)) { }
    if (ch == EOF) return 0;
    do {
        if (ch == 0) fits = 0;
        if (used + 1 < capacity) text[used++] = (char)ch;
        else fits = 0;
        ch = getchar();
    } while (ch != EOF && !isspace((unsigned char)ch));
    text[used] = '\0';
    return fits;
}
int readInt(int *value) {
    char token[64], *end;
    if (!readToken(token, sizeof token)) return 0;
    errno = 0; long result = strtol(token, &end, 10);
    if (errno || end == token || *end || result < -1000000 || result > 1000000) return 0;
    *value = (int)result; return 1;
}
int invalid(void) { puts("Invalid or incomplete input."); return 1; }

#define MAX_KEYS 100
/* Stable nodes have 1 or 2 keys. A third key/fourth child is temporary overflow. */
typedef struct Node { int count, key[3]; struct Node *child[4]; } Node;
Node pool[2 * MAX_KEYS + 1]; int used = 0;
Node *newNode(void) {
    Node *p = &pool[used++]; p->count = 0;
    for (int i = 0; i < 4; ++i) p->child[i] = NULL;
    return p;
}
int search(const Node *p, int key) {
    while (p) {
        int i = 0; while (i < p->count && key > p->key[i]) ++i;
        if (i < p->count && key == p->key[i]) return 1;
        p = p->child[i];
    }
    return 0;
}
/* Return 1 when a split promotes a key plus a new right child to the caller. */
int insertRecursive(Node *p, int key, int *promoted, Node **right) {
    int pos = 0; while (pos < p->count && key > p->key[pos]) ++pos;
    int value = key; Node *newRight = NULL;
    if (p->child[0]) {
        if (!insertRecursive(p->child[pos],key,&value,&newRight)) return 0;
    }
    for (int i = p->count; i > pos; --i) {
        p->key[i] = p->key[i - 1]; p->child[i + 1] = p->child[i];
    }
    p->key[pos] = value; p->child[pos + 1] = newRight; ++p->count;
    if (p->count <= 2) return 0;
    Node *sibling = newNode(); sibling->count = 1; sibling->key[0] = p->key[2];
    sibling->child[0] = p->child[2]; sibling->child[1] = p->child[3];
    *promoted = p->key[1]; *right = sibling;
    p->count = 1; p->child[2] = p->child[3] = NULL;
    return 1;
}
Node *insert(Node *root, int key) {
    if (!root) { root = newNode(); root->count = 1; root->key[0] = key; return root; }
    int promoted; Node *right;
    if (insertRecursive(root,key,&promoted,&right)) {
        Node *parent = newNode(); parent->count = 1; parent->key[0] = promoted;
        parent->child[0] = root; parent->child[1] = right; root = parent;
    }
    return root;
}
void inorder(const Node *p) {
    if (!p) return;
    for (int i = 0; i < p->count; ++i) { inorder(p->child[i]); printf(" %d",p->key[i]); }
    inorder(p->child[p->count]);
}
void structure(const Node *p, int depth) {
    if (!p) return;
    printf("Depth %d:",depth); for (int i = 0; i < p->count; ++i) printf(" %d",p->key[i]); putchar('\n');
    for (int i = 0; i <= p->count; ++i) structure(p->child[i],depth + 1);
}
/* 1 key: insert, 2 key: search, 3: inorder, 4: node structure, 0: exit.
   Unique keys only; at most 100. This experiment implements insertion, not deletion. */
int main(void) {
    Node *root = NULL; int count = 0, command, key;
    while (readInt(&command)) {
        if (!command) return 0;
        if (command == 3) { printf("Inorder:"); inorder(root); putchar('\n'); continue; }
        if (command == 4) { if (root) structure(root,0); else puts("Empty tree."); continue; }
        if ((command != 1 && command != 2) || !readInt(&key)) return invalid();
        if (command == 2) puts(search(root,key) ? "Found." : "Not found.");
        else if (search(root,key)) puts("Duplicate ignored.");
        else if (count == MAX_KEYS) puts("Tree is full.");
        else { root = insert(root,key); ++count; puts("Inserted."); }
    }
    return invalid();
}

Local compile: gcc -std=c17 two-three-tree.c -o program -lm

Open in C compiler transfers the source. Copy the sample input into the compiler’s input box separately before running.

UNDERSTAND THE CODE

Line-by-line explanation

Follow the exact source above, from input handling through the algorithm and its result. Line numbers count blank lines; related statements are grouped into steps. Start with main to follow execution, then open the functions it calls.

Start with main → · Algorithm and data structure →

Lines 1–5Include input, conversion and validation declarations
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>

stdio.h supplies terminal input/output. stdlib.h declares conversion and allocation functions. string.h supplies string and memory utilities. ctype.h supplies isspace, and errno.h supplies conversion error reporting. Headers declare facilities; including them does not execute an operation.

Lines 7–9Define a complete-token reader and initialise its state
/* Read one entire token; reject malformed, oversized and out-of-range input. */
int readToken(char text[], size_t capacity) {
    int ch; size_t used = 0; int fits = 1;

readToken receives a writable buffer and its capacity. ch is int so it can represent EOF as well as input characters. used counts stored characters, and fits starts as a success flag. One slot must remain for the null terminator.

Lines 10–11Skip leading whitespace and recognise end-of-file
    while ((ch = getchar()) != EOF && isspace((unsigned char)ch)) { }
    if (ch == EOF) return 0;

Read until a non-whitespace character appears. The unsigned-char cast supplies an allowed value to isspace. Return 0 if EOF arrives before a token begins.

Lines 12–15Collect a token without silently truncating it
    do {
        if (ch == 0) fits = 0;
        if (used + 1 < capacity) text[used++] = (char)ch;
        else fits = 0;

The do body handles the character already read. An embedded zero byte marks the token invalid. Store a character only while there is room for it and a null terminator; otherwise clear fits. Continue reading even after the buffer fills.

Lines 16–20Consume the full token and terminate the buffer
        ch = getchar();
    } while (ch != EOF && !isspace((unsigned char)ch));
    text[used] = '\0';
    return fits;
}

Read through the next whitespace or EOF, add a null character after the stored prefix, then return fits. A too-long token returns 0 rather than pretending that its stored prefix is the complete token.

Lines 21–23Wrap token input as a checked integer read
int readInt(int *value) {
    char token[64], *end;
    if (!readToken(token, sizeof token)) return 0;

value is an output address. A 64-slot local buffer receives the token; a failed token read returns 0 before attempting conversion.

Lines 24–25Convert base-ten text and check the whole result
    errno = 0; long result = strtol(token, &end, 10);
    if (errno || end == token || *end || result < -1000000 || result > 1000000) return 0;

Reset errno before strtol. &end receives the stopping address. Reject conversion errors, no converted digits, leftover characters, or values outside -1,000,000 to 1,000,000. Thus 12x is not accepted as the integer 12.

Lines 26–28Return the accepted value or a standard error status
    *value = (int)result; return 1;
}
int invalid(void) { puts("Invalid or incomplete input."); return 1; }

Store a validated integer through *value and return 1 from readInt. invalid is a separate helper that prints the error and returns 1 as main’s failure status; the same number has different roles in these two function contracts.

Lines 30–33Reserve nodes with temporary split capacity
#define MAX_KEYS 100
/* Stable nodes have 1 or 2 keys. A third key/fourth child is temporary overflow. */
typedef struct Node { int count, key[3]; struct Node *child[4]; } Node;
Node pool[2 * MAX_KEYS + 1]; int used = 0;

A stable node has one or two ordered keys and up to three children. The third key and fourth child slots temporarily hold overflow before splitting. The static pool supplies node storage within the bounded, insertion-only experiment.

Lines 34–38Initialise a fresh node from the pool
Node *newNode(void) {
    Node *p = &pool[used++]; p->count = 0;
    for (int i = 0; i < 4; ++i) p->child[i] = NULL;
    return p;
}

Select the next unused pool entry, advance used, zero its key count and set all four child pointers to NULL. No malloc or per-node free is involved in this representation.

Lines 39–46Search within a node before selecting a child
int search(const Node *p, int key) {
    while (p) {
        int i = 0; while (i < p->count && key > p->key[i]) ++i;
        if (i < p->count && key == p->key[i]) return 1;
        p = p->child[i];
    }
    return 0;
}

Find the first key not less than the target. Equality returns success; otherwise follow the child for that gap. Reaching a null child means the key is absent.

Lines 47–50Define the recursive insertion-and-promotion contract
/* Return 1 when a split promotes a key plus a new right child to the caller. */
int insertRecursive(Node *p, int key, int *promoted, Node **right) {
    int pos = 0; while (pos < p->count && key > p->key[pos]) ++pos;
    int value = key; Node *newRight = NULL;

pos locates the destination key gap. value starts as the requested key and newRight as NULL. promoted and right are output parameters for a split. Here return 1 means a split needs parent attention, not merely that an insertion succeeded.

Lines 51–53Insert into a child before modifying an internal node
    if (p->child[0]) {
        if (!insertRecursive(p->child[pos],key,&value,&newRight)) return 0;
    }

A nonnull first child identifies an internal node. Recurse into child[pos]. If the child absorbed the insertion without splitting, return 0 immediately; the current parent needs no new key.

Lines 54–58Make room for a key promoted from below
    for (int i = p->count; i > pos; --i) {
        p->key[i] = p->key[i - 1]; p->child[i + 1] = p->child[i];
    }
    p->key[pos] = value; p->child[pos + 1] = newRight; ++p->count;
    if (p->count <= 2) return 0;

Shift larger keys and their right-side child pointers one slot right, then insert value and newRight at pos. Increment count. If the node still has at most two keys, return 0 because no promotion is needed.

Lines 59–60Split an overflowing three-key node
    Node *sibling = newNode(); sibling->count = 1; sibling->key[0] = p->key[2];
    sibling->child[0] = p->child[2]; sibling->child[1] = p->child[3];

Create a right sibling containing the largest key and the two rightmost child subtrees. The original node will retain the smallest key and its leftmost child subtrees.

Lines 61–64Promote the middle key and return the sibling address
    *promoted = p->key[1]; *right = sibling;
    p->count = 1; p->child[2] = p->child[3] = NULL;
    return 1;
}

Write the middle key through promoted and the new sibling through right. Reduce the original node to one key and clear transferred child slots. Return 1 so the caller can insert the promoted key into its own level.

Lines 65–67Handle an empty root and prepare split outputs
Node *insert(Node *root, int key) {
    if (!root) { root = newNode(); root->count = 1; root->key[0] = key; return root; }
    int promoted; Node *right;

For an empty tree create a one-key root directly. Otherwise reserve local variables to receive a possible promotion from insertRecursive.

Lines 68–73Grow the tree height only when the root splits
    if (insertRecursive(root,key,&promoted,&right)) {
        Node *parent = newNode(); parent->count = 1; parent->key[0] = promoted;
        parent->child[0] = root; parent->child[1] = right; root = parent;
    }
    return root;
}

A split at the old root creates a new one-key parent. Attach the old left part and new right sibling as its children, then return that parent. Without a root split, return the existing root.

Lines 74–78Display sorted keys across all child gaps
void inorder(const Node *p) {
    if (!p) return;
    for (int i = 0; i < p->count; ++i) { inorder(p->child[i]); printf(" %d",p->key[i]); }
    inorder(p->child[p->count]);
}

For each key, traverse the child immediately before it, then print the key. Finally traverse the last child. This generalises binary inorder traversal to nodes with two keys.

Lines 79–83Display the node structure by depth
void structure(const Node *p, int depth) {
    if (!p) return;
    printf("Depth %d:",depth); for (int i = 0; i < p->count; ++i) printf(" %d",p->key[i]); putchar('\n');
    for (int i = 0; i <= p->count; ++i) structure(p->child[i],depth + 1);
}

Print all keys stored in a node on its depth line, then recurse into its count + 1 children at the next depth. A null child prints nothing.

Lines 84–88Document the insertion-only interface and start empty
/* 1 key: insert, 2 key: search, 3: inorder, 4: node structure, 0: exit.
   Unique keys only; at most 100. This experiment implements insertion, not deletion. */
int main(void) {
    Node *root = NULL; int count = 0, command, key;
    while (readInt(&command)) {

The program provides insertion, exact search, inorder display and structure display, with up to 100 unique keys. It does not implement deletion. main begins with a null root and zero accepted keys.

Lines 89–92Handle exit and displays before key input
        if (!command) return 0;
        if (command == 3) { printf("Inorder:"); inorder(root); putchar('\n'); continue; }
        if (command == 4) { if (root) structure(root,0); else puts("Empty tree."); continue; }
        if ((command != 1 && command != 2) || !readInt(&key)) return invalid();

Command 0 exits successfully. Display commands need no key and continue directly to the next command. Remaining accepted commands are 1 for insert and 2 for search.

Lines 93–99Search or insert only a new permitted key
        if (command == 2) puts(search(root,key) ? "Found." : "Not found.");
        else if (search(root,key)) puts("Duplicate ignored.");
        else if (count == MAX_KEYS) puts("Tree is full.");
        else { root = insert(root,key); ++count; puts("Inserted."); }
    }
    return invalid();
}

Report searches, ignore duplicates, reject insertion at the key limit, or retain insert’s new root and increase count. The static pool needs no heap cleanup. Missing exit or malformed commands return invalid input.

FOLLOW THE VALUES

Dry run

  1. Insert 10 and 20: the root contains [10,20].
  2. Insert 5: temporary keys [5,10,20] promote 10.
  3. The new root [10] has leaf children [5] and [20].
  4. Insert 6 and 12: leaves become [5,6] and [12,20], still at equal depth.

CHECK THE BEHAVIOUR

Sample input & output

These outputs were captured by running the exact C source shown above. Commands use the stated input format; the programs omit repeated menu prompts so the same sample can be pasted and run.

Sample 1

INPUT
1 10
1 20
1 5
1 6
1 12
1 30
1 7
1 17
3
4
2 12
0
OUTPUT
Inserted.
Inserted.
Inserted.
Inserted.
Inserted.
Inserted.
Inserted.
Inserted.
Inorder: 5 6 7 10 12 17 20 30
Depth 0: 10
Depth 1: 6
Depth 2: 5
Depth 2: 7
Depth 1: 20
Depth 2: 12 17
Depth 2: 30
Found.

Sample 2

INPUT
1 10
1 20
1 5
4
0
OUTPUT
Inserted.
Inserted.
Inserted.
Depth 0: 10
Depth 1: 5
Depth 1: 20

Sample 3

INPUT
3
2 1
1 1
1 1
3
0
OUTPUT
Inorder:
Not found.
Inserted.
Duplicate ignored.
Inorder: 1

Common mistakes

  • A 2–3 tree is not a minimum-degree-2 B-tree with three stable keys per node.
  • Keep both children when a split creates a new root.
  • Temporary overflow needs room for three keys and four child links.

WHY THIS GROWTH RATE?

Time and space complexity

Search/insert O(log n); inorder/structure display O(n); stored nodes O(n), recursive path O(log n).

Every internal node has two or three children and all leaves share a depth. That branching keeps height logarithmic in the number of stored keys.

Search checks at most two keys per level. Insertion can split at most one node per level, with only constant-size key/link moves at each split.

The fixed pool reserves 201 nodes for up to 100 keys; no dynamic allocation is needed. A generalized pool grows as O(n). Each persistent node holds at most two keys; the third slot is only temporary overflow.

Big-O describes growth in the stated input quantity. The educational limits keep examples safe; complexity discussions state which bounds depend on a generalized input size. Arithmetic on the accepted native numeric types is treated as constant-time.

PREPARE FOR YOUR LAB VIVA

Related viva questions & answers

6 focused questions

Try answering aloud, then expand the answer to check your reasoning. These questions focus on the concepts used in this program.

How does a 2-node differ from a 3-node?

A 2-node has one key and two children when internal; a 3-node has two keys and three children. Leaf nodes have those keys but no children.

Why is the middle of three overflowing keys promoted?

It separates the smallest key from the largest key. Moving it upward leaves two valid one-key nodes and preserves the search intervals on both sides.

Can an insertion increase the height below only one leaf?

No. Splits reorganize nodes at the same levels. Only a new root increases the tree’s height, and it raises the depth of every leaf equally.

Why are three key slots present if stable nodes hold at most two?

The extra slot safely represents temporary insertion overflow before splitting. It does not mean a stable node is allowed to retain three keys.

Why must a root split retain a pointer to the old root?

The old root becomes the new root’s left child, holding the lower surviving keys. Losing that link would disconnect part of the stored tree.

What does the fixed node pool simplify for this example?

It avoids partial dynamic-allocation failures during cascading splits. The 100-key limit bounds required nodes, while the algorithm still demonstrates the usual promotion and balancing behavior.