Merge Sort as Divide and Conquer in DSA Python - Time & Space Complexity
We want to understand how the time taken by merge sort changes as the list size grows.
How does splitting and merging affect the total work done?
Analyze the time complexity of the following merge sort code.
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
This code splits the list into halves, sorts each half, then merges them back in order.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Recursively splitting the list and merging sorted halves.
- How many times: The list is split until single elements remain, about log n times; merging happens at each level over all elements.
Each split divides the list in half, creating about log n levels. At each level, merging all parts touches all n elements.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 10 * 3.3 = 33 |
| 100 | About 100 * 6.6 = 660 |
| 1000 | About 1000 * 10 = 10,000 |
Pattern observation: Operations grow roughly as n times log n, so bigger lists take more than just a simple multiple.
Time Complexity: O(n log n)
This means the time grows a bit faster than linear but much slower than quadratic as input size increases.
[X] Wrong: "Merge sort is just like bubble sort and takes quadratic time because it compares elements repeatedly."
[OK] Correct: Merge sort splits the list and merges in a way that only compares elements in a controlled, efficient manner, avoiding repeated full passes.
Understanding merge sort's time complexity shows you how divide and conquer helps solve problems efficiently, a key skill in many coding challenges.
"What if we changed merge sort to split the list into three parts instead of two? How would the time complexity change?"
Practice
Merge Sort algorithm?Solution
Step 1: Understand the divide and conquer approach
Merge Sort splits the list into smaller parts to simplify sorting.Step 2: Merge sorted halves
After sorting each half, it merges them to form a sorted list.Final Answer:
Divide the list into halves, sort each half, then merge them -> Option DQuick Check:
Merge Sort = Divide and conquer [OK]
- Confusing merge sort with bubble sort
- Thinking merge sort sorts in place without merging
- Mixing merge sort with quicksort pivot method
Solution
Step 1: Identify when recursion should stop
Merge sort stops splitting when the list has 1 or 0 elements.Step 2: Correct base case condition
Checking if length is less than or equal to 1 returns the list as is.Final Answer:
if len(arr) <= 1: return arr -> Option BQuick Check:
Base case = list size ≤ 1 [OK]
- Using wrong base case like len(arr) == 0 only
- Returning None instead of the list
- Incorrect condition that never stops recursion
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
print(merge_sort([4, 1, 3, 2]))Solution
Step 1: Trace recursive splitting
List splits into [4,1] and [3,2], then further splits until single elements.Step 2: Merge sorted halves
Merge combines sorted lists step-by-step to form [1,2,3,4].Final Answer:
[1, 2, 3, 4] -> Option AQuick Check:
Sorted list = [1, 2, 3, 4] [OK]
- Confusing merge order causing wrong output
- Forgetting to extend remaining elements after loop
- Mixing up indices i and j in merge
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 resultSolution
Step 1: Check base case condition
Base case only handles lists of length 1, but empty lists cause infinite recursion.Step 2: Correct base case to include empty list
Change condition to len(arr) <= 1 to handle empty and single element lists.Final Answer:
Base case should be len(arr) <= 1, not len(arr) == 1 -> Option AQuick Check:
Base case must handle empty or single element [OK]
- Using strict equality for base case
- Starting indices at 1 causing index errors
- Changing comparison operator unnecessarily
Solution
Step 1: Understand merge calls in merge sort
Merge is called once for each internal node in the recursion tree (combining two sorted sublists).Step 2: Calculate number of merge calls for 8 elements
4 merges (size 2 sublists) + 2 merges (size 4) + 1 merge (size 8) = 7 total. Formula: n - 1 = 7.Final Answer:
7 -> Option CQuick Check:
Merge calls = n - 1 = 7 [OK]
- Counting only top-level merges
- Confusing merges with splits
- Using 2n-1 (confusing with total recursive calls to merge_sort)
Ready to prove it?
Pass the test and this pattern moves unknown
