Skip to lab content

EXPERIMENT 06 · DATA STRUCTURES LAB

Tree traversals — non-recursive

SOLUTION 06BC173 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.

Preorder uses one explicit stack and pushes right before left so left is popped first. Inorder pushes the left spine, then visits a node and moves right. Postorder uses two stacks: reverse root-right-left order to obtain left-right-root. Only these traversal functions are non-recursive; tree reading and cleanup still use recursion.

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.

Inorder with an explicit stack

Tree traversals — non-recursive: Inorder with an explicit stack

Open full-size flowchart ↗

Preorder with an explicit stack
Tree traversals — non-recursive: Preorder with an explicit stack

Open full-size flowchart ↗

Postorder with two stacks
Tree traversals — non-recursive: Postorder with two stacks

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;
    const Node *stack[MAX_NODES]; int top = 0;
    stack[top++] = root;
    while (top) {
        const Node *node = stack[--top]; printf(" %d", node->data);
        if (node->right) stack[top++] = node->right;
        if (node->left) stack[top++] = node->left;
    }
}
void inorder(const Node *root) {
    const Node *stack[MAX_NODES], *current = root; int top = 0;
    while (current || top) {
        while (current) { stack[top++] = current; current = current->left; }
        current = stack[--top]; printf(" %d", current->data);
        current = current->right;
    }
}
void postorder(const Node *root) {
    if (!root) return;
    const Node *stack1[MAX_NODES], *stack2[MAX_NODES]; int top1 = 0, top2 = 0;
    stack1[top1++] = root;
    while (top1) {
        const Node *node = stack1[--top1]; stack2[top2++] = node;
        if (node->left) stack1[top1++] = node->left;
        if (node->right) stack1[top1++] = node->right;
    }
    while (top2) printf(" %d", stack2[--top2]->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-iterative.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–53Prepare an explicit stack for preorder
void preorder(const Node *root) {
    if (!root) return;
    const Node *stack[MAX_NODES]; int top = 0;
    stack[top++] = root;

Return for an empty tree. The stack stores pointers to existing nodes, not copies of subtrees. top is the number of stored pointers, initially zero. Push root using stack[top++] so it becomes the first pending node.

Lines 54–55Pop and visit the next pending node
    while (top) {
        const Node *node = stack[--top]; printf(" %d", node->data);

Continue while top is nonzero. --top reduces the count before selecting the last pushed pointer, then print that node’s data. This is last-in, first-out behaviour.

Lines 56–59Push right before left so left is visited first
        if (node->right) stack[top++] = node->right;
        if (node->left) stack[top++] = node->left;
    }
}

The right child is pushed first and the left child second. Because the stack pops the last pushed child first, this yields node-left-right order. The braces finish the loop and preorder. Only existing child pointers are pushed.

Lines 60–61Prepare inorder’s current pointer and stack
void inorder(const Node *root) {
    const Node *stack[MAX_NODES], *current = root; int top = 0;

current begins at root. The stack remembers ancestors whose left subtrees are being explored. top again counts stored pointers.

Lines 62–63Save the path down the left side
    while (current || top) {
        while (current) { stack[top++] = current; current = current->left; }

Continue while either current exists or a saved ancestor remains. Push current, then move left repeatedly. Reaching NULL means the next inorder visit must come from the saved ancestors.

Lines 64–67Visit a saved ancestor and begin its right subtree
        current = stack[--top]; printf(" %d", current->data);
        current = current->right;
    }
}

Pop the most recent ancestor, print its data and set current to its right child. The outer condition then repeats the left descent for that subtree. For an empty tree both current and top start empty, so no pop occurs.

Lines 68–71Prepare two stacks for iterative postorder
void postorder(const Node *root) {
    if (!root) return;
    const Node *stack1[MAX_NODES], *stack2[MAX_NODES]; int top1 = 0, top2 = 0;
    stack1[top1++] = root;

For a nonempty tree, create two pointer stacks and zero their counts. Push root into stack1. stack2 will collect a temporary order that can be reversed by popping.

Lines 72–73Move the next node from stack1 to stack2
    while (top1) {
        const Node *node = stack1[--top1]; stack2[top2++] = node;

Pop a node from stack1 and push it into stack2. No data is printed at this stage, because postorder must put children before parents.

Lines 74–76Push left and then right onto stack1
        if (node->left) stack1[top1++] = node->left;
        if (node->right) stack1[top1++] = node->right;
    }

The right child is processed before the left because it is pushed last. This collects node-right-left order in stack2; the final reversal will turn it into left-right-node order. The brace ends the collecting loop.

Lines 77–78Pop stack2 to produce postorder
    while (top2) printf(" %d", stack2[--top2]->data);
}

Each --top2 removes the last stored pointer and prints its data. Popping reverses the collected order, giving the required left-right-node traversal. The next brace closes postorder.

Lines 79–80Start the driver with an empty tree
int main(void) {
    Node *root = NULL; int count = 0;

Declare main’s root pointer and total count before calling the shared tree reader.

Lines 81–83Reject bad input and free a partially built tree
    if (!readTree(&root, &count)) {
        puts("Invalid tree input or allocation failure."); freeTree(root); return 1;
    }

If readTree fails, print the message, release the reachable partial tree and return failure. Traversals run only after the complete input succeeds.

Lines 84–86Print all three iterative traversal results
    printf("Preorder:"); preorder(root); putchar('\n');
    printf("Inorder:"); inorder(root); putchar('\n');
    printf("Postorder:"); postorder(root); putchar('\n');

Each line prints a label, calls its traversal and adds a newline. The traversals use explicit stacks and leave the tree itself unchanged.

Lines 87–88Free the tree and finish main
    freeTree(root); return 0;
}

Release the allocated nodes and return success. The traversal functions are non-recursive, but this program still uses recursive readTree and freeTree helpers for construction and cleanup.

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.
  • Push right before left for preorder.
  • Two-stack postorder uses more memory than recursion on a balanced tree.

WHY THIS GROWTH RATE?

Time and space complexity

Each traversal O(n) time; preorder/inorder active stack O(h); two-stack postorder O(n). 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).

Preorder and inorder keep pending ancestors/branches in an explicit stack, using O(h) active entries. The postorder implementation stores every node in its second stack, giving O(n) active storage. Its two-stack method is not O(h) space. The arrays reserve 100 pointer slots each in this bounded example; a generalized implementation sizes them to the supported node count.

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.

Why is the right child pushed before the left child in iterative preorder?

A stack removes the most recently pushed item first. Pushing right and then left makes left the next node visited, reproducing root-left-right preorder.

What information does the explicit stack preserve during iterative inorder?

It holds ancestors whose left subtrees are still being processed or whose own visit is pending. Popping an ancestor after reaching NULL restores the next visit before moving into its right subtree.

Why is the inorder loop condition current or stack-not-empty?

Traversal must continue while either a current subtree remains or a saved ancestor awaits processing. Checking only current would stop too early whenever a left chain ends at NULL.

How do two stacks produce postorder in this implementation?

The first stack processes nodes in root-right-left order and puts them into the second stack. Popping the second reverses that order, producing left-right-root postorder.

Why are pointers stored in traversal stacks rather than just node values?

A value alone does not identify the node's children, especially when values repeat. A node pointer preserves the identity and child links needed to continue traversing the original tree.

Is the complete iterative-traversal program free of recursion?

No. Its three traversal functions use explicit stacks, but readTree and freeTree are recursive. The two-stack postorder also stores O(n) node pointers, so it should not be described as requiring only O(h) active stack space.