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:
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;, nottnode_t *left;. ANULLchild is an end; the tree is named by its root; an empty tree isroot == 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_edgeispush_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¶
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:
- Draw the final tree. Which key is the root? Which keys are leaves?
- Where would
28go if you inserted it next? Trace the path from the root. - Now start over with an empty tree and insert the sorted sequence
5 10 15 20 25. Draw that tree. -
What shape did the sorted insert produce, and what is its height? What does
searchcost on it, in big-O, compared to the tree from part 1? -
Check your understanding: the same five keys
5 10 15 20 25could 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:
- Write its keys in inorder (left, self, right).
- Write its keys in preorder (self, left, right).
- Write its keys in postorder (left, right, self).
-
One of those three sequences is special for a BST. Which one, and what is special about it?
-
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):
- Run BFS starting from vertex
0. Write the queue contents after each dequeue, and the final visit order. - Group the visit order into rings by distance from
0: which vertices are at distance 0, 1, 2? -
Run BFS again starting from vertex
5. Give the visit order. -
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 */
insert:if (root == NULL) return new_node(key);then recurse left or right and reassign (root->left = insert(root->left, key);), returningroot.search: base caseroot == 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 doessearchvisit 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_print: recurse left, printroot->key, recurse right. Confirm the output is sorted.tree_height: empty tree returns-1; otherwise1 + 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_treebe postorder? Rewrite it wrongly as "freerootfirst, then recurse intoroot->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:
(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):mallocthe struct,callocthe bucket array so every head startsNULL.add_edge(g, u, v):push_frontvontobuckets[u]anduontobuckets[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-1let 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.
- Mark
startvisited and enqueue it. While the queue is non-empty: dequeueu, 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?