Skip to lab content

EXPERIMENT 07 · DATA STRUCTURES LAB

Binary search tree — insert, search and delete

SOLUTION 07C173 tested sample runs

THE SYLLABUS TASK

What you need to solve

Write a program to implement a Binary Search Tree.
Input format & conventions

Commands: 1 key inserts, 2 key searches, 3 key deletes, 4 prints inorder, 0 exits. Keys are integers within ±1,000,000. At most 100 distinct keys; duplicates are ignored. The tree is not automatically balanced.

UNDERSTAND THE IDEA

Explanation

A BST maintains a strict rule: every left-subtree key is smaller than its parent and every right-subtree key is larger. This lets a search choose one child rather than scan both sides.

Insertion follows comparisons to an empty child position. Search follows the same path. Inorder traversal prints unique keys in ascending order.

Deletion has three cases: a leaf is removed directly; a one-child node is replaced by its child; a two-child node copies its inorder successor (smallest key in the right subtree), then deletes that successor node. The root pointer may change, so callers assign the returned pointer.

PLAN BEFORE CODING

Algorithm

  1. Initialize an empty root.
  2. Read an insertion/search/deletion/traversal command.
  3. Follow BST comparisons for the selected key.
  4. For deletion handle zero, one or two children.
  5. Keep keys unique, respect the node bound and print the result.
  6. Free all nodes on exit.

CONTROL THE PROGRAM

Commands and input

A command is an operation number followed by its arguments. Whitespace separates tokens; line breaks make your input easier to read. No interactive menu prompts are printed.

InputMeaning
1 keyInsert a unique key; duplicates are ignored.
2 keySearch and report Found / Not found.
3 keyDelete if present; handle all three child-count cases.
4Print ascending inorder traversal.
0Exit and free the tree.

SEE THE CONTROL FLOW

Flowcharts

Follow arrows from Start. Diamonds are decisions; Yes/No labels select a branch. Returning arrows repeat a loop. Function steps group the pointer/index updates explained in the code. Expand the operation charts for more detail. On a phone, scroll sideways within a diagram.

Program command loop

Binary search tree — insert, search and delete: Program command loop

Open full-size flowchart ↗

BST search
Binary search tree — insert, search and delete: BST search

Open full-size flowchart ↗

BST deletion cases
Binary search tree — insert, search and delete: BST deletion cases

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 a complete integer token; reject bad or oversized input safely. */
int readInt(int *value) {
    char token[64];
    int ch;
    size_t used = 0;
    while ((ch = getchar()) != EOF && isspace((unsigned char)ch)) { }
    if (ch == EOF) return 0;
    do {
        if (used < sizeof token - 1) token[used++] = (char)ch;
        else {
            while ((ch = getchar()) != EOF && !isspace((unsigned char)ch)) { }
            return 0;
        }
        ch = getchar();
    } while (ch != EOF && !isspace((unsigned char)ch));
    token[used] = '\0';
    char *end;
    errno = 0;
    long result = strtol(token, &end, 10);
    if (errno || *end != '\0' || result < -1000000 || result > 1000000) return 0;
    *value = (int)result;
    return 1;
}

#define MAX_NODES 100
typedef struct Node { int key; struct Node *left, *right; } Node;
Node *insert(Node *root, int key, int count, int *status) {
    if (!root) {
        if (count == MAX_NODES) { *status = -2; return NULL; }
        Node *node = malloc(sizeof *node);
        if (!node) { *status = -1; return NULL; }
        node->key = key; node->left = node->right = NULL; *status = 1;
        return node;
    }
    if (key < root->key) root->left = insert(root->left, key, count, status);
    else if (key > root->key) root->right = insert(root->right, key, count, status);
    else *status = 0;
    return root;
}
int search(const Node *root, int key) {
    while (root) {
        if (key == root->key) return 1;
        root = key < root->key ? root->left : root->right;
    }
    return 0;
}
Node *deleteKey(Node *root, int key) {
    if (!root) return NULL;
    if (key < root->key) root->left = deleteKey(root->left, key);
    else if (key > root->key) root->right = deleteKey(root->right, key);
    else {
        if (!root->left || !root->right) {
            Node *child = root->left ? root->left : root->right;
            free(root); return child;
        }
        Node *successor = root->right;
        while (successor->left) successor = successor->left;
        root->key = successor->key;
        root->right = deleteKey(root->right, successor->key);
    }
    return root;
}
void inorder(const Node *root) {
    if (!root) return;
    inorder(root->left); printf(" %d", root->key); inorder(root->right);
}
void freeTree(Node *root) {
    if (!root) return;
    freeTree(root->left); freeTree(root->right); free(root);
}
/* 1 key: insert; 2 key: search; 3 key: delete; 4: inorder; 0: exit. */
int main(void) {
    Node *root = NULL; int command, key, count = 0;
    while (readInt(&command)) {
        if (command == 0) { freeTree(root); return 0; }
        if (command == 4) { printf("Inorder:"); inorder(root); putchar('\n'); continue; }
        if (!readInt(&key)) break;
        if (command == 1) {
            int status = 0; root = insert(root, key, count, &status);
            if (status == 1) { ++count; puts("Inserted."); }
            else if (!status) puts("Duplicate ignored.");
            else puts("Insertion failed: node limit or allocation failure.");
        } else if (command == 2) puts(search(root, key) ? "Found." : "Not found.");
        else if (command == 3) {
            if (search(root, key)) { root = deleteKey(root, key); --count; puts("Deleted."); }
            else puts("Key not found.");
        } else break;
    }
    puts("Invalid or incomplete input. End with command 0.");
    freeTree(root); return 1;
}
Open in compiler ↗

Code loads into your existing compiler. Enter the sample input there; sign-in and execution rules stay the same.

Compile and run locally
gcc -std=c17 binary-search-tree.c -o lab
./lab

On Windows, run .\lab.exe after compiling with GCC.

UNDERSTAND THE CODE

Line-by-line explanation

These steps follow the exact source above, including its input helper, operations, command handling and cleanup. Line numbers count blank lines; related statements are grouped. Start with main to follow execution, then open the functions it calls.

Start with main → · Data structure and operations →

Line 1Include terminal input and output
#include <stdio.h>

stdio.h declares printf, puts, getchar and putchar. The input helper reads characters with getchar; the main program prints operation results using the output functions.

Line 2Include conversion and memory utilities
#include <stdlib.h>

stdlib.h declares strtol for converting an integer token. It also declares malloc and free, which the linked structures and tree programs use to obtain and release node storage.

Line 3Include string and memory utilities
#include <string.h>

string.h declares utilities such as memset. It is part of the shared header list; merely including a header does not call any function or change stored values.

Lines 4–5Include character tests and conversion error reporting
#include <ctype.h>
#include <errno.h>

ctype.h supplies isspace for recognising whitespace. errno.h supplies errno so the program can detect a range error reported by strtol.

Lines 7–8Define a checked integer-input helper
/* Read a complete integer token; reject bad or oversized input safely. */
int readInt(int *value) {

The comment states the purpose of readInt. Its int *value parameter receives the address of a caller variable. The helper returns 1 after storing a valid value there, or 0 on failure; these flags are separate from main’s process exit status.

Lines 9–11Reserve a token buffer and its reading state
    char token[64];
    int ch;
    size_t used = 0;

token holds up to 63 input characters and a null terminator. ch is int so it can distinguish character input from EOF. used starts at zero and counts the stored token characters.

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

getchar reads one character at a time. The loop discards whitespace before the token. The unsigned-char cast supplies an allowed argument to isspace. If EOF is reached before a token starts, return 0 without changing the caller variable.

Lines 14–15Store token characters while space remains
    do {
        if (used < sizeof token - 1) token[used++] = (char)ch;

The do-while body handles the first non-whitespace character already read. The size check leaves one buffer slot for a null terminator. used++ uses the old index for storage, then advances the count.

Lines 16–19Reject a token that is too long
        else {
            while ((ch = getchar()) != EOF && !isspace((unsigned char)ch)) { }
            return 0;
        }

When the buffer has no character space left, consume the rest of that token through whitespace or EOF, then return failure. The program does not silently treat a truncated prefix as the full input number.

Lines 20–21Read the next character and decide whether the token continues
        ch = getchar();
    } while (ch != EOF && !isspace((unsigned char)ch));

Keep collecting until whitespace or EOF. The terminating whitespace has been consumed by getchar; it is not stored as part of the token.

Lines 22–24Terminate the string and prepare a checked conversion
    token[used] = '\0';
    char *end;
    errno = 0;

Write the null terminator after the stored token. end will identify where conversion stopped. Reset errno before strtol so a previous library error cannot be mistaken for this conversion’s result.

Line 25Convert the complete token as a decimal integer
    long result = strtol(token, &end, 10);

strtol reads token in base 10 and returns a long. Passing &end lets it write the address of the first unconverted character. A sign such as - is allowed as part of a valid integer token.

Line 26Reject conversion errors, leftover characters and out-of-range values
    if (errno || *end != '\0' || result < -1000000 || result > 1000000) return 0;

A nonzero errno rejects a reported conversion error. *end must be the null terminator, so input such as 12x is rejected rather than accepted as 12. The allowed value range is -1,000,000 to 1,000,000.

Lines 27–29Store the accepted integer through its address
    *value = (int)result;
    return 1;
}

After validation, cast result to int and assign it through *value. Return 1 for success. The brace ends readInt. The caller variable remains unchanged on the earlier failure returns.

Lines 31–32Define the node limit and BST links
#define MAX_NODES 100
typedef struct Node { int key; struct Node *left, *right; } Node;

At most 100 distinct keys are stored. Each Node holds key and left/right child pointers. The ordering policy puts smaller keys in the left subtree and larger keys in the right; duplicate insertions do not create nodes.

Lines 33–35Insert into an empty subtree only if capacity permits
Node *insert(Node *root, int key, int count, int *status) {
    if (!root) {
        if (count == MAX_NODES) { *status = -2; return NULL; }

root is the current subtree root. count describes the current whole-tree node count. status is an output parameter: this branch sets -2 when a new node would exceed the limit and returns NULL for the still-empty subtree.

Lines 36–40Allocate and return a newly inserted leaf
        Node *node = malloc(sizeof *node);
        if (!node) { *status = -1; return NULL; }
        node->key = key; node->left = node->right = NULL; *status = 1;
        return node;
    }

Allocate one Node. If allocation fails, set status to -1 and return NULL. Otherwise store key, initialise both children to NULL, set status to 1 and return the new node address. The caller must keep that returned address.

Lines 41–42Reconnect the result of recursive insertion
    if (key < root->key) root->left = insert(root->left, key, count, status);
    else if (key > root->key) root->right = insert(root->right, key, count, status);

For a smaller key, insert into the left subtree and assign the returned root to root->left. For a larger key use the right side. The assignment is essential when the chosen child was previously empty.

Lines 43–45Ignore a duplicate and preserve the current root
    else *status = 0;
    return root;
}

Equality sets status to 0 without allocating. Return the current root in every existing-node case so callers keep the surrounding tree connected. count changes later in main only after status 1.

Lines 46–49Search by following only one branch at a time
int search(const Node *root, int key) {
    while (root) {
        if (key == root->key) return 1;
        root = key < root->key ? root->left : root->right;

While root is nonnull, return 1 on equality. Otherwise choose left for a smaller key or right for a larger key. The parameter is a local pointer copy, so moving it does not replace main’s root.

Lines 50–52Return not found after reaching a null link
    }
    return 0;
}

The loop ends when no candidate node remains. Return 0 without dereferencing NULL. The brace closes search.

Lines 53–56Locate the subtree containing the key to delete
Node *deleteKey(Node *root, int key) {
    if (!root) return NULL;
    if (key < root->key) root->left = deleteKey(root->left, key);
    else if (key > root->key) root->right = deleteKey(root->right, key);

An empty subtree returns NULL. A smaller key recurses left and a larger key recurses right. Assign each returned subtree root back to its parent link because deletion may change that link.

Lines 57–61Delete a matching node with zero or one child
    else {
        if (!root->left || !root->right) {
            Node *child = root->left ? root->left : root->right;
            free(root); return child;
        }

If either child is missing, select the other child, or NULL if both are missing. Free the current node and return the selected child. The caller reconnects that child in place of the removed root.

Lines 62–63Find the inorder successor for a two-child node
        Node *successor = root->right;
        while (successor->left) successor = successor->left;

Start in the right subtree, then move left until no left child remains. This finds the smallest key greater than the current key under the no-duplicates policy.

Lines 64–66Copy the successor key and delete its old node
        root->key = successor->key;
        root->right = deleteKey(root->right, successor->key);
    }

Replacing the current key preserves its surrounding links. Then recursively remove the duplicate successor key from the right subtree and reconnect the returned root. Freeing the original two-child node directly would lose its child links.

Lines 67–68Return the surviving subtree root
    return root;
}

After any recursive repair, return root so the caller can reconnect it. Earlier zero/one-child branches already returned their replacement child.

Lines 69–72Display keys with inorder traversal
void inorder(const Node *root) {
    if (!root) return;
    inorder(root->left); printf(" %d", root->key); inorder(root->right);
}

Skip empty subtrees. Visit left, print the current key, then visit right. Because this tree enforces the BST rule, the output is ascending.

Lines 73–76Release child nodes before their parent
void freeTree(Node *root) {
    if (!root) return;
    freeTree(root->left); freeTree(root->right); free(root);
}

A null subtree needs no work. Free both subtrees recursively, then free the parent. No link from an already-freed parent is read afterward.

Lines 77–79Document commands and create an empty BST
/* 1 key: insert; 2 key: search; 3 key: delete; 4: inorder; 0: exit. */
int main(void) {
    Node *root = NULL; int command, key, count = 0;

The command comment lists insert, search, delete, inorder and exit. root starts NULL and count starts at zero. The driver will keep count consistent with successful insertions and deletions.

Lines 80–82Read a command, clean up on exit or display immediately
    while (readInt(&command)) {
        if (command == 0) { freeTree(root); return 0; }
        if (command == 4) { printf("Inorder:"); inorder(root); putchar('\n'); continue; }

Command 0 frees the tree and returns success. Command 4 prints inorder without reading a key; continue jumps to the next command-reading iteration.

Lines 83–85Read a key and attempt insertion when requested
        if (!readInt(&key)) break;
        if (command == 1) {
            int status = 0; root = insert(root, key, count, &status);

The remaining operation branches need a key token. A missing key breaks to cleanup. For command 1, initialise status and assign insert’s returned root back to main’s root.

Lines 86–88Update the count only after a real insertion
            if (status == 1) { ++count; puts("Inserted."); }
            else if (!status) puts("Duplicate ignored.");
            else puts("Insertion failed: node limit or allocation failure.");

Status 1 means a node was created, so increment count. Status 0 reports an ignored duplicate. Negative status reports allocation failure or the node limit without incrementing count.

Line 89Search without modifying the BST
        } else if (command == 2) puts(search(root, key) ? "Found." : "Not found.");

For command 2, the conditional expression uses search’s flag to choose Found or Not found. The root and count are unchanged.

Lines 90–93Delete only when the key exists
        else if (command == 3) {
            if (search(root, key)) { root = deleteKey(root, key); --count; puts("Deleted."); }
            else puts("Key not found.");
        } else break;

For command 3, first search for the key. If found, retain deleteKey’s returned root, decrement count and print success. If absent, leave the tree and count unchanged. An unsupported command breaks out.

Lines 94–97Handle an abnormal end and free remaining nodes
    }
    puts("Invalid or incomplete input. End with command 0.");
    freeTree(root); return 1;
}

After the loop, print the invalid/incomplete-input message, free the current tree and return failure. This is reached only when command 0 did not already finish successfully.

FOLLOW THE STATE

Dry run

StepOperation / stateResult
Insert 50, 30, 70, 20, 40, 60, 80Build two subtreesInorder: 20 30 40 50 60 70 80
Delete 50 (two children)Successor is 60Copy 60 to root; remove old 60
Inorder after deletionTraverse left → root → right20 30 40 60 70 80

CHECK THE BEHAVIOUR

Sample input & output

These outputs were produced by compiling and running this exact source. Inputs are provided in the stated order; the programs do not print repeated menu prompts.

Sample 1

INPUT
1 50
1 30
1 70
1 20
1 40
1 60
1 80
4
2 40
3 50
4
0
OUTPUT
Inserted.
Inserted.
Inserted.
Inserted.
Inserted.
Inserted.
Inserted.
Inorder: 20 30 40 50 60 70 80
Found.
Deleted.
Inorder: 20 30 40 60 70 80

Sample 2

INPUT
2 5
3 5
4
1 10
1 10
3 10
4
0
OUTPUT
Not found.
Key not found.
Inorder:
Inserted.
Duplicate ignored.
Deleted.
Inorder:

Sample 3

INPUT
1 10
1 5
1 2
3 5
4
2 5
0
OUTPUT
Inserted.
Inserted.
Inserted.
Deleted.
Inorder: 2 10
Not found.

Common mistakes

  • Assign root = deleteKey(root, key), especially when deleting the root.
  • Do not claim every BST is balanced.
  • Do not leave duplicate copies after moving the successor key.

WHY THIS GROWTH RATE?

Time and space complexity

Insert/search/delete O(h), worst-case O(n); inorder/cleanup O(n). Stored nodes O(n).

Let n be the number of keys and h the tree height. Insert and search follow one downward path of at most h nodes. Delete also follows such a path; a two-child deletion adds a successor walk within the right subtree. A constant number of height-bounded walks remains O(h).

A balanced tree has h = O(log n), so those operations are O(log n) on that shape. Sorted insertions can form a chain with h = n: worst-case O(n) per operation. Building a tree from n sorted keys can sum 1 + 2 + … + n work and take O(n²). This implementation performs no balancing rotations.

Inorder and cleanup each visit all n nodes, giving O(n). Node storage is O(n); recursive insert/delete/traversal/cleanup can use O(h) extra call-stack space. The iterative search function itself uses O(1) working space.

Big-O describes growth in the stated input quantity. Fixed factors and lower-order terms are omitted. Supported input bounds keep the educational examples practical; operation costs depend on the actual representation and functions shown.

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.

Which ordering rule guides search in this binary search tree?

Every key in a node's left subtree is smaller and every key in its right subtree is larger. Comparing the target with the node therefore selects one child subtree instead of searching both.

Why does this BST ignore a duplicate insertion?

The implementation stores unique keys. An equal key returns the duplicate status without allocating a node or increasing count, preserving the strict smaller-left and larger-right ordering rule.

How does deletion handle a node with zero or one child?

It saves the existing child pointer, if any, frees the matched node and returns that child as the replacement subtree root. A leaf returns NULL; a one-child node returns its child.

Why is the smallest key in the right subtree a valid two-child replacement?

It is the inorder successor: the next greater key. Replacing the deleted key with it preserves the ordering boundaries, then deleting its original node avoids retaining a duplicate key.

Why is the returned pointer assigned back to root or a child link after deletion?

Deletion can replace the root of any subtree with NULL or a child. Assigning the return value reconnects that replacement; ignoring it could leave a parent or main root pointing at freed storage.

Why is O(log n) not guaranteed for this BST's operations?

This tree does not rebalance itself. Sorted insertions can create a chain with height n, making search, insertion and deletion O(n) in the worst case; their general bound is O(h).