Overview of Sorting Algorithms
Sorting algorithms are the most fundamental, widely applicable, and frequently tested algorithms in interviews.
A sorting algorithm is an algorithm that can arrange a string of data in a specific order. Where:
- Output result is an ascending sequence
- Output result is a permutation or reorganization of the original input
Two basic operations required for sortable objects are: Compare and Swap
Classification of Sorting Algorithms
| Category | Subcategories |
|---|---|
| Exchange Sort | Bubble Sort Cocktail Sort Odd-even Sort Comb Sort Gnome Sort Quicksort Stooge Sort Bogosort |
| Selection Sort | Selection Sort Heapsort Smoothsort Cartesian Tree Sort Tournament Sort Cycle Sort |
| Insertion Sort | Insertion Sort Shellsort Splay Sort Binary Search Tree Sort Library Sort Patience Sort |
| Merge Sort | Merge Sort Cascade Merge Sort Oscillating Merge Sort Polyphase Merge Sort Strand Sort |
| Concurrent Sort | Bitonic Sorter Batcher Odd-even Mergesort Pairwise Sorting Network |
| Hybrid Sort | Block Sort Timsort Introsort Spreadsort UnShuffle Sort |
| Other | Topological Sort Pancake Sort Spaghetti Sort |
- Stable/Unstable: Stable sorting algorithms maintain the relative order of records with equal keys. That is, if a sorting algorithm is stable, when there are two records R and S with equal keys, and R appears before S in the original list, R will also appear before S in the sorted list.
- Adaptive/Non-adaptive: Non-adaptive algorithms execute the same sequence of operations independent of data order. Adaptive sorting executes different operation sequences.
- Internal/External sorting: Internal sorting allows random access, while external sorting algorithms must access elements sequentially (at least within large data blocks)
- Applicability to linked lists.
- Whether it’s in-place sorting. In-place sorting algorithms don’t need extra space to save copies except for a few variables.
Sorting Algorithm Performance Quick Reference
| Category | Sorting Method | Average Time Complexity | Best Time Complexity | Worst Time Complexity | Space Complexity | Stability |
|---|---|---|---|---|---|---|
| Insertion Sort | Direct Insertion | $O(n^2)$ | $O(n)$ | $O(n^2)$ | $O(1)$ | Stable |
| Shell Sort | $O(n^{1.3})$ | $O(n)$ | $O(n^2)$ | $O(1)$ | Unstable | |
| Selection Sort | Direct Selection | $O(n^2)$ | $O(n^2)$ | $O(n^2)$ | $O(1)$ | Unstable |
| Heap Sort | $O(n\log_{2}{n})$ | $O(n\log_{2}{n})$ | $O(n\log_{2}{n})$ | $O(1)$ | Unstable | |
| Exchange Sort | Bubble Sort | $O(n^2)$ | $O(n^2)$ | $O(n^2)$ | $O(1)$ | Stable |
| Quicksort | $O(n\log_{2}{n})$ | $O(n\log_{2}{n})$ | $O(n^2)$ | $O(n\log_{2}{n})$ | Unstable | |
| Merge Sort | Merge Sort | $O(n\log_{2}{n})$ | $O(n\log_{2}{n})$ | $O(n\log_{2}{n})$ | $O(1)$ | Stable |
Memory Techniques
- Four major basic sorts: insertion, selection, exchange, merge.
- Advanced version of insertion is shell, selection is heap sort, bubble is quicksort.
- Only simple sorting methods are stable, but selection sort is unstable. Direct insertion, bubble, and merge are stable.
- Only heap sort and merge sort guarantee worst-case time complexity
- For small-scale data, basic sorting algorithms actually have certain advantages.
Analysis Framework
Any elements to be sorted need to implement two operations: compare and swap.
Here are Python examples, including functions to generate random arrays, verify if arrays are sorted, and verify sorting function correctness.
Selection Sort
Selection sort is one of the simplest basic sorting algorithms. It works by continuously selecting the smallest element from the remaining elements.
The core idea of selection sort is: divide data into (ordered region, unordered region), each time select the smallest element from the unordered region and place it at the end of the ordered region. For an array with n elements, it performs n-1 selections, because the last remaining element must be the largest.
Selection sort’s disadvantage is that its runtime has little dependency on already ordered parts in the file - it finds the minimum element every time without fully utilizing existing ordered portions in the original sequence. Conversely, selection sort is insensitive to initial input order, with little difference between worst and best cases.
Its advantage is that selection sort has more comparisons but the fewest swaps among all sorting algorithms. For element types where swap cost is much greater than comparison cost (large elements with small keys), selection sort can be used. For elements with high comparison costs (like string comparisons), insertion sort is a better choice.
Its advanced version is Heap Sort.
Analysis
- Loop invariant: At the start of each loop, the front segment
A[0:i]is ordered, the back segmentA[i:N]is unordered. - Initial condition: Before sorting begins
i=0, front segment empty arrayA[0:0]is ordered, back segment full arrayA[0:N]is unordered. - Termination condition: When iteration ends
i=N, at this time front segment full arrayA[0:N]is ordered, back segment empty arrayA[N:N]is unordered, entire array is ordered. - Maintenance property:
- Each iteration end causes the minimum element in unordered region
A[i:N]to swap positions withA[i]. This element becomes the maximum element in the ordered region. - Each iteration adds
A[i]to the ordered region, front ordered region grows toA[0:i+1], back unordered region shrinks toA[i+1:N]. - Next iteration begins with
i=i+1, loop invariant is maintained.
- Each iteration end causes the minimum element in unordered region
Implementation
Characteristics
- Unstable sorting
- In-place operation
- Non-adaptive sorting: insensitive to initial input order, little difference between worst and best cases.
- Fewest swaps among all basic sorting algorithms, but more comparisons
- During execution, the ordered region at the front remains unchanged.
- Suitable for linked list sorting.
| Item\Case | Average Case | Worst Case | Best Case |
|---|---|---|---|
| Time Complexity | $O(n^2)$ | $O(n^2)$ | $O(n^2)$ |
| Comparisons | $\frac{n(n-1)}{2}$ | $\frac{n(n-1)}{2}$ | $\frac{n(n-1)}{2}$ |
| Swaps | $n-1$ | $n-1$ | $n-1$ |
Insertion Sort
Insertion sort is a simple and intuitive basic sorting algorithm that pulls an element from the unordered region’s head and places it in the appropriate position in the ordered region. Similar to organizing playing cards. It’s a stable in-place sorting algorithm with good locality.
The core idea of insertion sort is: divide data into (ordered region, unordered region), take the first element from the unordered region and insert it into the correct position in the ordered region. Usually implemented by continuously moving forward to the appropriate position.
Unlike selection sort, insertion sort is an adaptive algorithm whose runtime is closely related to the degree of order in the original sequence. It’s also stable sorting and in-place sorting.
Analysis
- Loop invariant: At the start of each loop, front segment
A[0:i]is ordered, back segmentA[i:N]is unordered. - Initial condition: Before sorting begins
i=1, front single-element arrayA[0:1]is ordered, back arrayA[1:N]is unordered. - Termination condition: When iteration ends
i=N, at this time front full arrayA[0:N]is ordered, back empty arrayA[N:N]is unordered, entire array is ordered. - Maintenance property:
- Each iteration end places element
A[i]in the appropriate position in the front ordered array, - Each iteration adds
A[i]to the ordered region, front ordered region grows toA[0:i+1], back unordered region shrinks toA[i+1:N]. - Next iteration begins with
i=i+1, loop invariant is maintained.
- Each iteration end places element
Implementation
If the previous element of the current element is smaller than the current element, swap the two elements.
If the selected A[i] is smaller than all elements in the ordered region, it should be placed at position A[0]. At this time, the loop condition needs additional checking if the index has reached the end j>0. If it has reached the end, the previous loop already executed swap(A,0,1), placing the element in the appropriate position, and should break the loop to avoid index overflow.
Insertion sort can be simplified by using sentinel keys, but it’s not always useful - for example, when minimum values are hard to define or there’s no extra space. A clever approach is to perform one bubble or selection sort in the first iteration, placing the smallest element at the array head as a sentinel. Improved implementation:
The improved implementation mainly includes using reverse bubbling first to generate the minimum element sentinel at A[0], incidentally eliminating some inverse pairs. In subsequent loops, the iteration condition no longer needs to check if the index is out of bounds. Also changing iteration swaps to assignments can reduce half the assignment operations.
Characteristics
- Stable sorting
- In-place sorting
- Adaptive sorting algorithm, executes quickly when initial sequence is largely ordered.
- Fewer comparisons but many swaps.
- Only accesses elements in the ordered part, and sequentially, with good locality. But the ordered region changes during sorting.
| Item\Case | Average Case | Worst Case | Best Case |
|---|---|---|---|
| Time Complexity | $O(n^2)$ | $O(n^2)$ | $O(n)$ |
| Space Complexity | $O(n^2)$ | $O(n^2)$ | $O(1)$ |
| Comparisons | $\frac{n^2}{4}$ | $\frac{n(n-1)}{2}$ | $n-1$ |
| Swaps | $\frac{n^2}{4}$ | $\frac{n(n-1)}{2}$ | 0 |
Bubble Sort
Bubble sort is an exchange sort that works by continuously correcting inverse pairs in the sequence. Each round of bubbling causes the largest element from the front unordered region to float up to the back ordered region. It has the simplest implementation.
The core idea of bubble sort is: divide data into (unordered region, ordered region), find the largest element from the unordered region through swaps and place it at the front of the ordered region. Repeat n-1 times to ensure array is sorted.
Analysis
- Loop invariant: At the start of each loop, front segment
A[0:N-i]is unordered, back segmentA[N-i:N]is ordered. - Initial condition: Before sorting begins
i=0, front full arrayA[0:N]is unordered, back empty arrayA[N:N]is ordered, entire array is unordered. - Termination condition: When iteration ends
i=N, at this time front empty arrayA[0:0]is unordered, back full arrayA[0:N]is ordered, entire array is ordered. - Maintenance property:
- Each iteration end causes the largest element in
A[0:N-i]to float up toA[N-i], and this element is smaller than all elements in the back ordered region. - Each iteration shrinks the unordered region to
A[0:N-i-1]and grows the ordered region toA[N-i-1:N]. - Next iteration begins with
i=i+1, front segmentA[0:N-(i+1)]is unordered, back segmentA[N-(i+1):N]is ordered, loop invariant is maintained.
- Each iteration end causes the largest element in
Implementation
Characteristics
- Belongs to exchange sorting
- Stable sorting
- In-place sorting, space complexity $O(1)$
- Extremely simple implementation. Two iterations, outer
range(n-1), innerrange(n-1-i).
| Item\Case | Average Case | Worst Case | Best Case |
|---|---|---|---|
| Time Complexity | $O(n^2)$ | $O(n^2)$ | $O(n^2)$ |
| Comparisons | $\frac{n(n-1)}{2}$ | $\frac{n(n-1)}{2}$ | $\frac{n(n-1)}{2}$ |
| Swaps | Number of inversions | $\frac{n(n-1)}{2}$ | 0 |
Shell Sort
Shell sort is insertion sort with specified step sizes. Also called diminishing increment sort algorithm, it’s an improved version of insertion sort.
Insertion sort runs inefficiently because it performs swaps only between adjacent elements, so each element moves at most one position per swap. In extreme cases like the smallest element at the array’s tail, insertion sort needs N swaps to move it to the array’s front. Shell sort significantly improves execution efficiency by allowing swaps between non-adjacent elements.
Shell sort’s essence is rearranging the file so it has the property that taking every h-th element produces a sorted sequence. For example, h=3 requires sequences [0,3,6,...,3n,...] in the array to be sorted. Such files are called h-sorted. Sorting files with larger h makes smaller h sorting easier. When h=1, it becomes ordinary insertion sort. Therefore, using a step sequence ending with 1, continuously performing h-sorting can produce a sorted file.
Analysis
Shell sort’s key is using appropriate steps. When step is 1, it becomes insertion sort. So any step sequence should end with 1. Knuth’s step sequence is commonly used: $h_{i+1} = 3h_i +1$, i.e., 1,4, 13,40,121,364,....
Implementation
First generate the step sequence, with maximum step around one-tenth of array length. Then execute h-insertion sort according to step sequence ...,40,13,4,1, the difference being replacing all 1s in original insertion sort with h.
Characteristics
Best known shell step sequence is: 1, 5, 19, 41, 109
Belongs to advanced insertion sort
Unstable sorting
Adaptive sorting.
In-place sorting, space complexity $O(1)$
Counting Sort
Counting sort, also called key-indexed counting sort, doesn’t belong to comparison sorting. When the key range is determined and relatively small, counting sort can efficiently perform sorting.
Counting sort uses ideas similar to computing percentiles. First obtain the data’s distribution CDF. Then query each element’s rank() in the original array and place that element at the position specified by rank().
Implementation
Quicksort
Quicksort is an advanced exchange sort, also called partition-exchange sort. Quicksort is the most widely applied sorting algorithm. Belongs to generalized selection sort, uses divide-and-conquer.
Quicksort’s core idea is: partition the array into two parts, then sort both parts separately. The partitioning process is key, it must ensure:
- For some
i,a[i]is in its final position in the array. - Elements in
a[0],...,a[i-1]are all smaller thanA[i]. - Elements in
a[i+1],...a[N-1]are all larger thanA[i].
Analysis
Recursive version of qsort can be briefly expressed as follows (Fired version)
How to choose pivot is a big problem. Usually the last element can be chosen as pivot, but a better approach is randomly selecting an element.
Implementation
A more reasonable and concise implementation is as follows:
Boundary condition analysis: when exiting the while loop, we have i>j. At this time we need to prove the array satisfies:
- $k∈ [lo,j) , A_k < pivot$
- $k∈ (j,i) , A_k = pivot$
- $k∈ [i,hi) , A_k > pivot$
Don’t want to prove this anymore.
Characteristics
- Unstable sorting algorithm
- In-place sorting, recursive version needs space complexity $O(\log n)$ to save call information, which is relatively small.
- Average sorting complexity is $O(n\log n)$, with very small inner loops, can be efficiently implemented on most architectures. Usually faster than other $O(n \log n)$ sorting algorithms.
- Worst case complexity is $O(n^2)$
- For large files, quicksort performance is 5-10 times that of shell sort. But for small files, shell might be better. A common optimization is using other sorting methods like shell sort when
hi-lois smaller than a specific value like 12. This is also the approach used in GO’s standard librarysort. Sedgewick gives a small file threshold of 9.
| Item\Case | Average Case | Worst Case | Best Case |
|---|---|---|---|
| Time Complexity | $O(1.39 n\log_{2}{n})$ | $O(n\log_{2}{n})$ | $O(n\log_{2}{n})$ |
| Space Complexity | $O(\log n)$ | $O(n)$ | $O(\log n)$ |
| Comparisons | $O(n\log n)$ | $\frac{n(n-1)}{2}$ | $O(n\log n)$ |
Merge Sort
Merging is combining two sorted files into one larger ordered file.
- Merge implementation
Merging is the core of merge sort. The core idea is: if a subarray has reached the end, continue with elements from the other subarray; if neither has reached the end, compare the current elements of both subarrays.
For linked list merging, it’s slightly more complex. Here considering linked lists without head nodes, ending with nil, the merge logic is:
With the merge method, merge sort implementation is quite simple:
Stable sorting
Space complexity $O(n)$, needs basically equivalent additional storage space.
If the merge method used is stable, then merge sort is stable.
Uses divide-and-conquer, proposed by von Neumann.
Can run in parallel
Can be conveniently applied to linked lists, slow external storage, external sorting.
| Item\Case | Average Case | Worst Case | Best Case |
|---|---|---|---|
| Time Complexity | $O(n\log_{2}{n})$ | $O(n\log_{2}{n})$ | $O(n\log_{2}{n})$ |
| Comparisons | $O(n\log n)$ | $n\log_2n - n+1$ | $\frac{n\log_2n}{2}$ |
Heap Sort & Priority Queue
Heap sort is a special sorting method that utilizes priority queue properties. Well-implemented priority queues can achieve logarithmic-level insert element/delete maximum(minimum) element operations. Therefore, for n elements to be sorted, just build a priority queue and continuously extract the maximum (minimum) element to complete sorting.
Binary heaps are usually used to implement priority queues.
When each node of a binary tree is greater than or equal to its two children, it’s called heap-ordered. At this time, the root node is the root of the heap-ordered binary tree.
A binary heap is a set of elements that can be sorted using heap-ordered complete binary trees and stored in arrays by level.
How to represent a complete binary tree in an array: a simple way is to leave the first element empty, then place the binary tree root in A[1], A[2],A[3] are the root’s two children, and A[4],A[5],A[6],A[7] are the third level nodes.
This representation of complete binary trees has excellent properties: for a node at position k, its parent is at position floor(n/2) (which is n/2 in computation), and its two children are at positions 2k and 2k+1. A complete binary tree of size N has height floor(lgN).
Key heap operations are sink and swim operations. Inserting an element actually adds an element to the heap’s tail and makes it swim to the appropriate position. Deleting the maximum element essentially deletes the first array element, takes an element from the array’s tail to fill the gap, and makes it sink to the appropriate position.
Correspondingly, heap sort uses similar mechanisms. First create a heap from the array, then successively extract the maximum elements from the heap and place them at the array’s end.
Characteristics
- Heap sort can guarantee $O(n\log n)$ time complexity in the worst case while using constant extra space.
- Heap sort implementation is simple.
- Heap sort has poor access locality, frequently causing cache misses.
- Using dummy elements helps simplify heap sort code
