Sorting Algorithm
Today, I’m learning sorting algorithm, and taking some notes in this passage.
Selection Sort
I believe Selection Sort is the easiest sorting algorithm to understand, as it simply involves repeatedly finding the smallest elements in the unsorted portion of the array and swapping it with the first unsorted element.
Let’s take a look at the implementation:
Implementation
1 | /* Section Sort */ |
Bubble Sort
Bubble Sort offers a slight improvement over Selection Sort because it’s an adaptive sorting algorithm that can be optimized using a flag.
Implementation
1 | /* Bubble Sort */ |
Efficiency Optimization
We can observe that if no swaps occur during an inner loop iteration, the array is already sorted. This allows us to introduce a flag variable to track whether a swap has taken place. If no swaps occur, we can terminate the algorithm early, improving efficiency.
This optimization could decrease the time complexity to O(n) when the raw array is sorted.
1 | /* Bubble Sort with Flag */ |
Notice: Even after the optimization, the max time complexity is still O(n^2).
Insertion Sort
Insertion Sort is a practical and efficient sorting algorithm for small datasets due to its relatively low time complexity in such cases.
Implementation
1 | /* Insertion Sort */ |
Algorithm Characteristics
Insertion Sort is widely used due to it’s simplicity and efficiency in sorting small or nearly sorted datasets. While its worst-case time complexity is O(n^2), it generally performs better than Bubble Sort because it requires fewer element swaps.
Additionally, Insertion Sort is adaptive, meaning it performs particularly well when the input is already partially sorted. In the best case (when the array is already sorted), its time complexity is O(n), making it more efficient than many other simple sorting algorithms in such scenarios.
So, compared to Bubble Sort, Insertion Sort is often faster because it performs fewer operations in practice.
Quick Sort
As the name suggests, Quick Sort is a highly efficient sorting algorithm that leverages the divide-and-conquer strategy. It is widely used due to its excellent average-case time complexity and adaptability to various scenarios.
Implementation
The implementation of quick sort is more complex than the previous algorithms. Its core operation revolves around pivot partitioning.
Pivot Partitioning
When processing an array, we first choose a pivot element and then rearrange the array such that all elements smaller than the pivot appear before it, while all larger elements appear after it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/* Pivot Partitioning */
void swap(int *nums, int x, int y) {
int tmp = nums[x];
nums[x] = nums[y];
nums[y] = tmp;
}
// Pivot Partitioning
int partition(int *nums, int left, int right) {
// Choose the leftmost element as the pivot.
int l = left, r = right;
while(l < r) {
// Notice: Here we must place the loop for r first, or l will be left + 1 when the array have been sorted, triggering the swapping between left and left + 1.
while(l < r && nums[r] >= nums[left]) { r--; }
while(l < r && nums[l] <= nums[left]) { l++; }
swap(nums, l, r);
}
// Place left in the correct position.
swap(nums, left, l);
return l;
}Recursive Sorting
The partition function only partitions the array once. To fully sort the array, we must apply partitioning recursively.
1
2
3
4
5
6
7
8
9
10
11
12/* Quick Sort */
void quick_sort(int *nums, int left, int right) {
// Base case: Stop when the segment only contains 1 or 0 elements.
if(left <= right) { return; }
// Partition the array and obtain the pivot index.
int pivot = partition(nums, left, right);
// Recursively sort the left and right sub-arrays.
quick_sort(nums, left, pivot - 1);
quick_sort(nums, pivot + 1, right);
}
Pivot Optimization
Although Quick Sort is generally efficient, its performance can degrade under certain conditions. For example, when the input array is already sorted in reverse order and we always select the first element of the array. This results in highly unbalanced partitions, reducing Quick Sort to a worst-case O(n^2) complexity, similar to Bubble Sort.
To mitigate this issue, we could improve the pivot selection. A simple yet effective approach is to choose the median of three commonly used elements:
- Leftmost element
- Middle element
- Rightmost element
This strategy increases the likelihood of selecting a pivot that creates balanced partitions, improving the overall efficiency of the algorithm.
1 | /* Quick Sort (with optimization of pivot selection) */ |
Algorithm Characteristics
Quick Sort remains one of the most efficient and widely used sorting algorithms, particularly due to its O(nlog(n)) average-case time complexity. While its worst-case performance can degrade to O(n^2), careful pivot selection strategy maintain its efficiency in practical scenarios.
- Advantages
- fast
- in-place
- Disadvantages
- unstable
- time complexity will degrade under worst-case input.
Merge Sort
Merge Sort is also a highly efficient algorithm with a consistent time complexity of O(nlong(n)), making it a reliable choice for large datasets. It operates using the divide-and-conquer strategy, recursively breaking down the array into smaller sub-arrays before merging them back in sa sorted order.
Implementation
The core idea of Merge Sort is to repeatedly divide the input array into two smaller subarrays until each subarray contains only a single element. Then , pairs of subarrays are merged back together in sorted order to reconstruct the fully sorted array.
1 | /* Merge Sort */ |
Algorithm Characteristics
Merge Sort is widely used when need to handling large datasets efficiently, or when stability is a key requirement.
- Advantages:
- Constantly Fast: time complexity is O(nlog(n)) whatever the input is.
- Stable
- Disadvantages:
- Extra Space Usage: Merge Sort need a tmp array for instant restore.
- more unit-operation: This feature makes it shows lower efficiency in small-database scenarios.