Chapter 4 — Algorithmic Strategies: Exercise Solutions Guide
How to Use This Page
1. Attempt every exercise question on your own first — use your Samacheer Kalvi textbook and class notes. 2. Compare your answers with the solutions below only after you have written your own. 3. Read the explanations for MCQs and True/False items — understanding why an answer is correct is more important than the answer itself. 4. Practise the short and detailed answers by writing them in your own words; do not memorise verbatim. 5. Use the Exam Checklist at the end for a quick pre-exam revision round.
Key Concepts (Quick Recap)
- An algorithm is a step-by-step procedure for solving a problem in a finite number of steps.
- Time complexity measures how an algorithm's running time grows with input size; space complexity measures memory usage.
- Big O notation describes the upper bound (worst-case) growth rate: O(1), O(log n), O(n), O(n log n), O(n²), O(2ⁿ).
- Linear search checks each element sequentially; time complexity O(n); works on unsorted lists.
- Binary search halves the search space each step; time complexity O(log n); requires a sorted list.
- Bubble sort repeatedly swaps adjacent elements if they are in the wrong order; O(n²).
- Selection sort finds the minimum element and places it at the beginning; O(n²).
- Insertion sort builds a sorted list by inserting each element into its correct position; O(n²).
- Merge sort divides the list into halves, sorts each half, and merges them; O(n log n).
- Best case for sorting (already sorted): bubble sort and insertion sort — O(n); selection sort — O(n²).
- Stable sort preserves the relative order of equal elements; bubble sort, insertion sort, and merge sort are stable; selection sort is not.
I. Choose the Best Answer (MCQs)
1. What is the time complexity of linear search in the worst case? (a) O(1) (b) O(log n) (c) O(n) (d) O(n²)
Answer(c) O(n) In the worst case, linear search checks every element in the list (the target is at the end or not present). With n elements, it makes n comparisons, giving O(n) time complexity.
2. Binary search requires the list to be: (a) Unsorted (b) Sorted (c) In reverse order (d) Of even length
Answer(b) Sorted Binary search works by comparing the target with the middle element and eliminating half the list each time. This strategy only works if the list is sorted, so the algorithm knows which half to search next.
3. What is the time complexity of binary search? (a) O(n) (b) O(n²) (c) O(log n) (d) O(1)
Answer(c) O(log n) Binary search halves the search space with each comparison. Starting with n elements, after k comparisons, the search space is n/2^k. The maximum number of comparisons is log₂(n), giving O(log n) time complexity.
4. Which sorting algorithm repeatedly finds the minimum element and places it at the beginning? (a) Bubble sort (b) Insertion sort (c) Selection sort (d) Merge sort
Answer(c) Selection sort Selection sort works by finding the smallest element in the unsorted portion and swapping it with the first unsorted element. It repeats this for each position from left to right.
5. What is the worst-case time complexity of bubble sort? (a) O(n) (b) O(log n) (c) O(n log n) (d) O(n²)
Answer(d) O(n²) Bubble sort makes (n-1) passes, and in the worst case (reverse sorted), each pass makes (n-1-i) comparisons. Total comparisons = n(n-1)/2, which is O(n²).
6. Which of the following sorting algorithms has O(n log n) time complexity in all cases? (a) Bubble sort (b) Selection sort (c) Insertion sort (d) Merge sort
Answer(d) Merge sort Merge sort consistently divides the list in half (O(log n) divisions) and merges in O(n) time, giving O(n log n) in all cases — best, average, and worst. Bubble, selection, and insertion sorts all have O(n²) worst case.
7. What is the best-case time complexity of insertion sort? (a) O(n²) (b) O(n) (c) O(log n) (d) O(n log n)
Answer(b) O(n) When the list is already sorted, insertion sort only makes one comparison per element and does not shift any elements. This results in O(n) time for the best case.
8. In bubble sort, after the first pass, which element is guaranteed to be in its correct position? (a) The smallest element (b) The largest element (c) The middle element (d) All elements
Answer(b) The largest element In each pass of bubble sort, adjacent elements are compared and swapped if needed. This "bubbles up" the largest element to the end of the list. After the first pass, the largest element is in its correct position at the end.
9. Which of the following is a divide-and-conquer algorithm? (a) Bubble sort (b) Selection sort (c) Insertion sort (d) Merge sort
Answer(d) Merge sort Merge sort uses divide-and-conquer: it divides the list into halves, recursively sorts each half, and then merges the sorted halves. Bubble, selection, and insertion sorts are comparison-based but not divide-and-conquer.
10. If an algorithm takes 3n² + 5n + 7 operations, what is its Big O complexity? (a) O(3n²) (b) O(5n) (c) O(n²) (d) O(7)
Answer(c) O(n²) Big O notation keeps only the dominant term and drops constants. 3n² + 5n + 7 simplifies to O(n²) because n² grows faster than n or constant terms, and the coefficient 3 is dropped.
II. Fill in the Blanks
1. The time complexity of linear search in the worst case is O(n).
2. Binary search works only on a sorted list.
3. The time complexity of binary search is O(log n).
4. In selection sort, the minimum element is found and placed at the beginning in each pass.
5. The best-case time complexity of bubble sort is O(n) (when the list is already sorted with an optimised version).
6. Merge sort uses the divide-and-conquer strategy.
7. The worst-case time complexity of merge sort is O(n log n).
8. In bubble sort, adjacent elements are compared and swapped if they are in the wrong order.
9. Big O notation describes the worst-case (upper bound) time complexity of an algorithm.
10. An algorithm with O(1) time complexity means the running time is constant regardless of input size.
III. True or False
1. Linear search is more efficient than binary search for large sorted lists. False. Binary search has O(log n) complexity, which is much faster than linear search's O(n) for large sorted lists. Linear search is only preferred for small or unsorted lists.
2. Binary search can be applied to an unsorted list. False. Binary search requires the list to be sorted. On an unsorted list, halving the search space does not guarantee the target is in either half. It would give incorrect results.
3. Bubble sort is a stable sorting algorithm. True. Bubble sort only swaps adjacent elements when they are strictly out of order (i.e., when the left element is greater). Equal elements are not swapped, so their relative order is preserved.
4. The time complexity of selection sort is O(n²) in all cases (best, average, worst). True. Selection sort always performs n(n-1)/2 comparisons regardless of whether the list is already sorted, because it scans the entire unsorted portion for the minimum in every pass.
5. Merge sort requires additional memory for merging. True. Merge sort requires O(n) additional space for the temporary arrays used during the merge step. This is a trade-off for its O(n log n) time complexity.
6. Insertion sort is efficient for large datasets. False. Insertion sort has O(n²) worst-case and average-case time complexity, making it inefficient for large datasets. It is efficient for small or nearly sorted datasets where it performs close to O(n).
7. Binary search compares the target with the middle element and eliminates half the list. True. Binary search compares the target with the middle element. If the target is smaller, the right half is eliminated. If the target is larger, the left half is eliminated. This is why it achieves O(log n) complexity.
8. The best-case time complexity of bubble sort is O(n²). False. With an optimised bubble sort (that stops early if no swaps occur in a pass), the best-case time complexity is O(n) when the list is already sorted. Without optimisation, it is O(n²).
9. Space complexity of merge sort is O(1). False. Merge sort has a space complexity of O(n) because it requires additional temporary arrays for the merging step. Only O(1) space algorithms use constant extra memory.
10. Big O notation focuses on the dominant term and ignores constants and lower-order terms. True. Big O notation keeps only the highest-order term and drops constants. For example, 3n² + 5n + 7 becomes O(n²), and 2n log n + 10n becomes O(n log n).
IV. Match the Following
Set A
| A | B |
|---|
| 1. Linear search | c. O(n) time complexity |
| 2. Binary search | e. O(log n) time complexity |
| 3. Bubble sort | a. Adjacent element comparison and swap |
| 4. Selection sort | d. Finds minimum and places at beginning |
| 5. Merge sort | b. Divide-and-conquer with O(n log n) |
Set B
| A | B |
|---|
| 1. O(1) | d. Constant time |
| 2. O(log n) | a. Logarithmic time |
| 3. O(n) | e. Linear time |
| 4. O(n log n) | c. Linearithmic time |
| 5. O(n²) | b. Quadratic time |
V. Short Answer Questions
1. What is linear search? Write a Python function and explain its time complexity.
Answer
Linear search checks each element in the list one by one from the beginning until the target is found or the list ends.
def linear_search(arr, target):
for i in range(len(arr)):
if arr[i] == target:
return i # Found: return index
return -1 # Not found
# Example
numbers = [34, 12, 56, 78, 23, 9]
print(linear_search(numbers, 56)) # 2 (found at index 2)
print(linear_search(numbers, 99)) # -1 (not found)
Time complexity:
- Best case: O(1) — target is at the first position.
- Worst case: O(n) — target is at the last position or not present.
- Average case: O(n) — target is somewhere in the middle.
Linear search works on both sorted and unsorted lists but is slow for large datasets.
2. What is binary search? Write a Python function and explain why it is faster than linear search.
Answer
Binary search works on a sorted list by repeatedly dividing the search interval in half. It compares the target with the middle element and eliminates half the list each time.
def binary_search(arr, target):
low = 0
high = len(arr) - 1
while low <= high:
mid = (low + high) // 2
if arr[mid] == target:
return mid # Found: return index
elif arr[mid] < target:
low = mid + 1 # Search right half
else:
high = mid - 1 # Search left half
return -1 # Not found
# Example (list must be sorted)
numbers = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
print(binary_search(numbers, 23)) # 5
print(binary_search(numbers, 7)) # -1
Why binary search is faster:
- Linear search checks n elements in the worst case.
- Binary search checks at most log₂(n) elements.
- For 1,000,000 elements: linear search may need 1,000,000 comparisons; binary search needs at most 20 comparisons.
| List size (n) | Linear search (O(n)) | Binary search (O(log n)) |
|---|
| 100 | 100 | 7 |
| 1,000 | 1,000 | 10 |
| 1,000,000 | 1,000,000 | 20 |
Prerequisite: The list must be sorted before binary search can be applied.
3. Explain bubble sort with a Python function. Trace through an example.
Answer
Bubble sort repeatedly steps through the list, compares adjacent elements, and swaps them if they are in the wrong order. After each pass, the largest unsorted element "bubbles up" to its correct position.
def bubble_sort(arr):
n = len(arr)
for i in range(n - 1):
swapped = False
for j in range(n - 1 - i):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
if not swapped: # Optimisation: stop if no swaps
break
return arr
Trace through: [5, 3, 8, 1]
Pass 1:
- Compare 5, 3 → swap → [3, 5, 8, 1]
- Compare 5, 8 → no swap → [3, 5, 8, 1]
- Compare 8, 1 → swap → [3, 5, 1, 8] (8 is in correct position)
Pass 2:
- Compare 3, 5 → no swap → [3, 5, 1, 8]
- Compare 5, 1 → swap → [3, 1, 5, 8] (5 is in correct position)
Pass 3:
- Compare 3, 1 → swap → [1, 3, 5, 8] (3 is in correct position)
Result[1, 3, 5, 8]
Time complexity:
- Best case: O(n) — already sorted (with optimisation, one pass with no swaps).
- Worst case: O(n²) — reverse sorted.
- Average case: O(n²).
4. Explain selection sort with a Python function. Trace through an example.
Answer
Selection sort divides the list into sorted and unsorted portions. In each pass, it finds the minimum element in the unsorted portion and swaps it with the first unsorted element.
def selection_sort(arr):
n = len(arr)
for i in range(n - 1):
min_idx = i
for j in range(i + 1, n):
if arr[j] < arr[min_idx]:
min_idx = j
arr[i], arr[min_idx] = arr[min_idx], arr[i]
return arr
Trace through: [64, 25, 12, 22, 11]
Pass 1: Find min in [64, 25, 12, 22, 11] → 11 at index 4. Swap with index 0 → [11, 25, 12, 22, 64]
Pass 2: Find min in [25, 12, 22, 64] → 12 at index 2. Swap with index 1 → [11, 12, 25, 22, 64]
Pass 3: Find min in [25, 22, 64] → 22 at index 3. Swap with index 2 → [11, 12, 22, 25, 64]
Pass 4: Find min in [25, 64] → 25 at index 3. No swap needed → [11, 12, 22, 25, 64]
Result[11, 12, 22, 25, 64]
Time complexity: O(n²) in all cases (best, average, worst), because it always scans the entire unsorted portion.
5. Explain insertion sort with a Python function. What is its best-case time complexity and when does it occur?
Answer
Insertion sort builds a sorted list by taking each element and inserting it into its correct position in the already-sorted portion.
def insertion_sort(arr):
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
return arr
# Example
numbers = [12, 11, 13, 5, 6]
print(insertion_sort(numbers)) # [5, 6, 11, 12, 13]
Trace through: [12, 11, 13, 5]
- i=1: key=11, shift 12 right → [12, 12, 13, 5], insert 11 → [11, 12, 13, 5]
- i=2: key=13, no shifts needed → [11, 12, 13, 5]
- i=3: key=5, shift 13, 12, 11 right → [11, 11, 12, 13], insert 5 → [5, 11, 12, 13]
Best-case time complexity: O(n) This occurs when the list is already sorted. Each element only needs one comparison (no shifting), so the inner while loop runs zero times per element.
Worst-case time complexity: O(n²) This occurs when the list is reverse sorted. Each new element must be compared with all previous elements and shifted to the beginning.
6. What is Big O notation? Give examples for O(1), O(n), O(log n), O(n²), and O(n log n).
Answer
Big O notation describes the upper bound (worst-case) time complexity of an algorithm as a function of input size n. It focuses on the dominant term and ignores constants and lower-order terms.
| Big O | Name | Example |
|---|
| O(1) | Constant | Accessing an array element by index: arr[5] |
| O(log n) | Logarithmic | Binary search — halves the search space each step |
| O(n) | Linear | Linear search — checks each element once |
| O(n log n) | Linearithmic | Merge sort — divides and merges |
| O(n²) | Quadratic | Bubble sort, selection sort — nested loops |
Examples:
- O(1):
arr[0] — accessing any element by index takes constant time regardless of array size. - O(log n): Binary search on 1,000,000 elements needs at most 20 comparisons.
- O(n): Linear search on 1,000 elements needs at most 1,000 comparisons.
- O(n²): Bubble sort on 100 elements needs up to 100×99/2 = 4,950 comparisons.
- O(n log n): Merge sort on 1,000 elements needs about 1,000 × 10 = 10,000 operations.
Simplification rule: 3n² + 5n + 7 → O(n²) (keep dominant term, drop constants).
VI. Detailed Answer Questions
1. Explain binary search algorithm in detail with Python implementation. Trace through an example and compare it with linear search.
Answer
Binary search is an efficient algorithm for finding an element in a sorted list. It works by repeatedly dividing the search interval in half.
Algorithm: 1. Set low = 0 and high = len(arr) - 1. 2. While low <= high: a. Calculate mid = (low + high) // 2. b. If arr[mid] == target, return mid (found). c. If arr[mid] < target, set low = mid + 1 (search right half). d. If arr[mid] > target, set high = mid - 1 (search left half). 3. If the loop ends, return -1 (not found).
Python implementation:
def binary_search(arr, target):
low = 0
high = len(arr) - 1
while low <= high:
mid = (low + high) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1
# Example
numbers = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
print(binary_search(numbers, 23)) # 5
print(binary_search(numbers, 72)) # 8
print(binary_search(numbers, 7)) # -1
Trace: searching for 23 in [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
| Step | low | high | mid | arr[mid] | Comparison | Action |
|---|
| 1 | 0 | 9 | 4 | 16 | 23 > 16 | low = 5 |
| 2 | 5 | 9 | 7 | 56 | 23 < 56 | high = 6 |
| 3 | 5 | 6 | 5 | 23 | 23 == 23 | Found at index 5 |
Only 3 comparisons for a list of 10 elements!
Comparison: Linear search vs Binary search
| Feature | Linear Search | Binary Search |
|---|
| Prerequisite | Works on any list | Requires sorted list |
| Best case | O(1) | O(1) |
| Worst case | O(n) | O(log n) |
| Average case | O(n) | O(log n) |
| For n=1,000,000 | Up to 1,000,000 comparisons | Up to 20 comparisons |
| Implementation | Simple | Slightly more complex |
| Use case | Small or unsorted lists | Large sorted lists |
2. Explain merge sort algorithm in detail with Python implementation. Trace through an example and discuss its time and space complexity.
Answer
Merge sort is a divide-and-conquer algorithm that recursively splits the list into halves, sorts each half, and then merges the sorted halves.
Algorithm: 1. Divide: Split the list into two halves. 2. Conquer: Recursively sort each half. 3. Merge: Combine the two sorted halves into a single sorted list.
Python implementation:
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
# Example
numbers = [38, 27, 43, 3, 9, 82, 10]
print(merge_sort(numbers)) # [3, 9, 10, 27, 38, 43, 82]
Trace: [38, 27, 43, 3, 9, 82, 10]
[38, 27, 43, 3, 9, 82, 10]
/ \
[38, 27, 43] [3, 9, 82, 10]
/ \ / \
[38] [27, 43] [3, 9] [82, 10]
/ \ / \ / \
[27] [43] [3] [9] [82] [10]
Merging:
[27] + [43] → [27, 43] [3] + [9] → [3, 9] [82] + [10] → [10, 82]
[38] + [27, 43] → [27, 38, 43] [3, 9] + [10, 82] → [3, 9, 10, 82]
[27, 38, 43] + [3, 9, 10, 82] → [3, 9, 10, 27, 38, 43, 82]
Time complexity:
- Best case: O(n log n)
- Average case: O(n log n)
- Worst case: O(n log n)
Merge sort always divides the list into halves (log n levels) and merges n elements at each level, so the time complexity is consistently O(n log n).
Space complexity: O(n) — requires temporary arrays for merging.
Advantages:
- Consistent O(n log n) performance regardless of input.
- Stable sort (preserves order of equal elements).
- Suitable for large datasets and linked lists.
Disadvantages:
- Requires O(n) extra memory.
- Slower than insertion sort for very small lists.
3. Compare bubble sort, selection sort, and insertion sort with their algorithms, time complexities, and examples.
Answer
| Feature | Bubble Sort | Selection Sort | Insertion Sort |
|---|
| Strategy | Swap adjacent elements | Find min, place at beginning | Insert element into sorted portion |
| Best case | O(n) (already sorted) | O(n²) | O(n) (already sorted) |
| Average case | O(n²) | O(n²) | O(n²) |
| Worst case | O(n²) | O(n²) | O(n²) (reverse sorted) |
| Space | O(1) | O(1) | O(1) |
| Stable? | Yes | No | Yes |
| Adaptive? | Yes (stops early) | No | Yes |
| Swaps per pass | Many | One (min swap) | Shifts elements |
Bubble Sort — Python implementation:
def bubble_sort(arr):
n = len(arr)
for i in range(n - 1):
swapped = False
for j in range(n - 1 - i):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
if not swapped:
break
return arr
print(bubble_sort([5, 1, 4, 2, 8])) # [1, 2, 4, 5, 8]
Selection Sort — Python implementation:
def selection_sort(arr):
n = len(arr)
for i in range(n - 1):
min_idx = i
for j in range(i + 1, n):
if arr[j] < arr[min_idx]:
min_idx = j
arr[i], arr[min_idx] = arr[min_idx], arr[i]
return arr
print(selection_sort([5, 1, 4, 2, 8])) # [1, 2, 4, 5, 8]
Insertion Sort — Python implementation:
def insertion_sort(arr):
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
return arr
print(insertion_sort([5, 1, 4, 2, 8])) # [1, 2, 4, 5, 8]
When to use which:
- Bubble sort: Educational purposes; rarely used in practice.
- Selection sort: When memory writes are costly (minimum swaps); small datasets.
- Insertion sort: Nearly sorted data; small datasets; as part of more advanced algorithms (like Timsort).
4. Explain Big O notation with examples. How do you determine the time complexity of an algorithm?
Answer
Big O notation describes the upper bound (worst-case) growth rate of an algorithm's time or space requirements as a function of input size n. It focuses on the dominant term and ignores constants and lower-order terms.
Rules for determining Big O:
1. Drop constants: O(3n) → O(n); O(2n²) → O(n²) 2. Drop lower-order terms: O(n² + n) → O(n²); O(n log n + n) → O(n log n) 3. Keep the dominant term: For large n, n² dominates n, which dominates log n, which dominates 1.
Common Big O categories (from fastest to slowest):
| Big O | Name | Growth | Example |
|---|
| O(1) | Constant | No growth | Accessing array by index |
| O(log n) | Logarithmic | Very slow growth | Binary search |
| O(√n) | Square root | Moderate growth | Checking if n is prime |
| O(n) | Linear | Proportional to n | Linear search |
| O(n log n) | Linearithmic | n × log n | Merge sort |
| O(n²) | Quadratic | n × n | Bubble sort |
| O(2ⁿ) | Exponential | Doubles with each n | Recursive Fibonacci |
| O(n!) | Factorial | Extremely fast growth | Travelling salesman (brute force) |
Determining time complexity from code:
Example 1 — Single loop: O(n)
def print_items(n):
for i in range(n): # Runs n times
print(i)
# Time complexity: O(n)
Example 2 — Nested loop: O(n²)
def print_pairs(n):
for i in range(n): # Runs n times
for j in range(n): # Runs n times for each i
print(i, j)
# Time complexity: O(n × n) = O(n²)
Example 3 — Halving loop: O(log n)
def halve(n):
while n > 1:
n = n // 2 # Halves n each step
print(n)
# Time complexity: O(log n)
Example 4 — Sequential statements: O(n²)
def combined(n):
for i in range(n): # O(n)
print(i)
for i in range(n): # O(n²)
for j in range(n):
print(i, j)
# Time complexity: O(n) + O(n²) = O(n²)
# Drop the lower-order term O(n)
Example 5 — Binary search: O(log n)
def binary_search(arr, target):
low, high = 0, len(arr) - 1
while low <= high: # Halves search space each step
mid = (low + high) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1
# Time complexity: O(log n) — search space halves each iteration
5. Explain merge sort with a step-by-step trace. Compare merge sort with bubble sort in terms of time and space complexity.
Answer
Merge sort is a divide-and-conquer sorting algorithm that: 1. Divides the list into two halves. 2. Recursively sorts each half. 3. Merges the two sorted halves.
Step-by-step trace: sorting [6, 5, 3, 1, 8, 7, 2, 4]
Divide phase:
[6, 5, 3, 1, 8, 7, 2, 4]
↓ split
[6, 5, 3, 1] [8, 7, 2, 4]
↓ split ↓ split
[6, 5] [3, 1] [8, 7] [2, 4]
↓ split ↓ split ↓ split ↓ split
[6] [5] [3] [1] [8] [7] [2] [4]
Merge phase:
[6] [5] → [5, 6] [3] [1] → [1, 3] [8] [7] → [7, 8] [2] [4] → [2, 4]
[5, 6] [1, 3] → [1, 3, 5, 6] [7, 8] [2, 4] → [2, 4, 7, 8]
[1, 3, 5, 6] [2, 4, 7, 8] → [1, 2, 3, 4, 5, 6, 7, 8]
Merge step detail: merging [1, 3, 5, 6] and [2, 4, 7, 8]
left = [1, 3, 5, 6], right = [2, 4, 7, 8]
Compare 1, 2 → 1 is smaller → result = [1]
Compare 3, 2 → 2 is smaller → result = [1, 2]
Compare 3, 4 → 3 is smaller → result = [1, 2, 3]
Compare 5, 4 → 4 is smaller → result = [1, 2, 3, 4]
Compare 5, 7 → 5 is smaller → result = [1, 2, 3, 4, 5]
Compare 6, 7 → 6 is smaller → result = [1, 2, 3, 4, 5, 6]
Append remaining right: result = [1, 2, 3, 4, 5, 6, 7, 8]
Comparison: Merge Sort vs Bubble Sort
| Feature | Merge Sort | Bubble Sort |
|---|
| Strategy | Divide and conquer | Swap adjacent elements |
| Best case | O(n log n) | O(n) (optimised) |
| Average case | O(n log n) | O(n²) |
| Worst case | O(n log n) | O(n²) |
| Space complexity | O(n) | O(1) |
| Stable? | Yes | Yes |
| Adaptive? | No | Yes |
| Practical use | Large datasets, external sorting | Educational, small datasets |
| For n=10,000 | ~140,000 operations | ~50,000,000 operations |
Merge sort is significantly faster for large datasets (O(n log n) vs O(n²)), but requires extra O(n) memory. Bubble sort is simple but impractical for large lists.
Common Mistakes
1. Applying binary search to an unsorted list: Binary search only works on sorted lists. On an unsorted list, halving the search space is meaningless and produces incorrect results.
2. Confusing best-case and worst-case complexities: Bubble sort is O(n) best case (already sorted) but O(n²) worst case. Selection sort is O(n²) in all cases. Always specify which case you are analysing.
3. Forgetting that merge sort needs extra space: Merge sort requires O(n) additional memory for temporary arrays. This is a trade-off for its O(n log n) speed.
4. Confusing selection sort with insertion sort: Selection sort finds the minimum and swaps; insertion sort takes each element and inserts it into the sorted portion. They have different best-case behaviours.
5. Counting passes instead of comparisons: Bubble sort makes approximately n²/2 comparisons, not n passes. Remember that each pass makes fewer comparisons (n-1, n-2, ..., 1).
6. Dropping the wrong term in Big O: O(n² + n) simplifies to O(n²), not O(n). The highest-order term dominates for large n. Similarly, O(n log n + n) = O(n log n).
7. Assuming all O(n²) sorts are equally slow: Insertion sort can be O(n) on nearly sorted data, making it practical for small or nearly sorted lists. Selection sort is always O(n²) regardless of input.
Exam Checklist
- [ ] Can define linear search and trace through it with O(n) complexity
- [ ] Can define binary search and trace through it with O(log n) complexity
- [ ] Know that binary search requires a sorted list
- [ ] Can implement bubble sort and trace through an example
- [ ] Can implement selection sort and trace through an example
- [ ] Can implement insertion sort and trace through an example
- [ ] Can implement merge sort and trace through an example
- [ ] Can compare time and space complexities of all sorting algorithms
- [ ] Can explain Big O notation and simplify expressions (drop constants and lower-order terms)
- [ ] Can determine time complexity from code (single loop, nested loop, halving loop)