Skip to lab content

WEEK 6 · ADVANCED DATA STRUCTURES LAB

AVL tree — insert, search and delete

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

THE SYLLABUS TASK

What you need to solve

Implement AVL trees and maintain balance using rotations.
Input format & conventions

Commands: 1 key = insert; 2 key = search; 3 key = delete; 4 = preorder; 5 = inorder; 0 = exit. Up to 100 distinct signed keys. Duplicate insertion is ignored.

UNDERSTAND THE IDEA

Explanation

An AVL tree is a BST whose left and right subtree heights differ by at most one at every node. Each node stores its current height, with an empty child having height zero.

Insertion and deletion first follow BST rules. On the return path, heights are updated and any imbalance is repaired using rotations.

Right rotation repairs a left-left imbalance; left rotation repairs right-right. Left-right and right-left cases need two rotations. A rotation reconnects the middle subtree while preserving sorted key order.

TURN THE IDEA INTO STEPS

Step-by-step algorithm

  1. Locate an insertion, deletion or search position using BST comparisons.
  2. For insertion, allocate a checked new leaf; for deletion, reconnect a child or replace with an inorder successor.
  3. Return up the modified path, updating each height.
  4. If balance exceeds ±1, inspect the heavy child and apply the appropriate single or double rotation.
  5. Keep returned subtree roots connected and display requested traversal results.

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.

AVL repair on return from mutation

AVL repair on return from mutation

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_N 100
 typedef struct Node { int key, height; struct Node *left, *right; } Node;
int height(const Node *p) { return p ? p->height : 0; }
int maximum(int a, int b) { return a > b ? a : b; }
void update(Node *p) { p->height = 1 + maximum(height(p->left),height(p->right)); }
int balance(const Node *p) { return p ? height(p->left) - height(p->right) : 0; }
Node *rotateRight(Node *y) {
    Node *x = y->left, *middle = x->right;
    x->right = y; y->left = middle; update(y); update(x); return x;
}
Node *rotateLeft(Node *x) {
    Node *y = x->right, *middle = y->left;
    y->left = x; x->right = middle; update(x); update(y); return y;
}
Node *rebalance(Node *p) {
    if (!p) return NULL;
    update(p);
    if (balance(p) > 1) {
        if (balance(p->left) < 0) p->left = rotateLeft(p->left);
        return rotateRight(p);
    }
    if (balance(p) < -1) {
        if (balance(p->right) > 0) p->right = rotateRight(p->right);
        return rotateLeft(p);
    }
    return p;
}
Node *insert(Node *p, int key, int *ok) {
    if (!p) {
        Node *node = malloc(sizeof *node);
        if (!node) { *ok = 0; return NULL; }
        node->key = key; node->height = 1; node->left = node->right = NULL;
        *ok = 1; return node;
    }
    if (key < p->key) p->left = insert(p->left,key,ok);
    else if (key > p->key) p->right = insert(p->right,key,ok);
    else return p;
    return rebalance(p);
}
int search(const Node *p, int key) {
    while (p) { if (key == p->key) return 1; p = key < p->key ? p->left : p->right; }
    return 0;
}
Node *deleteKey(Node *p, int key) {
    if (!p) return NULL;
    if (key < p->key) p->left = deleteKey(p->left,key);
    else if (key > p->key) p->right = deleteKey(p->right,key);
    else {
        if (!p->left || !p->right) {
            Node *child = p->left ? p->left : p->right; free(p); return child;
        }
        Node *next = p->right; while (next->left) next = next->left;
        p->key = next->key; p->right = deleteKey(p->right,next->key);
    }
    return rebalance(p);
}
void inorder(const Node *p) { if (p) { inorder(p->left); printf(" %d",p->key); inorder(p->right); } }
void preorder(const Node *p) { if (p) { printf(" %d",p->key); preorder(p->left); preorder(p->right); } }
void destroy(Node *p) { if (p) { destroy(p->left); destroy(p->right); free(p); } }
/* 1 key: insert, 2 key: search, 3 key: delete, 4: preorder, 5: inorder, 0: exit. */
int main(void) {
    Node *root = NULL; int count = 0, command, key, status = 1;
    while (readInt(&command)) {
        if (!command) { status = 0; break; }
        if (command == 4 || command == 5) {
            printf(command == 4 ? "Preorder:" : "Inorder:");
            if (command == 4) preorder(root); else inorder(root); putchar('\n'); continue;
        }
        if (command < 1 || command > 3 || !readInt(&key)) break;
        if (command == 1) {
            if (search(root,key)) { puts("Duplicate ignored."); continue; }
            if (count == MAX_N) { puts("Tree is full."); continue; }
            int ok = 0; root = insert(root,key,&ok);
            if (ok) { ++count; puts("Inserted."); } else puts("Allocation failed; tree unchanged.");
        } else if (command == 2) puts(search(root,key) ? "Found." : "Not found.");
        else if (!search(root,key)) puts("Not found.");
        else { root = deleteKey(root,key); --count; puts("Deleted."); }
    }
    destroy(root); return status ? invalid() : 0;
}

Local compile: gcc -std=c17 avl-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–31Define the bounded AVL tree representation
#define MAX_N 100
 typedef struct Node { int key, height; struct Node *left, *right; } Node;

MAX_N is 100 distinct keys. Each node stores a key, cached height and two child pointers. The height field lets balancing decisions avoid recomputing entire subtree heights.

Lines 32–35Calculate and refresh height information
int height(const Node *p) { return p ? p->height : 0; }
int maximum(int a, int b) { return a > b ? a : b; }
void update(Node *p) { p->height = 1 + maximum(height(p->left),height(p->right)); }
int balance(const Node *p) { return p ? height(p->left) - height(p->right) : 0; }

height returns zero for NULL. update sets a node’s height to one plus the larger child height, so a leaf has height 1. balance is left height minus right height; a balanced AVL node must stay between -1 and 1.

Lines 36–39Rotate a left-heavy subtree to the right
Node *rotateRight(Node *y) {
    Node *x = y->left, *middle = x->right;
    x->right = y; y->left = middle; update(y); update(x); return x;
}

Save y’s left child x and x’s right subtree middle. Make y the right child of x and attach middle as y’s left child. Refresh y before x because y is now lower, then return x as the new subtree root. The inorder key order is preserved.

Lines 40–43Rotate a right-heavy subtree to the left
Node *rotateLeft(Node *x) {
    Node *y = x->right, *middle = y->left;
    y->left = x; x->right = middle; update(x); update(y); return y;
}

This is the mirror transformation: promote the right child y, move its left subtree into x’s right link, update the lower x first, then y, and return y.

Lines 44–46Refresh the current node before checking imbalance
Node *rebalance(Node *p) {
    if (!p) return NULL;
    update(p);

A null subtree returns NULL. Otherwise update its cached height because an insertion or deletion below may have changed a child’s height.

Lines 47–50Repair a left-heavy subtree
    if (balance(p) > 1) {
        if (balance(p->left) < 0) p->left = rotateLeft(p->left);
        return rotateRight(p);
    }

A balance above 1 means the left side is too tall. If that child leans right, first rotate the child left for the LR case. Then rotate the current node right, also covering the simpler LL case.

Lines 51–56Repair a right-heavy subtree or keep the root
    if (balance(p) < -1) {
        if (balance(p->right) > 0) p->right = rotateRight(p->right);
        return rotateLeft(p);
    }
    return p;
}

A balance below -1 needs a left rotation. First rotate a left-leaning right child right for the RL case. If neither imbalance condition applies, return p unchanged.

Lines 57–63Insert a new AVL leaf or report allocation failure
Node *insert(Node *p, int key, int *ok) {
    if (!p) {
        Node *node = malloc(sizeof *node);
        if (!node) { *ok = 0; return NULL; }
        node->key = key; node->height = 1; node->left = node->right = NULL;
        *ok = 1; return node;
    }

At a null subtree allocate one node. Failure sets *ok to zero without attaching storage. Success stores the key, height 1 and null children, sets ok and returns the new address.

Lines 64–68Reconnect recursive insertion and rebalance upward
    if (key < p->key) p->left = insert(p->left,key,ok);
    else if (key > p->key) p->right = insert(p->right,key,ok);
    else return p;
    return rebalance(p);
}

Choose left or right by key order and retain the returned child pointer. Equality creates no node. For a changed branch return rebalance(p), because rotation may replace this subtree’s root.

Lines 69–72Search by BST order without changing links
int search(const Node *p, int key) {
    while (p) { if (key == p->key) return 1; p = key < p->key ? p->left : p->right; }
    return 0;
}

Check one node at a time and move left or right. Return 1 on equality or 0 after reaching NULL. AVL balancing improves the path height but does not change the BST search rule.

Lines 73–77Find a key to delete and reconnect the selected branch
Node *deleteKey(Node *p, int key) {
    if (!p) return NULL;
    if (key < p->key) p->left = deleteKey(p->left,key);
    else if (key > p->key) p->right = deleteKey(p->right,key);
    else {

A null subtree returns NULL. Recurse left for a smaller key or right for a larger one, assigning the returned root back into the corresponding link.

Lines 78–80Remove a zero-child or one-child node
        if (!p->left || !p->right) {
            Node *child = p->left ? p->left : p->right; free(p); return child;
        }

Select the surviving child, which can be NULL for a leaf. Free p and return that child so the caller can reconnect it without keeping a freed-node address.

Lines 81–85Replace a two-child key and rebalance after deletion
        Node *next = p->right; while (next->left) next = next->left;
        p->key = next->key; p->right = deleteKey(p->right,next->key);
    }
    return rebalance(p);
}

Find the smallest key in the right subtree, copy it into p, then delete its old occurrence on the right. Return rebalance(p) so shrinking child heights can trigger the necessary rotations on the way back up.

Lines 86–88Provide ordered display, shape display and cleanup
void inorder(const Node *p) { if (p) { inorder(p->left); printf(" %d",p->key); inorder(p->right); } }
void preorder(const Node *p) { if (p) { printf(" %d",p->key); preorder(p->left); preorder(p->right); } }
void destroy(Node *p) { if (p) { destroy(p->left); destroy(p->right); free(p); } }

inorder visits left-key-right for sorted output. preorder visits key-left-right to help observe rotations. destroy releases both child subtrees before freeing the current node.

Lines 89–93Read commands and record the exit status
/* 1 key: insert, 2 key: search, 3 key: delete, 4: preorder, 5: inorder, 0: exit. */
int main(void) {
    Node *root = NULL; int count = 0, command, key, status = 1;
    while (readInt(&command)) {
        if (!command) { status = 0; break; }

main starts empty, count zero and failure status 1. Explicit command 0 changes status to 0 and leaves the loop, allowing the common cleanup at the end.

Lines 94–98Handle displays before reading key operands
        if (command == 4 || command == 5) {
            printf(command == 4 ? "Preorder:" : "Inorder:");
            if (command == 4) preorder(root); else inorder(root); putchar('\n'); continue;
        }
        if (command < 1 || command > 3 || !readInt(&key)) break;

Commands 4 and 5 display preorder or inorder and continue to the next command. Other accepted commands are 1–3 and each requires a valid key token.

Lines 99–103Insert only new keys within the node limit
        if (command == 1) {
            if (search(root,key)) { puts("Duplicate ignored."); continue; }
            if (count == MAX_N) { puts("Tree is full."); continue; }
            int ok = 0; root = insert(root,key,&ok);
            if (ok) { ++count; puts("Inserted."); } else puts("Allocation failed; tree unchanged.");

Check duplicates and capacity before allocation. Assign insert’s returned root, increment count only on success, and print the appropriate result. A failed allocation does not add a key.

Lines 104–109Search, delete and release the final tree
        } else if (command == 2) puts(search(root,key) ? "Found." : "Not found.");
        else if (!search(root,key)) puts("Not found.");
        else { root = deleteKey(root,key); --count; puts("Deleted."); }
    }
    destroy(root); return status ? invalid() : 0;
}

Command 2 reports presence. A deletion of an absent key leaves the tree unchanged; a present key is removed using the returned root and count decreases. After the loop destroy the tree, then return the recorded success or an invalid-input failure.

FOLLOW THE VALUES

Dry run

  1. Insert 10,20,30: the root 10 becomes right-heavy by two.
  2. A left rotation makes 20 the root with children 10 and 30.
  3. Continuing with 40,50,25 gives preorder [30,20,10,25,40,50].
  4. Inorder remains [10,20,25,30,40,50], showing that rotations preserve BST order.

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 30
1 40
1 50
1 25
4
5
2 25
3 30
5
0
OUTPUT
Inserted.
Inserted.
Inserted.
Inserted.
Inserted.
Inserted.
Preorder: 30 20 10 25 40 50
Inorder: 10 20 25 30 40 50
Found.
Deleted.
Inorder: 10 20 25 40 50

Sample 2

INPUT
1 30
1 10
1 20
4
0
OUTPUT
Inserted.
Inserted.
Inserted.
Preorder: 20 10 30

Sample 3

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

Common mistakes

  • Recompute the demoted node’s height before the promoted node’s height.
  • Reconnect the middle subtree during a rotation.
  • Deletion cases must inspect child balance, not reuse insertion-only key comparisons.

WHY THIS GROWTH RATE?

Time and space complexity

Search/insert/delete O(log n); traversal O(n); node storage O(n), mutation recursion O(log n).

AVL balance restricts height to O(log n). Search follows one root-to-leaf path; insertion and deletion also repair nodes along such a path.

Height updates and individual rotations are constant-time pointer changes. Deletion may rebalance multiple ancestors, still within O(log n).

Traversals visit all n nodes once. Stored nodes use O(n) memory; active recursive mutation frames use O(log n). Output and optional traversals are separate from one mutation’s cost.

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.

Why does an AVL node store its height?

The parent needs child heights to compute balance quickly. Updating a stored height from its children is constant time instead of rescanning the entire subtree.

What does balance factor +2 indicate?

The left subtree is two levels taller than the right. The node violates AVL balance and needs a right-side repair, possibly preceded by a left rotation on its left child.

Why is the middle subtree preserved during a left rotation?

Its keys lie between the old root and the promoted right child. Reattaching it as the old root’s right subtree keeps every key within its valid BST interval.

When is a double left-right rotation necessary?

The node is left-heavy, but its left child is right-heavy. Rotating that child left converts the shape into a left-left case, then a right rotation repairs the parent.

Why are deletion rotations selected using child balance factors?

The removed key does not necessarily describe the shape of the remaining heavy subtree. Its actual child heights determine which rotation will restore balance.

Can AVL rotations change inorder traversal order?

No. Correct rotations rearrange parent-child links while preserving the relative key intervals. Inorder therefore still lists the stored keys in ascending order.