📚 Chapters

📐 Advanced Data Structures & Algorithms

Unit 1 — Foundations of Algorithms & Array-Based Data Structures

📐 Chapter 1.1 — Foundations of Algorithms
📐 CHAPTER 1.1 — FOUNDATIONS OF ALGORITHMS MIND MAP
Asymptotic Notations → O (worst/upper), Ω (best/lower), Θ (tight/average bound)
Recursion → Base Case (stops it) + Recursive Case (calls itself with smaller input)
Recursion Tree → draw tree, find cost/level, multiply by number of levels
Back Substitution → expand recurrence repeatedly until pattern emerges, generalize with summation
Master Method → T(n)=aT(n/b)+f(n), compare f(n) with n^(log_b a) → 3 cases

1. Asymptotic Notations — Big O, Theta, Omega

📖 Asymptotic Notation: A mathematical way to describe how an algorithm's running time (or space) GROWS as the input size (n) grows large — ignoring constants and lower-order terms.
NotationRepresentsMeaning
Big O (O)Upper BoundWORST case — algorithm will NEVER take longer than this
Omega (Ω)Lower BoundBEST case — algorithm will take AT LEAST this long
Theta (Θ)Tight BoundAVERAGE/exact case — upper and lower bounds are the same order
Ω(g(n)) Θ(g(n)) O(g(n)) [Best Case] [Tight Bound] [Worst Case] ▁▁ ▁▁▁▁▁▁▁▁▁▁ ▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ╱ ╱ ╲ ╲ f(n)≥c·g(n) c₁g(n)≤f(n)≤c₂g(n) f(n)≤c·g(n) (lower limit) (sandwiched between) (upper limit)
Fig: Big O, Omega, Theta — Bounding f(n)
Formal Definitions:
O(g(n)): f(n) ≤ c·g(n) for all n ≥ n₀
Ω(g(n)): f(n) ≥ c·g(n) for all n ≥ n₀
Θ(g(n)): c₁·g(n) ≤ f(n) ≤ c₂·g(n) for all n ≥ n₀
ComplexityNameExample
O(1)ConstantArray access by index
O(log n)LogarithmicBinary Search
O(n)LinearLinear Search
O(n log n)LinearithmicMerge Sort, Quick Sort (average)
O(n²)QuadraticBubble Sort, nested loops
O(2ⁿ)ExponentialNaive Fibonacci recursion
Worked Justification — Linear Search Worst Case:
💡 Why Linear Search is O(n) in the worst case:
Linear Search checks each element ONE BY ONE from the start until it finds the target (or reaches the end).

Worst case happens when the target is the LAST element checked — either it's at the very last position, or it's NOT present at all. In both situations, ALL n elements must be examined.

Justification: Since the number of comparisons grows linearly with input size n (1 comparison for n=1, up to n comparisons for size n), the worst-case time complexity is O(n).
💡 Exam Tip: When a question asks to "state AND justify" a complexity, don't just write O(n) — explain WHY (i.e. describe the worst-case scenario that forces all n elements to be checked).

2. Recursion — Basics & How It Works

📖 Recursion: A technique where a function calls ITSELF to solve smaller instances of the same problem, until it reaches a BASE CASE that stops the recursion.
Every recursive function needs:
1. Base Case — the condition that stops recursion (prevents infinite calls)
2. Recursive Case — the function calling itself with a SMALLER input, moving toward the base case
C++ Example — Factorial:
int factorial(int n) { if (n == 0 || n == 1) // Base case return 1; return n * factorial(n - 1); // Recursive case } // factorial(4) = 4 * factorial(3) // = 4 * 3 * factorial(2) // = 4 * 3 * 2 * factorial(1) // = 4 * 3 * 2 * 1 = 24
Call Stack for factorial(4): factorial(4) = 4 * factorial(3) factorial(3) = 3 * factorial(2) factorial(2) = 2 * factorial(1) factorial(1) = 1 ← Base case hit factorial(2) returns 2*1 = 2 factorial(3) returns 3*2 = 6 factorial(4) returns 4*6 = 24
Fig: Recursive Call Stack — Unwinding
⚠️ Without a Base Case: The function calls itself forever → Stack Overflow error (each call uses stack memory, which eventually runs out).
💡 Exam Tip: "Write a recursive function" style questions always expect the base case shown explicitly — clearly state why it stops recursion, not just the code.

3. Solving Recurrences — Recursion Tree Method

📖 Recurrence Relation: An equation that defines an algorithm's running time T(n) in terms of its running time on SMALLER inputs.

📖 Recursion Tree Method: Draws out every recursive call as a tree, calculates the work done at EACH LEVEL, then sums the work across all levels to get the total.
Worked Example — T(n) = 2T(n/2) + n:
Level 0: n → cost = n ╱ ╲ Level 1: n/2 n/2 → cost = n/2+n/2 = n ╱ ╲ ╱ ╲ Level 2: n/4 n/4 n/4 n/4 → cost = 4×(n/4) = n . . . . ... continues until size = 1 ... Total levels = log₂(n) + 1 Cost per level = n (constant across all levels) Total cost = n × (log₂n + 1) = O(n log n)
Fig: Recursion Tree for T(n) = 2T(n/2) + n
Steps to Solve using Recursion Tree:
1. Draw the tree — each node = one recursive call, labeled with its input size
2. Find cost at EACH level (usually the non-recursive work, like the "+n" part)
3. Find total NUMBER of levels (usually log₂n for divide-by-2 recurrences)
4. Multiply cost-per-level × number-of-levels = total cost
5. Add cost of the leaves (base case) if it differs from other levels
Second Example — T(n) = 3T(n/4) + n²:
💡 Quick Solve:
Level 0 cost: n²
Level 1 cost: 3 × (n/4)² = 3n²/16
Level 2 cost: 9 × (n/16)² = 9n²/256
Ratio between levels = 3/16 (decreasing geometric series)
Since ratio < 1, the SUM is dominated by Level 0 → T(n) = O(n²)
💡 Exam Tip: If per-level cost is DECREASING (like example 2), the answer is dominated by the ROOT (first level). If cost is CONSTANT across levels (like example 1), multiply by total levels. If cost is INCREASING, the answer is dominated by the LEAVES.

4. Solving Recurrences — Back Substitution Method

📖 Back Substitution Method: Repeatedly SUBSTITUTES the recurrence relation into itself, expanding it step by step, until a clear PATTERN emerges — then generalizes that pattern for any n.
Worked Example — T(n) = T(n−1) + n, T(1) = 1:
T(n) = T(n-1) + n T(n-1) = T(n-2) + (n-1) T(n-2) = T(n-3) + (n-2) Substituting back: T(n) = T(n-2) + (n-1) + n T(n) = T(n-3) + (n-2) + (n-1) + n T(n) = T(n-k) + (n-k+1) + ... + (n-1) + n When n-k = 1, i.e. k = n-1: T(n) = T(1) + 2 + 3 + ... + n T(n) = 1 + [n(n+1)/2 - 1] T(n) = n(n+1)/2 = O(n²)
Steps to Solve using Back Substitution:
1. Write the recurrence for T(n-1), T(n-2), etc. by substituting smaller values
2. Substitute these back into the ORIGINAL equation, one level at a time
3. Look for the PATTERN after a few substitutions (usually forms a summation)
4. Express the pattern in terms of a general step "k"
5. Find the value of k where the recursion hits the BASE CASE
6. Substitute k back in and simplify using summation formulas
Cross-Verify — Applying Back Substitution to T(n) = 2T(n/2) + n:
T(n) = 2T(n/2) + n = 2[2T(n/4) + n/2] + n = 4T(n/4) + n + n = 4T(n/4) + 2n = 4[2T(n/8) + n/4] + 2n = 8T(n/8) + n + 2n = 8T(n/8) + 3n = ... pattern: 2^k · T(n/2^k) + k·n When n/2^k = 1, i.e. k = log₂n: T(n) = n·T(1) + n·log₂n T(n) = O(n log n) ← matches the Recursion Tree result!
💡 Exam Tip: Back Substitution works best for SIMPLE linear recurrences (T(n) = T(n-1) + f(n) type) — for divide-and-conquer recurrences (T(n/2) type), the Recursion Tree or Master Method is usually faster and cleaner. That said, as shown above, Back Substitution CAN verify the same answer as the Recursion Tree method — useful for double-checking your work.

5. Solving Recurrences — Master Method

📖 Master Method: A direct "plug-and-check" formula for solving recurrences of the specific form T(n) = aT(n/b) + f(n) — no need to draw trees or expand terms manually.
Standard Form: T(n) = aT(n/b) + f(n)
a = number of subproblems, b = factor by which size shrinks, f(n) = cost of work outside recursion

Compare f(n) with nlog_b(a)
CaseConditionResult
Case 1f(n) = O(nlog_b(a) − ε) for some ε > 0 (f(n) grows SLOWER)T(n) = Θ(nlog_b(a))
Case 2f(n) = Θ(nlog_b(a)) (f(n) grows at the SAME rate)T(n) = Θ(nlog_b(a) · log n)
Case 3f(n) = Ω(nlog_b(a) + ε) (f(n) grows FASTER) + regularity conditionT(n) = Θ(f(n))
Worked Example 1 — Merge Sort: T(n) = 2T(n/2) + n:
a=2, b=2, f(n)=n
nlog_b(a) = nlog₂2 = n¹ = n
f(n) = n = Θ(n¹) → Case 2 applies
T(n) = Θ(n log n)
Worked Example — Evaluating T(n) at a Specific Value (n=64):
💡 Q: For T(n) = 2T(n/2) + n, calculate T(n) when n = 64.

Step 1 — Solve using Master Method: a=2, b=2, f(n)=n → Case 2 → T(n) = Θ(n log n)
Step 2 — Substitute n = 64:
T(64) = 64 × log₂(64) = 64 × 6 = 384
(since 2⁶ = 64, so log₂64 = 6)
Worked Example 2 — T(n) = T(n/2) + 1 (Binary Search):
a=1, b=2, f(n)=1
nlog_b(a) = nlog₂1 = n⁰ = 1
f(n) = 1 = Θ(n⁰) → Case 2 applies
T(n) = Θ(log n)
⚠️ When Master Method DOESN'T Apply: If f(n) is not a simple polynomial (e.g. T(n) = 2T(n/2) + n/log n), or the recurrence isn't in the exact aT(n/b)+f(n) form — fall back to Recursion Tree method instead.
💡 Exam Tip: Always calculate nlog_b(a) FIRST, then compare it to f(n) — this single comparison decides which of the 3 cases applies. Practice identifying a, b, and f(n) correctly from the given recurrence — that's where most marks are lost.
🔃 Chapter 1.2 — Sorting Techniques
🔃 CHAPTER 1.2 — SORTING TECHNIQUES MIND MAP
Quick Sort → pick pivot, partition (smaller-left, larger-right), recurse; O(n log n) avg, O(n²) worst, in-place
Merge Sort → divide into halves, merge back sorted; ALWAYS O(n log n), needs O(n) extra space, stable
Radix Sort → sort digit-by-digit (LSD→MSD) using stable Counting Sort; O(d×(n+b))
Bucket Sort → distribute into buckets by range, sort each, concatenate; O(n+k) if uniform, O(n²) if skewed

1. Quick Sort

📖 Quick Sort: A Divide-and-Conquer algorithm that picks a "pivot" element, partitions the array so smaller elements go LEFT and larger go RIGHT of the pivot, then recursively sorts both halves.
C++ Implementation:
int partition(int arr[], int low, int high) { int pivot = arr[high]; // choosing last element as pivot int i = low - 1; for (int j = low; j < high; j++) { if (arr[j] < pivot) { i++; swap(arr[i], arr[j]); } } swap(arr[i + 1], arr[high]); return i + 1; // returns pivot's final position } void quickSort(int arr[], int low, int high) { if (low < high) { int pi = partition(arr, low, high); quickSort(arr, low, pi - 1); // sort left of pivot quickSort(arr, pi + 1, high); // sort right of pivot } }
Worked Dry-Run — Array: [38, 27, 43, 3, 9, 82, 10]:
Pivot = 10 (last element) Elements < 10: 3, 9 → placed at front [3, 9, | 10 | 38, 27, 43, 82] ← 10 is now at correct position (index 2) Recursively sort LEFT [3, 9] and RIGHT [38, 27, 43, 82] Left [3, 9]: pivot=9, 3<9 → [3, 9] already sorted Right [38, 27, 43, 82]: pivot=82 Elements < 82: 38, 27, 43 → [38, 27, 43, | 82] Sort [38, 27, 43]: pivot=43, 38,27<43 → [27, 38, | 43] Sort [27, 38]: pivot=38, 27<38 → [27, 38] sorted Final sorted array: [3, 9, 10, 27, 38, 43, 82]
PYQ-Style Example — Array: [29, 10, 14, 37, 13]:
Pivot = 13 (last element) Elements < 13: only 10 Partition: [10, | 13 | 14, 37, 29] ← 13 now at correct position (index 1) Recursively sort LEFT [10] (already sorted, 1 element) Recursively sort RIGHT [14, 37, 29]: Pivot = 29 (last element) Elements < 29: only 14 Partition: [14, | 29 | 37] ← 29 now at correct position Sort LEFT [14] (sorted) and RIGHT [37] (sorted) → [14, 29, 37] Final sorted array: [10, 13, 14, 29, 37]
💡 Note: The exact array digits in some scanned question papers can be hard to read clearly — always double-check the given numbers in your own paper before starting the partition trace, since even one misread digit changes the entire trace.
CaseTime ComplexityWhen It Happens
Best CaseO(n log n)Pivot always splits array into 2 equal halves
Average CaseO(n log n)Random pivot selection
Worst CaseO(n²)Array already sorted (or reverse sorted) + pivot = first/last element
💡 Exam Tip: Quick Sort is IN-PLACE (O(log n) space for recursion stack, no extra array needed) — this is its biggest advantage over Merge Sort. Worst case happens on already-sorted input if pivot is always first/last element — using a random or median pivot avoids this.

2. Merge Sort

📖 Merge Sort: A Divide-and-Conquer algorithm that splits the array into HALVES recursively until each piece has just 1 element, then MERGES pairs back together in sorted order.
C++ Implementation:
void merge(int arr[], int left, int mid, int right) { int n1 = mid - left + 1, n2 = right - mid; int L[n1], R[n2]; for (int i = 0; i < n1; i++) L[i] = arr[left + i]; for (int j = 0; j < n2; j++) R[j] = arr[mid + 1 + j]; int i = 0, j = 0, k = left; while (i < n1 && j < n2) { if (L[i] <= R[j]) arr[k++] = L[i++]; else arr[k++] = R[j++]; } while (i < n1) arr[k++] = L[i++]; while (j < n2) arr[k++] = R[j++]; } void mergeSort(int arr[], int left, int right) { if (left < right) { int mid = left + (right - left) / 2; mergeSort(arr, left, mid); // sort left half mergeSort(arr, mid + 1, right); // sort right half merge(arr, left, mid, right); // merge both halves } }
Worked Dry-Run — Array: [38, 27, 43, 3, 9, 82, 10]:
DIVIDE (split until single elements): [38,27,43,3,9,82,10] ↓ split [38,27,43,3] [9,82,10] ↓ split ↓ split [38,27] [43,3] [9,82] [10] ↓ ↓ ↓ [38][27] [43][3] [9][82] [10] CONQUER (merge back in sorted order): [38][27] → [27,38] [43][3] → [3,43] [27,38] + [3,43] → [3,27,38,43] [9][82] → [9,82] [9,82] + [10] → [9,10,82] FINAL MERGE: [3,27,38,43] + [9,10,82] → [3,9,10,27,38,43,82]
PYQ-Style Example — Array with a Negative Number: [17, 9, 13, 55, -2, 8, 40, 3]:
DIVIDE: [17,9,13,55,-2,8,40,3] ↓ split [17,9,13,55] [-2,8,40,3] ↓ split ↓ split [17,9] [13,55] [-2,8] [40,3] ↓ ↓ ↓ ↓ [17][9] [13][55] [-2][8] [40][3] CONQUER (merge back — negative numbers compare normally, -2 is smallest): [17][9] → [9,17] [13][55] → [13,55] [9,17]+[13,55] → [9,13,17,55] [-2][8] → [-2,8] [40][3] → [3,40] [-2,8]+[3,40] → [-2,3,8,40] FINAL MERGE: [9,13,17,55] + [-2,3,8,40] → [-2,3,8,9,13,17,40,55]
💡 Key Point: Merge Sort's comparison logic (L[i] <= R[j]) works identically for negative numbers — no special handling needed, since -2 < 3 is evaluated the same way as any other comparison.
CaseTime Complexity
Best / Average / WorstO(n log n) — ALWAYS, regardless of input
✅ Advantage: Guaranteed O(n log n) in every case — no worst-case degradation like Quick Sort. Also stable (equal elements keep their relative order).
❌ Disadvantage: Needs O(n) EXTRA space for the temporary arrays during merging — not in-place like Quick Sort.
💡 Exam Tip: "Sort the list using MERGESORT and write all steps" questions want BOTH the divide phase (splitting) AND the conquer phase (merging) shown clearly — don't skip straight to the final answer.

3. Radix Sort

📖 Radix Sort: A NON-comparison sorting algorithm that sorts numbers digit by digit — starting from the LEAST significant digit (LSD) to the most significant digit (MSD), using a stable sort (like Counting Sort) at each digit position.
C++ Implementation:
int getMax(int arr[], int n) { int mx = arr[0]; for (int i = 1; i < n; i++) if (arr[i] > mx) mx = arr[i]; return mx; } void countSort(int arr[], int n, int exp) { int output[n], count[10] = {0}; for (int i = 0; i < n; i++) count[(arr[i] / exp) % 10]++; for (int i = 1; i < 10; i++) count[i] += count[i - 1]; for (int i = n - 1; i >= 0; i--) { output[count[(arr[i] / exp) % 10] - 1] = arr[i]; count[(arr[i] / exp) % 10]--; } for (int i = 0; i < n; i++) arr[i] = output[i]; } void radixSort(int arr[], int n) { int m = getMax(arr, n); for (int exp = 1; m / exp > 0; exp *= 10) countSort(arr, n, exp); }
Worked Dry-Run — Array: [170, 45, 75, 90, 802, 24, 2, 66]:
Sort by ONES digit (exp=1): [170, 90, 802, 2, 24, 45, 75, 66] Sort by TENS digit (exp=10): [802, 2, 24, 45, 66, 170, 75, 90] Sort by HUNDREDS digit (exp=100): [2, 24, 45, 66, 75, 90, 170, 802] FINAL SORTED: [2, 24, 45, 66, 75, 90, 170, 802]
Time Complexity: O(d × (n + b))
d = number of digits in the max number, n = number of elements, b = base (usually 10)
💡 Exam Tip: Radix Sort's stability is CRITICAL — it must sort by each digit using a STABLE method (Counting Sort), otherwise the ordering from a previous digit-pass gets destroyed. This is a very common "why must the inner sort be stable" reasoning question.

4. Bucket Sort

📖 Bucket Sort: Distributes elements into a number of "buckets" based on their value range, sorts each bucket individually (often using Insertion Sort), then concatenates all buckets in order.
C++ Implementation (for values between 0 and 1):
void bucketSort(float arr[], int n) { vector buckets[n]; // Step 1: put each element into its bucket for (int i = 0; i < n; i++) { int bucketIndex = n * arr[i]; // scale value to bucket index buckets[bucketIndex].push_back(arr[i]); } // Step 2: sort each bucket individually for (int i = 0; i < n; i++) sort(buckets[i].begin(), buckets[i].end()); // Step 3: concatenate all buckets back into arr[] int index = 0; for (int i = 0; i < n; i++) for (int j = 0; j < buckets[i].size(); j++) arr[index++] = buckets[i][j]; }
Worked Dry-Run — Array: [0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12]:
n = 8 buckets, indices 0-7 (each bucket handles range 0.125) Bucket 0 (0.0-0.125): [0.12] Bucket 1 (0.125-0.25): [0.17, 0.21] Bucket 2 (0.25-0.375): [0.26] Bucket 3 (0.375-0.5): [0.39] Bucket 5 (0.625-0.75): [0.72] Bucket 6 (0.75-0.875): [0.78] Bucket 7 (0.875-1.0): [0.94] Sort each bucket individually, then concatenate: [0.12, 0.17, 0.21, 0.26, 0.39, 0.72, 0.78, 0.94]
CaseTime ComplexityCondition
Best/AverageO(n + k)Data uniformly distributed across buckets (k = number of buckets)
WorstO(n²)All elements land in the SAME bucket (highly skewed data)
💡 Exam Tip: Bucket Sort works best when input is UNIFORMLY distributed over a known range (like 0 to 1) — if data is skewed/clustered, most elements fall into few buckets and performance degrades toward O(n²), same as its per-bucket sort algorithm's worst case.
Ready for Exam? Sab padh liya? Ab Quick Revision karo — formulas, code aur key points ek jagah! Quick Revision Karo →
Quick Revision — Last Minute Exam Prep!
📌 How to Use: Read this 5-10 minutes before exam. Contains all important points in condensed form. Focus on tables, comparisons, and key formulas!

📐 Chapter 1.1 — Foundations of Algorithms

📖 3 Notations:
O (Big O) = worst/upper bound | Ω (Omega) = best/lower bound | Θ (Theta) = tight/average bound
🔑 Common Complexities (fastest → slowest):
O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ)
RECURSION TREE — Quick Steps:
1. Draw tree, 2. Cost per level, 3. Number of levels, 4. Multiply

Example recall: T(n)=2T(n/2)+n → cost/level=n, levels=log n → T(n)=O(n log n)
💡 Back Substitution — Quick Recall:
T(n)=T(n-1)+n → expand → T(n)=T(1)+2+3+...+n = n(n+1)/2 = O(n²)
✅ Master Method — 3 Cases:
T(n)=aT(n/b)+f(n), compare f(n) with n^(log_b a):
Case 1: f(n) smaller → T(n)=Θ(n^(log_b a))
Case 2: f(n) equal → T(n)=Θ(n^(log_b a)·log n)
Case 3: f(n) bigger → T(n)=Θ(f(n))

Merge Sort T(n)=2T(n/2)+n → Case 2 → Θ(n log n)
TopicKey FactTrick
Big O / Ω / ΘUpper / Lower / Tight boundDefault = worst case (Big O)
RecursionBase Case + Recursive CaseNo base case → Stack Overflow
Recursion TreeCost/level × levelsDecreasing cost → root dominates
Back SubstitutionExpand until pattern seenBest for T(n)=T(n-1)+f(n) type
Master MethodCompare f(n) vs n^(log_b a)3 cases based on comparison

🔃 Chapter 1.2 — Sorting Techniques

📖 Quick Sort:
Pick pivot → partition (smaller-left, larger-right) → recurse. In-place, O(n log n) avg, O(n²) worst (sorted input + bad pivot).
🔑 Merge Sort — Quick Recall:
Divide into halves → merge back sorted.
ALWAYS O(n log n) — no worst case degradation. Needs O(n) extra space. Stable.
RADIX SORT:
Sort digit-by-digit LSD→MSD using stable Counting Sort.
Time = O(d × (n+b)), d=digits, b=base(10)
Inner sort MUST be stable or ordering breaks!
💡 Bucket Sort — Quick Recall:
Distribute into buckets by range → sort each → concatenate.
O(n+k) if uniform data, O(n²) worst if all elements land in 1 bucket.
AlgorithmBestAverageWorstSpace
Quick SortO(n log n)O(n log n)O(n²)O(log n)
Merge SortO(n log n)O(n log n)O(n log n)O(n)
Radix SortO(d(n+b))O(d(n+b))O(d(n+b))O(n+b)
Bucket SortO(n+k)O(n+k)O(n²)O(n+k)

⚠️ Common Exam Mistakes

❌ Forgetting to state the base case explicitly in recursive code — always show it clearly
❌ Confusing Big O (worst case) with Big Θ (average/tight case) — they answer different questions
❌ In Master Method, forgetting to compare f(n) with n^(log_b a) FIRST before picking a case
❌ Writing Quick Sort as always O(n log n) — it's O(n²) in the WORST case (sorted input + bad pivot)
❌ Forgetting Merge Sort needs O(n) EXTRA space — it's NOT in-place like Quick Sort
❌ In Radix Sort, using an UNSTABLE inner sort — breaks the digit-by-digit ordering
❌ Skipping the divide phase when asked to "sort using Merge Sort, write all steps" — both divide AND merge steps are expected

✅ Pre-Exam Checklist

☑ Big O, Omega, Theta — definitions + which represents which case
☑ Recursion — base case + recursive case, stack overflow risk
☑ Recursion Tree Method — draw tree, sum cost per level
☑ Back Substitution Method — expand and generalize the pattern
☑ Master Method — all 3 cases, comparing f(n) to n^(log_b a)
☑ Quick Sort — C++ code, partition logic, best/avg/worst cases
☑ Merge Sort — C++ code, divide + merge steps, always O(n log n)
☑ Radix Sort — C++ code, digit-by-digit sorting, stability requirement
☑ Bucket Sort — C++ code, uniform vs skewed data performance
☑ Time/space complexity table for all 4 sorting algorithms

🎯 Exam Strategy

2 Mark Questions:
• Direct definition/code + 1 small example. Time: 3-4 minutes.
• "Write a recursive function" → show clean C++ code with base case clearly marked.

5 Mark Questions:
• Recurrence solving — show EVERY step (tree levels, substitution steps, or master method comparison), don't jump to the answer.
• Sorting trace questions — show BOTH the algorithm logic AND a full dry-run on the given array.
• Time: 7-8 minutes per question.

Marks-saving tip:
Even if a full trace isn't finished, writing correct C++ code/pseudocode and identifying the recurrence relation correctly earns partial marks!
🌟 All the Best!
ADSA is code + numerical heavy — recurrence relations aur sorting dry-runs by hand practice karo, C++ code bhi likhna practice karo, aur tu ready hai! 💪📐
📄 Previous Year Questions
📌 Source: Mid Semester Test-1 (MST-1), Academic Year 2025-2026 — Unit 1 only. Maximum Marks: 20, Time: 1 Hour.
Section A (5 × 2 = 10 marks)
2M MST-1
List the differences between Big O, Big Θ (Theta), and Big Ω (Omega) notations.
Big O (O): Upper bound — worst case, algorithm never takes longer than this.
Big Ω (Omega): Lower bound — best case, algorithm takes at least this long.
Big Θ (Theta): Tight bound — average/exact case, when upper and lower bounds are of the same order.
See Chapter 1.1, Section 1 for the full table and diagram.
2M MST-1
State and justify the worst case complexity of linear search.
Worst case complexity = O(n).

Justification: Linear Search checks elements one by one. The worst case occurs when the target is the LAST element, or not present at all — in both situations, all n elements must be checked, so the number of comparisons grows linearly with n.
2M MST-1
State the worst-case time complexity of Quick Sort and Merge Sort.
Quick Sort worst case: O(n²) — happens when the array is already sorted (or reverse sorted) AND the pivot chosen is always the first/last element.

Merge Sort worst case: O(n log n) — Merge Sort's complexity is the SAME in best, average, AND worst case, since it always splits exactly in half regardless of input.
2M MST-1
State how the Merge Sort algorithm uses recursion to divide and conquer the problem. Array: 17, 9, 13, 55, -2, 8, 40, 3
Merge Sort recursively DIVIDES the array into two halves until each piece has just 1 element, then CONQUERS by merging pairs back together in sorted order.

For [17,9,13,55,-2,8,40,3]: split into [17,9,13,55] and [-2,8,40,3], keep splitting until single elements, then merge back up — see Chapter 1.2, Section 2 for the full traced example with this exact array (including how the negative number -2 is handled).
2M MST-1
Given the recurrence relation T(n) = 2T(n/2) + n. Calculate the total time complexity T(n) when n = 64.
Using Master Method: a=2, b=2, f(n)=n → Case 2 applies → T(n) = Θ(n log n)

Substituting n=64: T(64) = 64 × log₂(64) = 64 × 6 = 384 (since 2⁶=64)
Section B (2 × 5 = 10 marks)
5M MST-1
Examine the recurrence relation T(n) = 2T(n/2) + n using the recursion tree method. Illustrate how work is distributed across recursive levels, and determine the total time complexity by back-substitution method.
Part 1 — Recursion Tree Method:
Draw out T(n) = 2T(n/2) + n as a tree. At the root, the recursive call splits into 2 subproblems of size n/2 each, and the non-recursive work done at this level is "n".

Level 0: 1 node of size n → cost = n
Level 1: 2 nodes of size n/2 each → cost = n/2 + n/2 = n
Level 2: 4 nodes of size n/4 each → cost = 4 × (n/4) = n
Level i: 2ⁱ nodes of size n/2ⁱ each → cost = 2ⁱ × (n/2ⁱ) = n

Notice the cost is CONSTANT ("n") at every single level — this is the key observation.

The tree keeps splitting until the subproblem size reaches 1 (the base case). Since we divide by 2 each time, the number of levels = log₂n + 1 (level 0 through level log₂n).

Total cost = (cost per level) × (number of levels) = n × (log₂n + 1) = n·log₂n + n
Dropping the lower-order term "n": T(n) = O(n log n)

Part 2 — Verifying with Back-Substitution:
T(n) = 2T(n/2) + n
Substitute T(n/2) = 2T(n/4) + n/2:
T(n) = 2[2T(n/4) + n/2] + n = 4T(n/4) + n + n = 4T(n/4) + 2n
Substitute again, T(n/4) = 2T(n/8) + n/4:
T(n) = 4[2T(n/8) + n/4] + 2n = 8T(n/8) + n + 2n = 8T(n/8) + 3n

A pattern emerges after k substitutions:
T(n) = 2ᵏ·T(n/2ᵏ) + k·n

The recursion bottoms out (base case T(1)) when n/2ᵏ = 1, i.e. k = log₂n
Substituting k = log₂n back in:
T(n) = 2^(log₂n)·T(1) + (log₂n)·n = n·T(1) + n·log₂n
Since T(1) is a constant: T(n) = O(n log n)

Conclusion: Both the Recursion Tree method and the Back-Substitution method arrive at the SAME answer, T(n) = O(n log n), confirming the result is correct. This recurrence is exactly the one that describes Merge Sort's running time.
5M MST-1
Given an unsorted array, apply the Quick Sort algorithm step-by-step to sort it. Show the partitioning at each recursive call and write the final sorted array.
Algorithm approach: Quick Sort picks a pivot (last element, by convention), then PARTITIONS the array so all elements smaller than the pivot move to its left and all larger elements move to its right. The pivot lands in its final sorted position after partitioning. The same process is then applied recursively to the left and right sub-arrays.

Worked Example — Array: [29, 10, 14, 37, 13]

Call 1 — partition(arr, 0, 4):
Pivot = arr[4] = 13 (last element)
Compare each element with pivot, moving smaller ones to the left side using index i:
• 29 vs 13 → 29 not smaller, skip
• 10 vs 13 → 10 IS smaller → swap into position → [10, 29, 14, 37, 13]
• 14 vs 13 → 14 not smaller, skip
• 37 vs 13 → 37 not smaller, skip
Place pivot in its correct spot (swap with position after last smaller element):
[10, 13, 14, 37, 29] — pivot 13 is now at index 1 (its FINAL sorted position)

Call 2 — recursively sort LEFT sub-array [10] (indices 0 to 0):
Only 1 element — already sorted, recursion stops here (base case: low ≥ high).

Call 3 — recursively sort RIGHT sub-array [14, 37, 29] (indices 2 to 4):
Pivot = 29 (last element of this sub-array)
• 14 vs 29 → 14 IS smaller → stays in place → [14, 37, 29]
• 37 vs 29 → 37 not smaller, skip
Place pivot in its correct spot: [14, 29, 37] — pivot 29 now at correct position

Call 4 & 5 — recursively sort [14] and [37]:
Both are single elements — already sorted, recursion stops.

Combining all sorted pieces:
[10] + [13] + [14, 29, 37] = [10, 13, 14, 29, 37]

Final Sorted Array: [10, 13, 14, 29, 37]

Note: if your paper shows a different/longer array, apply this exact same partition-then-recurse method — pick last element as pivot each time, move smaller elements left, place pivot in its final spot, then repeat on both halves.