Merge Sort Notes
Merge Sort Notes
Merge sort is one of the fastest sorting algorithms that works in O(nlogn)
time complexity.
It is also the best algorithm for sorting linked lists in O(nlogn) time.
The merging process of merge sort is an excellent idea to learn the two-
pointers approach. Here, both pointers move in the same direction to build
the partial solution. We can use this approach to solve several coding
questions.
If we observe the above diagram, the divide and conquer idea looks like this:
Divide part: We divide the sorting problem of size n into two equal sub-
problems of size n/2 by calculating the mid-index.
Combine part: Now we merge both sorted subarrays to get the final sorted
array. In other words, we combine the solution of two n/2 size sub-problems to
solve sorting problems of size n.
Conquer part 1: We recursively sort the left part of size n/2 by calling the
same function with mid as the right end,
i.e., mergeSortAlgorithm(A, l, mid).
Conquer part 2: We recursively sort the right part of size n/2 by calling
the same function with mid + 1 as the left end,
i.e., mergeSortAlgorithm(A, mid + 1, r).
Base case: If we find l == r during recursive calls, then the sub-array has
one element left, which is trivially sorted. So recursion will not go further
and return from there. In other words, the sub-array of size 1 is the smallest
version of the sorting problem for which recursion directly returns the
solution.
Note: Why are we not calculating mid using the formula (l + r)/2? Explore this
excellent Google blog to get an answer.
After the conquer step, both left part A[l…mid] and right part A[mid + 1…r]
will be sorted. Now we need to combine the solution of smaller
sub-problems to build a solution to the larger problem, i.e., merging both sorted
halves to create the larger sorted array.
The critical question is: Both sorted halves are part of the same array A[]. Can
we merge these two halves in O(n) time without using extra space? Try this
by doing some swapping and comparison operations.
Here is an O(n) time idea using O(n) extra space: If we store sorted halves
into two extra arrays of size n/2 (X[] and Y[]), we can transform this problem
into merging sorted arrays X[] and Y[] into the larger sorted array A[]. For this,
we compare values one by one in both smaller arrays and build the larger array
sorted A[]. How can we implement it? Let's think!
We use two pointers i and j to traverse X[] and Y[] from the start. We
compare elements in both arrays one by one and place a smaller value on array
A[]. Another way of thinking would be: After each comparison,
we add one element to the sorted output and incrementally build a partially
sorted array A[].
We allocate two extra arrays of size equal to the size of left and right sorted
parts i.e. size of left sorted part = mid - l + 1, size of right sorted part = r -
mid. After this, we copy left and right sorted parts of A[] into extra arrays.
Note: We include the value at the mid-index in the left part.
Step 2: Now we start the merging process using two pointers loop.
Now we run a loop until either of the smaller arrays reaches its end:
while (i < n1 && j < n2).
In the first step of the iteration, we compare X[0] and Y[0] and place the
smallest value at A[0]. Before moving forward to the second iteration, we
increment the pointer k in A[] and the pointer in the array containing the
smaller value (which may be i or j, depending on the comparison).
In a similar fashion, we move forward in all three arrays using pointers i,
j, and k. At each step, we compare X[i] and Y[j], place the smaller value in
A[k], and increment k by 1. Based on the comparison and position of the
smaller value, we also increment pointer i or j. If (X[i] <= Y[j]), we
increment i by 1; otherwise, we increment j by 1.
The loop will stop when one of the two pointers reaches the end of its array
(either i = n1 or j = n2). At this stage, there will be two boundary conditions:
Boundary condition 1: If we exit the loop due to j = n2, then we have reached
the end of array Y[] and placed all values of Y[] in A[]. But there may be
remaining values in X[] that still need to be put in the array A[]. These values
are greater than all values available in A[], so we copy the remaining values of
X[] into the end of the array A[].
Boundary condition 2: If we exit the loop due to the condition i = n1, we have
reached the end of array X[] and placed all its values in A[]. But there may still
be some values remaining in Y[] that need to be placed in
the array A[]. The idea is that these values are greater than all the values in A[],
so we copy the remaining values of Y[] into the end of array A[].
Space complexity = Extra space for storing left part + Extra space for storing
right part = O(n1) + O(n2) = O(n1 + n2) = O(n).
Divide part: The time complexity of the divide part is O(1), because calculating
the middle index takes constant time.
To calculate T(n), we need to add up the time complexities of the divide, conquer,
and combine parts:
T(n) = c, if n = 1
Note that the merge sort function works correctly when the number of
elements is not even. To simplify the analysis, we assume that n is a power of 2.
This assumption does not affect the order of growth in the analysis.
As we have seen in the above recursion tree, the cost at each level is O(n), and
there are O(logn) levels. So the time complexity of the merge sort algorithm is
the sum of the cost at each level, which is O(logn)*O(n)
= O(nlogn).
The best and worst-case time complexity of the merge sort algorithm is
O(nlogn). The idea is simple: irrespective of the input, merge sort divides the
input into equal halves and takes O(n) time at each level. Note: To learn about
the fundamentals of recursion analysis, you can explore the blog post on time
complexity analysis of recursion.
The Master method is a direct way to obtain the solution for recurrences that can
be transformed to the type T(n) = aT(n/b) + O(n^k), where a ≥ 1 and b > 1.
There are three cases for the analysis using the master theorem:
= aT(n/b) + O(n^k)
logb(a) = log2(2) = 1 = k
So k = logb(a). This means that the merge sort recurrence satisfies the 2nd case
of the master theorem. Merge sort time complexity T(n) = O(n^k * logn) =
O(n^1 * logn) = O(nlogn).
The space complexity for the recursion call stack is equal to the height of
the merge sort recursion tree, which is O(logn).
The space complexity of the merge sort algorithm is the sum of the space
complexities of the merging process and the recursion call stack, which is
O(n) + O(logn) = O(n). As we have seen here, the space complexity is
dominated by the extra space used by the merging process.