Skip to lab content

EXPERIMENT 06 · DATA STRUCTURES LAB

Tree traversals — recursive

SOLUTION 06AC173 tested sample runs

THE SYLLABUS TASK

What you need to solve

Write a program to implement recursive and non-recursive tree traversal methods.
Input format & conventions

Enter an explicit binary tree in preorder: node value, full left subtree, full right subtree. Use −1 for each empty child. −1 alone is an empty tree. Other integer values are supported within ±1,000,000; at most 100 nodes. No command 0 is needed.

UNDERSTAND THE IDEA

Explanation

A binary tree allows at most two children per node; it need not obey BST ordering. The input describes the exact shape rather than sorting values by insertion.

Preorder visits root, left, right. Inorder visits left, root, right. Postorder visits left, right, root. For root 1 with children 2 and 3, the orders are 1 2 3, 2 1 3, and 2 3 1.

Each traversal function checks root == NULL before visiting anything, then recursively follows the chosen order. The recursion stack remembers unfinished parent calls.

Decode the first sample

1 2 -1 -1 3 -1 -1 means root 1; left leaf 2; right leaf 3. Each leaf needs two −1 empty-child markers. The trailing markers describe links, not additional node values.

PLAN BEFORE CODING

Algorithm

  1. Read the tree using the −1 empty-child marker.
  2. Reject malformed input, allocation failure or more than 100 nodes.
  3. Run preorder, inorder and postorder using the named method.
  4. Print each order on its own line.
  5. Free all allocated tree nodes.

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.

Preorder recursive function

Tree traversals — recursive: Preorder recursive function

Open full-size flowchart ↗

Inorder recursive function
Tree traversals — recursive: Inorder recursive function

Open full-size flowchart ↗

Postorder recursive function
Tree traversals — recursive: Postorder recursive function

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 data; struct Node *left, *right; } Node;
/* Preorder input: value, left subtree, right subtree; -1 means NULL. */
int readTree(Node **root, int *count) {
    int value;
    *root = NULL;
    if (!readInt(&value)) return 0;
    if (value == -1) return 1;
    if (*count == MAX_NODES) return 0;
    Node *node = malloc(sizeof *node);
    if (!node) return 0;
    node->data = value; node->left = node->right = NULL;
    *root = node; ++(*count);
    return readTree(&node->left, count) && readTree(&node->right, count);
}
void freeTree(Node *root) {
    if (!root) return;
    freeTree(root->left); freeTree(root->right); free(root);
}
void preorder(const Node *root) {
    if (!root) return;
    printf(" %d", root->data); preorder(root->left); preorder(root->right);
}
void inorder(const Node *root) {
    if (!root) return;
    inorder(root->left); printf(" %d", root->data); inorder(root->right);
}
void postorder(const Node *root) {
    if (!root) return;
    postorder(root->left); postorder(root->right); printf(" %d", root->data);
}
int main(void) {
    Node *root = NULL; int count = 0;
    if (!readTree(&root, &count)) {
        puts("Invalid tree input or allocation failure."); freeTree(root); return 1;
    }
    printf("Preorder:"); preorder(root); putchar('\n');
    printf("Inorder:"); inorder(root); putchar('\n');
    printf("Postorder:"); postorder(root); putchar('\n');
    freeTree(root); return 0;
}
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 tree-traversals-recursive.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–32Limit the tree size and define a binary node
#define MAX_NODES 100
typedef struct Node { int data; struct Node *left, *right; } Node;

MAX_NODES is 100. Each Node stores data and the addresses of its left and right children. A null child pointer represents an empty subtree.

Lines 33–34Read a tree from preorder input
/* Preorder input: value, left subtree, right subtree; -1 means NULL. */
int readTree(Node **root, int *count) {

The comment describes value, left subtree, right subtree order, using -1 as an empty-subtree marker. Node **root receives the address of a caller-owned child or root pointer, allowing this helper to assign that pointer.

Lines 35–38Initialise the output pointer and read the next marker
    int value;
    *root = NULL;
    if (!readInt(&value)) return 0;
    if (value == -1) return 1;

Begin with *root = NULL so failed or empty input leaves a known pointer state. Return failure if no valid integer is read. A value of -1 represents an empty subtree and returns success without allocating a node.

Lines 39–41Check the node limit and allocate one node
    if (*count == MAX_NODES) return 0;
    Node *node = malloc(sizeof *node);
    if (!node) return 0;

Reject another nonempty node when count already equals 100. Allocate one Node and return failure on a null allocation result. The counter is shared across all recursive calls.

Lines 42–43Initialise and attach the new node before reading children
    node->data = value; node->left = node->right = NULL;
    *root = node; ++(*count);

Set data, initialise both children to NULL, then store node through the output pointer and increment the total count. If a later child read fails, the partial tree remains reachable for cleanup.

Lines 44–45Recursively build left and right subtrees
    return readTree(&node->left, count) && readTree(&node->right, count);
}

&node->left and &node->right are addresses of the actual child pointers. Both reads must succeed for the function to return success. Logical AND skips reading the right subtree when the left read fails.

Lines 46–49Free a tree in child-before-parent order
void freeTree(Node *root) {
    if (!root) return;
    freeTree(root->left); freeTree(root->right); free(root);
}

Return immediately on a null pointer. Recursively release the left subtree and then the right, then free the current node. Releasing the parent last avoids needing to read its links after free.

Lines 50–53Preorder: visit the node before its children
void preorder(const Node *root) {
    if (!root) return;
    printf(" %d", root->data); preorder(root->left); preorder(root->right);
}

Stop on NULL. For a real node, print its data, recursively traverse left, then right. For root 1 with children 2 and 3, this yields 1, 2, 3. Returning from a recursive call resumes at the next statement in its caller.

Lines 54–57Inorder: visit between the left and right subtrees
void inorder(const Node *root) {
    if (!root) return;
    inorder(root->left); printf(" %d", root->data); inorder(root->right);
}

The null-pointer check is the base case. Traverse left, print the current node, then traverse right. For the same three-node tree, the order is 2, 1, 3. Inorder is sorted only when the input tree also satisfies the BST ordering rule.

Lines 58–61Postorder: visit after both child subtrees
void postorder(const Node *root) {
    if (!root) return;
    postorder(root->left); postorder(root->right); printf(" %d", root->data);
}

Again stop on NULL. Traverse left, then right, and print the current node last. The three-node example gives 2, 3, 1. A parent is printed only after both recursive calls return.

Lines 62–63Start with an empty root and zero created nodes
int main(void) {
    Node *root = NULL; int count = 0;

main declares the root pointer and shared count. readTree will update them through their addresses.

Lines 64–66Handle a partial or invalid tree safely
    if (!readTree(&root, &count)) {
        puts("Invalid tree input or allocation failure."); freeTree(root); return 1;
    }

A failed read may already have allocated some nodes. Print the error, pass the reachable root to freeTree, and return failure instead of traversing an incomplete input as though it were valid.

Lines 67–69Run the three traversals independently
    printf("Preorder:"); preorder(root); putchar('\n');
    printf("Inorder:"); inorder(root); putchar('\n');
    printf("Postorder:"); postorder(root); putchar('\n');

Print a label, call the appropriate traversal on the same root, then print a newline. These traversals do not modify links, so all three see the same tree.

Lines 70–71Release the tree after the reports
    freeTree(root); return 0;
}

freeTree releases every allocated node. Return success from main only after the traversal output and cleanup are complete.

FOLLOW THE STATE

Dry run

StepOperation / stateResult
Input 1 2 −1 −1 3 −1 −1Root 1; left 2; right 3Three-node tree
PreorderRoot → left → right1 2 3
InorderLeft → root → right2 1 3
PostorderLeft → right → root2 3 1

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 2 -1 -1 3 -1 -1
OUTPUT
Preorder: 1 2 3
Inorder: 2 1 3
Postorder: 2 3 1

Sample 2

INPUT
-1
OUTPUT
Preorder:
Inorder:
Postorder:

Sample 3

INPUT
10 5 -1 7 -1 -1 20 15 -1 -1 -1
OUTPUT
Preorder: 10 5 7 20 15
Inorder: 5 7 10 15 20
Postorder: 7 5 15 20 10

Common mistakes

  • Include two −1 markers for every leaf’s missing children.
  • Do not mistake a binary tree for a binary search tree.
  • Keep each traversal’s visit position correct.

WHY THIS GROWTH RATE?

Time and space complexity

Each traversal O(n) time; recursive working space O(h). Tree storage O(n).

Let n be the node count and h the height. Every traversal visits each node exactly once and performs constant work per visit. Therefore one traversal costs O(n), and printing all three costs 3 × O(n) = O(n). The input has 2n + 1 node/NULL tokens, so construction is also O(n).

At most h recursive calls for non-NULL ancestors are simultaneously active; the extra call stack is O(h). For a balanced tree h is O(log n); a chain can have h = n and require O(n) stack space.

The allocated nodes require O(n) storage. Recursive reading and cleanup also use O(h) call-stack space; choosing non-recursive traversal does not remove recursion from those separate helper functions.

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.

How do preorder, inorder and postorder differ in the recursive functions?

Preorder visits root, left, right; inorder visits left, root, right; postorder visits left, right, root. Each uses the same child structure but places the root's print statement at a different stage.

Why does each recursive traversal immediately return for a NULL node?

NULL represents an absent child. Returning avoids dereferencing it and provides the base case that ends the recursion at the tree's boundaries.

Why does preorder input include -1 markers for missing children?

Values alone do not uniquely specify a general binary tree's shape. The markers state where subtrees are absent, allowing the reader to reconstruct both left and right child positions.

Can -1 also be stored as ordinary node data in this input format?

No. This format reserves -1 to mean an absent subtree. Supporting that data value would require a different absence marker or a separate encoding convention.

Why can inorder traversal of a general binary tree produce unsorted output?

Inorder defines only a visiting order, not an ordering property. Sorted output requires the additional binary-search-tree rule; the tree read by this experiment need not satisfy that rule.

Why is recursive traversal space described using tree height h?

At any moment, active calls correspond to a path from root toward a descendant. That path has O(h) nodes; a skewed tree can make h proportional to n, while a balanced tree has logarithmic height.