Unit V β Complete Solutions to All Practice Questions
Table of Contents
Practice Questions β BST
Q1. Construct a BST by inserting: 45, 15, 79, 90, 10, 55, 12, 20, 50
Insert each key β if less than current node go left, if greater go right.
Step-by-step:
- Insert 45 β root
- Insert 15 β 15 < 45, go left
- Insert 79 β 79 > 45, go right
- Insert 90 β 90 > 45 > 79, go right of 79
- Insert 10 β 10 < 45 < 15, go left of 15
- Insert 55 β 55 > 45, right; 55 < 79, left of 79
- Insert 12 β 12 < 45, left; 12 < 15, left; 12 > 10, right of 10
- Insert 20 β 20 < 45, left; 20 > 15, right of 15
- Insert 50 β 50 > 45, right; 50 < 79, left; 50 < 55, left of 55
Final BST:
45
/ \
15 79
/ \ / \
10 20 55 90
\ /
12 50
Q2. Delete nodes 15, 45, and 79 from the BST constructed in Q1.
Delete 15 (two children):
- In-order successor of 15 is 20 (smallest in right subtree)
- Replace 15 with 20, delete the original 20
45
/ \
20 79
/ / \
10 55 90
\ /
12 50
Delete 45 (two children β root):
- In-order successor of 45 is 50 (smallest in right subtree)
- Replace 45 with 50, delete the original 50
50
/ \
20 79
/ / \
10 55 90
\
12
Delete 79 (two children):
- In-order successor of 79 is 90 (smallest in right subtree β only child)
- Replace 79 with 90, remove original 90
50
/ \
20 90
/ /
10 55
\
12
Three cases of BST deletion:
- Leaf node β simply remove
- One child β replace with child
- Two children β replace with in-order successor (or predecessor), then delete successor
Q3. What is the in-order, pre-order, and post-order traversal of the tree in Q1?
Tree from Q1:
45
/ \
15 79
/ \ / \
10 20 55 90
\ /
12 50
In-order (Left, Root, Right): Visit left subtree, then root, then right subtree.
\[10, 12, 15, 20, 45, 50, 55, 79, 90\]Note: In-order traversal of a BST always gives sorted order.
Pre-order (Root, Left, Right):
\[45, 15, 10, 12, 20, 79, 55, 50, 90\]Post-order (Left, Right, Root):
\[12, 10, 20, 15, 50, 55, 90, 79, 45\]Q4. Write recursive and iterative versions of BST search.
Recursive:
TreeNode* searchRecursive(TreeNode* root, int key) {
if (root == NULL || root->data == key)
return root;
if (key < root->data)
return searchRecursive(root->left, key);
else
return searchRecursive(root->right, key);
}
Iterative:
TreeNode* searchIterative(TreeNode* root, int key) {
while (root != NULL && root->data != key) {
if (key < root->data)
root = root->left;
else
root = root->right;
}
return root; // NULL if not found, node if found
}
| Aspect | Recursive | Iterative |
|---|---|---|
| Time | $O(h)$ | $O(h)$ |
| Space | $O(h)$ β stack frames | $O(1)$ β no extra space |
| Best case | $O(1)$ β root is key | $O(1)$ |
| Worst case | $O(n)$ β skewed tree | $O(n)$ β skewed tree |
| Balanced BST | $O(\log n)$ | $O(\log n)$ |
Q5. What happens if you insert a sorted array into a BST? How can you build a balanced BST from a sorted array?
Inserting sorted array [10, 20, 30, 40, 50]:
Each element is greater than all previous, so it always goes to the right:
10
\
20
\
30
\
40
\
50
This creates a skewed tree (essentially a linked list). Height = $n - 1 = 4$. All operations become $O(n)$ instead of $O(\log n)$.
Building a balanced BST from sorted array:
Use the middle element as root recursively:
TreeNode* sortedArrayToBST(int arr[], int left, int right) {
if (left > right)
return NULL;
int mid = (left + right) / 2;
TreeNode* root = createNode(arr[mid]);
root->left = sortedArrayToBST(arr, left, mid - 1);
root->right = sortedArrayToBST(arr, mid + 1, right);
return root;
}
For [10, 20, 30, 40, 50]:
- Mid = 30 (root)
- Left: [10, 20] β mid = 10 with right child 20
- Right: [40, 50] β mid = 40 with right child 50
30
/ \
10 40
\ \
20 50
Height = 2 (balanced!). Operations: $O(\log n)$.
Practice Questions β AVL Tree
Q1. Insert 50, 20, 60, 10, 8, 15, 32, 46, 11, 48 into an AVL tree. Show rotations.
Insert 50: Root = 50
Insert 20: 20 < 50, left child. BFs: 50(+1), 20(0). OK.
Insert 60: 60 > 50, right child. BFs: 50(0), 20(0), 60(0). OK.
50
/ \
20 60
Insert 10: 10 < 50, left; 10 < 20, left. BFs: 50(+2!) β IMBALANCED!
Path: 50 β 20 β 10 (Left-Left) β LL Rotation (Right Rotate at 50)
Before: After LL rotation:
50(+2) 20
/ \ / \
20 60 10 50
/ / \
10 (empty) 60
Wait β let me recalculate. BF(20) = 1, BF(50) = 2. LL case.
Right rotate at 50:
20
/ \
10 50
\
60
BFs: 20(0), 10(0), 50(-1), 60(0). Balanced!
Insert 8: 8 < 20, left; 8 < 10, left.
20
/ \
10 50
/ \
8 60
BFs: 8(0), 10(+1), 20(+1), 50(-1), 60(0). All OK.
Insert 15: 15 < 20, left; 15 > 10, right.
20
/ \
10 50
/ \ \
8 15 60
BFs: 20(+1), 10(0), 50(-1). OK.
Insert 32: 32 > 20, right; 32 < 50, left.
20
/ \
10 50
/ \ / \
8 15 32 60
BFs: 20(0). OK.
Insert 46: 46 > 20, right; 46 < 50, left; 46 > 32, right.
20
/ \
10 50
/ \ / \
8 15 32 60
\
46
BFs: 32(-1), 50(+1), 20(-1). OK.
Insert 11: 11 < 20, left; 11 > 10, right; 11 < 15, left.
20
/ \
10 50
/ \ / \
8 15 32 60
/ \
11 46
BFs: 15(+1), 10(-1), 20(+1). OK.
Insert 48: 48 > 20, right; 48 < 50, left; 48 > 32, right; 48 > 46, right.
20
/ \
10 50
/ \ / \
8 15 32 60
/ \
11 46
\
48
BFs: 46(-1), 32(-2!) β IMBALANCED at 32!
Path: 32 β 46 β 48 (Right-Right) β RR Rotation (Left Rotate at 32)
After RR rotation at 32:
20
/ \
10 50
/ \ / \
8 15 46 60
/ / \
11 32 48
BFs: 46(0), 32(0), 48(0), 50(0). Balanced!
Final AVL Tree:
20
/ \
10 50
/ \ / \
8 15 46 60
/ / \
11 32 48
Q2. What is a balance factor? For what values of BF is a tree AVL-balanced?
Balance Factor (BF) of a node:
\[BF = \text{Height(Left Subtree)} - \text{Height(Right Subtree)}\]An AVL tree is balanced when every node has:
\[BF \in \{-1, 0, +1\}\]| BF | Meaning |
|---|---|
| +1 | Left subtree is 1 level taller (left-heavy) |
| 0 | Both subtrees have equal height (perfectly balanced) |
| -1 | Right subtree is 1 level taller (right-heavy) |
| +2 or more | Left-heavy imbalance β needs rotation |
| -2 or less | Right-heavy imbalance β needs rotation |
Q3. Explain all four types of AVL rotations with diagrams.
1. LL Rotation (Right Rotation):
Imbalance at node A, caused by insertion into the left subtree of Aβs left child.
Before: After (Right Rotate A):
A (+2) B (0)
/ / \
B (+1) C A (0)
/
C
2. RR Rotation (Left Rotation):
Imbalance at node A, caused by insertion into the right subtree of Aβs right child.
Before: After (Left Rotate A):
A (-2) B (0)
\ / \
B (-1) A C
\
C
3. LR Rotation (Left-Right):
Imbalance at A, caused by insertion into the right subtree of Aβs left child. Needs TWO rotations.
Before: Step 1 (Left Rotate B): Step 2 (Right Rotate A):
A (+2) A (+2) C (0)
/ / / \
B (-1) C (+1) B A
\ /
C B
4. RL Rotation (Right-Left):
Imbalance at A, caused by insertion into the left subtree of Aβs right child. Needs TWO rotations.
Before: Step 1 (Right Rotate B): Step 2 (Left Rotate A):
A (-2) A (-2) C (0)
\ \ / \
B (+1) C (-1) A B
/ \
C B
Summary:
| Case | BF(Node) | BF(Child) | Rotation |
|---|---|---|---|
| LL | +2 | +1 | Right rotate at node |
| RR | -2 | -1 | Left rotate at node |
| LR | +2 | -1 | Left rotate child, then right rotate node |
| RL | -2 | +1 | Right rotate child, then left rotate node |
Q4. What is the maximum height of an AVL tree with $n$ nodes?
The maximum height of an AVL tree with $n$ nodes is:
\[h_{\max} \leq 1.44 \log_2(n + 2) - 0.328\]Practically: $h \approx 1.44 \log_2 n$
This comes from the minimum number of nodes $N(h)$ in an AVL tree of height $h$:
\[N(h) = N(h-1) + N(h-2) + 1\]Base cases: $N(0) = 1$, $N(1) = 2$
| Height $h$ | Min nodes $N(h)$ |
|---|---|
| 0 | 1 |
| 1 | 2 |
| 2 | 4 |
| 3 | 7 |
| 4 | 12 |
| 5 | 20 |
$N(h)$ grows like Fibonacci numbers: $N(h) \approx \phi^h$ where $\phi = 1.618β¦$
This means AVL trees guarantee $O(\log n)$ height, making all operations $O(\log n)$.
Q5. Compare BST and AVL tree. Why donβt we always use AVL trees?
| Feature | BST | AVL Tree | Β | Β |
|---|---|---|---|---|
| Balance | Not guaranteed | Always balanced ($ | BF | \leq 1$) |
| Height | $O(n)$ worst case (skewed) | $O(\log n)$ always | Β | Β |
| Search | $O(n)$ worst | $O(\log n)$ worst | Β | Β |
| Insert | $O(n)$ worst | $O(\log n)$ + rotation cost | Β | Β |
| Delete | $O(n)$ worst | $O(\log n)$ + rotation cost | Β | Β |
| Rotations | None | Up to 2 per insert, $O(\log n)$ per delete | Β | Β |
| Implementation | Simple | Complex (balance tracking, rotations) | Β | Β |
Why not always use AVL trees?
- Extra overhead: Each insertion/deletion must check balance and potentially perform rotations
- More storage: Each node needs to store height or balance factor
- Insert-heavy workloads: The rotation cost makes AVL slower for frequent insertions
- Simpler alternatives exist: For random data, a plain BST is often balanced enough
- Red-black trees offer a middle ground β less strictly balanced but fewer rotations
When to use AVL: When search-heavy workloads demand guaranteed $O(\log n)$ performance (e.g., databases, dictionaries).
Practice Questions β Graphs
Q1. Represent the graph using adjacency matrix and adjacency list.
Graph:
A --- B
| / |
| / |
C --- D
Edges: (A,B), (A,C), (B,C), (B,D), (C,D)
Adjacency Matrix (0-indexed: A=0, B=1, C=2, D=3):
| Β | A | B | C | D |
|---|---|---|---|---|
| A | 0 | 1 | 1 | 0 |
| B | 1 | 0 | 1 | 1 |
| C | 1 | 1 | 0 | 1 |
| D | 0 | 1 | 1 | 0 |
Adjacency List:
A β [B, C]
B β [A, C, D]
C β [A, B, D]
D β [B, C]
| Representation | Space | Edge lookup | Add edge | List neighbors |
|---|---|---|---|---|
| Matrix | $O(V^2)$ | $O(1)$ | $O(1)$ | $O(V)$ |
| List | $O(V+E)$ | $O(\deg(v))$ | $O(1)$ | $O(\deg(v))$ |
Q2. Perform BFS and DFS on the graph from Q1 starting at vertex A.
BFS (Breadth-First Search) β uses Queue:
Queue: [A] Visit: A
Queue: [B, C] Visit: B (neighbor of A), C (neighbor of A)
Queue: [C, D] Visit: C already seen. Visit D (neighbor of B)
Queue: [D] Visit: D already seen
Queue: [] Done!
BFS Order: A β B β C β D
BFS Trace:
| Step | Queue | Visited | Current |
|---|---|---|---|
| 0 | [A] | {A} | β |
| 1 | [B, C] | {A, B, C} | A |
| 2 | [C, D] | {A, B, C, D} | B |
| 3 | [D] | {A, B, C, D} | C |
| 4 | [] | {A, B, C, D} | D |
DFS (Depth-First Search) β uses Stack (or recursion):
Start at A β go to first unvisited neighbor B
At B β go to first unvisited neighbor C
At C β go to first unvisited neighbor D
At D β no unvisited neighbors, backtrack
DFS Order: A β B β C β D
(Note: Order depends on the order neighbors are stored. If alphabetical: AβBβCβD)
DFS Trace (recursive):
| Call | Current | Neighbors | Visit |
|---|---|---|---|
| dfs(A) | A | B, C | A |
| dfs(B) | B | Aβ, C, D | B |
| dfs(C) | C | Aβ, Bβ, D | C |
| dfs(D) | D | Bβ, Cβ | D |
| backtrack | β | β | β |
Q3. What is topological sorting? Can it be applied to undirected graphs?
Topological sorting is a linear ordering of vertices in a Directed Acyclic Graph (DAG) such that for every directed edge $(u, v)$, vertex $u$ comes before vertex $v$ in the ordering.
Example:
A β B β D
| β
ββ C ββββ
Topological orders: A, B, C, D or A, C, B, D (both valid)
Can it be applied to undirected graphs? NO.
Reasons:
- No direction β In undirected graphs, edge {A,B} means both AβB and BβA, creating the requirement that A comes before B AND B comes before A β a contradiction.
- Cycles everywhere β Every edge in an undirected graph forms a cycle of length 2 (AβBβA). Topological sort requires a DAG (no cycles).
- No partial order β Topological sort relies on a partial order defined by edge direction, which doesnβt exist in undirected graphs.
Algorithm (Kahnβs β BFS-based):
- Compute in-degree of all vertices
- Enqueue all vertices with in-degree 0
- While queue not empty: dequeue $u$, output $u$, reduce in-degree of all neighbors
-
If output count β $ V $, graph has a cycle
Q4. Run Dijkstraβs algorithm on a weighted graph.
Graph (adjacency matrix):
0 1 2 3 4
0 [ 0 4 0 0 8 ]
1 [ 4 0 0 2 0 ]
2 [ 0 0 0 1 7 ]
3 [ 0 2 1 0 0 ]
4 [ 8 0 7 0 0 ]
Source: vertex 0
| Step | Visited | dist[0] | dist[1] | dist[2] | dist[3] | dist[4] | Pick |
|---|---|---|---|---|---|---|---|
| Init | {} | 0 | β | β | β | β | 0 |
| 1 | {0} | 0 | 4 | β | β | 8 | 1 (dist=4) |
| 2 | {0,1} | 0 | 4 | β | 6 | 8 | 3 (dist=6) |
| 3 | {0,1,3} | 0 | 4 | 7 | 6 | 8 | 2 (dist=7) |
| 4 | {0,1,3,2} | 0 | 4 | 7 | 6 | 8 | 4 (dist=8) |
Step 1: From 0: dist[1] = 0+4=4, dist[4] = 0+8=8
Step 2: From 1: dist[3] = min(β, 4+2) = 6
Step 3: From 3: dist[2] = min(β, 6+1) = 7, dist[1] = min(4, 6+2) = 4 (no change)
Step 4: From 2: dist[4] = min(8, 7+7) = 8 (no change)
Final shortest distances from 0: {0:0, 1:4, 2:7, 3:6, 4:8}
Shortest paths:
- 0β1: 0β1 (cost 4)
- 0β2: 0β1β3β2 (cost 7)
- 0β3: 0β1β3 (cost 6)
- 0β4: 0β4 (cost 8)
Q5. Compare BFS and DFS β data structures used, time complexity, and applications.
| Feature | BFS | DFS |
|---|---|---|
| Data Structure | Queue | Stack (or recursion) |
| Strategy | Level by level (breadth) | As deep as possible first |
| Time | $O(V + E)$ | $O(V + E)$ |
| Space | $O(V)$ β queue stores a level | $O(V)$ β stack stores a path |
| Shortest Path | Yes (unweighted) | No |
| Completeness | Finds nearest solution first | May explore far before near |
| Memory | More (stores all nodes at current depth) | Less (stores only one path) |
BFS Applications:
- Shortest path in unweighted graphs
- Level-order traversal of trees
- Web crawling (explore nearby pages first)
- Social network β find people within $k$ connections
DFS Applications:
- Topological sorting
- Cycle detection
- Finding connected components
- Solving mazes/puzzles (backtracking)
- Strongly connected components (Tarjanβs, Kosarajuβs)
Q6. What is a DAG? Give a real-life example of topological sorting.
DAG = Directed Acyclic Graph
A graph that is:
- Directed β edges have direction (arrows)
- Acyclic β no cycles (cannot follow edges in a loop back to the starting vertex)
Real-life example: Course Prerequisites
Math 101 β Math 201 β Math 301
β β
CS 101 β CS 201 ββββββββββ
β
CS 301
Topological sort: Math 101, CS 101, Math 201, CS 201, CS 301, Math 301
This ordering ensures you take prerequisites before any course that depends on them.
Other examples:
- Build systems: Compile dependencies in order (Makefiles)
- Task scheduling: Complete prerequisite tasks first
- Package managers: Install dependencies before the package
- Spreadsheet formulas: Evaluate cells that others depend on first
Practice Questions β Heaps
Q1. Build a max-heap from: [4, 10, 3, 5, 1, 8, 7]. Show each step.
Method: Build using bottom-up heapify. Start from the last non-leaf node (index $\lfloor n/2 \rfloor - 1 = 2$).
Initial array as tree:
4
/ \
10 3
/ \ / \
5 1 8 7
Heapify index 2 (value 3): Children: 8, 7. Max child = 8 > 3 β swap.
4
/ \
10 8
/ \ / \
5 1 3 7
Heapify index 1 (value 10): Children: 5, 1. Max child = 5 < 10 β no swap.
Heapify index 0 (value 4): Children: 10, 8. Max child = 10 > 4 β swap.
10
/ \
4 8
/ \ / \
5 1 3 7
Now heapify down index 1 (value 4): Children: 5, 1. Max child = 5 > 4 β swap.
10
/ \
5 8
/ \ / \
4 1 3 7
Final max-heap: [10, 5, 8, 4, 1, 3, 7]
Verification: Every parent β₯ its children. β
Q2. Perform 3 extractMax operations on the heap from Q1.
Heap: [10, 5, 8, 4, 1, 3, 7]
Extract 1 β Remove 10: Swap root (10) with last element (7), remove 10, heapify down.
[7, 5, 8, 4, 1, 3]
β 7 < 8 β swap with 8
[8, 5, 7, 4, 1, 3]
β 7 > 3 β no more swaps
Extracted: 10. Heap: [8, 5, 7, 4, 1, 3]
Extract 2 β Remove 8: Swap root (8) with last element (3), remove 8, heapify down.
[3, 5, 7, 4, 1]
β 3 < 7 β swap with 7
[7, 5, 3, 4, 1]
β 3 has no children below β done
Extracted: 8. Heap: [7, 5, 3, 4, 1]
Extract 3 β Remove 7: Swap root (7) with last element (1), remove 7, heapify down.
[1, 5, 3, 4]
β 1 < 5 β swap with 5
[5, 1, 3, 4]
β 1 < 4 β swap with 4
[5, 4, 3, 1]
Extracted: 7. Heap: [5, 4, 3, 1]
Elements extracted in order: 10, 8, 7 (decreasing β this is Heap Sort!)
Q3. What is the difference between a max-heap and a min-heap?
| Feature | Max-Heap | Min-Heap |
|---|---|---|
| Property | Parent β₯ children | Parent β€ children |
| Root | Maximum element | Minimum element |
| Extract | extractMax() | extractMin() |
| Use case | Find/remove largest | Find/remove smallest |
| Priority Queue | Highest priority = highest value | Highest priority = lowest value |
| Heap Sort | Produces ascending order | Produces descending order |
Max-Heap example:
50
/ \
30 40
/ \
10 20
Root = 50 (maximum)
Min-Heap example:
10
/ \
20 30
/ \
50 40
Root = 10 (minimum)
Q4. Why is a binary heap stored as an array instead of using pointers?
A binary heap is a complete binary tree β all levels are fully filled except possibly the last, which is filled left to right. This property makes array storage ideal:
Array index formulas (0-indexed):
- Parent of node $i$: $\lfloor (i-1)/2 \rfloor$
- Left child of node $i$: $2i + 1$
- Right child of node $i$: $2i + 2$
Advantages of array storage:
| Advantage | Explanation |
|---|---|
| No pointer overhead | Save 2 pointers per node (8-16 bytes each) |
| Cache-friendly | Contiguous memory β excellent cache locality |
| O(1) parent/child access | Simple arithmetic instead of following pointers |
| No memory fragmentation | Single array vs scattered heap allocations |
| Simple implementation | No malloc/free for tree operations |
Why it works: The complete tree property guarantees no βgapsβ in the array. Every index from 0 to $n-1$ is used.
This would NOT work for BSTs or AVL trees because they are not necessarily complete.
Q5. Implement Heap Sort using a max-heap.
void heapify(int arr[], int n, int i) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest])
largest = left;
if (right < n && arr[right] > arr[largest])
largest = right;
if (largest != i) {
int temp = arr[i];
arr[i] = arr[largest];
arr[largest] = temp;
heapify(arr, n, largest);
}
}
void heapSort(int arr[], int n) {
// Step 1: Build max-heap (O(n))
for (int i = n / 2 - 1; i >= 0; i--)
heapify(arr, n, i);
// Step 2: Extract elements one by one (O(n log n))
for (int i = n - 1; i > 0; i--) {
// Swap root (max) with last element
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// Heapify reduced heap
heapify(arr, i, 0);
}
}
Trace for [4, 10, 3, 5, 1]:
Build heap: [10, 5, 3, 4, 1]
| Step | Swap | Array | Heap portion |
|---|---|---|---|
| 1 | 10β1 | [1, 5, 3, 4, 10] | heapify β [5, 4, 3, 1, 10] |
| 2 | 5β1 | [1, 4, 3, 5, 10] | heapify β [4, 1, 3, 5, 10] |
| 3 | 4β3 | [3, 1, 4, 5, 10] | heapify β [3, 1, 4, 5, 10] |
| 4 | 3β1 | [1, 3, 4, 5, 10] | done |
Sorted: [1, 3, 4, 5, 10]
| Property | Value |
|---|---|
| Time | $O(n \log n)$ always |
| Space | $O(1)$ β in-place |
| Stable | No |
| Best for | Guaranteed $O(n \log n)$ without extra space |
Practice Questions β Hashing
Q1. What is hashing? Why is it useful?
Hashing is a technique that maps data (keys) to fixed-size values (hash codes) using a hash function, which determines the index/slot in a hash table where the data is stored.
\[h(key) = \text{hash function}(key) \rightarrow \text{index in table}\]Why hashing is useful:
| Operation | Array | BST | Hash Table |
|---|---|---|---|
| Search | $O(n)$ | $O(\log n)$ | $O(1)$ avg |
| Insert | $O(1)$/$O(n)$ | $O(\log n)$ | $O(1)$ avg |
| Delete | $O(n)$ | $O(\log n)$ | $O(1)$ avg |
Applications:
- Dictionaries / Symbol tables (compilers)
- Database indexing
- Caching (memoization)
- Password storage (hash + salt)
- Data deduplication
- Sets (checking membership in $O(1)$)
Q2. Insert keys 25, 42, 96, 101, 57, 31, 67 into a hash table of size 7.
Hash function: $h(k) = k \mod 7$
| Key | $k \mod 7$ |
|---|---|
| 25 | 4 |
| 42 | 0 |
| 96 | 5 |
| 101 | 3 |
| 57 | 1 |
| 31 | 3 (collision with 101!) |
| 67 | 4 (collision with 25!) |
(a) Separate Chaining:
Each slot is a linked list:
| Index | Chain |
|---|---|
| 0 | 42 β NULL |
| 1 | 57 β NULL |
| 2 | NULL |
| 3 | 101 β 31 β NULL |
| 4 | 25 β 67 β NULL |
| 5 | 96 β NULL |
| 6 | NULL |
(b) Linear Probing ($h(k, i) = (h(k) + i) \mod 7$):
| Key | Hash | Probes | Final Index |
|---|---|---|---|
| 25 | 4 | 4 (empty) | 4 |
| 42 | 0 | 0 (empty) | 0 |
| 96 | 5 | 5 (empty) | 5 |
| 101 | 3 | 3 (empty) | 3 |
| 57 | 1 | 1 (empty) | 1 |
| 31 | 3 | 3β4β5β6 | 6 |
| 67 | 4 | 4β5β6β(all full!)β 2 | 2 |
Table: [42, 57, 67, 101, 25, 96, 31]
(c) Quadratic Probing ($h(k, i) = (h(k) + i^2) \mod 7$):
| Key | Hash | Probes | Final Index |
|---|---|---|---|
| 25 | 4 | 4 | 4 |
| 42 | 0 | 0 | 0 |
| 96 | 5 | 5 | 5 |
| 101 | 3 | 3 | 3 |
| 57 | 1 | 1 | 1 |
| 31 | 3 | 3(full), 3+1=4(full), 3+4=0(full), 3+9=12%7=5(full), 3+16=19%7=5(full), 3+25=28%7=0(full)β¦ | Try: $(3+1)\%7=4$βfull, $(3+4)\%7=0$βfull, $(3+9)\%7=5$βfull, $(3+16)\%7=5$βfull. Hmm β letβs recheck. $i=1: (3+1)\%7=4$ full. $i=2: (3+4)\%7=0$ full. $i=3: (3+9)\%7=5$ full. $i=4: (3+16)\%7=5$ full. $i=5: (3+25)\%7=0$ full. $i=6: (3+36)\%7=4$ full. Cycle! Cannot insert. |
With table size 7 (prime), quadratic probing can fail when table is > half full. Some keys may need rehashing.
31 goes to index 2 (trying alternate sequence) or requires table resize.
67: $h = 4$, $i=1: 5$ full, $i=2: (4+4)\%7=1$ full, $i=3: (4+9)\%7=6$β6.
Q3. What is a collision? Compare separate chaining and open addressing.
Collision: When two or more keys hash to the same index: $h(k_1) = h(k_2)$ but $k_1 \neq k_2$.
| Feature | Separate Chaining | Open Addressing |
|---|---|---|
| Method | Each slot has a linked list | Find another empty slot in the table |
| Load factor | Can exceed 1.0 ($\alpha > 1$) | Must be $\alpha \leq 1$ |
| Memory | Extra for pointers | No extra (all in array) |
| Cache | Poor (linked list traversal) | Better (array access) |
| Deletion | Easy (remove from list) | Tricky (need tombstone markers) |
| Clustering | No clustering | Primary/secondary clustering |
| Performance | Graceful degradation | Degrades sharply near $\alpha = 1$ |
| Best when | Many collisions expected | Load factor stays < 0.75 |
Q4. Explain linear probing, quadratic probing, and double hashing with examples.
All are open addressing methods β when a collision occurs, probe for the next empty slot.
1. Linear Probing: $h(k, i) = (h(k) + i) \mod m$
Probe sequence: $h(k), h(k)+1, h(k)+2, \ldots$
Problem: Primary clustering β long clusters of occupied slots form, slowing searches.
2. Quadratic Probing: $h(k, i) = (h(k) + i^2) \mod m$
Probe sequence: $h(k), h(k)+1, h(k)+4, h(k)+9, \ldots$
Better than linear β jumps further, reduces primary clustering. Problem: Secondary clustering β keys with the same hash follow the same probe sequence.
3. Double Hashing: $h(k, i) = (h_1(k) + i \cdot h_2(k)) \mod m$
Uses a second hash function to determine the step size. Common choice: $h_2(k) = R - (k \mod R)$ where $R$ is a prime < $m$.
Example with m=7, keys: 10, 17, 24
$h(k) = k \mod 7$. All three hash to index 3!
| Method | 10β3 | 17 probes | 24 probes |
|---|---|---|---|
| Linear | 3 | 3β4 | 3β4β5 |
| Quadratic | 3 | 3β4 | 3β4β7%7=0 |
| Double ($h_2 = 5-(k\%5)$) | 3 | 3β3+3=6 | 3β3+1=4 |
Double hashing gives the most uniform distribution of probes.
Q5. What is the load factor $\alpha$? How does it affect performance?
Where $n$ = number of keys stored, $m$ = table size.
Effect on performance (Open Addressing with Uniform Hashing):
| $\alpha$ | Avg. probes (unsuccessful) | Avg. probes (successful) |
|---|---|---|
| 0.25 | 1.33 | 1.15 |
| 0.50 | 2.0 | 1.39 |
| 0.75 | 4.0 | 1.85 |
| 0.90 | 10.0 | 2.56 |
| 0.99 | 100.0 | 4.65 |
Formulas:
- Unsuccessful search: $\frac{1}{1-\alpha}$
- Successful search: $\frac{1}{\alpha} \ln \frac{1}{1-\alpha}$
For Separate Chaining:
- Average chain length = $\alpha$ (can exceed 1)
- Unsuccessful search: $O(1 + \alpha)$
- Successful search: $O(1 + \alpha/2)$
Best practice: Keep $\alpha < 0.75$ for open addressing. Rehash (double table size) when threshold is exceeded.
Q6. Why should the hash table size be a prime number?
Reason: A prime table size reduces the chance of patterns in key values causing clustering.
Example β Non-prime size (m=10): If keys are multiples of 5: 5, 10, 15, 20, 25β¦
- $5 \mod 10 = 5$
- $10 \mod 10 = 0$
- $15 \mod 10 = 5$ β collision!
- $20 \mod 10 = 0$ β collision!
Only slots 0 and 5 are used β terrible distribution.
Example β Prime size (m=7): Same keys: 5, 10, 15, 20, 25
- $5 \mod 7 = 5$
- $10 \mod 7 = 3$
- $15 \mod 7 = 1$
- $20 \mod 7 = 6$
- $25 \mod 7 = 4$
All different slots β much better distribution!
Mathematical reason: When $m$ is prime, it shares no common factors with the key (unless the key is a multiple of $m$). This means the modulo operation distributes keys more uniformly. With composite $m$, keys that share a factor with $m$ tend to cluster.
Additional reasons:
- Quadratic probing is guaranteed to visit all slots only when $m$ is prime
- Double hashing works best with prime table sizes
Comprehensive Unit V Practice Set
1. Define: binary tree, BST, AVL tree, complete binary tree, full binary tree.
| Term | Definition |
|---|---|
| Binary Tree | A tree where each node has at most 2 children (left and right) |
| BST | A binary tree where left child < root < right child for every node |
| AVL Tree | A BST that is self-balancing β balance factor of every node is in {-1, 0, +1} |
| Complete Binary Tree | All levels fully filled except possibly the last, which is filled left to right |
| Full Binary Tree | Every node has 0 or 2 children (no node has exactly one child) |
Examples:
Complete: Full: Neither:
1 1 1
/ \ / \ / \
2 3 2 3 2 3
/ \ / \ \
4 5 4 5 5
2. What are the three depth-first traversals of a binary tree?
Sample tree:
1
/ \
2 3
/ \
4 5
| Traversal | Order | Result |
|---|---|---|
| In-order | Left, Root, Right | 4, 2, 5, 1, 3 |
| Pre-order | Root, Left, Right | 1, 2, 4, 5, 3 |
| Post-order | Left, Right, Root | 4, 5, 2, 3, 1 |
Mnemonics:
- In-order: Root is in the middle
- Pre-order: Root comes first (pre = before)
- Post-order: Root comes last (post = after)
3. Why is in-order traversal of a BST always sorted?
In-order visits: left subtree β root β right subtree.
By the BST property:
- All nodes in the left subtree have values less than the root
- All nodes in the right subtree have values greater than the root
So in-order traversal visits all smaller values first (left subtree), then the current value (root), then all larger values (right subtree). Recursively, this produces values in ascending order.
Proof by induction: If in-order traversal of any BST subtree gives sorted output, then visiting left (sorted, all < root) + root + right (sorted, all > root) = sorted sequence for the whole subtree.
4. What is the balance factor in an AVL tree? When do rotations occur?
$BF = Height(Left) - Height(Right)$
| Rotations occur when $ | BF | > 1$ (i.e., BF = +2 or -2) after an insertion or deletion. |
| BF at node | BF at child | Rotation |
|---|---|---|
| +2 | +1 | LL (single right rotation) |
| +2 | -1 | LR (left-right double rotation) |
| -2 | -1 | RR (single left rotation) |
| -2 | +1 | RL (right-left double rotation) |
5. What is the difference between a graph and a tree?
| Feature | Tree | Graph |
|---|---|---|
| Cycles | No cycles (acyclic) | May have cycles |
| Root | Has a designated root | No root (unless rooted graph) |
| Edges | $n-1$ edges for $n$ nodes | Any number of edges |
| Path | Unique path between any two nodes | Multiple paths possible |
| Connectivity | Always connected | Not necessarily |
| Direction | Top-down (parentβchild) | Can be directed or undirected |
| Hierarchy | Hierarchical | Not necessarily |
A tree is a special case of a graph: a connected, acyclic, undirected graph.
6. What is hashing? Name three hash functions.
Hashing maps keys to table indices using a hash function for $O(1)$ average-case access.
Three hash functions:
-
Division Method: $h(k) = k \mod m$ β simple, effective when $m$ is prime.
-
Mid-Square Method: Square the key, extract middle digits. $h(k) = \text{middle digits of } k^2$.
-
Multiplication Method: $h(k) = \lfloor m \cdot (kA \mod 1) \rfloor$ where $A \approx 0.6180339887$ (Knuthβs suggestion).
7. Construct a BST by inserting 50, 30, 70, 20, 40, 60, 80. Write all three traversals.
50
/ \
30 70
/ \ / \
20 40 60 80
In-order: 20, 30, 40, 50, 60, 70, 80 (sorted!)
Pre-order: 50, 30, 20, 40, 70, 60, 80
Post-order: 20, 40, 30, 60, 80, 70, 50
8. Insert 30, 20, 10, 25, 40, 35 into an AVL tree. Show all rotations.
Insert 30, 20: No imbalance.
30
/
20
Insert 10: BF(30) = +2 β LL Rotation (right rotate at 30)
Before: After:
30(+2) 20
/ / \
20(+1) 10 30
/
10
Insert 25: 25 > 20, right; 25 < 30, left.
20
/ \
10 30
/
25
BFs: 30(+1), 20(-1). OK.
Insert 40: 40 > 20, right; 40 > 30, right.
20
/ \
10 30
/ \
25 40
BFs: 30(0), 20(-1). OK.
Insert 35: 35 > 20, right; 35 > 30, right; 35 < 40, left.
20
/ \
10 30
/ \
25 40
/
35
BFs: 40(+1), 30(-2!) β RL Rotation at 30.
Step 1 β Right rotate at 40:
20
/ \
10 30
/ \
25 35
\
40
Step 2 β Left rotate at 30:
20
/ \
10 35
/ \
30 40
/
25
BFs: 30(+1), 35(+1), 20(-1). All OK!
Final AVL tree:
20
/ \
10 35
/ \
30 40
/
25
9. Run BFS and DFS on the graph starting from vertex A.
Graph:
A --- B --- E
| / |
C --- D F
Edges: (A,B), (A,C), (B,E), (C,D), (D,E), (E,F)
BFS (from A, using Queue):
| Step | Queue | Visited | Dequeue |
|---|---|---|---|
| 0 | [A] | {A} | β |
| 1 | [B, C] | {A,B,C} | A |
| 2 | [C, E] | {A,B,C,E} | B |
| 3 | [E, D] | {A,B,C,E,D} | C |
| 4 | [D, F] | {A,B,C,E,D,F} | E |
| 5 | [F] | {A,B,C,E,D,F} | D (no new neighbors) |
| 6 | [] | {A,B,C,E,D,F} | F |
BFS Order: A, B, C, E, D, F
DFS (from A, recursive):
dfs(A) β visit A, neighbors: B, C
dfs(B) β visit B, neighbors: Aβ, E
dfs(E) β visit E, neighbors: Bβ, D, F
dfs(D) β visit D, neighbors: C, Eβ
dfs(C) β visit C, neighbors: Aβ, Dβ β backtrack
backtrack from D
dfs(F) β visit F, neighbors: Eβ β backtrack
backtrack from E
backtrack from B
backtrack from A
DFS Order: A, B, E, D, C, F
10. Run Dijkstraβs algorithm from vertex S.
Graph:
S --2-- A --3-- D
| | |
6 1 5
| | |
B --4-- C --2-- E
| Step | Visited | dist[S] | dist[A] | dist[B] | dist[C] | dist[D] | dist[E] | Pick |
|---|---|---|---|---|---|---|---|---|
| Init | {} | 0 | β | β | β | β | β | S |
| 1 | {S} | 0 | 2 | 6 | β | β | β | A(2) |
| 2 | {S,A} | 0 | 2 | 6 | 3 | 5 | β | C(3) |
| 3 | {S,A,C} | 0 | 2 | 6 | 3 | 5 | 5 | D(5) or E(5) |
| 4 | {S,A,C,D} | 0 | 2 | 6 | 3 | 5 | 5 | E(5) |
| 5 | {S,A,C,D,E} | 0 | 2 | 6 | 3 | 5 | 5 | B(6) |
Trace details:
- From S: dist[A] = 2, dist[B] = 6
- From A: dist[C] = min(β, 2+1) = 3, dist[D] = min(β, 2+3) = 5
- From C: dist[B] = min(6, 3+4) = 6 (no change), dist[E] = min(β, 3+2) = 5
- From D: dist[E] = min(5, 5+5) = 5 (no change)
- From E: no improvements
Shortest distances from S: S=0, A=2, B=6, C=3, D=5, E=5
Shortest paths:
- SβA: SβA (2)
- SβB: SβB (6)
- SβC: SβAβC (3)
- SβD: SβAβD (5)
- SβE: SβAβCβE (5)
11. Build a max-heap from [3, 1, 6, 5, 2, 4]. Then extract max twice.
Build max-heap (bottom-up heapify):
Initial tree:
3
/ \
1 6
/ \ /
5 2 4
Heapify index 2 (value 6): children: 4. 6 > 4 β OK.
Heapify index 1 (value 1): children: 5, 2. Max = 5 > 1 β swap.
3
/ \
5 6
/ \ /
1 2 4
Heapify index 0 (value 3): children: 5, 6. Max = 6 > 3 β swap.
6
/ \
5 3
/ \ /
1 2 4
Continue heapifying 3: child 4 > 3 β swap.
6
/ \
5 4
/ \ /
1 2 3
Max-heap: [6, 5, 4, 1, 2, 3]
Extract max 1 β Remove 6: Swap 6 with last (3): [3, 5, 4, 1, 2]. Pop 6. Heapify: 3 < 5 β swap. [5, 3, 4, 1, 2]. 3 > 1 and 2 β OK.
Heap: [5, 3, 4, 1, 2]. Extracted: 6.
Extract max 2 β Remove 5: Swap 5 with last (2): [2, 3, 4, 1]. Pop 5. Heapify: 2 < 4 β swap. [4, 3, 2, 1]. OK.
Heap: [4, 3, 2, 1]. Extracted: 5.
12. Insert keys 10, 22, 31, 4, 15, 28, 17, 88, 59 into a hash table of size 11.
Hash function: $h(k) = k \mod 11$
| Key | $k \mod 11$ |
|---|---|
| 10 | 10 |
| 22 | 0 |
| 31 | 9 |
| 4 | 4 |
| 15 | 4 |
| 28 | 6 |
| 17 | 6 |
| 88 | 0 |
| 59 | 4 |
(a) Separate Chaining:
| Index | Chain |
|---|---|
| 0 | 22 β 88 β NULL |
| 1 | NULL |
| 2 | NULL |
| 3 | NULL |
| 4 | 4 β 15 β 59 β NULL |
| 5 | NULL |
| 6 | 28 β 17 β NULL |
| 7 | NULL |
| 8 | NULL |
| 9 | 31 β NULL |
| 10 | 10 β NULL |
(b) Linear Probing:
| Key | Hash | Probes | Index |
|---|---|---|---|
| 10 | 10 | 10 β | 10 |
| 22 | 0 | 0 β | 0 |
| 31 | 9 | 9 β | 9 |
| 4 | 4 | 4 β | 4 |
| 15 | 4 | 4β5 β | 5 |
| 28 | 6 | 6 β | 6 |
| 17 | 6 | 6β7 β | 7 |
| 88 | 0 | 0β1 β | 1 |
| 59 | 4 | 4β5β6β7β8 β | 8 |
Final table:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| 22 | 88 | β | β | 4 | 15 | 28 | 17 | 59 | 31 | 10 |
13. Explain binary tree traversals with recursive code and detailed trace.
typedef struct TreeNode {
int data;
struct TreeNode *left, *right;
} TreeNode;
void inorder(TreeNode* root) {
if (root == NULL) return;
inorder(root->left);
printf("%d ", root->data);
inorder(root->right);
}
void preorder(TreeNode* root) {
if (root == NULL) return;
printf("%d ", root->data);
preorder(root->left);
preorder(root->right);
}
void postorder(TreeNode* root) {
if (root == NULL) return;
postorder(root->left);
postorder(root->right);
printf("%d ", root->data);
}
Trace on tree:
1
/ \
2 3
/ \
4 5
In-order trace:
inorder(1) β inorder(2) β inorder(4) β inorder(NULL) β return
β print 4
β inorder(NULL) β return
β print 2
β inorder(5) β inorder(NULL) β print 5 β inorder(NULL)
β print 1
β inorder(3) β inorder(NULL) β print 3 β inorder(NULL)
Output: 4 2 5 1 3
| Pre-order: 1 2 4 5 3 | Post-order: 4 5 2 3 1 |
Time: $O(n)$, Space: $O(h)$ where $h$ is the tree height.
14. Explain BST insertion and deletion with all three cases.
Insertion: Compare key with root, go left if smaller, right if larger, insert when NULL is reached.
TreeNode* insert(TreeNode* root, int key) {
if (root == NULL) return createNode(key);
if (key < root->data)
root->left = insert(root->left, key);
else if (key > root->data)
root->right = insert(root->right, key);
return root;
}
Deletion β Three cases:
Case 1: Leaf node β Simply remove it.
Delete 4:
5 5
/ \ β / \
3 7 3 7
/
4
Case 2: One child β Replace node with its child.
Delete 3 (has left child 2):
5 5
/ \ β / \
3 7 2 7
/
2
Case 3: Two children β Replace with in-order successor (smallest in right subtree) or predecessor.
Delete 5 (root, has two children):
In-order successor of 5 = 7
5 7
/ \ β /
3 7 3
TreeNode* deleteNode(TreeNode* root, int key) {
if (root == NULL) return NULL;
if (key < root->data)
root->left = deleteNode(root->left, key);
else if (key > root->data)
root->right = deleteNode(root->right, key);
else {
// Node found
if (root->left == NULL) { // Case 1 & 2
TreeNode* temp = root->right;
free(root);
return temp;
} else if (root->right == NULL) { // Case 2
TreeNode* temp = root->left;
free(root);
return temp;
}
// Case 3: Two children
TreeNode* succ = root->right;
while (succ->left != NULL) succ = succ->left;
root->data = succ->data;
root->right = deleteNode(root->right, succ->data);
}
return root;
}
15. Explain AVL tree rotations. Why are AVL trees preferred over plain BSTs?
See Q3 of AVL Tree section for detailed rotation diagrams.
Why AVL over BST:
- BST can degrade to $O(n)$ for sorted/nearly-sorted input (skewed tree)
- AVL guarantees $O(\log n)$ for ALL operations
- Critical for databases and real-time systems where worst-case performance matters
Trade-off: AVL has rotation overhead per insert/delete β worth it only when search frequency significantly exceeds modification frequency.
16. Compare BFS and DFS.
See Q5 of Graphs section for the detailed comparison table.
Key difference in behavior:
Graph: A - B - E
| |
C - D - F
BFS from A: A, B, C, E, D, F (level by level)
DFS from A: A, B, E, D, C, F (deep first, then backtrack)
BFS explores all neighbors before going deeper. DFS goes as deep as possible before backtracking.
17. Explain Dijkstraβs shortest path algorithm.
See Q4 of Graphs section for a complete trace, or Q10 of Comprehensive section for another example.
Algorithm summary:
- Initialize: dist[source] = 0, all others = β
- Repeat V times: Pick unvisited vertex $u$ with minimum dist
- For each neighbor $v$ of $u$: if $dist[u] + weight(u,v) < dist[v]$, update $dist[v]$
- Mark $u$ as visited
Time: $O(V^2)$ with array, $O((V+E)\log V)$ with min-heap
Limitation: Does NOT work with negative edge weights (use Bellman-Ford instead).
18. Explain hashing with collision resolution techniques.
See Q3 and Q4 of the Hashing section for detailed explanations.
Summary comparison:
| Method | Pros | Cons |
|---|---|---|
| Separate Chaining | Simple, no clustering, $\alpha > 1$ OK | Extra memory for pointers |
| Linear Probing | Simple, cache-friendly | Primary clustering |
| Quadratic Probing | Reduces clustering | Secondary clustering, may not find empty slot |
| Double Hashing | Best distribution, no clustering | Needs second hash function |
19. Explain priority queues and binary heaps.
Priority Queue: An ADT where elements have priorities. The highest-priority element is served first regardless of insertion order.
Binary Heap is the most common implementation:
| Operation | Time |
|---|---|
| Insert | $O(\log n)$ |
| Extract Max/Min | $O(\log n)$ |
| Peek | $O(1)$ |
| Build heap | $O(n)$ |
Insert (Heap Up / Sift Up):
void insert(int heap[], int* n, int value) {
heap[*n] = value;
int i = *n;
(*n)++;
while (i > 0 && heap[i] > heap[(i-1)/2]) {
int temp = heap[i];
heap[i] = heap[(i-1)/2];
heap[(i-1)/2] = temp;
i = (i - 1) / 2;
}
}
Extract Max (Heap Down / Sift Down):
int extractMax(int heap[], int* n) {
int max = heap[0];
(*n)--;
heap[0] = heap[*n];
heapify(heap, *n, 0);
return max;
}
Example: Insert 15 into max-heap [20, 10, 8, 5, 3]:
[20, 10, 8, 5, 3, 15]
15 > parent(8) β swap: [20, 10, 15, 5, 3, 8]
15 > parent(20)? No β done.
20. Explain topological sorting. When is it applicable? Give an algorithm and trace on a DAG.
Topological sorting produces a linear ordering of vertices in a DAG such that for every directed edge $u \to v$, $u$ appears before $v$.
Applicable only to DAGs (Directed Acyclic Graphs). Cannot be applied to:
- Undirected graphs
- Graphs with cycles
Kahnβs Algorithm (BFS-based):
1. Compute in-degree of all vertices
2. Enqueue all vertices with in-degree 0
3. While queue not empty:
a. Dequeue vertex u, add to result
b. For each neighbor v of u:
- Decrease in-degree[v] by 1
- If in-degree[v] == 0, enqueue v
4. If result size β |V|, graph has a cycle
Trace on DAG:
A β B β D
β β
C βββββββ
Edges: AβB, AβC, BβD, CβD
In-degrees: A=0, B=1, C=1, D=2
| Step | Queue | Dequeue | Update in-degrees | Result |
|---|---|---|---|---|
| Init | [A] | β | A:0, B:1, C:1, D:2 | [] |
| 1 | [B, C] | A | B:0, C:0, D:2 | [A] |
| 2 | [C, D] | B | C:0, D:1 | [A, B] |
| 3 | [D] | C | D:0 | [A, B, C] |
| 4 | [] | D | β | [A, B, C, D] |
Topological Order: A, B, C, D β
Also valid: A, C, B, D (multiple valid orderings exist).
DFS-based approach: Run DFS and push each vertex to a stack after all its descendants are processed. Pop the stack for topological order.
Time: $O(V + E)$ for both algorithms.