skip to content
Site header image Minh Ton

Advanced Sorting Algorithms - DSA #6

Efficient Sorting Algorithms such as Heap Sort, Quick Sort, and Merge Sort, why are they faster than elementary ones?

Last Updated:

👋
Hey there! Welcome to my series of notes on Data Structures and Algorithms!


In this note, we will discover three super efficient sorting algorithms: Heap Sort, Quick Sort, and Merge Sort!

Efficient Sorting Algorithms

The elementary sorting algorithms discussed in my previous note, Basic Sorting Algorithms - DSA #5, are of the order Θ(n2)\Theta(n^2), which is pretty inefficient especially for large datasets.

There are more efficient sorting algorithms that are of the order O(nlogn)\mathcal{O}(n\log n), and three typical algorithms are Heap Sort, Quick Sort, and Merge Sort.

Heap Sort

Heap Sort was invented by J. W. J. Williams in 1964. This was also the birth of the heap, presented already by Williams as a useful data structure.

The idea of Heap Sort comes from the method of sorting by the Selection Sort — repeatedly choosing the least key (or the smallest item) and move it to the start/end of the array. How can this idea possibly be improved?

Basic Concept of Heap Sort

The First Stage

Traditional Selection Sort finds the smallest element in each pass using n1n−1 comparisons. This process discards useful information, as it only identifies a single least item per scan. Instead of merely identifying the smallest element in each pass, comparisons can be structured hierarchically:

  • Compare items in pairs, requiring n2\frac{n}{2} comparisons to find the smaller of each pair.
  • Compare the smaller values from the previous step, using n4\frac{n}{4} comparisons.
  • This continues recursively, reducing the number of comparisons at each level.

Suppose we have the array 8,3,6,2,5,9,4,18, 3, 6, 2, 5, 9, 4, 1. We compare in pairs:(8,3)3(8, 3) \Rightarrow 3, (6,2)2(6, 2) \Rightarrow 2, (5,9)5(5, 9) \Rightarrow 5, (4,1)1(4, 1) \Rightarrow 1. The total comparisons of this round is 8/2=48 / 2 = 4.

After the first round, we are left with n2\frac{n}{2} elements. In the second round, we again compare them in pairs, needing n4\frac{n}{4} comparisons. The process continues until one element remains.

With n1n - 1 comparisons, we can build a selection tree, and identify the root as the desired least key. This is the idea of the heap data structure.

Let’s construct a heap from the above example array:

836259418\quad 3\quad 6\quad 2\quad 5\quad 9\quad 4\quad 1

Compare the elements in pairs of the array to select the smaller element of each pair:

3251836259413 \hspace{0.9cm} 2 \hspace{0.9cm} 5 \hspace{0.9cm} 1 \\ \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge \\ 8\quad 3\quad 6\quad 2\quad 5\quad 9\quad 4\quad 1

The number of comparisons in this round is n2=4\frac{n}{2} = 4.

In the next round, continue comparing the remaining elements in pairs:

213251836259412 \hspace{2cm} 1 \\ \hspace{1.8cm}\wedge\hspace{1.8cm} \wedge\hspace{1.8cm} \\ 3 \hspace{0.9cm} 2 \hspace{0.9cm} 5 \hspace{0.9cm} 1 \\ \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge \\ 8\quad 3\quad 6\quad 2\quad 5\quad 9\quad 4\quad 1

The number of comparisons in this round is n22=n4=2\frac{\frac{n}{2}}{2} = \frac{n}{4} = 2.

Identify the desired least key:

1213251836259411 \\ \wedge \\ 2 \hspace{2cm} 1 \\ \hspace{1.8cm}\wedge\hspace{1.8cm} \wedge\hspace{1.8cm} \\ 3 \hspace{0.9cm} 2 \hspace{0.9cm} 5 \hspace{0.9cm} 1 \\ \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge \\ 8\quad 3\quad 6\quad 2\quad 5\quad 9\quad 4\quad 1

The number of comparisons in this round is n42=n8=1\frac{\frac{n}{4}}{2} = \frac{n}{8} = 1. The above is a simple heap data structure from the given integers.

The Second Stage

The following steps will be repeated until the heap is full of the key :

  • Extract the least key at the root and replace it by an empty hole.
  • Descend down along the path marked by the least key and replace it by an empty hole.
  • Fill the empty hole at the bottom with and ascend up along the path marked by empty holes, filling them with or the key at the alternative branch.

Each of the above repetitions is called a selection step.

Let’s get back to the heap we’ve constructed in the previous step:

1213251836259411 \\ \wedge \\ 2 \hspace{2cm} 1 \\ \wedge\hspace{1.8cm} \wedge \\ 3 \hspace{0.9cm} 2 \hspace{0.9cm} 5 \hspace{0.9cm} 1 \\ \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge \\ 8\quad 3\quad 6\quad 2\quad 5\quad 9\quad 4\quad 1

Let’s use this to visualize how this would work (\square as empty holes):

23258362594\square \\ \wedge \\ 2 \hspace{2cm} \square \\ \wedge\hspace{1.8cm} \wedge \\ 3 \hspace{0.9cm} 2 \hspace{0.9cm} 5 \hspace{0.9cm} \square \\ \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge \\ 8\quad 3\quad 6\quad 2\quad 5\quad 9\quad 4\quad \hspace{-0.2cm}∞

1. Select 11 as the least key, descend down the path marked by 11, and mark the path by empty holes. The hole at the bottom is filled with .

4354836594\square\\ \wedge \\ \square \hspace{2cm} 4 \\ \wedge\hspace{1.8cm} \wedge \\ 3 \hspace{0.9cm} \square \hspace{0.9cm} 5 \hspace{0.9cm} 4 \\ \wedge\hspace{0.65cm} \wedge\hspace{0.85cm} \wedge\hspace{0.75cm} \wedge \\ 8\quad 3\quad 6\quad \hspace{-0.2cm}∞\quad 5\quad 9\quad 4\quad \hspace{-0.2cm}∞

3. Select 22 as the least key, descend down the path marked by 22, and mark the path by empty holes. The hole at the bottom is filled with .

465486594\square \\ \wedge \\ \square \hspace{2cm} 4 \\ \wedge\hspace{1.8cm} \wedge \\ \square \hspace{0.9cm} 6 \hspace{0.9cm} 5 \hspace{0.9cm} 4 \\ \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge \\ 8\quad \hspace{-0.2cm}∞\quad 6\quad \hspace{-0.2cm}∞\quad 5\quad 9\quad 4\quad \hspace{-0.2cm}∞

5. Select 33 as the least key, descend down the path marked by 33, and mark the path by empty holes. The hole at the bottom is filled with .

4686586594 \\ \wedge \\ 6 \hspace{2cm} \square \\ \wedge\hspace{1.8cm} \wedge \\ 8 \hspace{0.9cm} 6 \hspace{0.9cm} 5 \hspace{0.9cm} \square \\ \wedge\hspace{0.65cm} \wedge\hspace{0.85cm} \wedge\hspace{0.75cm} \wedge \\ 8\quad \hspace{-0.2cm}∞\quad 6\quad \hspace{-0.2cm}∞\quad 5\quad 9\quad ∞ \quad \hspace{-0.2cm}∞

7. Select 44 as the least key, descend down the path marked by 44, and mark the path by empty holes. The hole at the bottom is filled with .

686869\square \\ \wedge \\ 6 \hspace{2cm} \square \\ \wedge\hspace{1.8cm} \wedge \\ 8 \hspace{0.9cm} 6 \hspace{0.9cm} \square \hspace{0.9cm} \hspace{-0.2cm}∞ \\ \wedge\hspace{0.65cm} \wedge\hspace{0.85cm} \wedge\hspace{0.75cm} \wedge \\ 8\quad \hspace{-0.2cm}∞\quad 6\quad \hspace{-0.2cm}∞\quad ∞\quad 9\quad ∞ \quad \hspace{-0.2cm}∞

9. Select 55 as the least key, descend down the path marked by 55, and mark the path by empty holes. The hole at the bottom is filled with .

98989\square \\ \wedge \\ \square \hspace{2cm} 9 \\ \wedge\hspace{1.8cm} \wedge \\ 8 \hspace{0.9cm} \square \hspace{0.9cm} 9 \hspace{0.9cm} \hspace{-0.2cm}∞ \\ \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge \\ 8\quad \hspace{-0.2cm}∞\quad ∞\quad \hspace{-0.2cm}∞\quad ∞\quad 9\quad ∞ \quad \hspace{-0.2cm}∞

11. Select 66 as the least key, descend down the path marked by 66, and mark the path by empty holes. The hole at the bottom is filled with .

999\square \\ \wedge \\ \square \hspace{2cm} 9 \\ \wedge\hspace{1.8cm} \wedge \\ \square \hspace{0.9cm} \hspace{-0.2cm}∞ \hspace{0.9cm} 9 \hspace{0.9cm} \hspace{-0.2cm}∞ \\ \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge \\ ∞\quad \hspace{-0.2cm}∞\quad ∞\quad \hspace{-0.2cm}∞\quad ∞\quad 9\quad ∞ \quad \hspace{-0.2cm}∞

13. Select 88 as the least key, descend down the path marked by 88, and mark the path by empty holes. The hole at the bottom is filled with .

\square \\ \wedge \\ ∞ \hspace{2cm} \square \\ \wedge\hspace{1.8cm} \wedge \\ ∞ \hspace{0.9cm} \hspace{-0.2cm}∞ \hspace{0.9cm} \square \hspace{0.9cm} \hspace{-0.2cm}∞ \\ \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge \\ ∞\quad \hspace{-0.2cm}∞\quad ∞\quad \hspace{-0.2cm}∞\quad ∞\quad \hspace{-0.2cm}∞\quad ∞ \quad \hspace{-0.2cm}∞

15. Select 99 as the least key, descend down the path marked by 99, and mark the path by empty holes. The hole at the bottom is filled with .

224325483625942\\ \wedge \\ 2 \hspace{2cm} 4 \\ \wedge\hspace{1.8cm} \wedge \\ 3 \hspace{0.9cm} 2 \hspace{0.9cm} 5 \hspace{0.9cm} 4 \\ \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge \\ 8\quad 3\quad 6\quad 2\quad 5\quad 9\quad 4\quad \hspace{-0.2cm}∞

2. Ascend up along the path marked by empty hole and fill with 44. In the pair (2,4)(2, 4), since 2<42 < 4 so 22 is chosen to fill the remaining empty holes instead of 44.

33436548365943 \\ \wedge \\ 3 \hspace{2cm} 4 \\ \wedge\hspace{1.8cm} \wedge \\ 3 \hspace{0.9cm} 6 \hspace{0.9cm} 5 \hspace{0.9cm} 4 \\ \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge \\ 8\quad 3\quad 6\quad \hspace{-0.2cm}∞\quad 5\quad 9\quad 4\quad \hspace{-0.2cm}∞

4. Ascend up along the path marked by empty hole and fill with 66. In the pair (3,6)(3, 6), since 3<63 < 6 so 33 is chosen to fill the remaining empty holes instead of 66.

4648654865944 \\ \wedge \\ 6 \hspace{2cm} 4 \\ \wedge\hspace{1.8cm} \wedge \\ 8 \hspace{0.9cm} 6 \hspace{0.9cm} 5 \hspace{0.9cm} 4 \\ \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge \\ 8\quad \hspace{-0.2cm}∞\quad 6\quad \hspace{-0.2cm}∞\quad 5\quad 9\quad 4\quad \hspace{-0.2cm}∞

6. Ascend up along the path marked by empty hole and fill with 88. In the pair (8,6)(8, 6), since 8>68 > 6 so 66 is chosen to fill the remaining empty holes instead of 88. The same applies to the pair (6,4)(6, 4).

56586586595 \\ \wedge \\ 6 \hspace{2cm} 5 \\ \wedge\hspace{1.8cm} \wedge \\ 8 \hspace{0.9cm} 6 \hspace{0.9cm} 5 \hspace{0.9cm} \hspace{-0.2cm}∞ \\ \wedge\hspace{0.65cm} \wedge\hspace{0.85cm} \wedge\hspace{0.75cm} \wedge \\ 8\quad \hspace{-0.2cm}∞\quad 6\quad \hspace{-0.2cm}∞\quad 5\quad 9\quad ∞ \quad \hspace{-0.2cm}∞

8. Ascend up along the path marked by empty hole and fill with . In the pair (5,)(5, ∞), we choose 55 to fill the remaining empty holes instead of .

6698698696 \\ \wedge \\ 6 \hspace{2cm} 9 \\ \wedge\hspace{1.8cm} \wedge \\ 8 \hspace{0.9cm} 6 \hspace{0.9cm} 9 \hspace{0.9cm} \hspace{-0.2cm}∞ \\ \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge \\ 8\quad \hspace{-0.2cm}∞\quad 6\quad \hspace{-0.2cm}∞\quad ∞\quad 9\quad ∞ \quad \hspace{-0.2cm}∞

10. Ascend up along the path marked by empty hole and fill with 99. In the pair (6,9)(6, 9), since 6<96 < 9 so 66 is chosen to fill the remaining empty holes instead of 99.

88989898 \\ \wedge \\ 8 \hspace{2cm} 9 \\ \wedge\hspace{1.8cm} \wedge \\ 8 \hspace{0.9cm} ∞ \hspace{0.9cm} 9 \hspace{0.9cm} \hspace{-0.2cm}∞ \\ \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge \\ 8\quad ∞\quad ∞\quad ∞\quad ∞\quad 9\quad ∞ \quad ∞

12. Ascend up along the path marked by empty hole and fill with . In the pair (8,)(8, ∞), we choose 88 to fill the remaining empty holes.

99999 \\ \wedge \\ ∞ \hspace{2cm} 9 \\ \wedge\hspace{1.8cm} \wedge \\ ∞ \hspace{0.9cm} \hspace{-0.2cm}∞ \hspace{0.9cm} 9 \hspace{0.9cm} \hspace{-0.2cm}∞ \\ \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge \\ ∞\quad \hspace{-0.2cm}∞\quad ∞\quad \hspace{-0.2cm}∞\quad ∞\quad 9\quad ∞ \quad \hspace{-0.2cm}∞

14. Ascend up along the path marked by empty hole and fill with . In the pair (,9)(∞, 9), we choose 99 to fill the remaining empty holes.

∞ \\ \wedge \\ ∞ \hspace{2cm} \hspace{-0.2cm}∞ \\ \wedge\hspace{1.8cm} \wedge \\ ∞ \hspace{0.9cm} \hspace{-0.2cm}∞ \hspace{0.9cm} ∞ \hspace{0.9cm} \hspace{-0.2cm}∞ \\ \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge\hspace{0.75cm} \wedge \\ ∞\quad \hspace{-0.2cm}∞\quad ∞\quad \hspace{-0.2cm}∞\quad ∞\quad \hspace{-0.2cm}∞\quad ∞ \quad \hspace{-0.2cm}∞

16. Ascend up along the path marked by empty hole and fill with . Since the heap is full of the key , the selection process stops.

From the illustration, we can see that the algorithm repeatedly select the root key, and they’re actually picked up in the correct order: 123456891 → 2 → 3 → 4 → 5 → 6 → 8 → 9! Isn’t that cool?

Remarks

Without a doubt, after nn selection steps, the tree is full of , and the sorting process is terminated. Each of the nn selection steps requires log2n\log_2{n} comparisons; thus, the second stage requires a linearithmic running time.

However, this approach has several weaknesses:

  • The heap requires 2n12n - 1 units of storage to store nn items.
  • The occurrences of in the heap are the source of many unnecessary comparisons.
  • Most importantly, it is easier said than done, as it is too difficult for computers to simulate!

An Ingenious Idea

Heap Definition

From the above drawbacks, clearly, we need to find a way to store the structure using nn units of storage instead of 2n12n - 1. From this, the author of Heap Sort proposes a super cool idea:

A heap is a sequence of elements h1,h2,,hnh_1, h_2, \dots, h_n such that:
hih2ihih2i+1h_i ≤ h_{2i} \\ h_i ≤ h_{2i + 1}

for all i=1,2,,n2i = 1, 2, \dots, \lfloor \frac{n}{2} \rfloor.

The sequence of elements hn2+1,,hnh_{\lfloor \frac{n}{2} \rfloor + 1}, \dots, h_n is a natural heap, and the element h1h_1 of a heap is its least value.

This is an example heap:

h1h2h3h4h5h6h72539648h_1 \quad h_2 \quad h_3 \quad h_4 \quad h_5 \quad h_6 \quad h_7 \\ 2 \hspace{0.5cm} 5 \hspace{0.5cm} 3 \hspace{0.5cm} 9 \hspace{0.5cm} 6 \hspace{0.5cm} 4 \hspace{0.5cm} 8

where [9648][9 \quad 6 \quad 4 \quad 8] is a natural heap.

Let’s arrange this sequence of elements into a pictorial representation of a heap:

23396482 \\ \wedge \\ 3 \hspace{1.8cm} 3 \\ \wedge\hspace{1.7cm} \wedge \\ 9 \hspace{0.8cm} 6 \hspace{0.87cm} 4 \hspace{0.8cm} 8

Finally, a heap can be stored using nn units of storage!

Heap Construction

How can we reconstruct a heap if the element at the root is not the smallest key? The solution is as follows:

  • A new heap is obtained by letting the element on top of the subheap “sift down” along the path of the smaller comparand, which at the same time move up.
  • The “sift down” process stops when the element on top of the subheap is smaller than or equal to both its comparands.

Let’s visualize the process with this tree:

15501040702030\downarrow15 \\ \wedge \\ 50 \hspace{1.8cm} \uparrow10 \\ \wedge\hspace{2.2cm} \wedge \\ 40 \hspace{0.9cm} 70 \hspace{0.9cm} 20 \hspace{0.9cm} 30

We choose the subheap containing the elements 10,15,5010, 15, 50. Sift 1515 down to the smaller comparand 1010, and move 1010 up towards the position of 1515.

1050154070203010 \\ \wedge \\ \downarrow50 \hspace{1.8cm} 15 \\ \wedge\hspace{2cm} \wedge \\ \uparrow40 \hspace{0.9cm} 70 \hspace{0.9cm} 20 \hspace{0.9cm} 30

There’s actually another subheap to take into account. We will now choose the subheap of 40,50,7040, 50, 70. Sift 5050 down to 4040, and move 4040 up towards the position of 5050.

1040155070203010 \\ \wedge \\ 40 \hspace{2.2cm} 15 \\ \wedge\hspace{2.2cm} \wedge \\ 50 \hspace{0.9cm} 70 \hspace{0.9cm} 20 \hspace{0.9cm} 30

After the sifting process, we now have a valid heap!

The Sifting Algorithm

As mentioned above, given an array of nn integers a1,a2,ana_1, a_2, … a_n, the sequence of elements an2+1,ana_{\lfloor\frac{n}{2}\rfloor + 1}, … a_n is a natural heap.

Starting from the natural heap, we extend it to the left where in each step a new element is included and properly positioned by a sift.

left = n / 2;

while (left > 0) {
	sift(a, left, n);
	left--;
}

Here is how we would implement the sifting algorithm:

sift(a[], left, right) {
	i = left; j = 2 * i;
	x = a[i]; 
	// Extract the least key
	// We need to store the key to another variable, x
	
	while (j ≤ right) {
		// j < right: Check if a[i] 
		// has one or two comparands
		if (j < right)
			// a[j] > a[j + 1]: Make sure that a[j] is
			// always the smaller among the 2 comparands
			if (a[j] > a[j + 1]) j++;
		if (x ≤ a[j]) break;
		a[i] = a[j];      // "sift down" along the path
		i = j; j = 2 * i; // of the smaller comparand
	}
	a[i] = x;
}

Let’s simulate this with an example array (the natural heap is marked as \lfloor --- \rfloor):

a1a2a3a4a5a6a7a8a9795824315\begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 7 & 9 & 5 & 8 & 2 & 4 & 3 & 1 & 5 \\ \hline \end{array} \\ \hspace{2.8cm} \lfloor ---------- \rfloor

Let’s start with left=n2=4left = \frac{n}{2} = 4, i=left=4i = left = 4, j=i2=8j = i * 2 = 8. The value of rightright is always n=9n = 9 throughout the process.

left=4,i=4,j=8,right=9x=8:a1a2a3a4a5a6a7a8a9795824315left = 4, i = 4, j = 8, right = 9\\ x = 8: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 7 & 9 & 5 & 8 & 2 & 4 & 3 & 1 & 5 \\ \hline \end{array} \\ \hspace{3.9cm} \lfloor ---------- \rfloor
left=4,i=j=8,j=16,right=9x=8:a1a2a3a4a5a6a7a8a9795124385left = 4, i = j = 8, j = 16, right = 9\\ x = 8: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 7 & 9 & 5 & 1 & 2 & 4 & 3 & 8 & 5 \\ \hline \end{array} \\ \hspace{3.9cm} \lfloor ---------- \rfloor

a[j]a[j] is already smaller compared to a[j+1]a[j + 1]. Since a[i]=8a[i] = 8 is larger than a[j]=1a[j] = 1, these two elements will be swapped. Then we will update i=j=8i = j = 8 and j=i2=16j = i * 2 = 16. Since j>rightj > right, there’s no need for another sifting!

left=3,i=3,j=6,right=9x=7:a1a2a3a4a5a6a7a8a9795124385left = 3, i = 3, j = 6, right = 9\\ x = 7: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 7 & 9 & 5 & 1 & 2 & 4 & 3 & 8 & 5 \\ \hline \end{array} \\ \hspace{3.2cm} \lfloor ------------ \rfloor

We only want the smaller between a[j]a[j] and a[j1]a[j - 1], so jj is incremented by 11 to get the index of that smaller element.

left=3,i=3,j=67,right=9x=7:a1a2a3a4a5a6a7a8a9795124385left = 3, i = 3, j = 6 \rightarrow 7, right = 9\\ x = 7: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 7 & 9 & 5 & 1 & 2 & 4 & 3 & 8 & 5 \\ \hline \end{array} \\ \hspace{3.2cm} \lfloor ------------ \rfloor
left=3,i=j=7,j=14,right=9x=7:a1a2a3a4a5a6a7a8a9793124585left = 3, i = j = 7, j = 14, right = 9\\ x = 7: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 7 & 9 & 3 & 1 & 2 & 4 & 5 & 8 & 5 \\ \hline \end{array} \\ \hspace{3.2cm} \lfloor ------------ \rfloor

a[i]=5>a[j]=3a[i] = 5 > a[j] = 3 so a[i]a[j]a[i] ⇄ a[j]. Then, i=j=7,j=i2=14>righti = j = 7, j = i * 2 = 14 > right so this stage is done.

left=2,i=2,j=4,right=9x=9:a1a2a3a4a5a6a7a8a9793124585left = 2, i = 2, j = 4, right = 9\\ x = 9: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 7 & 9 & 3 & 1 & 2 & 4 & 5 & 8 & 5 \\ \hline \end{array} \\ \hspace{2.5cm} \lfloor -------------- \rfloor

a[i]=9>a[j]=1a[i] = 9 > a[j] = 1 so a[i]a[j]a[i] ⇄ a[j]. Then, i=j=4,j=i2=8<righti = j = 4, j = i * 2 = 8 < right. We need to do another sifting.

left=2,i=4,j=89,right=9x=9:a1a2a3a4a5a6a7a8a9713924585left = 2, i = 4, j = 8 \rightarrow 9, right = 9\\ x = 9: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 7 & 1 & 3 & 9 & 2 & 4 & 5 & 8 & 5 \\ \hline \end{array} \\ \hspace{2.5cm} \lfloor -------------- \rfloor
left=2,i=j=9,j=18,right=9x=9:a1a2a3a4a5a6a7a8a9713524589left = 2, i = j = 9, j = 18, right = 9\\ x = 9: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 7 & 1 & 3 & 5 & 2 & 4 & 5 & 8 & 9 \\ \hline \end{array} \\ \hspace{2.5cm} \lfloor -------------- \rfloor

We increment jj to 99 to get the smaller element. a[i]=9>a[j]=5a[i] = 9 > a[j] = 5 so a[i]a[j]a[i] ⇄ a[j]. Then, i=j=9,j=i2=18>righti = j = 9, j = i * 2 = 18 > right so this stage is done.

left=1,i=1,j=2,right=9x=7:a1a2a3a4a5a6a7a8a9713524589left = 1, i = 1, j = 2, right = 9\\ x = 7: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 7 & 1 & 3 & 5 & 2 & 4 & 5 & 8 & 9 \\ \hline \end{array} \\ \hspace{1.8cm} \lfloor ---------------- \rfloor
left=1,i=j=2,j=4,right=9x=7:a1a2a3a4a5a6a7a8a9173524589left = 1, i = j = 2, j = 4, right = 9\\ x = 7: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 1 & 7 & 3 & 5 & 2 & 4 & 5 & 8 & 9 \\ \hline \end{array} \\ \hspace{1.8cm} \lfloor ---------------- \rfloor

a[i]=7>a[j]=1a[i] = 7 > a[j] = 1 so a[i]a[j]a[i] ⇄ a[j]. Then, i=j=2,j=i2=4<righti = j = 2, j = i * 2 = 4 < right. We need to do another sifting.

left=1,i=2,j=45,right=9x=7:a1a2a3a4a5a6a7a8a9173524589left = 1, i = 2, j = 4 \rightarrow 5, right = 9 \\ x = 7: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 1 & 7 & 3 & 5 & 2 & 4 & 5 & 8 & 9 \\ \hline \end{array} \\ \hspace{1.8cm} \lfloor ---------------- \rfloor
left=1,i=j=5,j=10,right=9x=7:a1a2a3a4a5a6a7a8a9123574589left = 1, i = j = 5, j = 10, right = 9\\ x = 7: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 1 & 2 & 3 & 5 & 7 & 4 & 5 & 8 & 9 \\ \hline \end{array} \\ \hspace{1.8cm} \lfloor ---------------- \rfloor

We increment jj to 55 to get the smaller element. a[i]=7>a[j]=2a[i] = 7 > a[j] = 2 so a[i]a[j]a[i] ⇄ a[j]. Then, i=j=5,j=i2=10>righti = j = 5, j = i * 2 = 10 > right so this stage is done.

Our final heap should be:

a1a2a3a4a5a6a7a8a9123574589\begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 1 & 2 & 3 & 5 & 7 & 4 & 5 & 8 & 9 \\ \hline \end{array}

The Sorting Process

So we’ve discovered how to construct a heap. How can we actually use this heap for sorting our data?

Let’s assume we’re given a heap of nn elements. In order to obtain the sorted elements, nn steps have to be executed:

  • Pick up the element at the top of the heap (and store it somewhere). This allows us to get the smallest one among the nn elements.
  • Reconstruct the remaining elements to form a heap.

Here, two questions arise. Where can we store those emerging top elements? Creating a new array b[n]b[n] isn’t a good solution because it would double the amount of memory required for the algorithm. Also, how can we reconstruct the remaining elements to form a heap? Do we need to write something new do this or can we reuse something we’ve already done?

A solution for all of these questions is as follows:

  • In each step, the leftmost element and the rightmost element of the current heap are exchanged.
  • Then, let the new leftmost element sift down into its proper position.
  • After n1n - 1 steps, the array will be sorted (in the reverse order).

This is how the solution is implemented:

right = n; 
// Index of the right-most element
// of the current heap

while (right > 1) {
	a[1] ⇄ a[right];
	right--;
	sift(a, 1, right);
}

Let’s get our hands dirty and sort the heap we’ve just constructed above, shall we? This is our constructed heap, now we’d like to sort all of the elements which form this heap.

right=9:a1a2a3a4a5a6a7a8a9123574589right = 9: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 1 & 2 & 3 & 5 & 7 & 4 & 5 & 8 & 9 \\ \hline \end{array}

First, exchange a[1]a[1] with a[right]a[right]. Then we’re going to let the new a[1]a[1] sift down to its proper position in the heap marked with \lfloor --- \rfloor:

right=9:a1a2a3a4a5a6a7a8a9923574581right = 9: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 9 & 2 & 3 & 5 & 7 & 4 & 5 & 8 & 1 \\ \hline \end{array} \\ \hspace{1.8cm} \lfloor -------------- \rfloor

I will only demonstrate the changes of the heap without much detail, since we’ve already discussed the sifting algorithm in the above section. If you want to see the sifting process as well, just click on the \blacktriangleright button before the changes.

Sifting process

right=9:a1a2a3a4a5a6a7a8a9293574581right = 9: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 2 & 9 & 3 & 5 & 7 & 4 & 5 & 8 & 1 \\ \hline \end{array} \\ \hspace{1.8cm} \lfloor -------------- \rfloor
right=9:a1a2a3a4a5a6a7a8a9253974581right = 9: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 2 & 5 & 3 & 9 & 7 & 4 & 5 & 8 & 1 \\ \hline \end{array} \\ \hspace{1.8cm} \lfloor -------------- \rfloor
right=9:a1a2a3a4a5a6a7a8a9253874591right = 9: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 2 & 5 & 3 & 8 & 7 & 4 & 5 & 9 & 1 \\ \hline \end{array} \\ \hspace{1.8cm} \lfloor -------------- \rfloor

right=8:a1a2a3a4a5a6a7a8a9953874521right = 8: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 9 & 5 & 3 & 8 & 7 & 4 & 5 & 2 & 1 \\ \hline \end{array} \\ \hspace{1.1cm} \lfloor ------------ \rfloor
Sifting process

right=8:a1a2a3a4a5a6a7a8a9359874521right = 8: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 3 & 5 & 9 & 8 & 7 & 4 & 5 & 2 & 1 \\ \hline \end{array} \\ \hspace{1.1cm} \lfloor ------------ \rfloor
right=8:a1a2a3a4a5a6a7a8a9354879521right = 8: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 3 & 5 & 4 & 8 & 7 & 9 & 5 & 2 & 1 \\ \hline \end{array} \\ \hspace{1.1cm} \lfloor ------------ \rfloor

right=7:a1a2a3a4a5a6a7a8a9554879321right = 7: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 5 & 5 & 4 & 8 & 7 & 9 & 3 & 2 & 1 \\ \hline \end{array} \\ \hspace{0.4cm} \lfloor ---------- \rfloor
Sifting process

right=7:a1a2a3a4a5a6a7a8a9455879321right = 7: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 4 & 5 & 5 & 8 & 7 & 9 & 3 & 2 & 1 \\ \hline \end{array} \\ \hspace{0.4cm} \lfloor ---------- \rfloor

right=6:a1a2a3a4a5a6a7a8a9955874321right = 6: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 9 & 5 & 5 & 8 & 7 & 4 & 3 & 2 & 1 \\ \hline \end{array} \\ \hspace{-0.3cm} \lfloor -------- \rfloor
Sifting process

right=6:a1a2a3a4a5a6a7a8a9595874321right = 6: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 5 & 9 & 5 & 8 & 7 & 4 & 3 & 2 & 1 \\ \hline \end{array} \\ \hspace{-0.3cm} \lfloor -------- \rfloor
right=6:a1a2a3a4a5a6a7a8a9575894321right = 6: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 5 & 7 & 5 & 8 & 9 & 4 & 3 & 2 & 1 \\ \hline \end{array} \\ \hspace{-0.3cm} \lfloor -------- \rfloor

right=5:a1a2a3a4a5a6a7a8a9975854321right = 5: \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 9 & 7 & 5 & 8 & 5 & 4 & 3 & 2 & 1 \\ \hline \end{array} \\ \hspace{-1.0cm} \lfloor ------ \rfloor
......

We will do this repeatedly until our array is sorted!

a1a2a3a4a5a6a7a8a9987554321\begin{array}{|c|c|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 & a_8 & a_9 \\ \hline 9 & 8 & 7 & 5 & 5 & 4 & 3 & 2 & 1 \\ \hline \end{array}

Performance Analysis of Heap Sort

The Heap Sort algorithm runs in O(nlogn)\mathcal{O}(n\log n) because it consists of two stages:

  • The heap construction stage: The algorithm executes n2\lfloor\frac{n}{2}\rfloor sift steps where each step runs in O(logn)\mathcal{O}(\log n) time. As a result, this stage takes O(nlogn)\mathcal{O}(n \log n) time.
  • The sorting stage: The algorithm executes n1n - 1 steps where each step runs in O(logn)\mathcal{O}(\log n) time. Therefore, this stage takes O(nlogn)\mathcal{O}(n \log n) time.

As a result, the worst-case complexity of Heap Sort is O(nlogn)\mathcal{O}(n \log n), and this is also similar to the average case. For the best case, it is O(n)\mathcal{O}(n) when every elements in the array is identical to each other.

Heap Sort only requires O(1)\mathcal{O}(1) - constant space.

Quick Sort

The Quick Sort algorithm was developed in 1959 by Tony Hoare. It was selected as one of the 10 algorithms “with the greatest influence on the development and practice of science and engineering in the 20th century”.

Basic Concept of Quick Sort

Quick Sort is based on the idea of an array partition.

An Array Partition

An array partition is an arrangement of the array’s elements so that:

{a1,...,as1}as{as+1,...,an}\{a_1, ..., a_{s-1}\} ≤ a_s ≤ \{a_{s+1}, ... , a_n\}

After a partition is achieved, asa_s will be in its final position in the sorted array.

We can continue sorting the two subarrays to the left and to the right of asa_s independently by the same method.

For example, assume that we have the array:

4135698as\begin{array}{|c|c|c|c|c|c|c|} \hline 4 & 1 & 3 & 5 & 6 & 9 & 8 \\ \hline \end{array} \\ \uparrow \\ \lfloor --- \rfloor ≤ a_s ≤\lfloor --- \rfloor

In the given array, as=5a_s = 5 is in its final position because {4,1,3}<5<{6,9,8}\{4, 1, 3\} < 5 < \{6, 9, 8\}. This position will never be visited again by the algorithm.

This is the implementation for the above idea:

QuickSort(a[], left, right) {
	if (left < right) {
		s = Partition(a, left, right);
		QuickSort(a, left, s - 1);
		QuickSort(a, s + 1, right);
	}
}

The Partition Function

The Partition() function starts by selecting a pivot pp, which is an element of the (sub)array being partitioned. For simplicity, we’re going to assume that p=aleftp = a_{left}.

The function will then scan the (sub)array from both ends, comparing the (sub)array’s elements to the pivot pp.

ijpall arepppall arep\hspace{0.5cm} i \rightarrow \hspace{1cm} \leftarrow j\\ \begin{array}{|c|c|c|c|c|c|} \hline p & \text{all are} ≤ p & ≥p & \dots & ≤p & \text{all are} ≥p \\ \hline \end{array}
  • ii: left to right scan, stops when encountering aipa_i ≥ p.
  • jj: right to left scan, stops when encountering aipa_i ≤ p.

After both scans stop, three situations may arise:

  • If i<ji < j:

    The function exchanges aia_i and aja_j, then it resume the scans by incrementing ii and decrementing jj by 11.

    ij5317629pivotswap & resumethe scan\hspace{1.2cm} i\rightarrow \leftarrow j \\ \begin{array}{|c|c|c|c|c|c|c|} \hline 5 & 3 & 1 & 7 & 6 & 2 & 9 \\ \hline \end{array} \\ \hspace{-0.5cm}\uparrow \hspace{1.3cm}\lfloor --- \rfloor \\ \hspace{-0.1cm}\text{pivot} \hspace{0.7cm} \text{swap \& resume} \\ \hspace{1.3cm}\text{the scan}
  • If i>ji > j:

    The function will have partitioned the (sub)array after exchanging the pivot p=aleftp = a_{left} with aja_j. The split position is s=js = j.

    i5all arep46all arepji4all arep56all arepj\hspace{0.5cm} i \rightarrow \\ \begin{array}{|c|c|c|c|c|} \hline 5 & \text{all are} ≤ p & 4 & 6 & \text{all are} ≥p \\ \hline \end{array}\\ \hspace{0.5cm} \leftarrow j \\[10pt]\\ \hspace{0.8cm} i \downarrow \\ \begin{array}{|c|c|c|c|c|} \hline 4 & \text{all are} ≤ p & 5 & 6 & \text{all are} ≥p \\ \hline \end{array}\\ \hspace{0.3cm} \uparrow j
    • a[j]=p=5a[j] = p = 5: new value of a[j]a[j]; p=a[j]=4p = a[j] = 4: new pivot value
    • The array is separated by the element with the value 55.
  • If i=ji = j:

    The value that two scanning indices are pointing to must be equal to pp. Thus, we have the (sub)array partitioned, with the split position s=i=js = i = j.

    i5all arep5all arepj\hspace{1cm} i \rightarrow \\ \begin{array}{|c|c|c|c|c|} \hline 5 & \text{all are} ≤ p & 5 & \text{all are} ≥p \\ \hline \end{array}\\ \leftarrow j \\
    • If we swap a[i]a[j]a[i] ⇄ a[j], it would be redundant. pa[j]p ⇄ a[j], if we do that it’s still correct but redundant). We should only return ii or jj as the split position in this case.

Let’s see how this Partition() function is implemented:

Partition(a[], left, right) {
	p = a[left];
	i = left;
	j = right + 1;
	do {
		do i++; while (a[i] < p);
		do j--; while (a[j] > p);
		a[i] ⇄ a[j];
	} while (i < j);
	a[i] ⇄ a[j]; // undo last swap when i ≥ j
	a[left] ⇄ a[j];
	return j;
} 

This is called the Hoare’s Partition Algorithm.

When this function runs, ii is less than jj (i<ji < j) for most of the iterations, so the loop primarily handles this case.

If i>ji > j, this only happens once per partition operation, at which point we need to undo the last swap (a[i]a[j]a[i] ⇄ a[j]). This is because in the last iteration where i<ji < j, a[i]a[i] and a[j]a[j] were swapped unnecessarily before exiting the loop.

As for the final pivot swap (a[left]a[j]a[left] ⇄ a[j]), while it may be redundant when i=ji = j, its time complexity is constant, making it an acceptable trade-off for maintaining the simplicity and clarity of the function.

Overall, the Partition() function is just a beautiful piece of code.

Performance Analysis of Quick Sort

In the Quick Sort algorithm, the basic operation is the comparisons in the do-while loop. Therefore, the time efficiency of Quick Sort depends on the value of pp selected in each partition.

  • In the best case: The value of pp is always the median of the (sub)array, allowing the pivot to consistently divide the array into two nearly equal halves. This balanced partitioning results in a logarithmic number of recursive calls, each processing linear elements.
    B(n)Θ(nlogn)B(n) \in \Theta(n\log n)
  • In the worst case: The value of pp is always the maximum or minimum value of the (sub)array. In such cases, the depth of recursion becomes linear, and the algorithm degrades to quadratic time complexity.
    W(n)Θ(n2)W(n) \in \Theta(n^2)
  • In the average case: On average, even with random pivot selections, the partitions tend to be reasonably balanced, leading to efficient sorting times.
    A(n)1.39nlog2nΘ(nlogn)A(n) ≈ 1.39n\log_2n \in \Theta(n\log n)

As for space efficiency, due to the recursive call stack, Quick Sort uses O(n)\mathcal{O}(n) space.

Merge Sort

Merge Sort is a divide-and-conquer approach that was invented by John von Neumann in 1945.

Basic Concept of Merge Sort

How This Works

Conceptually, the algorithm works as follows:

  • Divide the unsorted array into nn subarrays, each containing one element. This can be done by divide the array recursively into two halves until it can no longer able to be divided.
  • Repeatedly merge sorted subarrays to produce new sorted subarrays until there is only one subarray remaining.

This remaining subarray will be the sorted array.

Let’s use Quick Sort to sort an example array:

4853961\begin{array}{|c|c|c|c|c|c|c|} \hline 4 & 8 & 5 & 3 & 9 & 6 & 1 \\ \hline \end{array}

We’ll divide this array recursively into two halves:

4853961485396148539614853961\begin{array}{|c|c|c|c|c|c|c|} \hline 4 & 8 & 5 & 3 & 9 & 6 & 1 \\ \hline \end{array} \\[8pt]\downarrow\\\\[8pt] \begin{array}{|c|c|c|} \hline 4 & 8 & 5 \\ \hline \end{array} \quad \begin{array}{|c|c|c|c|} \hline 3 & 9 & 6 & 1 \\ \hline \end{array} \\[8pt]\downarrow\\\\[8pt] \begin{array}{|c|} \hline 4 \\ \hline \end{array} \quad \begin{array}{|c|c|} \hline 8 & 5 \\ \hline \end{array} \quad \begin{array}{|c|c|} \hline 3 & 9 \\ \hline \end{array} \quad \begin{array}{|c|c|} \hline 6 & 1 \\ \hline \end{array} \\ \\[8pt]\downarrow\\\\[8pt] \begin{array}{|c|} \hline 4 \\ \hline \end{array} \quad \begin{array}{|c|} \hline 8 \\ \hline \end{array} \quad \begin{array}{|c|} \hline 5 \\ \hline \end{array} \quad \begin{array}{|c|} \hline 3 \\ \hline \end{array} \quad \begin{array}{|c|} \hline 9 \\ \hline \end{array} \quad \begin{array}{|c|} \hline 6 \\ \hline \end{array} \quad \begin{array}{|c|} \hline 1 \\ \hline \end{array} \quad

When we’ve already split the array into nn pieces, we will merge these pieces back together. As we merge, values from each subarray are compared so that the smallest value always comes first.

4853961483569134581691345689\begin{array}{|c|} \hline 4 \\ \hline \end{array} \quad \begin{array}{|c|} \hline 8 \\ \hline \end{array} \quad \begin{array}{|c|} \hline 5 \\ \hline \end{array} \quad \begin{array}{|c|} \hline 3 \\ \hline \end{array} \quad \begin{array}{|c|} \hline 9 \\ \hline \end{array} \quad \begin{array}{|c|} \hline 6 \\ \hline \end{array} \quad \begin{array}{|c|} \hline 1 \\ \hline \end{array} \\ \\[8pt]\downarrow\\\\[8pt] \begin{array}{|c|c|} \hline 4 & 8 \\ \hline \end{array} \quad \begin{array}{|c|c|} \hline 3 & 5 \\ \hline \end{array} \quad \begin{array}{|c|c|} \hline 6 & 9 \\ \hline \end{array} \quad \begin{array}{|c|} \hline 1 \\ \hline \end{array} \\ \\[8pt]\downarrow\\\\[8pt] \begin{array}{|c|c|c|c|} \hline 3 & 4 & 5 & 8 \\ \hline \end{array} \quad \begin{array}{|c|c|c|} \hline 1 & 6 & 9 \\ \hline \end{array} \\ \\[8pt]\downarrow\\\\[8pt] \begin{array}{|c|c|c|c|c|c|c|} \hline 1 & 3 & 4 & 5 & 6 & 8 & 9 \\ \hline \end{array}

After the merging process, we finally have a sorted array.

The implementation for Merge Sort is pretty straightforward:

MergeSort(a[], low, high) {
	if (low >= high) return;
	mid = (low + high) / 2;
	MergeSort(a, low, mid);
	MergeSort(a, mid + 1, high);
	Merge(a, low, mid, high)
}

The Merge Function

In this function, we definitely need to create a temporary array T[n]T[n] to store the elements of the merged sorted arrays. In this array, the left half will be stored from low to mid, while mid + 1 to high is for the right half.

Using the while loop, we’ll select one element from the left half, one element from the right half, and compare them together. Then we’ll insert the smaller one into the temporary array, followed by copying any remaining elements.

Finally, the temporary array is copied back into the original array.

Merge(a, low, mid, high) {
	n = high - low + 1;
	T[n] = [];         // Create a temporary array
	left = low;        // Starting index of the left half
	right = mid + 1;   // Starting index of the right half
	k = 0;             // Pointer for the temporary array
	
	// Merge the two subarrays into T
	while (left ≤ mid && right ≤ high) {
		if (a[left] <= a[right]) T[k++] = a[left++];
		else T[k++] = a[right++];
	}
	
	// Copy any remaining elements 
	// from the left subarray
	while (left ≤ mid) T[k++] = a[left++];
	
	// Copy any remaining elements 
	// from the right subarray
	while (right ≤ high) T[k++] = a[right++];
	
	// Copy the merged result back 
	// into the original array
	for (i = low; i ≤ high; i++) {
		a[i] = T[i - low];
	}
}

Performance Analysis of Merge Sort

The array is repeatedly split in half until an array of size 11 is reached. The number of splits required is log2n\log_2 n. At each level of recursion, the merging process processes all nn elements once. Thus, each level contributes Θ(n)\Theta(n) time.

Since there are log2n\log_2 n levels and each level performs Θ(n)\Theta(n) work, the overall time is:

Θ(n)×Θ(logn)=Θ(nlogn)\Theta(n) × \Theta(\log n) = \Theta(n \log n)
  • For the best case: Even if the array is already sorted, merge sort will still perform all Θ(nlogn)\Theta(n \log n) operations, as it does not take advantage of any pre-existing order during the merge process.
  • For the average case: The process of dividing and merging remains the same of Θ(nlogn)\Theta(n \log n) regardless of the initial order of elements.
  • For the worst case: The time complexity is Θ(nlogn)Θ(n \log ⁡n) as well, making Merge Sort a very efficient sorting algorithm.
C(n)=Θ(nlogn)C(n) = \Theta(n \log n)

Merge Sort allocates an auxiliary array of size nn (or at least the size of the current subarray being merged) to hold the merged output. Therefore, the extra space required is O(n)\mathcal{O}(n). In addition, the depth of the recursion is O(logn)\mathcal{O}(\log n), which means the additional space used by the call stack is O(logn)\mathcal{O}(\log n). Compared to the auxiliary array, this is relatively minor.

Combining the space for the temporary array and the recursion stack, the overall space complexity is O(n)\mathcal{O}(n).



👏 Thanks for reading!