Skip to content

Handout: Lecture 14 In-Class Exercises

Part A is pen-and-paper: build and read binary search trees by hand, and hand-simulate a breadth-first traversal. Part B is at the keyboard: build a BST and search it, walk it in order and free it, then build a graph as an array of linked lists and traverse it breadth-first. None of these repeats an earlier exercise; they drill today's moves - the two-child node, recursion on trees, the three traversals, the adjacency list, and BFS with a queue and a visited array.

Try each one yourself first; we will discuss in class, and the solutions are in a separate document afterward. Compile with warnings on:

clang -Wall -Wextra -std=c17 myprog.c -o myprog

Reminders that will keep you out of trouble today:

  • A tree node is a self-referential struct with two child pointers (left, right), so it must keep its tag - struct tnode *left;, not tnode_t *left;. A NULL child is an end; the tree is named by its root; an empty tree is root == NULL.
  • The BST invariant: every key in the left subtree is smaller than the node, every key in the right subtree is larger. Insert and search both recurse into one child; insert returns the (sub)tree so the parent can reattach it - root->left = insert(root->left, key); - the tree version of return-the-head.
  • Inorder (left, self, right) prints a BST in sorted order. Postorder (left, right, self) is the order you must free a tree in: children before parent.
  • An adjacency list is an array of linked lists indexed by vertex number (no hash, no collisions). add_edge is push_front; an undirected edge is stored at both endpoints.
  • BFS uses a queue (FIFO) and a visited array. Mark a vertex visited when you enqueue it, never on dequeue, or cycles cause redundant (or infinite) work.

Set up

mkdir -p ~/cmsc14300/lec14
cd ~/cmsc14300/lec14

Start every C exercise from these includes and this node type:

#include <stdio.h>
#include <stdlib.h>

struct tnode { int key; struct tnode *left, *right; };
typedef struct tnode tnode_t;

Part A - Reasoning on paper (pen and paper)

Exercise A1 - Build a BST, then break it

Start from an empty tree and insert these keys in this order, drawing the tree after all of them:

42  25  63  12  30  55  70
  1. Draw the final tree. Which key is the root? Which keys are leaves?
  2. Where would 28 go if you inserted it next? Trace the path from the root.
  3. Now start over with an empty tree and insert the sorted sequence 5 10 15 20 25. Draw that tree.
  4. What shape did the sorted insert produce, and what is its height? What does search cost on it, in big-O, compared to the tree from part 1?

  5. Check your understanding: the same five keys 5 10 15 20 25 could form a short, bushy tree or the tall chain you just drew. What determines which one you get, and why does a BST built from already-sorted input lose the whole advantage a BST was supposed to give you?

Exercise A2 - Read a tree three ways

Here is a BST:

            (8)
           /   \
        (3)      (10)
       /   \        \
    (1)    (6)      (14)
           /  \      /
        (4)   (7)  (13)
  1. Write its keys in inorder (left, self, right).
  2. Write its keys in preorder (self, left, right).
  3. Write its keys in postorder (left, right, self).
  4. One of those three sequences is special for a BST. Which one, and what is special about it?

  5. Check your understanding: you free a tree by freeing every node exactly once. Which of the three traversals is the correct order to free the nodes in, and what specifically goes wrong if you free a node before recursing into its children?

Exercise A3 - Hand-simulate a breadth-first traversal

An undirected graph has 6 vertices with these adjacency lists (neighbors listed in the order BFS will scan them):

0: 1  2
1: 0  3  4
2: 0  4
3: 1  5
4: 1  2  5
5: 3  4
  1. Run BFS starting from vertex 0. Write the queue contents after each dequeue, and the final visit order.
  2. Group the visit order into rings by distance from 0: which vertices are at distance 0, 1, 2?
  3. Run BFS again starting from vertex 5. Give the visit order.

  4. Check your understanding: the graph has a cycle (1 - 4 - 5 - 3 - 1). What in the BFS algorithm stops it from going around that cycle forever, and what would happen if you marked a vertex visited on dequeue instead of on enqueue?


Part B - At the keyboard

Exercise B1 - Insert and search a BST

Write insert and search, then read a count n followed by n integer keys, build a BST by inserting them, and then answer membership queries: read integers until end of input and print whether each is in the tree.

tnode_t *insert(tnode_t *root, int key);   /* recurse into one child, return root */
tnode_t *search(tnode_t *root, int key);   /* return the node, or NULL */
input:                          output:
5                               40: found
50 30 70 40 90                  55: not found
40 55 90                        90: found
  • insert: if (root == NULL) return new_node(key); then recurse left or right and reassign (root->left = insert(root->left, key);), returning root.
  • search: base case root == NULL || root->key == key, otherwise recurse into exactly one child.
  • Check your understanding: feed the program the keys already sorted (5 / 10 20 30 40 50). It still works - but how many nodes does search visit in the worst case now, and why is that the degenerate case from Exercise A1?

Exercise B2 - Inorder print, height, and free

Extend B1's tree. Write inorder_print, tree_height, and free_tree, then on the tree built from 50 30 70 40 90 print the keys inorder, print the height, and free the whole tree.

void inorder_print(tnode_t *root);   /* left, self, right */
int  tree_height(tnode_t *root);     /* edges on the longest path; empty = -1 */
void free_tree(tnode_t *root);       /* postorder: children before parent */
inorder: 30 40 50 70 90
height : 2
  • inorder_print: recurse left, print root->key, recurse right. Confirm the output is sorted.
  • tree_height: empty tree returns -1; otherwise 1 + max(height(left), height(right)).
  • free_tree: free the left subtree, then the right subtree, then the node itself. Build with warnings on and confirm it is clean (valgrind if available).
  • Check your understanding: why must free_tree be postorder? Rewrite it wrongly as "free root first, then recurse into root->left" in your head - which pointer are you reading after it has been freed?

Exercise B3 - Build a graph as an array of linked lists

Represent an undirected graph with nverts vertices as an array of adjacency lists. Write graph_create, add_edge (store the edge at both endpoints with push_front), and print_graph. Build this graph and print it:

edges: {0,1} {0,3} {1,2} {2,3} {3,4}
0: -> 3 -> 1
1: -> 2 -> 0
2: -> 3 -> 1
3: -> 4 -> 2 -> 0
4: -> 3

(Neighbors read newest-first because add_edge uses push_front; the order within a list does not matter.)

struct adj { int to; struct adj *next; };
typedef struct adj adj_t;
struct graph { int nverts; adj_t **buckets; };
typedef struct graph graph_t;
  • graph_create(nverts): malloc the struct, calloc the bucket array so every head starts NULL.
  • add_edge(g, u, v): push_front v onto buckets[u] and u onto buckets[v].
  • Check your understanding: this is the same array-of-linked-lists shape as the Lecture 10 hash table. Name the one thing a hash table needed that this does not - and explain why vertices 0 .. V-1 let you skip it.

Exercise B4 - Breadth-first traversal

Add bfs(graph_t *g, int start) to B3's graph. Use an integer array as a queue (head/tail indices) and a calloced visited array. Print the visit order starting from vertex 0.

bfs from 0: 0 3 1 4 2
  • Mark start visited and enqueue it. While the queue is non-empty: dequeue u, print it, and for each neighbor not yet visited, mark it and enqueue it.
  • The visit order depends on adjacency-list order, so if your lists differ from the sample the ring order may differ - that is fine, as long as vertices come out in rings by distance from the start.
  • Check your understanding: you marked vertices visited on enqueue. Suppose you moved the marking to dequeue instead. On this graph, name one vertex that would get enqueued twice, and explain why the traversal still terminates even though it now does extra work.

Stretch - take it further

Depth-first traversal, and is the graph connected?

Write a recursive depth-first traversal dfs(graph_t *g, int u, int *visited) that marks u visited, prints it, and recurses into each unvisited neighbor. Compare its visit order to BFS on the graph from B3 - DFS plunges down one path before backing up, BFS fans out in rings.

Then use a traversal to answer a real question: is the graph connected? Run one traversal from vertex 0, then check whether every vertex was visited. If any visited[v] is still 0, vertex v is unreachable from 0 and the graph is not connected. Test it by deleting the {3,4} edge and confirming vertex 4 becomes unreachable.

  • DFS needs no queue - the call stack is its queue, which is exactly the Lecture 6 connection: each recursive call is a frame, and "back up" is a return.
  • One sentence to write down: BFS and DFS visit the same set of reachable vertices (so either answers "connected?"), but in a different order. What decides the order in each?