This commit is contained in:
krahets
2025-12-31 19:37:45 +08:00
parent 29ec0c699d
commit 3c9d5689c4
279 changed files with 40895 additions and 16087 deletions
+357 -71
View File
@@ -2,14 +2,14 @@
comments: true
---
# 11.3   Bubble sort
# 11.3   Bubble Sort
<u>Bubble sort</u> works by continuously comparing and swapping adjacent elements. This process is like bubbles rising from the bottom to the top, hence the name "bubble sort."
<u>Bubble sort (bubble sort)</u> achieves sorting by continuously comparing and swapping adjacent elements. This process is like bubbles rising from the bottom to the top, hence the name bubble sort.
As shown in Figure 11-4, the bubbling process can be simulated using element swaps: start from the leftmost end of the array and move right, comparing each pair of adjacent elements. If the left element is greater than the right element, swap them. After the traversal, the largest element will have bubbled up to the rightmost end of the array.
As shown in Figure 11-4, the bubbling process can be simulated using element swap operations: starting from the leftmost end of the array and traversing to the right, compare the size of adjacent elements, and if "left element > right element", swap them. After completing the traversal, the largest element will be moved to the rightmost end of the array.
=== "<1>"
![Simulating bubble process using element swap](bubble_sort.assets/bubble_operation_step1.png){ class="animation-figure" }
![Simulating bubble using element swap operation](bubble_sort.assets/bubble_operation_step1.png){ class="animation-figure" }
=== "<2>"
![bubble_operation_step2](bubble_sort.assets/bubble_operation_step2.png){ class="animation-figure" }
@@ -29,20 +29,20 @@ As shown in Figure 11-4, the bubbling process can be simulated using element swa
=== "<7>"
![bubble_operation_step7](bubble_sort.assets/bubble_operation_step7.png){ class="animation-figure" }
<p align="center"> Figure 11-4 &nbsp; Simulating bubble process using element swap </p>
<p align="center"> Figure 11-4 &nbsp; Simulating bubble using element swap operation </p>
## 11.3.1 &nbsp; Algorithm process
## 11.3.1 &nbsp; Algorithm Flow
Assume the array has length $n$. The steps of bubble sort are shown in Figure 11-5:
Assume the array has length $n$. The steps of bubble sort are shown in Figure 11-5.
1. First, perform one "bubble" pass on $n$ elements, **swapping the largest element to its correct position**.
2. Next, perform a "bubble" pass on the remaining $n - 1$ elements, **swapping the second largest element to its correct position**.
3. Continue in this manner; after $n - 1$ such passes, **the largest $n - 1$ elements will have been moved to their correct positions**.
4. The only remaining element **must** be the smallest, so **no** further sorting is required. At this point, the array is sorted.
1. First, perform "bubbling" on $n$ elements, **swapping the largest element of the array to its correct position**.
2. Next, perform "bubbling" on the remaining $n - 1$ elements, **swapping the second largest element to its correct position**.
3. And so on. After $n - 1$ rounds of "bubbling", **the largest $n - 1$ elements have all been swapped to their correct positions**.
4. The only remaining element must be the smallest element, requiring no sorting, so the array sorting is complete.
![Bubble sort process](bubble_sort.assets/bubble_sort_overview.png){ class="animation-figure" }
![Bubble sort flow](bubble_sort.assets/bubble_sort_overview.png){ class="animation-figure" }
<p align="center"> Figure 11-5 &nbsp; Bubble sort process </p>
<p align="center"> Figure 11-5 &nbsp; Bubble sort flow </p>
Example code is as follows:
@@ -52,9 +52,9 @@ Example code is as follows:
def bubble_sort(nums: list[int]):
"""Bubble sort"""
n = len(nums)
# Outer loop: unsorted range is [0, i]
# Outer loop: unsorted interval is [0, i]
for i in range(n - 1, 0, -1):
# Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range
# Inner loop: swap the largest element in the unsorted interval [0, i] to the rightmost end of the interval
for j in range(i):
if nums[j] > nums[j + 1]:
# Swap nums[j] and nums[j + 1]
@@ -68,11 +68,11 @@ Example code is as follows:
void bubbleSort(vector<int> &nums) {
// Outer loop: unsorted range is [0, i]
for (int i = nums.size() - 1; i > 0; i--) {
// Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
// Here, the std
// Using std::swap() function here
swap(nums[j], nums[j + 1]);
}
}
@@ -87,7 +87,7 @@ Example code is as follows:
void bubbleSort(int[] nums) {
// Outer loop: unsorted range is [0, i]
for (int i = nums.length - 1; i > 0; i--) {
// Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
@@ -103,113 +103,237 @@ Example code is as follows:
=== "C#"
```csharp title="bubble_sort.cs"
[class]{bubble_sort}-[func]{BubbleSort}
/* Bubble sort */
void BubbleSort(int[] nums) {
// Outer loop: unsorted range is [0, i]
for (int i = nums.Length - 1; i > 0; i--) {
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
(nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);
}
}
}
}
```
=== "Go"
```go title="bubble_sort.go"
[class]{}-[func]{bubbleSort}
/* Bubble sort */
func bubbleSort(nums []int) {
// Outer loop: unsorted range is [0, i]
for i := len(nums) - 1; i > 0; i-- {
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for j := 0; j < i; j++ {
if nums[j] > nums[j+1] {
// Swap nums[j] and nums[j + 1]
nums[j], nums[j+1] = nums[j+1], nums[j]
}
}
}
}
```
=== "Swift"
```swift title="bubble_sort.swift"
[class]{}-[func]{bubbleSort}
/* Bubble sort */
func bubbleSort(nums: inout [Int]) {
// Outer loop: unsorted range is [0, i]
for i in nums.indices.dropFirst().reversed() {
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for j in 0 ..< i {
if nums[j] > nums[j + 1] {
// Swap nums[j] and nums[j + 1]
nums.swapAt(j, j + 1)
}
}
}
}
```
=== "JS"
```javascript title="bubble_sort.js"
[class]{}-[func]{bubbleSort}
/* Bubble sort */
function bubbleSort(nums) {
// Outer loop: unsorted range is [0, i]
for (let i = nums.length - 1; i > 0; i--) {
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for (let j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
let tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
}
}
}
}
```
=== "TS"
```typescript title="bubble_sort.ts"
[class]{}-[func]{bubbleSort}
/* Bubble sort */
function bubbleSort(nums: number[]): void {
// Outer loop: unsorted range is [0, i]
for (let i = nums.length - 1; i > 0; i--) {
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for (let j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
let tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
}
}
}
}
```
=== "Dart"
```dart title="bubble_sort.dart"
[class]{}-[func]{bubbleSort}
/* Bubble sort */
void bubbleSort(List<int> nums) {
// Outer loop: unsorted range is [0, i]
for (int i = nums.length - 1; i > 0; i--) {
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
}
}
}
}
```
=== "Rust"
```rust title="bubble_sort.rs"
[class]{}-[func]{bubble_sort}
/* Bubble sort */
fn bubble_sort(nums: &mut [i32]) {
// Outer loop: unsorted range is [0, i]
for i in (1..nums.len()).rev() {
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for j in 0..i {
if nums[j] > nums[j + 1] {
// Swap nums[j] and nums[j + 1]
nums.swap(j, j + 1);
}
}
}
}
```
=== "C"
```c title="bubble_sort.c"
[class]{}-[func]{bubbleSort}
/* Bubble sort */
void bubbleSort(int nums[], int size) {
// Outer loop: unsorted range is [0, i]
for (int i = size - 1; i > 0; i--) {
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}
}
}
}
```
=== "Kotlin"
```kotlin title="bubble_sort.kt"
[class]{}-[func]{bubbleSort}
/* Bubble sort */
fun bubbleSort(nums: IntArray) {
// Outer loop: unsorted range is [0, i]
for (i in nums.size - 1 downTo 1) {
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for (j in 0..<i) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
val temp = nums[j]
nums[j] = nums[j + 1]
nums[j + 1] = temp
}
}
}
}
```
=== "Ruby"
```ruby title="bubble_sort.rb"
[class]{}-[func]{bubble_sort}
### Bubble sort ###
def bubble_sort(nums)
n = nums.length
# Outer loop: unsorted range is [0, i]
for i in (n - 1).downto(1)
# Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for j in 0...i
if nums[j] > nums[j + 1]
# Swap nums[j] and nums[j + 1]
nums[j], nums[j + 1] = nums[j + 1], nums[j]
end
end
end
end
```
=== "Zig"
## 11.3.2 &nbsp; Efficiency Optimization
```zig title="bubble_sort.zig"
[class]{}-[func]{bubbleSort}
```
We notice that if no swap operations are performed during a certain round of "bubbling", it means the array has already completed sorting and can directly return the result. Therefore, we can add a flag `flag` to monitor this situation and return immediately once it occurs.
## 11.3.2 &nbsp; Efficiency optimization
If no swaps occur during a round of "bubbling," the array is already sorted, so we can return immediately. To detect this, we can add a `flag` variable; whenever no swaps are made in a pass, we set the flag and return early.
Even with this optimization, the worst time complexity and average time complexity of bubble sort remains $O(n^2)$. However, if the input array is already sorted, the best-case time complexity can be as low as $O(n)$.
After optimization, the worst-case time complexity and average time complexity of bubble sort remain $O(n^2)$; but when the input array is completely ordered, the best-case time complexity can reach $O(n)$.
=== "Python"
```python title="bubble_sort.py"
def bubble_sort_with_flag(nums: list[int]):
"""Bubble sort (optimized with flag)"""
"""Bubble sort (flag optimization)"""
n = len(nums)
# Outer loop: unsorted range is [0, i]
# Outer loop: unsorted interval is [0, i]
for i in range(n - 1, 0, -1):
flag = False # Initialize flag
# Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range
# Inner loop: swap the largest element in the unsorted interval [0, i] to the rightmost end of the interval
for j in range(i):
if nums[j] > nums[j + 1]:
# Swap nums[j] and nums[j + 1]
nums[j], nums[j + 1] = nums[j + 1], nums[j]
flag = True # Record swapped elements
flag = True # Record element swap
if not flag:
break # If no elements were swapped in this round of "bubbling", exit
break # No elements were swapped in this round of "bubbling", exit directly
```
=== "C++"
```cpp title="bubble_sort.cpp"
/* Bubble sort (optimized with flag)*/
/* Bubble sort (flag optimization)*/
void bubbleSortWithFlag(vector<int> &nums) {
// Outer loop: unsorted range is [0, i]
for (int i = nums.size() - 1; i > 0; i--) {
bool flag = false; // Initialize flag
// Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
// Here, the std
// Using std::swap() function here
swap(nums[j], nums[j + 1]);
flag = true; // Record swapped elements
flag = true; // Record element swap
}
}
if (!flag)
break; // If no elements were swapped in this round of "bubbling", exit
break; // No elements were swapped in this round of "bubbling", exit directly
}
}
```
@@ -217,23 +341,23 @@ Even with this optimization, the worst time complexity and average time complexi
=== "Java"
```java title="bubble_sort.java"
/* Bubble sort (optimized with flag) */
/* Bubble sort (flag optimization) */
void bubbleSortWithFlag(int[] nums) {
// Outer loop: unsorted range is [0, i]
for (int i = nums.length - 1; i > 0; i--) {
boolean flag = false; // Initialize flag
// Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
flag = true; // Record swapped elements
flag = true; // Record element swap
}
}
if (!flag)
break; // If no elements were swapped in this round of "bubbling", exit
break; // No elements were swapped in this round of "bubbling", exit directly
}
}
```
@@ -241,71 +365,233 @@ Even with this optimization, the worst time complexity and average time complexi
=== "C#"
```csharp title="bubble_sort.cs"
[class]{bubble_sort}-[func]{BubbleSortWithFlag}
/* Bubble sort (flag optimization) */
void BubbleSortWithFlag(int[] nums) {
// Outer loop: unsorted range is [0, i]
for (int i = nums.Length - 1; i > 0; i--) {
bool flag = false; // Initialize flag
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
(nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);
flag = true; // Record element swap
}
}
if (!flag) break; // No elements were swapped in this round of "bubbling", exit directly
}
}
```
=== "Go"
```go title="bubble_sort.go"
[class]{}-[func]{bubbleSortWithFlag}
/* Bubble sort (flag optimization) */
func bubbleSortWithFlag(nums []int) {
// Outer loop: unsorted range is [0, i]
for i := len(nums) - 1; i > 0; i-- {
flag := false // Initialize flag
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for j := 0; j < i; j++ {
if nums[j] > nums[j+1] {
// Swap nums[j] and nums[j + 1]
nums[j], nums[j+1] = nums[j+1], nums[j]
flag = true // Record element swap
}
}
if flag == false { // No elements were swapped in this round of "bubbling", exit directly
break
}
}
}
```
=== "Swift"
```swift title="bubble_sort.swift"
[class]{}-[func]{bubbleSortWithFlag}
/* Bubble sort (flag optimization) */
func bubbleSortWithFlag(nums: inout [Int]) {
// Outer loop: unsorted range is [0, i]
for i in nums.indices.dropFirst().reversed() {
var flag = false // Initialize flag
for j in 0 ..< i {
if nums[j] > nums[j + 1] {
// Swap nums[j] and nums[j + 1]
nums.swapAt(j, j + 1)
flag = true // Record element swap
}
}
if !flag { // No elements were swapped in this round of "bubbling", exit directly
break
}
}
}
```
=== "JS"
```javascript title="bubble_sort.js"
[class]{}-[func]{bubbleSortWithFlag}
/* Bubble sort (flag optimization) */
function bubbleSortWithFlag(nums) {
// Outer loop: unsorted range is [0, i]
for (let i = nums.length - 1; i > 0; i--) {
let flag = false; // Initialize flag
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for (let j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
let tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
flag = true; // Record element swap
}
}
if (!flag) break; // No elements were swapped in this round of "bubbling", exit directly
}
}
```
=== "TS"
```typescript title="bubble_sort.ts"
[class]{}-[func]{bubbleSortWithFlag}
/* Bubble sort (flag optimization) */
function bubbleSortWithFlag(nums: number[]): void {
// Outer loop: unsorted range is [0, i]
for (let i = nums.length - 1; i > 0; i--) {
let flag = false; // Initialize flag
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for (let j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
let tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
flag = true; // Record element swap
}
}
if (!flag) break; // No elements were swapped in this round of "bubbling", exit directly
}
}
```
=== "Dart"
```dart title="bubble_sort.dart"
[class]{}-[func]{bubbleSortWithFlag}
/* Bubble sort (flag optimization) */
void bubbleSortWithFlag(List<int> nums) {
// Outer loop: unsorted range is [0, i]
for (int i = nums.length - 1; i > 0; i--) {
bool flag = false; // Initialize flag
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
flag = true; // Record element swap
}
}
if (!flag) break; // No elements were swapped in this round of "bubbling", exit directly
}
}
```
=== "Rust"
```rust title="bubble_sort.rs"
[class]{}-[func]{bubble_sort_with_flag}
/* Bubble sort (flag optimization) */
fn bubble_sort_with_flag(nums: &mut [i32]) {
// Outer loop: unsorted range is [0, i]
for i in (1..nums.len()).rev() {
let mut flag = false; // Initialize flag
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for j in 0..i {
if nums[j] > nums[j + 1] {
// Swap nums[j] and nums[j + 1]
nums.swap(j, j + 1);
flag = true; // Record element swap
}
}
if !flag {
break; // No elements were swapped in this round of "bubbling", exit directly
};
}
}
```
=== "C"
```c title="bubble_sort.c"
[class]{}-[func]{bubbleSortWithFlag}
/* Bubble sort (flag optimization) */
void bubbleSortWithFlag(int nums[], int size) {
// Outer loop: unsorted range is [0, i]
for (int i = size - 1; i > 0; i--) {
bool flag = false;
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
flag = true;
}
}
if (!flag)
break;
}
}
```
=== "Kotlin"
```kotlin title="bubble_sort.kt"
[class]{}-[func]{bubbleSortWithFlag}
/* Bubble sort (flag optimization) */
fun bubbleSortWithFlag(nums: IntArray) {
// Outer loop: unsorted range is [0, i]
for (i in nums.size - 1 downTo 1) {
var flag = false // Initialize flag
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for (j in 0..<i) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
val temp = nums[j]
nums[j] = nums[j + 1]
nums[j + 1] = temp
flag = true // Record element swap
}
}
if (!flag) break // No elements were swapped in this round of "bubbling", exit directly
}
}
```
=== "Ruby"
```ruby title="bubble_sort.rb"
[class]{}-[func]{bubble_sort_with_flag}
### Bubble sort (flag optimization) ###
def bubble_sort_with_flag(nums)
n = nums.length
# Outer loop: unsorted range is [0, i]
for i in (n - 1).downto(1)
flag = false # Initialize flag
# Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
for j in 0...i
if nums[j] > nums[j + 1]
# Swap nums[j] and nums[j + 1]
nums[j], nums[j + 1] = nums[j + 1], nums[j]
flag = true # Record element swap
end
end
break unless flag # No elements were swapped in this round of "bubbling", exit directly
end
end
```
=== "Zig"
## 11.3.3 &nbsp; Algorithm Characteristics
```zig title="bubble_sort.zig"
[class]{}-[func]{bubbleSortWithFlag}
```
## 11.3.3 &nbsp; Algorithm characteristics
- **Time complexity of $O(n^2)$, adaptive sorting.** Each round of "bubbling" traverses array segments of length $n - 1$, $n - 2$, $\dots$, $2$, $1$, which sums to $(n - 1) n / 2$. With a `flag` optimization, the best-case time complexity can reach $O(n)$ when the array is already sorted.
- **Space complexity of $O(1)$, in-place sorting.** Only a constant amount of extra space is used by pointers $i$ and $j$.
- **Stable sorting.** Because equal elements are not swapped during "bubbling," their original order is preserved, making this a stable sort.
- **Time complexity of $O(n^2)$, adaptive sorting**: The array lengths traversed in each round of "bubbling" are $n - 1$, $n - 2$, $\dots$, $2$, $1$, totaling $(n - 1) n / 2$. After introducing the `flag` optimization, the best-case time complexity can reach $O(n)$.
- **Space complexity of $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space.
- **Stable sorting**: Since equal elements are not swapped during "bubbling".
+301 -41
View File
@@ -2,25 +2,25 @@
comments: true
---
# 11.8 &nbsp; Bucket sort
# 11.8 &nbsp; Bucket Sort
The previously mentioned sorting algorithms are all "comparison-based sorting algorithms," which sort elements by comparing their values. Such sorting algorithms cannot have better time complexity of $O(n \log n)$. Next, we will discuss several "non-comparison sorting algorithms" that could achieve linear time complexity.
The several sorting algorithms mentioned earlier all belong to "comparison-based sorting algorithms", which achieve sorting by comparing the size of elements. The time complexity of such sorting algorithms cannot exceed $O(n \log n)$. Next, we will explore several "non-comparison sorting algorithms", whose time complexity can reach linear order.
<u>Bucket sort</u> is a typical application of the divide-and-conquer strategy. It works by setting up a series of ordered buckets, each containing a range of data, and distributing the input data evenly across these buckets. And then, the data in each bucket is sorted individually. Finally, the sorted data from all the buckets is merged in sequence to produce the final result.
<u>Bucket sort (bucket sort)</u> is a typical application of the divide-and-conquer strategy. It works by setting up buckets with size order, each bucket corresponding to a data range, evenly distributing data to each bucket; then, sorting within each bucket separately; finally, merging all data in the order of the buckets.
## 11.8.1 &nbsp; Algorithm process
## 11.8.1 &nbsp; Algorithm Flow
Consider an array of length $n$, with float numbers in the range $[0, 1)$. The bucket sort process is illustrated in Figure 11-13.
Consider an array of length $n$, whose elements are floating-point numbers in the range $[0, 1)$. The flow of bucket sort is shown in Figure 11-13.
1. Initialize $k$ buckets and distribute $n$ elements into these $k$ buckets.
2. Sort each bucket individually (using the built-in sorting function of the programming language).
3. Merge the results in the order from the smallest to the largest bucket.
1. Initialize $k$ buckets and distribute the $n$ elements into the $k$ buckets.
2. Sort each bucket separately (here we use the built-in sorting function of the programming language).
3. Merge the results in order from smallest to largest bucket.
![Bucket sort algorithm process](bucket_sort.assets/bucket_sort_overview.png){ class="animation-figure" }
![Bucket sort algorithm flow](bucket_sort.assets/bucket_sort_overview.png){ class="animation-figure" }
<p align="center"> Figure 11-13 &nbsp; Bucket sort algorithm process </p>
<p align="center"> Figure 11-13 &nbsp; Bucket sort algorithm flow </p>
The code is shown as follows:
The code is as follows:
=== "Python"
@@ -60,7 +60,7 @@ The code is shown as follows:
for (float num : nums) {
// Input data range is [0, 1), use num * k to map to index range [0, k-1]
int i = num * k;
// Add number to bucket_idx
// Add num to bucket bucket_idx
buckets[i].push_back(num);
}
// 2. Sort each bucket
@@ -114,92 +114,352 @@ The code is shown as follows:
=== "C#"
```csharp title="bucket_sort.cs"
[class]{bucket_sort}-[func]{BucketSort}
/* Bucket sort */
void BucketSort(float[] nums) {
// Initialize k = n/2 buckets, expected to allocate 2 elements per bucket
int k = nums.Length / 2;
List<List<float>> buckets = [];
for (int i = 0; i < k; i++) {
buckets.Add([]);
}
// 1. Distribute array elements into various buckets
foreach (float num in nums) {
// Input data range is [0, 1), use num * k to map to index range [0, k-1]
int i = (int)(num * k);
// Add num to bucket i
buckets[i].Add(num);
}
// 2. Sort each bucket
foreach (List<float> bucket in buckets) {
// Use built-in sorting function, can also replace with other sorting algorithms
bucket.Sort();
}
// 3. Traverse buckets to merge results
int j = 0;
foreach (List<float> bucket in buckets) {
foreach (float num in bucket) {
nums[j++] = num;
}
}
}
```
=== "Go"
```go title="bucket_sort.go"
[class]{}-[func]{bucketSort}
/* Bucket sort */
func bucketSort(nums []float64) {
// Initialize k = n/2 buckets, expected to allocate 2 elements per bucket
k := len(nums) / 2
buckets := make([][]float64, k)
for i := 0; i < k; i++ {
buckets[i] = make([]float64, 0)
}
// 1. Distribute array elements into various buckets
for _, num := range nums {
// Input data range is [0, 1), use num * k to map to index range [0, k-1]
i := int(num * float64(k))
// Add num to bucket i
buckets[i] = append(buckets[i], num)
}
// 2. Sort each bucket
for i := 0; i < k; i++ {
// Use built-in slice sorting function, can also be replaced with other sorting algorithms
sort.Float64s(buckets[i])
}
// 3. Traverse buckets to merge results
i := 0
for _, bucket := range buckets {
for _, num := range bucket {
nums[i] = num
i++
}
}
}
```
=== "Swift"
```swift title="bucket_sort.swift"
[class]{}-[func]{bucketSort}
/* Bucket sort */
func bucketSort(nums: inout [Double]) {
// Initialize k = n/2 buckets, expected to allocate 2 elements per bucket
let k = nums.count / 2
var buckets = (0 ..< k).map { _ in [Double]() }
// 1. Distribute array elements into various buckets
for num in nums {
// Input data range is [0, 1), use num * k to map to index range [0, k-1]
let i = Int(num * Double(k))
// Add num to bucket i
buckets[i].append(num)
}
// 2. Sort each bucket
for i in buckets.indices {
// Use built-in sorting function, can also replace with other sorting algorithms
buckets[i].sort()
}
// 3. Traverse buckets to merge results
var i = nums.startIndex
for bucket in buckets {
for num in bucket {
nums[i] = num
i += 1
}
}
}
```
=== "JS"
```javascript title="bucket_sort.js"
[class]{}-[func]{bucketSort}
/* Bucket sort */
function bucketSort(nums) {
// Initialize k = n/2 buckets, expected to allocate 2 elements per bucket
const k = nums.length / 2;
const buckets = [];
for (let i = 0; i < k; i++) {
buckets.push([]);
}
// 1. Distribute array elements into various buckets
for (const num of nums) {
// Input data range is [0, 1), use num * k to map to index range [0, k-1]
const i = Math.floor(num * k);
// Add num to bucket i
buckets[i].push(num);
}
// 2. Sort each bucket
for (const bucket of buckets) {
// Use built-in sorting function, can also replace with other sorting algorithms
bucket.sort((a, b) => a - b);
}
// 3. Traverse buckets to merge results
let i = 0;
for (const bucket of buckets) {
for (const num of bucket) {
nums[i++] = num;
}
}
}
```
=== "TS"
```typescript title="bucket_sort.ts"
[class]{}-[func]{bucketSort}
/* Bucket sort */
function bucketSort(nums: number[]): void {
// Initialize k = n/2 buckets, expected to allocate 2 elements per bucket
const k = nums.length / 2;
const buckets: number[][] = [];
for (let i = 0; i < k; i++) {
buckets.push([]);
}
// 1. Distribute array elements into various buckets
for (const num of nums) {
// Input data range is [0, 1), use num * k to map to index range [0, k-1]
const i = Math.floor(num * k);
// Add num to bucket i
buckets[i].push(num);
}
// 2. Sort each bucket
for (const bucket of buckets) {
// Use built-in sorting function, can also replace with other sorting algorithms
bucket.sort((a, b) => a - b);
}
// 3. Traverse buckets to merge results
let i = 0;
for (const bucket of buckets) {
for (const num of bucket) {
nums[i++] = num;
}
}
}
```
=== "Dart"
```dart title="bucket_sort.dart"
[class]{}-[func]{bucketSort}
/* Bucket sort */
void bucketSort(List<double> nums) {
// Initialize k = n/2 buckets, expected to allocate 2 elements per bucket
int k = nums.length ~/ 2;
List<List<double>> buckets = List.generate(k, (index) => []);
// 1. Distribute array elements into various buckets
for (double _num in nums) {
// Input data range is [0, 1), use _num * k to map to index range [0, k-1]
int i = (_num * k).toInt();
// Add _num to bucket bucket_idx
buckets[i].add(_num);
}
// 2. Sort each bucket
for (List<double> bucket in buckets) {
bucket.sort();
}
// 3. Traverse buckets to merge results
int i = 0;
for (List<double> bucket in buckets) {
for (double _num in bucket) {
nums[i++] = _num;
}
}
}
```
=== "Rust"
```rust title="bucket_sort.rs"
[class]{}-[func]{bucket_sort}
/* Bucket sort */
fn bucket_sort(nums: &mut [f64]) {
// Initialize k = n/2 buckets, expected to allocate 2 elements per bucket
let k = nums.len() / 2;
let mut buckets = vec![vec![]; k];
// 1. Distribute array elements into various buckets
for &num in nums.iter() {
// Input data range is [0, 1), use num * k to map to index range [0, k-1]
let i = (num * k as f64) as usize;
// Add num to bucket i
buckets[i].push(num);
}
// 2. Sort each bucket
for bucket in &mut buckets {
// Use built-in sorting function, can also replace with other sorting algorithms
bucket.sort_by(|a, b| a.partial_cmp(b).unwrap());
}
// 3. Traverse buckets to merge results
let mut i = 0;
for bucket in buckets.iter() {
for &num in bucket.iter() {
nums[i] = num;
i += 1;
}
}
}
```
=== "C"
```c title="bucket_sort.c"
[class]{}-[func]{bucketSort}
/* Bucket sort */
void bucketSort(float nums[], int n) {
int k = n / 2; // Initialize k = n/2 buckets
int *sizes = malloc(k * sizeof(int)); // Record each bucket's size
float **buckets = malloc(k * sizeof(float *)); // Array of dynamic arrays (buckets)
// Pre-allocate sufficient space for each bucket
for (int i = 0; i < k; ++i) {
buckets[i] = (float *)malloc(n * sizeof(float));
sizes[i] = 0;
}
// 1. Distribute array elements into various buckets
for (int i = 0; i < n; ++i) {
int idx = (int)(nums[i] * k);
buckets[idx][sizes[idx]++] = nums[i];
}
// 2. Sort each bucket
for (int i = 0; i < k; ++i) {
qsort(buckets[i], sizes[i], sizeof(float), compare);
}
// 3. Merge sorted buckets
int idx = 0;
for (int i = 0; i < k; ++i) {
for (int j = 0; j < sizes[i]; ++j) {
nums[idx++] = buckets[i][j];
}
// Free memory
free(buckets[i]);
}
}
```
=== "Kotlin"
```kotlin title="bucket_sort.kt"
[class]{}-[func]{bucketSort}
/* Bucket sort */
fun bucketSort(nums: FloatArray) {
// Initialize k = n/2 buckets, expected to allocate 2 elements per bucket
val k = nums.size / 2
val buckets = mutableListOf<MutableList<Float>>()
for (i in 0..<k) {
buckets.add(mutableListOf())
}
// 1. Distribute array elements into various buckets
for (num in nums) {
// Input data range is [0, 1), use num * k to map to index range [0, k-1]
val i = (num * k).toInt()
// Add num to bucket i
buckets[i].add(num)
}
// 2. Sort each bucket
for (bucket in buckets) {
// Use built-in sorting function, can also replace with other sorting algorithms
bucket.sort()
}
// 3. Traverse buckets to merge results
var i = 0
for (bucket in buckets) {
for (num in bucket) {
nums[i++] = num
}
}
}
```
=== "Ruby"
```ruby title="bucket_sort.rb"
[class]{}-[func]{bucket_sort}
### Bucket sort ###
def bucket_sort(nums)
# Initialize k = n/2 buckets, expected to allocate 2 elements per bucket
k = nums.length / 2
buckets = Array.new(k) { [] }
# 1. Distribute array elements into various buckets
nums.each do |num|
# Input data range is [0, 1), use num * k to map to index range [0, k-1]
i = (num * k).to_i
# Add num to bucket i
buckets[i] << num
end
# 2. Sort each bucket
buckets.each do |bucket|
# Use built-in sorting function, can also replace with other sorting algorithms
bucket.sort!
end
# 3. Traverse buckets to merge results
i = 0
buckets.each do |bucket|
bucket.each do |num|
nums[i] = num
i += 1
end
end
end
```
=== "Zig"
## 11.8.2 &nbsp; Algorithm Characteristics
```zig title="bucket_sort.zig"
[class]{}-[func]{bucketSort}
```
Bucket sort is suitable for processing very large data volumes. For example, if the input data contains 1 million elements and system memory cannot load all the data at once, the data can be divided into 1000 buckets, each bucket sorted separately, and then the results merged.
## 11.8.2 &nbsp; Algorithm characteristics
- **Time complexity of $O(n + k)$**: Assuming the elements are evenly distributed among the buckets, then the number of elements in each bucket is $\frac{n}{k}$. Assuming sorting a single bucket uses $O(\frac{n}{k} \log\frac{n}{k})$ time, then sorting all buckets uses $O(n \log\frac{n}{k})$ time. **When the number of buckets $k$ is relatively large, the time complexity approaches $O(n)$**. Merging results requires traversing all buckets and elements, taking $O(n + k)$ time. In the worst case, all data is distributed into one bucket, and sorting that bucket uses $O(n^2)$ time.
- **Space complexity of $O(n + k)$, non-in-place sorting**: Additional space is required for $k$ buckets and a total of $n$ elements.
- Whether bucket sort is stable depends on whether the algorithm for sorting elements within buckets is stable.
Bucket sort is suitable for handling very large data sets. For example, if the input data includes 1 million elements, and system memory limitations prevent loading all the data at the same time, you can divide the data into 1,000 buckets and sort each bucket separately before merging the results.
## 11.8.3 &nbsp; How to Achieve Even Distribution
- **Time complexity is $O(n + k)$**: Assuming the elements are evenly distributed across the buckets, the number of elements in each bucket is $n/k$. Assuming sorting a single bucket takes $O(n/k \log(n/k))$ time, sorting all buckets takes $O(n \log(n/k))$ time. **When the number of buckets $k$ is relatively large, the time complexity approaches $O(n)$**. Merging the results requires traversing all buckets and elements, taking $O(n + k)$ time. In the worst case, all data is distributed into a single bucket, and sorting that bucket takes $O(n^2)$ time.
- **Space complexity is $O(n + k)$, non-in-place sorting**: It requires additional space for $k$ buckets and a total of $n$ elements.
- Whether bucket sort is stable depends on whether the sorting algorithm used within each bucket is stable.
Theoretically, bucket sort can achieve $O(n)$ time complexity. **The key is to evenly distribute elements to each bucket**, because real data is often not evenly distributed. For example, if we want to evenly distribute all products on Taobao into 10 buckets by price range, there may be very many products below 100 yuan and very few above 1000 yuan. If the price intervals are evenly divided into 10, the difference in the number of products in each bucket will be very large.
## 11.8.3 &nbsp; How to achieve even distribution
To achieve even distribution, we can first set an approximate dividing line to roughly divide the data into 3 buckets. **After distribution is complete, continue dividing buckets with more products into 3 buckets until the number of elements in all buckets is roughly equal**.
The theoretical time complexity of bucket sort can reach $O(n)$. **The key is to evenly distribute the elements across all buckets** as real-world data is often not uniformly distributed. For example, we may want to evenly distribute all products on eBay by price range into 10 buckets. However, the distribution of product prices may not be even, with many under $100 and few over $500. If the price range is evenly divided into 10, the difference in the number of products in each bucket will be significant.
As shown in Figure 11-14, this method essentially creates a recursion tree, with the goal of making the values of leaf nodes as even as possible. Of course, it is not necessary to divide the data into 3 buckets every round; the specific division method can be flexibly chosen according to data characteristics.
To achieve even distribution, we can initially set an approximate boundary to roughly divide the data into 3 buckets. **After the distribution is complete, the buckets with more items can be further divided into 3 buckets, until the number of elements in all buckets is roughly equal**.
![Recursively dividing buckets](bucket_sort.assets/scatter_in_buckets_recursively.png){ class="animation-figure" }
As shown in Figure 11-14, this method essentially constructs a recursive tree, aiming to ensure the element counts in leaf nodes are as even as possible. Of course, you don't have to divide the data into 3 buckets each round - the partitioning strategy can be adaptively tailored to the data's unique characteristics.
<p align="center"> Figure 11-14 &nbsp; Recursively dividing buckets </p>
![Recursive division of buckets](bucket_sort.assets/scatter_in_buckets_recursively.png){ class="animation-figure" }
If we know the probability distribution of product prices in advance, **we can set the price dividing line for each bucket based on the data probability distribution**. It is worth noting that the data distribution does not necessarily need to be specifically calculated, but can also be approximated using a certain probability model based on data characteristics.
<p align="center"> Figure 11-14 &nbsp; Recursive division of buckets </p>
If we know the probability distribution of product prices in advance, **we can set the price boundaries for each bucket based on the data probability distribution**. It is worth noting that it is not necessarily required to specifically calculate the data distribution; instead, it can be approximated based on data characteristics using a probability model.
As shown in Figure 11-15, assuming that product prices follow a normal distribution, we can define reasonable price intervals to balance the distribution of items across the buckets.
As shown in Figure 11-15, we assume that product prices follow a normal distribution, which allows us to reasonably set price intervals to evenly distribute products to each bucket.
![Dividing buckets based on probability distribution](bucket_sort.assets/scatter_in_buckets_distribution.png){ class="animation-figure" }
+558 -66
View File
@@ -2,23 +2,23 @@
comments: true
---
# 11.9 &nbsp; Counting sort
# 11.9 &nbsp; Counting Sort
<u>Counting sort</u> achieves sorting by counting the number of elements, usually applied to integer arrays.
<u>Counting sort (counting sort)</u> achieves sorting by counting the number of elements, typically applied to integer arrays.
## 11.9.1 &nbsp; Simple implementation
## 11.9.1 &nbsp; Simple Implementation
Let's start with a simple example. Given an array `nums` of length $n$, where all elements are "non-negative integers", the overall process of counting sort is shown in Figure 11-16.
Let's start with a simple example. Given an array `nums` of length $n$, where the elements are all "non-negative integers", the overall flow of counting sort is shown in Figure 11-16.
1. Traverse the array to find the maximum number, denoted as $m$, then create an auxiliary array `counter` of length $m + 1$.
2. **Use `counter` to count the occurrence of each number in `nums`**, where `counter[num]` corresponds to the occurrence of the number `num`. The counting method is simple, just traverse `nums` (suppose the current number is `num`), and increase `counter[num]` by $1$ each round.
3. **Since the indices of `counter` are naturally ordered, all numbers are essentially sorted already**. Next, we traverse `counter`, and fill in `nums` in ascending order of occurrence.
1. Traverse the array to find the largest number, denoted as $m$, and then create an auxiliary array `counter` of length $m + 1$.
2. **Use `counter` to count the number of occurrences of each number in `nums`**, where `counter[num]` corresponds to the number of occurrences of the number `num`. The counting method is simple: just traverse `nums` (let the current number be `num`), and increase `counter[num]` by $1$ in each round.
3. **Since each index of `counter` is naturally ordered, this is equivalent to all numbers being sorted**. Next, we traverse `counter` and fill in `nums` in ascending order based on the number of occurrences of each number.
![Counting sort process](counting_sort.assets/counting_sort_overview.png){ class="animation-figure" }
![Counting sort flow](counting_sort.assets/counting_sort_overview.png){ class="animation-figure" }
<p align="center"> Figure 11-16 &nbsp; Counting sort process </p>
<p align="center"> Figure 11-16 &nbsp; Counting sort flow </p>
The code is shown below:
The code is as follows:
=== "Python"
@@ -30,7 +30,7 @@ The code is shown below:
m = 0
for num in nums:
m = max(m, num)
# 2. Count the occurrence of each digit
# 2. Count the occurrence of each number
# counter[num] represents the occurrence of num
counter = [0] * (m + 1)
for num in nums:
@@ -54,7 +54,7 @@ The code is shown below:
for (int num : nums) {
m = max(m, num);
}
// 2. Count the occurrence of each digit
// 2. Count the occurrence of each number
// counter[num] represents the occurrence of num
vector<int> counter(m + 1, 0);
for (int num : nums) {
@@ -81,7 +81,7 @@ The code is shown below:
for (int num : nums) {
m = Math.max(m, num);
}
// 2. Count the occurrence of each digit
// 2. Count the occurrence of each number
// counter[num] represents the occurrence of num
int[] counter = new int[m + 1];
for (int num : nums) {
@@ -100,92 +100,292 @@ The code is shown below:
=== "C#"
```csharp title="counting_sort.cs"
[class]{counting_sort}-[func]{CountingSortNaive}
/* Counting sort */
// Simple implementation, cannot be used for sorting objects
void CountingSortNaive(int[] nums) {
// 1. Count the maximum element m in the array
int m = 0;
foreach (int num in nums) {
m = Math.Max(m, num);
}
// 2. Count the occurrence of each number
// counter[num] represents the occurrence of num
int[] counter = new int[m + 1];
foreach (int num in nums) {
counter[num]++;
}
// 3. Traverse counter, filling each element back into the original array nums
int i = 0;
for (int num = 0; num < m + 1; num++) {
for (int j = 0; j < counter[num]; j++, i++) {
nums[i] = num;
}
}
}
```
=== "Go"
```go title="counting_sort.go"
[class]{}-[func]{countingSortNaive}
/* Counting sort */
// Simple implementation, cannot be used for sorting objects
func countingSortNaive(nums []int) {
// 1. Count the maximum element m in the array
m := 0
for _, num := range nums {
if num > m {
m = num
}
}
// 2. Count the occurrence of each number
// counter[num] represents the occurrence of num
counter := make([]int, m+1)
for _, num := range nums {
counter[num]++
}
// 3. Traverse counter, filling each element back into the original array nums
for i, num := 0, 0; num < m+1; num++ {
for j := 0; j < counter[num]; j++ {
nums[i] = num
i++
}
}
}
```
=== "Swift"
```swift title="counting_sort.swift"
[class]{}-[func]{countingSortNaive}
/* Counting sort */
// Simple implementation, cannot be used for sorting objects
func countingSortNaive(nums: inout [Int]) {
// 1. Count the maximum element m in the array
let m = nums.max()!
// 2. Count the occurrence of each number
// counter[num] represents the occurrence of num
var counter = Array(repeating: 0, count: m + 1)
for num in nums {
counter[num] += 1
}
// 3. Traverse counter, filling each element back into the original array nums
var i = 0
for num in 0 ..< m + 1 {
for _ in 0 ..< counter[num] {
nums[i] = num
i += 1
}
}
}
```
=== "JS"
```javascript title="counting_sort.js"
[class]{}-[func]{countingSortNaive}
/* Counting sort */
// Simple implementation, cannot be used for sorting objects
function countingSortNaive(nums) {
// 1. Count the maximum element m in the array
let m = Math.max(...nums);
// 2. Count the occurrence of each number
// counter[num] represents the occurrence of num
const counter = new Array(m + 1).fill(0);
for (const num of nums) {
counter[num]++;
}
// 3. Traverse counter, filling each element back into the original array nums
let i = 0;
for (let num = 0; num < m + 1; num++) {
for (let j = 0; j < counter[num]; j++, i++) {
nums[i] = num;
}
}
}
```
=== "TS"
```typescript title="counting_sort.ts"
[class]{}-[func]{countingSortNaive}
/* Counting sort */
// Simple implementation, cannot be used for sorting objects
function countingSortNaive(nums: number[]): void {
// 1. Count the maximum element m in the array
let m: number = Math.max(...nums);
// 2. Count the occurrence of each number
// counter[num] represents the occurrence of num
const counter: number[] = new Array<number>(m + 1).fill(0);
for (const num of nums) {
counter[num]++;
}
// 3. Traverse counter, filling each element back into the original array nums
let i = 0;
for (let num = 0; num < m + 1; num++) {
for (let j = 0; j < counter[num]; j++, i++) {
nums[i] = num;
}
}
}
```
=== "Dart"
```dart title="counting_sort.dart"
[class]{}-[func]{countingSortNaive}
/* Counting sort */
// Simple implementation, cannot be used for sorting objects
void countingSortNaive(List<int> nums) {
// 1. Count the maximum element m in the array
int m = 0;
for (int _num in nums) {
m = max(m, _num);
}
// 2. Count the occurrence of each number
// counter[_num] represents occurrence count of _num
List<int> counter = List.filled(m + 1, 0);
for (int _num in nums) {
counter[_num]++;
}
// 3. Traverse counter, filling each element back into the original array nums
int i = 0;
for (int _num = 0; _num < m + 1; _num++) {
for (int j = 0; j < counter[_num]; j++, i++) {
nums[i] = _num;
}
}
}
```
=== "Rust"
```rust title="counting_sort.rs"
[class]{}-[func]{counting_sort_naive}
/* Counting sort */
// Simple implementation, cannot be used for sorting objects
fn counting_sort_naive(nums: &mut [i32]) {
// 1. Count the maximum element m in the array
let m = *nums.iter().max().unwrap();
// 2. Count the occurrence of each number
// counter[num] represents the occurrence of num
let mut counter = vec![0; m as usize + 1];
for &num in nums.iter() {
counter[num as usize] += 1;
}
// 3. Traverse counter, filling each element back into the original array nums
let mut i = 0;
for num in 0..m + 1 {
for _ in 0..counter[num as usize] {
nums[i] = num;
i += 1;
}
}
}
```
=== "C"
```c title="counting_sort.c"
[class]{}-[func]{countingSortNaive}
/* Counting sort */
// Simple implementation, cannot be used for sorting objects
void countingSortNaive(int nums[], int size) {
// 1. Count the maximum element m in the array
int m = 0;
for (int i = 0; i < size; i++) {
if (nums[i] > m) {
m = nums[i];
}
}
// 2. Count the occurrence of each number
// counter[num] represents the occurrence of num
int *counter = calloc(m + 1, sizeof(int));
for (int i = 0; i < size; i++) {
counter[nums[i]]++;
}
// 3. Traverse counter, filling each element back into the original array nums
int i = 0;
for (int num = 0; num < m + 1; num++) {
for (int j = 0; j < counter[num]; j++, i++) {
nums[i] = num;
}
}
// 4. Free memory
free(counter);
}
```
=== "Kotlin"
```kotlin title="counting_sort.kt"
[class]{}-[func]{countingSortNaive}
/* Counting sort */
// Simple implementation, cannot be used for sorting objects
fun countingSortNaive(nums: IntArray) {
// 1. Count the maximum element m in the array
var m = 0
for (num in nums) {
m = max(m, num)
}
// 2. Count the occurrence of each number
// counter[num] represents the occurrence of num
val counter = IntArray(m + 1)
for (num in nums) {
counter[num]++
}
// 3. Traverse counter, filling each element back into the original array nums
var i = 0
for (num in 0..<m + 1) {
var j = 0
while (j < counter[num]) {
nums[i] = num
j++
i++
}
}
}
```
=== "Ruby"
```ruby title="counting_sort.rb"
[class]{}-[func]{counting_sort_naive}
```
=== "Zig"
```zig title="counting_sort.zig"
[class]{}-[func]{countingSortNaive}
### Counting sort ###
def counting_sort_naive(nums)
# Simple implementation, cannot be used for sorting objects
# 1. Count the maximum element m in the array
m = 0
nums.each { |num| m = [m, num].max }
# 2. Count the occurrence of each number
# counter[num] represents the occurrence of num
counter = Array.new(m + 1, 0)
nums.each { |num| counter[num] += 1 }
# 3. Traverse counter, filling each element back into the original array nums
i = 0
for num in 0...(m + 1)
(0...counter[num]).each do
nums[i] = num
i += 1
end
end
end
```
!!! note "Connection between counting sort and bucket sort"
From the perspective of bucket sort, we can consider each index of the counting array `counter` in counting sort as a bucket, and the process of counting as distributing elements into the corresponding buckets. Essentially, counting sort is a special case of bucket sort for integer data.
From the perspective of bucket sort, we can regard each index of the counting array `counter` in counting sort as a bucket, and the process of counting quantities as distributing each element to the corresponding bucket. Essentially, counting sort is a special case of bucket sort for integer data.
## 11.9.2 &nbsp; Complete implementation
## 11.9.2 &nbsp; Complete Implementation
Observant readers might notice, **if the input data is an object, the above step `3.` is invalid**. Suppose the input data is a product object, we want to sort the products by the price (a class member variable), but the above algorithm can only give the sorted price as the result.
Observant readers may have noticed that **if the input data is objects, step `3.` above becomes invalid**. Suppose the input data is product objects, and we want to sort the products by price (a member variable of the class), but the above algorithm can only give the sorting result of prices.
So how can we get the sorting result for the original data? First, we calculate the "prefix sum" of `counter`. As the name suggests, the prefix sum at index `i`, `prefix[i]`, equals the sum of the first `i` elements of the array:
So how can we obtain the sorting result of the original data? We first calculate the "prefix sum" of `counter`. As the name suggests, the prefix sum at index `i`, `prefix[i]`, equals the sum of the first `i` elements of the array:
$$
\text{prefix}[i] = \sum_{j=0}^i \text{counter[j]}
$$
**The prefix sum has a clear meaning, `prefix[num] - 1` represents the index of the last occurrence of element `num` in the result array `res`**. This information is crucial, as it tells us where each element should appear in the result array. Next, we traverse each element `num` of the original array `nums` in reverse order, performing the following two steps in each iteration.
**The prefix sum has a clear meaning: `prefix[num] - 1` represents the index of the last occurrence of element `num` in the result array `res`**. This information is very critical because it tells us where each element should appear in the result array. Next, we traverse each element `num` of the original array `nums` in reverse order, performing the following two steps in each iteration.
1. Fill `num` into the array `res` at the index `prefix[num] - 1`.
2. Decrease the prefix sum `prefix[num]` by $1$ to obtain the next index to place `num`.
1. Fill `num` into the array `res` at index `prefix[num] - 1`.
2. Decrease the prefix sum `prefix[num]` by $1$ to get the index for the next placement of `num`.
After the traversal, the array `res` contains the sorted result, and finally, `res` replaces the original array `nums`. The complete counting sort process is shown in Figure 11-17.
After the traversal is complete, the array `res` contains the sorted result, and finally `res` is used to overwrite the original array `nums`. The complete counting sort flow is shown in Figure 11-17.
=== "<1>"
![Counting sort process](counting_sort.assets/counting_sort_step1.png){ class="animation-figure" }
![Counting sort steps](counting_sort.assets/counting_sort_step1.png){ class="animation-figure" }
=== "<2>"
![counting_sort_step2](counting_sort.assets/counting_sort_step2.png){ class="animation-figure" }
@@ -208,9 +408,9 @@ After the traversal, the array `res` contains the sorted result, and finally, `r
=== "<8>"
![counting_sort_step8](counting_sort.assets/counting_sort_step8.png){ class="animation-figure" }
<p align="center"> Figure 11-17 &nbsp; Counting sort process </p>
<p align="center"> Figure 11-17 &nbsp; Counting sort steps </p>
The implementation code of counting sort is shown below:
The implementation code of counting sort is as follows:
=== "Python"
@@ -220,7 +420,7 @@ The implementation code of counting sort is shown below:
# Complete implementation, can sort objects and is a stable sort
# 1. Count the maximum element m in the array
m = max(nums)
# 2. Count the occurrence of each digit
# 2. Count the occurrence of each number
# counter[num] represents the occurrence of num
counter = [0] * (m + 1)
for num in nums:
@@ -253,7 +453,7 @@ The implementation code of counting sort is shown below:
for (int num : nums) {
m = max(m, num);
}
// 2. Count the occurrence of each digit
// 2. Count the occurrence of each number
// counter[num] represents the occurrence of num
vector<int> counter(m + 1, 0);
for (int num : nums) {
@@ -289,7 +489,7 @@ The implementation code of counting sort is shown below:
for (int num : nums) {
m = Math.max(m, num);
}
// 2. Count the occurrence of each digit
// 2. Count the occurrence of each number
// counter[num] represents the occurrence of num
int[] counter = new int[m + 1];
for (int num : nums) {
@@ -319,79 +519,371 @@ The implementation code of counting sort is shown below:
=== "C#"
```csharp title="counting_sort.cs"
[class]{counting_sort}-[func]{CountingSort}
/* Counting sort */
// Complete implementation, can sort objects and is a stable sort
void CountingSort(int[] nums) {
// 1. Count the maximum element m in the array
int m = 0;
foreach (int num in nums) {
m = Math.Max(m, num);
}
// 2. Count the occurrence of each number
// counter[num] represents the occurrence of num
int[] counter = new int[m + 1];
foreach (int num in nums) {
counter[num]++;
}
// 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
// counter[num]-1 is the last index where num appears in res
for (int i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. Traverse nums in reverse order, placing each element into the result array res
// Initialize the array res to record results
int n = nums.Length;
int[] res = new int[n];
for (int i = n - 1; i >= 0; i--) {
int num = nums[i];
res[counter[num] - 1] = num; // Place num at the corresponding index
counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num
}
// Use result array res to overwrite the original array nums
for (int i = 0; i < n; i++) {
nums[i] = res[i];
}
}
```
=== "Go"
```go title="counting_sort.go"
[class]{}-[func]{countingSort}
/* Counting sort */
// Complete implementation, can sort objects and is a stable sort
func countingSort(nums []int) {
// 1. Count the maximum element m in the array
m := 0
for _, num := range nums {
if num > m {
m = num
}
}
// 2. Count the occurrence of each number
// counter[num] represents the occurrence of num
counter := make([]int, m+1)
for _, num := range nums {
counter[num]++
}
// 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
// counter[num]-1 is the last index where num appears in res
for i := 0; i < m; i++ {
counter[i+1] += counter[i]
}
// 4. Traverse nums in reverse order, placing each element into the result array res
// Initialize the array res to record results
n := len(nums)
res := make([]int, n)
for i := n - 1; i >= 0; i-- {
num := nums[i]
// Place num at the corresponding index
res[counter[num]-1] = num
// Decrement the prefix sum by 1, getting the next index to place num
counter[num]--
}
// Use result array res to overwrite the original array nums
copy(nums, res)
}
```
=== "Swift"
```swift title="counting_sort.swift"
[class]{}-[func]{countingSort}
/* Counting sort */
// Complete implementation, can sort objects and is a stable sort
func countingSort(nums: inout [Int]) {
// 1. Count the maximum element m in the array
let m = nums.max()!
// 2. Count the occurrence of each number
// counter[num] represents the occurrence of num
var counter = Array(repeating: 0, count: m + 1)
for num in nums {
counter[num] += 1
}
// 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
// counter[num]-1 is the last index where num appears in res
for i in 0 ..< m {
counter[i + 1] += counter[i]
}
// 4. Traverse nums in reverse order, placing each element into the result array res
// Initialize the array res to record results
var res = Array(repeating: 0, count: nums.count)
for i in nums.indices.reversed() {
let num = nums[i]
res[counter[num] - 1] = num // Place num at the corresponding index
counter[num] -= 1 // Decrement the prefix sum by 1, getting the next index to place num
}
// Use result array res to overwrite the original array nums
for i in nums.indices {
nums[i] = res[i]
}
}
```
=== "JS"
```javascript title="counting_sort.js"
[class]{}-[func]{countingSort}
/* Counting sort */
// Complete implementation, can sort objects and is a stable sort
function countingSort(nums) {
// 1. Count the maximum element m in the array
let m = Math.max(...nums);
// 2. Count the occurrence of each number
// counter[num] represents the occurrence of num
const counter = new Array(m + 1).fill(0);
for (const num of nums) {
counter[num]++;
}
// 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
// counter[num]-1 is the last index where num appears in res
for (let i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. Traverse nums in reverse order, placing each element into the result array res
// Initialize the array res to record results
const n = nums.length;
const res = new Array(n);
for (let i = n - 1; i >= 0; i--) {
const num = nums[i];
res[counter[num] - 1] = num; // Place num at the corresponding index
counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num
}
// Use result array res to overwrite the original array nums
for (let i = 0; i < n; i++) {
nums[i] = res[i];
}
}
```
=== "TS"
```typescript title="counting_sort.ts"
[class]{}-[func]{countingSort}
/* Counting sort */
// Complete implementation, can sort objects and is a stable sort
function countingSort(nums: number[]): void {
// 1. Count the maximum element m in the array
let m: number = Math.max(...nums);
// 2. Count the occurrence of each number
// counter[num] represents the occurrence of num
const counter: number[] = new Array<number>(m + 1).fill(0);
for (const num of nums) {
counter[num]++;
}
// 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
// counter[num]-1 is the last index where num appears in res
for (let i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. Traverse nums in reverse order, placing each element into the result array res
// Initialize the array res to record results
const n = nums.length;
const res: number[] = new Array<number>(n);
for (let i = n - 1; i >= 0; i--) {
const num = nums[i];
res[counter[num] - 1] = num; // Place num at the corresponding index
counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num
}
// Use result array res to overwrite the original array nums
for (let i = 0; i < n; i++) {
nums[i] = res[i];
}
}
```
=== "Dart"
```dart title="counting_sort.dart"
[class]{}-[func]{countingSort}
/* Counting sort */
// Complete implementation, can sort objects and is a stable sort
void countingSort(List<int> nums) {
// 1. Count the maximum element m in the array
int m = 0;
for (int _num in nums) {
m = max(m, _num);
}
// 2. Count the occurrence of each number
// counter[_num] represents occurrence count of _num
List<int> counter = List.filled(m + 1, 0);
for (int _num in nums) {
counter[_num]++;
}
// 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
// That is, counter[_num]-1 is the last occurrence index of _num in res
for (int i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. Traverse nums in reverse order, placing each element into the result array res
// Initialize the array res to record results
int n = nums.length;
List<int> res = List.filled(n, 0);
for (int i = n - 1; i >= 0; i--) {
int _num = nums[i];
res[counter[_num] - 1] = _num; // Place _num at corresponding index
counter[_num]--; // Decrement prefix sum by 1 to get next placement index for _num
}
// Use result array res to overwrite the original array nums
nums.setAll(0, res);
}
```
=== "Rust"
```rust title="counting_sort.rs"
[class]{}-[func]{counting_sort}
/* Counting sort */
// Complete implementation, can sort objects and is a stable sort
fn counting_sort(nums: &mut [i32]) {
// 1. Count the maximum element m in the array
let m = *nums.iter().max().unwrap() as usize;
// 2. Count the occurrence of each number
// counter[num] represents the occurrence of num
let mut counter = vec![0; m + 1];
for &num in nums.iter() {
counter[num as usize] += 1;
}
// 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
// counter[num]-1 is the last index where num appears in res
for i in 0..m {
counter[i + 1] += counter[i];
}
// 4. Traverse nums in reverse order, placing each element into the result array res
// Initialize the array res to record results
let n = nums.len();
let mut res = vec![0; n];
for i in (0..n).rev() {
let num = nums[i];
res[counter[num as usize] - 1] = num; // Place num at the corresponding index
counter[num as usize] -= 1; // Decrement the prefix sum by 1, getting the next index to place num
}
// Use result array res to overwrite the original array nums
nums.copy_from_slice(&res)
}
```
=== "C"
```c title="counting_sort.c"
[class]{}-[func]{countingSort}
/* Counting sort */
// Complete implementation, can sort objects and is a stable sort
void countingSort(int nums[], int size) {
// 1. Count the maximum element m in the array
int m = 0;
for (int i = 0; i < size; i++) {
if (nums[i] > m) {
m = nums[i];
}
}
// 2. Count the occurrence of each number
// counter[num] represents the occurrence of num
int *counter = calloc(m, sizeof(int));
for (int i = 0; i < size; i++) {
counter[nums[i]]++;
}
// 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
// counter[num]-1 is the last index where num appears in res
for (int i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. Traverse nums in reverse order, placing each element into the result array res
// Initialize the array res to record results
int *res = malloc(sizeof(int) * size);
for (int i = size - 1; i >= 0; i--) {
int num = nums[i];
res[counter[num] - 1] = num; // Place num at the corresponding index
counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num
}
// Use result array res to overwrite the original array nums
memcpy(nums, res, size * sizeof(int));
// 5. Free memory
free(res);
free(counter);
}
```
=== "Kotlin"
```kotlin title="counting_sort.kt"
[class]{}-[func]{countingSort}
/* Counting sort */
// Complete implementation, can sort objects and is a stable sort
fun countingSort(nums: IntArray) {
// 1. Count the maximum element m in the array
var m = 0
for (num in nums) {
m = max(m, num)
}
// 2. Count the occurrence of each number
// counter[num] represents the occurrence of num
val counter = IntArray(m + 1)
for (num in nums) {
counter[num]++
}
// 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
// counter[num]-1 is the last index where num appears in res
for (i in 0..<m) {
counter[i + 1] += counter[i]
}
// 4. Traverse nums in reverse order, placing each element into the result array res
// Initialize the array res to record results
val n = nums.size
val res = IntArray(n)
for (i in n - 1 downTo 0) {
val num = nums[i]
res[counter[num] - 1] = num // Place num at the corresponding index
counter[num]-- // Decrement the prefix sum by 1, getting the next index to place num
}
// Use result array res to overwrite the original array nums
for (i in 0..<n) {
nums[i] = res[i]
}
}
```
=== "Ruby"
```ruby title="counting_sort.rb"
[class]{}-[func]{counting_sort}
### Counting sort ###
def counting_sort(nums)
# Complete implementation, can sort objects and is a stable sort
# 1. Count the maximum element m in the array
m = nums.max
# 2. Count the occurrence of each number
# counter[num] represents the occurrence of num
counter = Array.new(m + 1, 0)
nums.each { |num| counter[num] += 1 }
# 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
# counter[num]-1 is the last index where num appears in res
(0...m).each { |i| counter[i + 1] += counter[i] }
# 4. Traverse nums in reverse, fill elements into result array res
# Initialize the array res to record results
n = nums.length
res = Array.new(n, 0)
(n - 1).downto(0).each do |i|
num = nums[i]
res[counter[num] - 1] = num # Place num at the corresponding index
counter[num] -= 1 # Decrement the prefix sum by 1, getting the next index to place num
end
# Use result array res to overwrite the original array nums
(0...n).each { |i| nums[i] = res[i] }
end
```
=== "Zig"
## 11.9.3 &nbsp; Algorithm Characteristics
```zig title="counting_sort.zig"
[class]{}-[func]{countingSort}
```
## 11.9.3 &nbsp; Algorithm characteristics
- **Time complexity is $O(n + m)$, non-adaptive sort**: It involves traversing `nums` and `counter`, both using linear time. Generally, $n \gg m$, and the time complexity tends towards $O(n)$.
- **Space complexity is $O(n + m)$, non-in-place sort**: It uses array `res` of lengths $n$ and array `counter` of length $m$ respectively.
- **Stable sort**: Since elements are filled into `res` in a "right-to-left" order, reversing the traversal of `nums` can prevent changing the relative position between equal elements, thereby achieving a stable sort. Actually, traversing `nums` in order can also produce the correct sorting result, but the outcome is unstable.
- **Time complexity of $O(n + m)$, non-adaptive sorting**: Involves traversing `nums` and traversing `counter`, both using linear time. Generally, $n \gg m$, and time complexity tends toward $O(n)$.
- **Space complexity of $O(n + m)$, non-in-place sorting**: Uses arrays `res` and `counter` of lengths $n$ and $m$ respectively.
- **Stable sorting**: Since elements are filled into `res` in a "right-to-left" order, traversing `nums` in reverse can avoid changing the relative positions of equal elements, thereby achieving stable sorting. In fact, traversing `nums` in forward order can also yield correct sorting results, but the result would be unstable.
## 11.9.4 &nbsp; Limitations
By now, you might find counting sort very clever, as it can achieve efficient sorting merely by counting quantities. However, the prerequisites for using counting sort are relatively strict.
By this point, you might think counting sort is very clever, as it can achieve efficient sorting just by counting quantities. However, the prerequisites for using counting sort are relatively strict.
**Counting sort is only suitable for non-negative integers**. If you want to apply it to other types of data, you need to ensure that these data can be converted to non-negative integers without changing the original order of the elements. For example, for an array containing negative integers, you can first add a constant to all numbers, converting them all to positive numbers, and then convert them back after sorting is complete.
**Counting sort is only suitable for non-negative integers**. If you want to apply it to other types of data, you need to ensure that the data can be converted to non-negative integers without changing the relative size relationships between elements. For example, for an integer array containing negative numbers, you can first add a constant to all numbers to convert them all to positive numbers, and then convert them back after sorting is complete.
**Counting sort is suitable for large datasets with a small range of values**. For example, in the above example, $m$ should not be too large, otherwise, it will occupy too much space. And when $n \ll m$, counting sort uses $O(m)$ time, which may be slower than $O(n \log n)$ sorting algorithms.
**Counting sort is suitable for situations where the data volume is large but the data range is small**. For example, in the above example, $m$ cannot be too large, otherwise it will occupy too much space. And when $n \ll m$, counting sort uses $O(m)$ time, which may be slower than $O(n \log n)$ sorting algorithms.
+389 -53
View File
@@ -2,34 +2,34 @@
comments: true
---
# 11.7 &nbsp; Heap sort
# 11.7 &nbsp; Heap Sort
!!! tip
Before reading this section, please ensure you have completed the "Heap" chapter.
<u>Heap sort</u> is an efficient sorting algorithm based on the heap data structure. We can implement heap sort using the "heap creation" and "element extraction" operations we have already learned.
<u>Heap sort (heap sort)</u> is an efficient sorting algorithm based on the heap data structure. We can use the "build heap operation" and "element out-heap operation" that we have already learned to implement heap sort.
1. Input the array and construct a min-heap, where the smallest element is at the top of the heap.
2. Continuously perform the extraction operation, record the extracted elements sequentially to obtain a sorted list from smallest to largest.
1. Input the array and build a min-heap, at which point the smallest element is at the heap top.
2. Continuously perform the out-heap operation, record the out-heap elements in sequence, and an ascending sorted sequence can be obtained.
Although the above method is feasible, it requires an additional array to store the popped elements, which is somewhat space-consuming. In practice, we usually use a more elegant implementation.
Although the above method is feasible, it requires an additional array to save the popped elements, which is quite wasteful of space. In practice, we usually use a more elegant implementation method.
## 11.7.1 &nbsp; Algorithm flow
## 11.7.1 &nbsp; Algorithm Flow
Suppose the array length is $n$, the heap sort process is as follows.
Assume the array length is $n$. The flow of heap sort is shown in Figure 11-12.
1. Input the array and establish a max-heap. After this step, the largest element is positioned at the top of the heap.
2. Swap the top element of the heap (the first element) with the heap's bottom element (the last element). Following this swap, reduce the heap's length by $1$ and increase the sorted elements count by $1$.
3. Starting from the heap top, perform the sift-down operation from top to bottom. After the sift-down, the heap's property is restored.
4. Repeat steps `2.` and `3.` Loop for $n - 1$ rounds to complete the sorting of the array.
1. Input the array and build a max-heap. After completion, the largest element is at the heap top.
2. Swap the heap top element (first element) with the heap bottom element (last element). After the swap is complete, reduce the heap length by $1$ and increase the count of sorted elements by $1$.
3. Starting from the heap top element, perform top-to-bottom heapify operation (sift down). After heapify is complete, the heap property is restored.
4. Loop through steps `2.` and `3.` After looping $n - 1$ rounds, the array sorting can be completed.
!!! tip
In fact, the element extraction operation also includes steps `2.` and `3.`, with an additional step to pop (remove) the extracted element from the heap.
In fact, the element out-heap operation also includes steps `2.` and `3.`, with just an additional step to pop the element.
=== "<1>"
![Heap sort process](heap_sort.assets/heap_sort_step1.png){ class="animation-figure" }
![Heap sort steps](heap_sort.assets/heap_sort_step1.png){ class="animation-figure" }
=== "<2>"
![heap_sort_step2](heap_sort.assets/heap_sort_step2.png){ class="animation-figure" }
@@ -64,9 +64,9 @@ Suppose the array length is $n$, the heap sort process is as follows.
=== "<12>"
![heap_sort_step12](heap_sort.assets/heap_sort_step12.png){ class="animation-figure" }
<p align="center"> Figure 11-12 &nbsp; Heap sort process </p>
<p align="center"> Figure 11-12 &nbsp; Heap sort steps </p>
In the code implementation, we used the sift-down function `sift_down()` from the "Heap" chapter. It is important to note that since the heap's length decreases as the maximum element is extracted, we need to add a length parameter $n$ to the `sift_down()` function to specify the current effective length of the heap. The code is shown below:
In the code implementation, we use the same top-to-bottom heapify function `sift_down()` from the "Heap" chapter. It is worth noting that since the heap length will decrease as the largest element is extracted, we need to add a length parameter $n$ to the `sift_down()` function to specify the current effective length of the heap. The code is as follows:
=== "Python"
@@ -109,7 +109,7 @@ In the code implementation, we used the sift-down function `sift_down()` from th
/* Heap length is n, start heapifying node i, from top to bottom */
void siftDown(vector<int> &nums, int n, int i) {
while (true) {
// Determine the largest node among i, l, r, noted as ma
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
int l = 2 * i + 1;
int r = 2 * i + 2;
int ma = i;
@@ -117,7 +117,7 @@ In the code implementation, we used the sift-down function `sift_down()` from th
ma = l;
if (r < n && nums[r] > nums[ma])
ma = r;
// If node i is the largest or indices l, r are out of bounds, no further heapification needed, break
// Swap two nodes
if (ma == i) {
break;
}
@@ -136,7 +136,7 @@ In the code implementation, we used the sift-down function `sift_down()` from th
}
// Extract the largest element from the heap and repeat for n-1 rounds
for (int i = nums.size() - 1; i > 0; --i) {
// Swap the root node with the rightmost leaf node (swap the first element with the last element)
// Delete node
swap(nums[0], nums[i]);
// Start heapifying the root node, from top to bottom
siftDown(nums, i, 0);
@@ -150,7 +150,7 @@ In the code implementation, we used the sift-down function `sift_down()` from th
/* Heap length is n, start heapifying node i, from top to bottom */
void siftDown(int[] nums, int n, int i) {
while (true) {
// Determine the largest node among i, l, r, noted as ma
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
int l = 2 * i + 1;
int r = 2 * i + 2;
int ma = i;
@@ -158,7 +158,7 @@ In the code implementation, we used the sift-down function `sift_down()` from th
ma = l;
if (r < n && nums[r] > nums[ma])
ma = r;
// If node i is the largest or indices l, r are out of bounds, no further heapification needed, break
// Swap two nodes
if (ma == i)
break;
// Swap two nodes
@@ -178,7 +178,7 @@ In the code implementation, we used the sift-down function `sift_down()` from th
}
// Extract the largest element from the heap and repeat for n-1 rounds
for (int i = nums.length - 1; i > 0; i--) {
// Swap the root node with the rightmost leaf node (swap the first element with the last element)
// Delete node
int tmp = nums[0];
nums[0] = nums[i];
nums[i] = tmp;
@@ -191,93 +191,429 @@ In the code implementation, we used the sift-down function `sift_down()` from th
=== "C#"
```csharp title="heap_sort.cs"
[class]{heap_sort}-[func]{SiftDown}
/* Heap length is n, start heapifying node i, from top to bottom */
void SiftDown(int[] nums, int n, int i) {
while (true) {
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
int l = 2 * i + 1;
int r = 2 * i + 2;
int ma = i;
if (l < n && nums[l] > nums[ma])
ma = l;
if (r < n && nums[r] > nums[ma])
ma = r;
// Swap two nodes
if (ma == i)
break;
// Swap two nodes
(nums[ma], nums[i]) = (nums[i], nums[ma]);
// Loop downwards heapification
i = ma;
}
}
[class]{heap_sort}-[func]{HeapSort}
/* Heap sort */
void HeapSort(int[] nums) {
// Build heap operation: heapify all nodes except leaves
for (int i = nums.Length / 2 - 1; i >= 0; i--) {
SiftDown(nums, nums.Length, i);
}
// Extract the largest element from the heap and repeat for n-1 rounds
for (int i = nums.Length - 1; i > 0; i--) {
// Delete node
(nums[i], nums[0]) = (nums[0], nums[i]);
// Start heapifying the root node, from top to bottom
SiftDown(nums, i, 0);
}
}
```
=== "Go"
```go title="heap_sort.go"
[class]{}-[func]{siftDown}
/* Heap length is n, start heapifying node i, from top to bottom */
func siftDown(nums *[]int, n, i int) {
for true {
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
l := 2*i + 1
r := 2*i + 2
ma := i
if l < n && (*nums)[l] > (*nums)[ma] {
ma = l
}
if r < n && (*nums)[r] > (*nums)[ma] {
ma = r
}
// Swap two nodes
if ma == i {
break
}
// Swap two nodes
(*nums)[i], (*nums)[ma] = (*nums)[ma], (*nums)[i]
// Loop downwards heapification
i = ma
}
}
[class]{}-[func]{heapSort}
/* Heap sort */
func heapSort(nums *[]int) {
// Build heap operation: heapify all nodes except leaves
for i := len(*nums)/2 - 1; i >= 0; i-- {
siftDown(nums, len(*nums), i)
}
// Extract the largest element from the heap and repeat for n-1 rounds
for i := len(*nums) - 1; i > 0; i-- {
// Delete node
(*nums)[0], (*nums)[i] = (*nums)[i], (*nums)[0]
// Start heapifying the root node, from top to bottom
siftDown(nums, i, 0)
}
}
```
=== "Swift"
```swift title="heap_sort.swift"
[class]{}-[func]{siftDown}
/* Heap length is n, start heapifying node i, from top to bottom */
func siftDown(nums: inout [Int], n: Int, i: Int) {
var i = i
while true {
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
let l = 2 * i + 1
let r = 2 * i + 2
var ma = i
if l < n, nums[l] > nums[ma] {
ma = l
}
if r < n, nums[r] > nums[ma] {
ma = r
}
// Swap two nodes
if ma == i {
break
}
// Swap two nodes
nums.swapAt(i, ma)
// Loop downwards heapification
i = ma
}
}
[class]{}-[func]{heapSort}
/* Heap sort */
func heapSort(nums: inout [Int]) {
// Build heap operation: heapify all nodes except leaves
for i in stride(from: nums.count / 2 - 1, through: 0, by: -1) {
siftDown(nums: &nums, n: nums.count, i: i)
}
// Extract the largest element from the heap and repeat for n-1 rounds
for i in nums.indices.dropFirst().reversed() {
// Delete node
nums.swapAt(0, i)
// Start heapifying the root node, from top to bottom
siftDown(nums: &nums, n: i, i: 0)
}
}
```
=== "JS"
```javascript title="heap_sort.js"
[class]{}-[func]{siftDown}
/* Heap length is n, start heapifying node i, from top to bottom */
function siftDown(nums, n, i) {
while (true) {
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
let l = 2 * i + 1;
let r = 2 * i + 2;
let ma = i;
if (l < n && nums[l] > nums[ma]) {
ma = l;
}
if (r < n && nums[r] > nums[ma]) {
ma = r;
}
// Swap two nodes
if (ma === i) {
break;
}
// Swap two nodes
[nums[i], nums[ma]] = [nums[ma], nums[i]];
// Loop downwards heapification
i = ma;
}
}
[class]{}-[func]{heapSort}
/* Heap sort */
function heapSort(nums) {
// Build heap operation: heapify all nodes except leaves
for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {
siftDown(nums, nums.length, i);
}
// Extract the largest element from the heap and repeat for n-1 rounds
for (let i = nums.length - 1; i > 0; i--) {
// Delete node
[nums[0], nums[i]] = [nums[i], nums[0]];
// Start heapifying the root node, from top to bottom
siftDown(nums, i, 0);
}
}
```
=== "TS"
```typescript title="heap_sort.ts"
[class]{}-[func]{siftDown}
/* Heap length is n, start heapifying node i, from top to bottom */
function siftDown(nums: number[], n: number, i: number): void {
while (true) {
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
let l = 2 * i + 1;
let r = 2 * i + 2;
let ma = i;
if (l < n && nums[l] > nums[ma]) {
ma = l;
}
if (r < n && nums[r] > nums[ma]) {
ma = r;
}
// Swap two nodes
if (ma === i) {
break;
}
// Swap two nodes
[nums[i], nums[ma]] = [nums[ma], nums[i]];
// Loop downwards heapification
i = ma;
}
}
[class]{}-[func]{heapSort}
/* Heap sort */
function heapSort(nums: number[]): void {
// Build heap operation: heapify all nodes except leaves
for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {
siftDown(nums, nums.length, i);
}
// Extract the largest element from the heap and repeat for n-1 rounds
for (let i = nums.length - 1; i > 0; i--) {
// Delete node
[nums[0], nums[i]] = [nums[i], nums[0]];
// Start heapifying the root node, from top to bottom
siftDown(nums, i, 0);
}
}
```
=== "Dart"
```dart title="heap_sort.dart"
[class]{}-[func]{siftDown}
/* Heap length is n, start heapifying node i, from top to bottom */
void siftDown(List<int> nums, int n, int i) {
while (true) {
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
int l = 2 * i + 1;
int r = 2 * i + 2;
int ma = i;
if (l < n && nums[l] > nums[ma]) ma = l;
if (r < n && nums[r] > nums[ma]) ma = r;
// Swap two nodes
if (ma == i) break;
// Swap two nodes
int temp = nums[i];
nums[i] = nums[ma];
nums[ma] = temp;
// Loop downwards heapification
i = ma;
}
}
[class]{}-[func]{heapSort}
/* Heap sort */
void heapSort(List<int> nums) {
// Build heap operation: heapify all nodes except leaves
for (int i = nums.length ~/ 2 - 1; i >= 0; i--) {
siftDown(nums, nums.length, i);
}
// Extract the largest element from the heap and repeat for n-1 rounds
for (int i = nums.length - 1; i > 0; i--) {
// Delete node
int tmp = nums[0];
nums[0] = nums[i];
nums[i] = tmp;
// Start heapifying the root node, from top to bottom
siftDown(nums, i, 0);
}
}
```
=== "Rust"
```rust title="heap_sort.rs"
[class]{}-[func]{sift_down}
/* Heap length is n, start heapifying node i, from top to bottom */
fn sift_down(nums: &mut [i32], n: usize, mut i: usize) {
loop {
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
let l = 2 * i + 1;
let r = 2 * i + 2;
let mut ma = i;
if l < n && nums[l] > nums[ma] {
ma = l;
}
if r < n && nums[r] > nums[ma] {
ma = r;
}
// Swap two nodes
if ma == i {
break;
}
// Swap two nodes
nums.swap(i, ma);
// Loop downwards heapification
i = ma;
}
}
[class]{}-[func]{heap_sort}
/* Heap sort */
fn heap_sort(nums: &mut [i32]) {
// Build heap operation: heapify all nodes except leaves
for i in (0..nums.len() / 2).rev() {
sift_down(nums, nums.len(), i);
}
// Extract the largest element from the heap and repeat for n-1 rounds
for i in (1..nums.len()).rev() {
// Delete node
nums.swap(0, i);
// Start heapifying the root node, from top to bottom
sift_down(nums, i, 0);
}
}
```
=== "C"
```c title="heap_sort.c"
[class]{}-[func]{siftDown}
/* Heap length is n, start heapifying node i, from top to bottom */
void siftDown(int nums[], int n, int i) {
while (1) {
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
int l = 2 * i + 1;
int r = 2 * i + 2;
int ma = i;
if (l < n && nums[l] > nums[ma])
ma = l;
if (r < n && nums[r] > nums[ma])
ma = r;
// Swap two nodes
if (ma == i) {
break;
}
// Swap two nodes
int temp = nums[i];
nums[i] = nums[ma];
nums[ma] = temp;
// Loop downwards heapification
i = ma;
}
}
[class]{}-[func]{heapSort}
/* Heap sort */
void heapSort(int nums[], int n) {
// Build heap operation: heapify all nodes except leaves
for (int i = n / 2 - 1; i >= 0; --i) {
siftDown(nums, n, i);
}
// Extract the largest element from the heap and repeat for n-1 rounds
for (int i = n - 1; i > 0; --i) {
// Delete node
int tmp = nums[0];
nums[0] = nums[i];
nums[i] = tmp;
// Start heapifying the root node, from top to bottom
siftDown(nums, i, 0);
}
}
```
=== "Kotlin"
```kotlin title="heap_sort.kt"
[class]{}-[func]{siftDown}
/* Heap length is n, start heapifying node i, from top to bottom */
fun siftDown(nums: IntArray, n: Int, li: Int) {
var i = li
while (true) {
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
val l = 2 * i + 1
val r = 2 * i + 2
var ma = i
if (l < n && nums[l] > nums[ma])
ma = l
if (r < n && nums[r] > nums[ma])
ma = r
// Swap two nodes
if (ma == i)
break
// Swap two nodes
val temp = nums[i]
nums[i] = nums[ma]
nums[ma] = temp
// Loop downwards heapification
i = ma
}
}
[class]{}-[func]{heapSort}
/* Heap sort */
fun heapSort(nums: IntArray) {
// Build heap operation: heapify all nodes except leaves
for (i in nums.size / 2 - 1 downTo 0) {
siftDown(nums, nums.size, i)
}
// Extract the largest element from the heap and repeat for n-1 rounds
for (i in nums.size - 1 downTo 1) {
// Delete node
val temp = nums[0]
nums[0] = nums[i]
nums[i] = temp
// Start heapifying the root node, from top to bottom
siftDown(nums, i, 0)
}
}
```
=== "Ruby"
```ruby title="heap_sort.rb"
[class]{}-[func]{sift_down}
### Heap length is n, heapify from node i, top to bottom ###
def sift_down(nums, n, i)
while true
# If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
l = 2 * i + 1
r = 2 * i + 2
ma = i
ma = l if l < n && nums[l] > nums[ma]
ma = r if r < n && nums[r] > nums[ma]
# Swap two nodes
break if ma == i
# Swap two nodes
nums[i], nums[ma] = nums[ma], nums[i]
# Loop downwards heapification
i = ma
end
end
[class]{}-[func]{heap_sort}
### Heap sort ###
def heap_sort(nums)
# Build heap operation: heapify all nodes except leaves
(nums.length / 2 - 1).downto(0) do |i|
sift_down(nums, nums.length, i)
end
# Extract the largest element from the heap and repeat for n-1 rounds
(nums.length - 1).downto(1) do |i|
# Delete node
nums[0], nums[i] = nums[i], nums[0]
# Start heapifying the root node, from top to bottom
sift_down(nums, i, 0)
end
end
```
=== "Zig"
## 11.7.2 &nbsp; Algorithm Characteristics
```zig title="heap_sort.zig"
[class]{}-[func]{siftDown}
[class]{}-[func]{heapSort}
```
## 11.7.2 &nbsp; Algorithm characteristics
- **Time complexity is $O(n \log n)$, non-adaptive sort**: The heap creation uses $O(n)$ time. Extracting the largest element from the heap takes $O(\log n)$ time, looping for $n - 1$ rounds.
- **Space complexity is $O(1)$, in-place sort**: A few pointer variables use $O(1)$ space. The element swapping and heapifying operations are performed on the original array.
- **Non-stable sort**: The relative positions of equal elements may change during the swapping of the heap's top and bottom elements.
- **Time complexity of $O(n \log n)$, non-adaptive sorting**: The build heap operation uses $O(n)$ time. Extracting the largest element from the heap has a time complexity of $O(\log n)$, looping a total of $n - 1$ rounds.
- **Space complexity of $O(1)$, in-place sorting**: A few pointer variables use $O(1)$ space. Element swapping and heapify operations are both performed on the original array.
- **Non-stable sorting**: When swapping the heap top element and heap bottom element, the relative positions of equal elements may change.
+12 -12
View File
@@ -9,20 +9,20 @@ icon: material/sort-ascending
!!! abstract
Sorting is like a magical key that turns chaos into order, enabling us to understand and handle data more efficiently.
Sorting is like a magic key that transforms chaos into order, enabling us to understand and process data more efficiently.
Whether it's simple ascending order or complex categorical arrangements, sorting reveals the harmonious beauty of data.
Whether it's simple ascending order or complex categorized arrangements, sorting demonstrates the harmonious beauty of data.
## Chapter contents
- [11.1 &nbsp; Sorting algorithms](sorting_algorithm.md)
- [11.2 &nbsp; Selection sort](selection_sort.md)
- [11.3 &nbsp; Bubble sort](bubble_sort.md)
- [11.4 &nbsp; Insertion sort](insertion_sort.md)
- [11.5 &nbsp; Quick sort](quick_sort.md)
- [11.6 &nbsp; Merge sort](merge_sort.md)
- [11.7 &nbsp; Heap sort](heap_sort.md)
- [11.8 &nbsp; Bucket sort](bucket_sort.md)
- [11.9 &nbsp; Counting sort](counting_sort.md)
- [11.10 &nbsp; Radix sort](radix_sort.md)
- [11.1 &nbsp; Sorting Algorithms](sorting_algorithm.md)
- [11.2 &nbsp; Selection Sort](selection_sort.md)
- [11.3 &nbsp; Bubble Sort](bubble_sort.md)
- [11.4 &nbsp; Insertion Sort](insertion_sort.md)
- [11.5 &nbsp; Quick Sort](quick_sort.md)
- [11.6 &nbsp; Merge Sort](merge_sort.md)
- [11.7 &nbsp; Heap Sort](heap_sort.md)
- [11.8 &nbsp; Bucket Sort](bucket_sort.md)
- [11.9 &nbsp; Counting Sort](counting_sort.md)
- [11.10 &nbsp; Radix Sort](radix_sort.md)
- [11.11 &nbsp; Summary](summary.md)
+168 -45
View File
@@ -2,30 +2,30 @@
comments: true
---
# 11.4 &nbsp; Insertion sort
# 11.4 &nbsp; Insertion Sort
<u>Insertion sort</u> is a simple sorting algorithm that works very much like the process of manually sorting a deck of cards.
<u>Insertion sort (insertion sort)</u> is a simple sorting algorithm that works very similarly to the process of manually organizing a deck of cards.
Specifically, we select a base element from the unsorted interval, compare it with the elements in the sorted interval to its left, and insert the element into the correct position.
Specifically, we select a base element from the unsorted interval, compare the element with elements in the sorted interval to its left one by one, and insert the element into the correct position.
Figure 11-6 illustrates how an element is inserted into the array. Assuming the base element is `base`, we need to shift all elements from the target index up to `base` one position to the right, then assign `base` to the target index.
Figure 11-6 shows the operation flow of inserting an element into the array. Let the base element be `base`. We need to move all elements from the target index to `base` one position to the right, and then assign `base` to the target index.
![Single insertion operation](insertion_sort.assets/insertion_operation.png){ class="animation-figure" }
<p align="center"> Figure 11-6 &nbsp; Single insertion operation </p>
## 11.4.1 &nbsp; Algorithm process
## 11.4.1 &nbsp; Algorithm Flow
The overall process of insertion sort is shown in Figure 11-7.
The overall flow of insertion sort is shown in Figure 11-7.
1. Consider the first element of the array as sorted.
2. Select the second element as `base`, insert it into its correct position, **leaving the first two elements sorted**.
3. Select the third element as `base`, insert it into its correct position, **leaving the first three elements sorted**.
4. Continuing in this manner, in the final iteration, the last element is taken as `base`, and after inserting it into the correct position, **all elements are sorted**.
1. Initially, the first element of the array has completed sorting.
2. Select the second element of the array as `base`, and after inserting it into the correct position, **the first 2 elements of the array are sorted**.
3. Select the third element as `base`, and after inserting it into the correct position, **the first 3 elements of the array are sorted**.
4. And so on. In the last round, select the last element as `base`, and after inserting it into the correct position, **all elements are sorted**.
![Insertion sort process](insertion_sort.assets/insertion_sort_overview.png){ class="animation-figure" }
![Insertion sort flow](insertion_sort.assets/insertion_sort_overview.png){ class="animation-figure" }
<p align="center"> Figure 11-7 &nbsp; Insertion sort process </p>
<p align="center"> Figure 11-7 &nbsp; Insertion sort flow </p>
Example code is as follows:
@@ -34,11 +34,11 @@ Example code is as follows:
```python title="insertion_sort.py"
def insertion_sort(nums: list[int]):
"""Insertion sort"""
# Outer loop: sorted range is [0, i-1]
# Outer loop: sorted interval is [0, i-1]
for i in range(1, len(nums)):
base = nums[i]
j = i - 1
# Inner loop: insert base into the correct position within the sorted range [0, i-1]
# Inner loop: insert base into the correct position within the sorted interval [0, i-1]
while j >= 0 and nums[j] > base:
nums[j + 1] = nums[j] # Move nums[j] to the right by one position
j -= 1
@@ -50,10 +50,10 @@ Example code is as follows:
```cpp title="insertion_sort.cpp"
/* Insertion sort */
void insertionSort(vector<int> &nums) {
// Outer loop: sorted range is [0, i-1]
// Outer loop: sorted interval is [0, i-1]
for (int i = 1; i < nums.size(); i++) {
int base = nums[i], j = i - 1;
// Inner loop: insert base into the correct position within the sorted range [0, i-1]
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j]; // Move nums[j] to the right by one position
j--;
@@ -68,10 +68,10 @@ Example code is as follows:
```java title="insertion_sort.java"
/* Insertion sort */
void insertionSort(int[] nums) {
// Outer loop: sorted range is [0, i-1]
// Outer loop: sorted interval is [0, i-1]
for (int i = 1; i < nums.length; i++) {
int base = nums[i], j = i - 1;
// Inner loop: insert base into the correct position within the sorted range [0, i-1]
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j]; // Move nums[j] to the right by one position
j--;
@@ -84,85 +84,208 @@ Example code is as follows:
=== "C#"
```csharp title="insertion_sort.cs"
[class]{insertion_sort}-[func]{InsertionSort}
/* Insertion sort */
void InsertionSort(int[] nums) {
// Outer loop: sorted interval is [0, i-1]
for (int i = 1; i < nums.Length; i++) {
int bas = nums[i], j = i - 1;
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
while (j >= 0 && nums[j] > bas) {
nums[j + 1] = nums[j]; // Move nums[j] to the right by one position
j--;
}
nums[j + 1] = bas; // Assign base to the correct position
}
}
```
=== "Go"
```go title="insertion_sort.go"
[class]{}-[func]{insertionSort}
/* Insertion sort */
func insertionSort(nums []int) {
// Outer loop: sorted interval is [0, i-1]
for i := 1; i < len(nums); i++ {
base := nums[i]
j := i - 1
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
for j >= 0 && nums[j] > base {
nums[j+1] = nums[j] // Move nums[j] to the right by one position
j--
}
nums[j+1] = base // Assign base to the correct position
}
}
```
=== "Swift"
```swift title="insertion_sort.swift"
[class]{}-[func]{insertionSort}
/* Insertion sort */
func insertionSort(nums: inout [Int]) {
// Outer loop: sorted interval is [0, i-1]
for i in nums.indices.dropFirst() {
let base = nums[i]
var j = i - 1
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
while j >= 0, nums[j] > base {
nums[j + 1] = nums[j] // Move nums[j] to the right by one position
j -= 1
}
nums[j + 1] = base // Assign base to the correct position
}
}
```
=== "JS"
```javascript title="insertion_sort.js"
[class]{}-[func]{insertionSort}
/* Insertion sort */
function insertionSort(nums) {
// Outer loop: sorted interval is [0, i-1]
for (let i = 1; i < nums.length; i++) {
let base = nums[i],
j = i - 1;
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j]; // Move nums[j] to the right by one position
j--;
}
nums[j + 1] = base; // Assign base to the correct position
}
}
```
=== "TS"
```typescript title="insertion_sort.ts"
[class]{}-[func]{insertionSort}
/* Insertion sort */
function insertionSort(nums: number[]): void {
// Outer loop: sorted interval is [0, i-1]
for (let i = 1; i < nums.length; i++) {
const base = nums[i];
let j = i - 1;
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j]; // Move nums[j] to the right by one position
j--;
}
nums[j + 1] = base; // Assign base to the correct position
}
}
```
=== "Dart"
```dart title="insertion_sort.dart"
[class]{}-[func]{insertionSort}
/* Insertion sort */
void insertionSort(List<int> nums) {
// Outer loop: sorted interval is [0, i-1]
for (int i = 1; i < nums.length; i++) {
int base = nums[i], j = i - 1;
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j]; // Move nums[j] to the right by one position
j--;
}
nums[j + 1] = base; // Assign base to the correct position
}
}
```
=== "Rust"
```rust title="insertion_sort.rs"
[class]{}-[func]{insertion_sort}
/* Insertion sort */
fn insertion_sort(nums: &mut [i32]) {
// Outer loop: sorted interval is [0, i-1]
for i in 1..nums.len() {
let (base, mut j) = (nums[i], (i - 1) as i32);
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
while j >= 0 && nums[j as usize] > base {
nums[(j + 1) as usize] = nums[j as usize]; // Move nums[j] to the right by one position
j -= 1;
}
nums[(j + 1) as usize] = base; // Assign base to the correct position
}
}
```
=== "C"
```c title="insertion_sort.c"
[class]{}-[func]{insertionSort}
/* Insertion sort */
void insertionSort(int nums[], int size) {
// Outer loop: sorted interval is [0, i-1]
for (int i = 1; i < size; i++) {
int base = nums[i], j = i - 1;
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
while (j >= 0 && nums[j] > base) {
// Move nums[j] to the right by one position
nums[j + 1] = nums[j];
j--;
}
// Assign base to the correct position
nums[j + 1] = base;
}
}
```
=== "Kotlin"
```kotlin title="insertion_sort.kt"
[class]{}-[func]{insertionSort}
/* Insertion sort */
fun insertionSort(nums: IntArray) {
// Outer loop: sorted elements are 1, 2, ..., n
for (i in nums.indices) {
val base = nums[i]
var j = i - 1
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j] // Move nums[j] to the right by one position
j--
}
nums[j + 1] = base // Assign base to the correct position
}
}
```
=== "Ruby"
```ruby title="insertion_sort.rb"
[class]{}-[func]{insertion_sort}
### Insertion sort ###
def insertion_sort(nums)
n = nums.length
# Outer loop: sorted interval is [0, i-1]
for i in 1...n
base = nums[i]
j = i - 1
# Inner loop: insert base into the correct position within the sorted interval [0, i-1]
while j >= 0 && nums[j] > base
nums[j + 1] = nums[j] # Move nums[j] to the right by one position
j -= 1
end
nums[j + 1] = base # Assign base to the correct position
end
end
```
=== "Zig"
## 11.4.2 &nbsp; Algorithm Characteristics
```zig title="insertion_sort.zig"
[class]{}-[func]{insertionSort}
```
- **Time complexity of $O(n^2)$, adaptive sorting**: In the worst case, each insertion operation requires loops of $n - 1$, $n-2$, $\dots$, $2$, $1$, summing to $(n - 1) n / 2$, so the time complexity is $O(n^2)$. When encountering ordered data, the insertion operation will terminate early. When the input array is completely ordered, insertion sort achieves the best-case time complexity of $O(n)$.
- **Space complexity of $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space.
- **Stable sorting**: During the insertion operation process, we insert elements to the right of equal elements, without changing their order.
## 11.4.2 &nbsp; Algorithm characteristics
## 11.4.3 &nbsp; Advantages of Insertion Sort
- **Time complexity is $O(n^2)$, adaptive sorting**: In the worst case, each insertion operation requires $n - 1$, $n-2$, ..., $2$, $1$ loops, summing up to $(n - 1) n / 2$, thus the time complexity is $O(n^2)$. In the case of ordered data, the insertion operation will terminate early. When the input array is completely ordered, insertion sort achieves the best time complexity of $O(n)$.
- **Space complexity is $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space.
- **Stable sorting**: During the insertion operation, we insert elements to the right of equal elements, not changing their order.
The time complexity of insertion sort is $O(n^2)$, while the time complexity of quick sort, which we will learn about next, is $O(n \log n)$. Although insertion sort has a higher time complexity, **insertion sort is usually faster for smaller data volumes**.
## 11.4.3 &nbsp; Advantages of insertion sort
This conclusion is similar to the applicable situations of linear search and binary search. Algorithms like quick sort with $O(n \log n)$ complexity are sorting algorithms based on divide-and-conquer strategy and often contain more unit computation operations. When the data volume is small, $n^2$ and $n \log n$ are numerically close, and complexity does not dominate; the number of unit operations per round plays a decisive role.
The time complexity of insertion sort is $O(n^2)$, while the time complexity of quicksort, which we will study next, is $O(n \log n)$. Although insertion sort has a higher time complexity, **it is usually faster in small input sizes**.
In fact, the built-in sorting functions in many programming languages (such as Java) adopt insertion sort. The general approach is: for long arrays, use sorting algorithms based on divide-and-conquer strategy, such as quick sort; for short arrays, directly use insertion sort.
This conclusion is similar to that for linear and binary search. Algorithms like quicksort that have a time complexity of $O(n \log n)$ and are based on the divide-and-conquer strategy often involve more unit operations. For small input sizes, the numerical values of $n^2$ and $n \log n$ are close, and complexity does not dominate, with the number of unit operations per round playing a decisive role.
Although bubble sort, selection sort, and insertion sort all have a time complexity of $O(n^2)$, in actual situations, **insertion sort is used significantly more frequently than bubble sort and selection sort**, mainly for the following reasons.
In fact, many programming languages (such as Java) use insertion sort within their built-in sorting functions. The general approach is: for long arrays, use sorting algorithms based on divide-and-conquer strategies, such as quicksort; for short arrays, use insertion sort directly.
Although bubble sort, selection sort, and insertion sort all have a time complexity of $O(n^2)$, in practice, **insertion sort is commonly used than bubble sort and selection sort**, mainly for the following reasons.
- Bubble sort is based on element swapping, which requires the use of a temporary variable, involving 3 unit operations; insertion sort is based on element assignment, requiring only 1 unit operation. Therefore, **the computational overhead of bubble sort is generally higher than that of insertion sort**.
- The time complexity of selection sort is always $O(n^2)$. **Given a set of partially ordered data, insertion sort is usually more efficient than selection sort**.
- Bubble sort is based on element swapping, requiring the use of a temporary variable, involving 3 unit operations; insertion sort is based on element assignment, requiring only 1 unit operation. Therefore, **the computational overhead of bubble sort is usually higher than that of insertion sort**.
- Selection sort has a time complexity of $O(n^2)$ in any case. **If given a set of partially ordered data, insertion sort is usually more efficient than selection sort**.
- Selection sort is unstable and cannot be applied to multi-level sorting.
+454 -57
View File
@@ -2,28 +2,28 @@
comments: true
---
# 11.6 &nbsp; Merge sort
# 11.6 &nbsp; Merge Sort
<u>Merge sort</u> is a sorting algorithm based on the divide-and-conquer strategy, involving the "divide" and "merge" phases shown in Figure 11-10.
<u>Merge sort (merge sort)</u> is a sorting algorithm based on the divide-and-conquer strategy, which includes the "divide" and "merge" phases shown in Figure 11-10.
1. **Divide phase**: Recursively split the array from the midpoint, transforming the sorting problem of a long array into shorter arrays.
2. **Merge phase**: Stop dividing when the length of the sub-array is 1, and then begin merging. The two shorter sorted arrays are continuously merged into a longer sorted array until the process is complete.
1. **Divide phase**: Recursively split the array from the midpoint, transforming the sorting problem of a long array into the sorting problems of shorter arrays.
2. **Merge phase**: When the sub-array length is 1, terminate the division and start merging, continuously merging two shorter sorted arrays into one longer sorted array until the process is complete.
![The divide and merge phases of merge sort](merge_sort.assets/merge_sort_overview.png){ class="animation-figure" }
![Divide and merge phases of merge sort](merge_sort.assets/merge_sort_overview.png){ class="animation-figure" }
<p align="center"> Figure 11-10 &nbsp; The divide and merge phases of merge sort </p>
<p align="center"> Figure 11-10 &nbsp; Divide and merge phases of merge sort </p>
## 11.6.1 &nbsp; Algorithm workflow
## 11.6.1 &nbsp; Algorithm Flow
As shown in Figure 11-11, the "divide phase" recursively splits the array from the midpoint into two sub-arrays from top to bottom.
1. Calculate the midpoint `mid`, recursively divide the left sub-array (interval `[left, mid]`) and the right sub-array (interval `[mid + 1, right]`).
2. Continue with step `1.` recursively until sub-array length becomes 1, then stops.
1. Calculate the array midpoint `mid`, recursively divide the left sub-array (interval `[left, mid]`) and right sub-array (interval `[mid + 1, right]`).
2. Recursively execute step `1.` until the sub-array interval length is 1, then terminate.
The "merge phase" combines the left and right sub-arrays into a sorted array from bottom to top. It is important to note that, merging starts with sub-arrays of length 1, and each sub-array is sorted during the merge phase.
The "merge phase" merges the left sub-array and right sub-array into a sorted array from bottom to top. Note that merging starts from sub-arrays of length 1, and each sub-array in the merge phase is sorted.
=== "<1>"
![Merge sort process](merge_sort.assets/merge_sort_step1.png){ class="animation-figure" }
![Merge sort steps](merge_sort.assets/merge_sort_step1.png){ class="animation-figure" }
=== "<2>"
![merge_sort_step2](merge_sort.assets/merge_sort_step2.png){ class="animation-figure" }
@@ -52,14 +52,14 @@ The "merge phase" combines the left and right sub-arrays into a sorted array fro
=== "<10>"
![merge_sort_step10](merge_sort.assets/merge_sort_step10.png){ class="animation-figure" }
<p align="center"> Figure 11-11 &nbsp; Merge sort process </p>
<p align="center"> Figure 11-11 &nbsp; Merge sort steps </p>
It can be observed that the order of recursion in merge sort is consistent with the post-order traversal of a binary tree.
It can be observed that the recursive order of merge sort is consistent with the post-order traversal of a binary tree.
- **Post-order traversal**: First recursively traverse the left subtree, then the right subtree, and finally process the root node.
- **Merge sort**: First recursively process the left sub-array, then the right sub-array, and finally perform the merge.
- **Post-order traversal**: First recursively traverse the left subtree, then recursively traverse the right subtree, and finally process the root node.
- **Merge sort**: First recursively process the left sub-array, then recursively process the right sub-array, and finally perform the merge.
The implementation of merge sort is shown in the following code. Note that the interval to be merged in `nums` is `[left, right]`, while the corresponding interval in `tmp` is `[0, right - left]`.
The implementation of merge sort is shown in the code below. Note that the interval to be merged in `nums` is `[left, right]`, while the corresponding interval in `tmp` is `[0, right - left]`.
=== "Python"
@@ -98,8 +98,8 @@ The implementation of merge sort is shown in the following code. Note that the i
# Termination condition
if left >= right:
return # Terminate recursion when subarray length is 1
# Partition stage
mid = left + (right - left) // 2 # Calculate midpoint
# Divide and conquer stage
mid = (left + right) // 2 # Calculate midpoint
merge_sort(nums, left, mid) # Recursively process the left subarray
merge_sort(nums, mid + 1, right) # Recursively process the right subarray
# Merge stage
@@ -141,7 +141,7 @@ The implementation of merge sort is shown in the following code. Note that the i
// Termination condition
if (left >= right)
return; // Terminate recursion when subarray length is 1
// Partition stage
// Divide and conquer stage
int mid = left + (right - left) / 2; // Calculate midpoint
mergeSort(nums, left, mid); // Recursively process the left subarray
mergeSort(nums, mid + 1, right); // Recursively process the right subarray
@@ -185,7 +185,7 @@ The implementation of merge sort is shown in the following code. Note that the i
// Termination condition
if (left >= right)
return; // Terminate recursion when subarray length is 1
// Partition stage
// Divide and conquer stage
int mid = left + (right - left) / 2; // Calculate midpoint
mergeSort(nums, left, mid); // Recursively process the left subarray
mergeSort(nums, mid + 1, right); // Recursively process the right subarray
@@ -197,102 +197,499 @@ The implementation of merge sort is shown in the following code. Note that the i
=== "C#"
```csharp title="merge_sort.cs"
[class]{merge_sort}-[func]{Merge}
/* Merge left subarray and right subarray */
void Merge(int[] nums, int left, int mid, int right) {
// Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
// Create a temporary array tmp to store the merged results
int[] tmp = new int[right - left + 1];
// Initialize the start indices of the left and right subarrays
int i = left, j = mid + 1, k = 0;
// While both subarrays still have elements, compare and copy the smaller element into the temporary array
while (i <= mid && j <= right) {
if (nums[i] <= nums[j])
tmp[k++] = nums[i++];
else
tmp[k++] = nums[j++];
}
// Copy the remaining elements of the left and right subarrays into the temporary array
while (i <= mid) {
tmp[k++] = nums[i++];
}
while (j <= right) {
tmp[k++] = nums[j++];
}
// Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
for (k = 0; k < tmp.Length; ++k) {
nums[left + k] = tmp[k];
}
}
[class]{merge_sort}-[func]{MergeSort}
/* Merge sort */
void MergeSort(int[] nums, int left, int right) {
// Termination condition
if (left >= right) return; // Terminate recursion when subarray length is 1
// Divide and conquer stage
int mid = left + (right - left) / 2; // Calculate midpoint
MergeSort(nums, left, mid); // Recursively process the left subarray
MergeSort(nums, mid + 1, right); // Recursively process the right subarray
// Merge stage
Merge(nums, left, mid, right);
}
```
=== "Go"
```go title="merge_sort.go"
[class]{}-[func]{merge}
/* Merge left subarray and right subarray */
func merge(nums []int, left, mid, right int) {
// Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
// Create a temporary array tmp to store the merged results
tmp := make([]int, right-left+1)
// Initialize the start indices of the left and right subarrays
i, j, k := left, mid+1, 0
// While both subarrays still have elements, compare and copy the smaller element into the temporary array
for i <= mid && j <= right {
if nums[i] <= nums[j] {
tmp[k] = nums[i]
i++
} else {
tmp[k] = nums[j]
j++
}
k++
}
// Copy the remaining elements of the left and right subarrays into the temporary array
for i <= mid {
tmp[k] = nums[i]
i++
k++
}
for j <= right {
tmp[k] = nums[j]
j++
k++
}
// Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
for k := 0; k < len(tmp); k++ {
nums[left+k] = tmp[k]
}
}
[class]{}-[func]{mergeSort}
/* Merge sort */
func mergeSort(nums []int, left, right int) {
// Termination condition
if left >= right {
return
}
// Divide and conquer stage
mid := left + (right - left) / 2
mergeSort(nums, left, mid)
mergeSort(nums, mid+1, right)
// Merge stage
merge(nums, left, mid, right)
}
```
=== "Swift"
```swift title="merge_sort.swift"
[class]{}-[func]{merge}
/* Merge left subarray and right subarray */
func merge(nums: inout [Int], left: Int, mid: Int, right: Int) {
// Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
// Create a temporary array tmp to store the merged results
var tmp = Array(repeating: 0, count: right - left + 1)
// Initialize the start indices of the left and right subarrays
var i = left, j = mid + 1, k = 0
// While both subarrays still have elements, compare and copy the smaller element into the temporary array
while i <= mid, j <= right {
if nums[i] <= nums[j] {
tmp[k] = nums[i]
i += 1
} else {
tmp[k] = nums[j]
j += 1
}
k += 1
}
// Copy the remaining elements of the left and right subarrays into the temporary array
while i <= mid {
tmp[k] = nums[i]
i += 1
k += 1
}
while j <= right {
tmp[k] = nums[j]
j += 1
k += 1
}
// Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
for k in tmp.indices {
nums[left + k] = tmp[k]
}
}
[class]{}-[func]{mergeSort}
/* Merge sort */
func mergeSort(nums: inout [Int], left: Int, right: Int) {
// Termination condition
if left >= right { // Terminate recursion when subarray length is 1
return
}
// Divide and conquer stage
let mid = left + (right - left) / 2 // Calculate midpoint
mergeSort(nums: &nums, left: left, right: mid) // Recursively process the left subarray
mergeSort(nums: &nums, left: mid + 1, right: right) // Recursively process the right subarray
// Merge stage
merge(nums: &nums, left: left, mid: mid, right: right)
}
```
=== "JS"
```javascript title="merge_sort.js"
[class]{}-[func]{merge}
/* Merge left subarray and right subarray */
function merge(nums, left, mid, right) {
// Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
// Create a temporary array tmp to store the merged results
const tmp = new Array(right - left + 1);
// Initialize the start indices of the left and right subarrays
let i = left,
j = mid + 1,
k = 0;
// While both subarrays still have elements, compare and copy the smaller element into the temporary array
while (i <= mid && j <= right) {
if (nums[i] <= nums[j]) {
tmp[k++] = nums[i++];
} else {
tmp[k++] = nums[j++];
}
}
// Copy the remaining elements of the left and right subarrays into the temporary array
while (i <= mid) {
tmp[k++] = nums[i++];
}
while (j <= right) {
tmp[k++] = nums[j++];
}
// Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
for (k = 0; k < tmp.length; k++) {
nums[left + k] = tmp[k];
}
}
[class]{}-[func]{mergeSort}
/* Merge sort */
function mergeSort(nums, left, right) {
// Termination condition
if (left >= right) return; // Terminate recursion when subarray length is 1
// Divide and conquer stage
let mid = Math.floor(left + (right - left) / 2); // Calculate midpoint
mergeSort(nums, left, mid); // Recursively process the left subarray
mergeSort(nums, mid + 1, right); // Recursively process the right subarray
// Merge stage
merge(nums, left, mid, right);
}
```
=== "TS"
```typescript title="merge_sort.ts"
[class]{}-[func]{merge}
/* Merge left subarray and right subarray */
function merge(nums: number[], left: number, mid: number, right: number): void {
// Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
// Create a temporary array tmp to store the merged results
const tmp = new Array(right - left + 1);
// Initialize the start indices of the left and right subarrays
let i = left,
j = mid + 1,
k = 0;
// While both subarrays still have elements, compare and copy the smaller element into the temporary array
while (i <= mid && j <= right) {
if (nums[i] <= nums[j]) {
tmp[k++] = nums[i++];
} else {
tmp[k++] = nums[j++];
}
}
// Copy the remaining elements of the left and right subarrays into the temporary array
while (i <= mid) {
tmp[k++] = nums[i++];
}
while (j <= right) {
tmp[k++] = nums[j++];
}
// Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
for (k = 0; k < tmp.length; k++) {
nums[left + k] = tmp[k];
}
}
[class]{}-[func]{mergeSort}
/* Merge sort */
function mergeSort(nums: number[], left: number, right: number): void {
// Termination condition
if (left >= right) return; // Terminate recursion when subarray length is 1
// Divide and conquer stage
let mid = Math.floor(left + (right - left) / 2); // Calculate midpoint
mergeSort(nums, left, mid); // Recursively process the left subarray
mergeSort(nums, mid + 1, right); // Recursively process the right subarray
// Merge stage
merge(nums, left, mid, right);
}
```
=== "Dart"
```dart title="merge_sort.dart"
[class]{}-[func]{merge}
/* Merge left subarray and right subarray */
void merge(List<int> nums, int left, int mid, int right) {
// Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
// Create a temporary array tmp to store the merged results
List<int> tmp = List.filled(right - left + 1, 0);
// Initialize the start indices of the left and right subarrays
int i = left, j = mid + 1, k = 0;
// While both subarrays still have elements, compare and copy the smaller element into the temporary array
while (i <= mid && j <= right) {
if (nums[i] <= nums[j])
tmp[k++] = nums[i++];
else
tmp[k++] = nums[j++];
}
// Copy the remaining elements of the left and right subarrays into the temporary array
while (i <= mid) {
tmp[k++] = nums[i++];
}
while (j <= right) {
tmp[k++] = nums[j++];
}
// Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
for (k = 0; k < tmp.length; k++) {
nums[left + k] = tmp[k];
}
}
[class]{}-[func]{mergeSort}
/* Merge sort */
void mergeSort(List<int> nums, int left, int right) {
// Termination condition
if (left >= right) return; // Terminate recursion when subarray length is 1
// Divide and conquer stage
int mid = left + (right - left) ~/ 2; // Calculate midpoint
mergeSort(nums, left, mid); // Recursively process the left subarray
mergeSort(nums, mid + 1, right); // Recursively process the right subarray
// Merge stage
merge(nums, left, mid, right);
}
```
=== "Rust"
```rust title="merge_sort.rs"
[class]{}-[func]{merge}
/* Merge left subarray and right subarray */
fn merge(nums: &mut [i32], left: usize, mid: usize, right: usize) {
// Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
// Create a temporary array tmp to store the merged results
let tmp_size = right - left + 1;
let mut tmp = vec![0; tmp_size];
// Initialize the start indices of the left and right subarrays
let (mut i, mut j, mut k) = (left, mid + 1, 0);
// While both subarrays still have elements, compare and copy the smaller element into the temporary array
while i <= mid && j <= right {
if nums[i] <= nums[j] {
tmp[k] = nums[i];
i += 1;
} else {
tmp[k] = nums[j];
j += 1;
}
k += 1;
}
// Copy the remaining elements of the left and right subarrays into the temporary array
while i <= mid {
tmp[k] = nums[i];
k += 1;
i += 1;
}
while j <= right {
tmp[k] = nums[j];
k += 1;
j += 1;
}
// Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
for k in 0..tmp_size {
nums[left + k] = tmp[k];
}
}
[class]{}-[func]{merge_sort}
/* Merge sort */
fn merge_sort(nums: &mut [i32], left: usize, right: usize) {
// Termination condition
if left >= right {
return; // Terminate recursion when subarray length is 1
}
// Divide and conquer stage
let mid = left + (right - left) / 2; // Calculate midpoint
merge_sort(nums, left, mid); // Recursively process the left subarray
merge_sort(nums, mid + 1, right); // Recursively process the right subarray
// Merge stage
merge(nums, left, mid, right);
}
```
=== "C"
```c title="merge_sort.c"
[class]{}-[func]{merge}
/* Merge left subarray and right subarray */
void merge(int *nums, int left, int mid, int right) {
// Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
// Create a temporary array tmp to store the merged results
int tmpSize = right - left + 1;
int *tmp = (int *)malloc(tmpSize * sizeof(int));
// Initialize the start indices of the left and right subarrays
int i = left, j = mid + 1, k = 0;
// While both subarrays still have elements, compare and copy the smaller element into the temporary array
while (i <= mid && j <= right) {
if (nums[i] <= nums[j]) {
tmp[k++] = nums[i++];
} else {
tmp[k++] = nums[j++];
}
}
// Copy the remaining elements of the left and right subarrays into the temporary array
while (i <= mid) {
tmp[k++] = nums[i++];
}
while (j <= right) {
tmp[k++] = nums[j++];
}
// Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
for (k = 0; k < tmpSize; ++k) {
nums[left + k] = tmp[k];
}
// Free memory
free(tmp);
}
[class]{}-[func]{mergeSort}
/* Merge sort */
void mergeSort(int *nums, int left, int right) {
// Termination condition
if (left >= right)
return; // Terminate recursion when subarray length is 1
// Divide and conquer stage
int mid = left + (right - left) / 2; // Calculate midpoint
mergeSort(nums, left, mid); // Recursively process the left subarray
mergeSort(nums, mid + 1, right); // Recursively process the right subarray
// Merge stage
merge(nums, left, mid, right);
}
```
=== "Kotlin"
```kotlin title="merge_sort.kt"
[class]{}-[func]{merge}
/* Merge left subarray and right subarray */
fun merge(nums: IntArray, left: Int, mid: Int, right: Int) {
// Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
// Create a temporary array tmp to store the merged results
val tmp = IntArray(right - left + 1)
// Initialize the start indices of the left and right subarrays
var i = left
var j = mid + 1
var k = 0
// While both subarrays still have elements, compare and copy the smaller element into the temporary array
while (i <= mid && j <= right) {
if (nums[i] <= nums[j])
tmp[k++] = nums[i++]
else
tmp[k++] = nums[j++]
}
// Copy the remaining elements of the left and right subarrays into the temporary array
while (i <= mid) {
tmp[k++] = nums[i++]
}
while (j <= right) {
tmp[k++] = nums[j++]
}
// Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
for (l in tmp.indices) {
nums[left + l] = tmp[l]
}
}
[class]{}-[func]{mergeSort}
/* Merge sort */
fun mergeSort(nums: IntArray, left: Int, right: Int) {
// Termination condition
if (left >= right) return // Terminate recursion when subarray length is 1
// Divide and conquer stage
val mid = left + (right - left) / 2 // Calculate midpoint
mergeSort(nums, left, mid) // Recursively process the left subarray
mergeSort(nums, mid + 1, right) // Recursively process the right subarray
// Merge stage
merge(nums, left, mid, right)
}
```
=== "Ruby"
```ruby title="merge_sort.rb"
[class]{}-[func]{merge}
### Merge left and right subarrays ###
def merge(nums, left, mid, right)
# Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
# Create temporary array tmp to store merged result
tmp = Array.new(right - left + 1, 0)
# Initialize the start indices of the left and right subarrays
i, j, k = left, mid + 1, 0
# While both subarrays still have elements, compare and copy the smaller element into the temporary array
while i <= mid && j <= right
if nums[i] <= nums[j]
tmp[k] = nums[i]
i += 1
else
tmp[k] = nums[j]
j += 1
end
k += 1
end
# Copy the remaining elements of the left and right subarrays into the temporary array
while i <= mid
tmp[k] = nums[i]
i += 1
k += 1
end
while j <= right
tmp[k] = nums[j]
j += 1
k += 1
end
# Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
(0...tmp.length).each do |k|
nums[left + k] = tmp[k]
end
end
[class]{}-[func]{merge_sort}
### Merge sort ###
def merge_sort(nums, left, right)
# Termination condition
# Terminate recursion when subarray length is 1
return if left >= right
# Divide and conquer stage
mid = left + (right - left) / 2 # Calculate midpoint
merge_sort(nums, left, mid) # Recursively process the left subarray
merge_sort(nums, mid + 1, right) # Recursively process the right subarray
# Merge stage
merge(nums, left, mid, right)
end
```
=== "Zig"
## 11.6.2 &nbsp; Algorithm Characteristics
```zig title="merge_sort.zig"
[class]{}-[func]{merge}
- **Time complexity of $O(n \log n)$, non-adaptive sorting**: The division produces a recursion tree of height $\log n$, and the total number of merge operations at each level is $n$, so the overall time complexity is $O(n \log n)$.
- **Space complexity of $O(n)$, non-in-place sorting**: The recursion depth is $\log n$, using $O(\log n)$ size of stack frame space. The merge operation requires the aid of an auxiliary array, using $O(n)$ size of additional space.
- **Stable sorting**: In the merge process, the order of equal elements remains unchanged.
[class]{}-[func]{mergeSort}
```
## 11.6.3 &nbsp; Linked List Sorting
## 11.6.2 &nbsp; Algorithm characteristics
For linked lists, merge sort has significant advantages over other sorting algorithms, **and can optimize the space complexity of linked list sorting tasks to $O(1)$**.
- **Time complexity of $O(n \log n)$, non-adaptive sort**: The division creates a recursion tree of height $\log n$, with each layer merging a total of $n$ operations, resulting in an overall time complexity of $O(n \log n)$.
- **Space complexity of $O(n)$, non-in-place sort**: The recursion depth is $\log n$, using $O(\log n)$ stack frame space. The merging operation requires auxiliary arrays, using an additional space of $O(n)$.
- **Stable sort**: During the merging process, the order of equal elements remains unchanged.
- **Divide phase**: "Iteration" can be used instead of "recursion" to implement linked list division work, thus saving the stack frame space used by recursion.
- **Merge phase**: In linked lists, node insertion and deletion operations can be achieved by just changing references (pointers), so there is no need to create additional linked lists during the merge phase (merging two short ordered linked lists into one long ordered linked list).
## 11.6.3 &nbsp; Linked List sorting
For linked lists, merge sort has significant advantages over other sorting algorithms. **It can optimize the space complexity of the linked list sorting task to $O(1)$**.
- **Divide phase**: "Iteration" can be used instead of "recursion" to perform the linked list division work, thus saving the stack frame space used by recursion.
- **Merge phase**: In linked lists, node insertion and deletion operations can be achieved by changing references (pointers), so no extra lists need to be created during the merge phase (combining two short ordered lists into one long ordered list).
The implementation details are relatively complex, and interested readers can consult related materials for learning.
The specific implementation details are quite complex, and interested readers can consult related materials for learning.
File diff suppressed because it is too large Load Diff
+486 -60
View File
@@ -2,33 +2,33 @@
comments: true
---
# 11.10 &nbsp; Radix sort
# 11.10 &nbsp; Radix Sort
The previous section introduced counting sort, which is suitable for scenarios where the data size $n$ is large but the data range $m$ is small. Suppose we need to sort $n = 10^6$ student IDs, where each ID is an $8$-digit number. This means the data range $m = 10^8$ is very large. Using counting sort in this case would require significant memory space. Radix sort can avoid this situation.
The previous section introduced counting sort, which is suitable for situations where the data volume $n$ is large but the data range $m$ is small. Suppose we need to sort $n = 10^6$ student IDs, and the student ID is an 8-digit number, which means the data range $m = 10^8$ is very large. Using counting sort would require allocating a large amount of memory space, whereas radix sort can avoid this situation.
<u>Radix sort</u> shares the same core concept as counting sort, which also sorts by counting the frequency of elements. Meanwhile, radix sort builds upon this by utilizing the progressive relationship between the digits of numbers. It processes and sorts the digits one at a time, achieving the final sorted order.
<u>Radix sort (radix sort)</u> has a core idea consistent with counting sort, which also achieves sorting by counting quantities. Building on this, radix sort utilizes the progressive relationship between the digits of numbers, sorting each digit in turn to obtain the final sorting result.
## 11.10.1 &nbsp; Algorithm process
## 11.10.1 &nbsp; Algorithm Flow
Taking the student ID data as an example, assume the least significant digit is the $1^{st}$ and the most significant is the $8^{th}$, the radix sort process is illustrated in Figure 11-18.
Taking student ID data as an example, assume the lowest digit is the $1$st digit and the highest digit is the $8$th digit. The flow of radix sort is shown in Figure 11-18.
1. Initialize digit $k = 1$.
2. Perform "counting sort" on the $k^{th}$ digit of the student IDs. After completion, the data will be sorted from smallest to largest based on the $k^{th}$ digit.
3. Increment $k$ by $1$, then return to step `2.` and continue iterating until all digits have been sorted, at which point the process ends.
1. Initialize the digit $k = 1$.
2. Perform "counting sort" on the $k$th digit of the student IDs. After completion, the data will be sorted from smallest to largest according to the $k$th digit.
3. Increase $k$ by $1$, then return to step `2.` and continue iterating until all digits are sorted, at which point the process ends.
![Radix sort algorithm process](radix_sort.assets/radix_sort_overview.png){ class="animation-figure" }
![Radix sort algorithm flow](radix_sort.assets/radix_sort_overview.png){ class="animation-figure" }
<p align="center"> Figure 11-18 &nbsp; Radix sort algorithm process </p>
<p align="center"> Figure 11-18 &nbsp; Radix sort algorithm flow </p>
Below we dissect the code implementation. For a number $x$ in base $d$, to obtain its $k^{th}$ digit $x_k$, the following calculation formula can be used:
Below we analyze the code implementation. For a $d$-base number $x$, to get its $k$th digit $x_k$, the following calculation formula can be used:
$$
x_k = \lfloor\frac{x}{d^{k-1}}\rfloor \bmod d
$$
Where $\lfloor a \rfloor$ denotes rounding down the floating point number $a$, and $\bmod \: d$ denotes taking the modulus of $d$. For student ID data, $d = 10$ and $k \in [1, 8]$.
Where $\lfloor a \rfloor$ denotes rounding down the floating-point number $a$, and $\bmod \: d$ denotes taking the modulo (remainder) with respect to $d$. For student ID data, $d = 10$ and $k \in [1, 8]$.
Additionally, we need to slightly modify the counting sort code to allow sorting based on the $k^{th}$ digit:
Additionally, we need to slightly modify the counting sort code to make it sort based on the $k$th digit of the number:
=== "Python"
@@ -183,121 +183,547 @@ Additionally, we need to slightly modify the counting sort code to allow sorting
=== "C#"
```csharp title="radix_sort.cs"
[class]{radix_sort}-[func]{Digit}
/* Get the k-th digit of element num, where exp = 10^(k-1) */
int Digit(int num, int exp) {
// Passing exp instead of k can avoid repeated expensive exponentiation here
return (num / exp) % 10;
}
[class]{radix_sort}-[func]{CountingSortDigit}
/* Counting sort (based on nums k-th digit) */
void CountingSortDigit(int[] nums, int exp) {
// Decimal digit range is 0~9, therefore need a bucket array of length 10
int[] counter = new int[10];
int n = nums.Length;
// Count the occurrence of digits 0~9
for (int i = 0; i < n; i++) {
int d = Digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d
counter[d]++; // Count the occurrence of digit d
}
// Calculate prefix sum, converting "occurrence count" into "array index"
for (int i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// Traverse in reverse, based on bucket statistics, place each element into res
int[] res = new int[n];
for (int i = n - 1; i >= 0; i--) {
int d = Digit(nums[i], exp);
int j = counter[d] - 1; // Get the index j for d in the array
res[j] = nums[i]; // Place the current element at index j
counter[d]--; // Decrease the count of d by 1
}
// Use result to overwrite the original array nums
for (int i = 0; i < n; i++) {
nums[i] = res[i];
}
}
[class]{radix_sort}-[func]{RadixSort}
/* Radix sort */
void RadixSort(int[] nums) {
// Get the maximum element of the array, used to determine the maximum number of digits
int m = int.MinValue;
foreach (int num in nums) {
if (num > m) m = num;
}
// Traverse from the lowest to the highest digit
for (int exp = 1; exp <= m; exp *= 10) {
// Perform counting sort on the k-th digit of array elements
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// i.e., exp = 10^(k-1)
CountingSortDigit(nums, exp);
}
}
```
=== "Go"
```go title="radix_sort.go"
[class]{}-[func]{digit}
/* Get the k-th digit of element num, where exp = 10^(k-1) */
func digit(num, exp int) int {
// Passing exp instead of k can avoid repeated expensive exponentiation here
return (num / exp) % 10
}
[class]{}-[func]{countingSortDigit}
/* Counting sort (based on nums k-th digit) */
func countingSortDigit(nums []int, exp int) {
// Decimal digit range is 0~9, therefore need a bucket array of length 10
counter := make([]int, 10)
n := len(nums)
// Count the occurrence of digits 0~9
for i := 0; i < n; i++ {
d := digit(nums[i], exp) // Get the k-th digit of nums[i], noted as d
counter[d]++ // Count the occurrence of digit d
}
// Calculate prefix sum, converting "occurrence count" into "array index"
for i := 1; i < 10; i++ {
counter[i] += counter[i-1]
}
// Traverse in reverse, based on bucket statistics, place each element into res
res := make([]int, n)
for i := n - 1; i >= 0; i-- {
d := digit(nums[i], exp)
j := counter[d] - 1 // Get the index j for d in the array
res[j] = nums[i] // Place the current element at index j
counter[d]-- // Decrease the count of d by 1
}
// Use result to overwrite the original array nums
for i := 0; i < n; i++ {
nums[i] = res[i]
}
}
[class]{}-[func]{radixSort}
/* Radix sort */
func radixSort(nums []int) {
// Get the maximum element of the array, used to determine the maximum number of digits
max := math.MinInt
for _, num := range nums {
if num > max {
max = num
}
}
// Traverse from the lowest to the highest digit
for exp := 1; max >= exp; exp *= 10 {
// Perform counting sort on the k-th digit of array elements
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// i.e., exp = 10^(k-1)
countingSortDigit(nums, exp)
}
}
```
=== "Swift"
```swift title="radix_sort.swift"
[class]{}-[func]{digit}
/* Get the k-th digit of element num, where exp = 10^(k-1) */
func digit(num: Int, exp: Int) -> Int {
// Passing exp instead of k can avoid repeated expensive exponentiation here
(num / exp) % 10
}
[class]{}-[func]{countingSortDigit}
/* Counting sort (based on nums k-th digit) */
func countingSortDigit(nums: inout [Int], exp: Int) {
// Decimal digit range is 0~9, therefore need a bucket array of length 10
var counter = Array(repeating: 0, count: 10)
// Count the occurrence of digits 0~9
for i in nums.indices {
let d = digit(num: nums[i], exp: exp) // Get the k-th digit of nums[i], noted as d
counter[d] += 1 // Count the occurrence of digit d
}
// Calculate prefix sum, converting "occurrence count" into "array index"
for i in 1 ..< 10 {
counter[i] += counter[i - 1]
}
// Traverse in reverse, based on bucket statistics, place each element into res
var res = Array(repeating: 0, count: nums.count)
for i in nums.indices.reversed() {
let d = digit(num: nums[i], exp: exp)
let j = counter[d] - 1 // Get the index j for d in the array
res[j] = nums[i] // Place the current element at index j
counter[d] -= 1 // Decrease the count of d by 1
}
// Use result to overwrite the original array nums
for i in nums.indices {
nums[i] = res[i]
}
}
[class]{}-[func]{radixSort}
/* Radix sort */
func radixSort(nums: inout [Int]) {
// Get the maximum element of the array, used to determine the maximum number of digits
var m = Int.min
for num in nums {
if num > m {
m = num
}
}
// Traverse from the lowest to the highest digit
for exp in sequence(first: 1, next: { m >= ($0 * 10) ? $0 * 10 : nil }) {
// Perform counting sort on the k-th digit of array elements
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// i.e., exp = 10^(k-1)
countingSortDigit(nums: &nums, exp: exp)
}
}
```
=== "JS"
```javascript title="radix_sort.js"
[class]{}-[func]{digit}
/* Get the k-th digit of element num, where exp = 10^(k-1) */
function digit(num, exp) {
// Passing exp instead of k can avoid repeated expensive exponentiation here
return Math.floor(num / exp) % 10;
}
[class]{}-[func]{countingSortDigit}
/* Counting sort (based on nums k-th digit) */
function countingSortDigit(nums, exp) {
// Decimal digit range is 0~9, therefore need a bucket array of length 10
const counter = new Array(10).fill(0);
const n = nums.length;
// Count the occurrence of digits 0~9
for (let i = 0; i < n; i++) {
const d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d
counter[d]++; // Count the occurrence of digit d
}
// Calculate prefix sum, converting "occurrence count" into "array index"
for (let i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// Traverse in reverse, based on bucket statistics, place each element into res
const res = new Array(n).fill(0);
for (let i = n - 1; i >= 0; i--) {
const d = digit(nums[i], exp);
const j = counter[d] - 1; // Get the index j for d in the array
res[j] = nums[i]; // Place the current element at index j
counter[d]--; // Decrease the count of d by 1
}
// Use result to overwrite the original array nums
for (let i = 0; i < n; i++) {
nums[i] = res[i];
}
}
[class]{}-[func]{radixSort}
/* Radix sort */
function radixSort(nums) {
// Get the maximum element of the array, used to determine the maximum number of digits
let m = Math.max(... nums);
// Traverse from the lowest to the highest digit
for (let exp = 1; exp <= m; exp *= 10) {
// Perform counting sort on the k-th digit of array elements
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// i.e., exp = 10^(k-1)
countingSortDigit(nums, exp);
}
}
```
=== "TS"
```typescript title="radix_sort.ts"
[class]{}-[func]{digit}
/* Get the k-th digit of element num, where exp = 10^(k-1) */
function digit(num: number, exp: number): number {
// Passing exp instead of k can avoid repeated expensive exponentiation here
return Math.floor(num / exp) % 10;
}
[class]{}-[func]{countingSortDigit}
/* Counting sort (based on nums k-th digit) */
function countingSortDigit(nums: number[], exp: number): void {
// Decimal digit range is 0~9, therefore need a bucket array of length 10
const counter = new Array(10).fill(0);
const n = nums.length;
// Count the occurrence of digits 0~9
for (let i = 0; i < n; i++) {
const d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d
counter[d]++; // Count the occurrence of digit d
}
// Calculate prefix sum, converting "occurrence count" into "array index"
for (let i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// Traverse in reverse, based on bucket statistics, place each element into res
const res = new Array(n).fill(0);
for (let i = n - 1; i >= 0; i--) {
const d = digit(nums[i], exp);
const j = counter[d] - 1; // Get the index j for d in the array
res[j] = nums[i]; // Place the current element at index j
counter[d]--; // Decrease the count of d by 1
}
// Use result to overwrite the original array nums
for (let i = 0; i < n; i++) {
nums[i] = res[i];
}
}
[class]{}-[func]{radixSort}
/* Radix sort */
function radixSort(nums: number[]): void {
// Get the maximum element of the array, used to determine the maximum number of digits
let m: number = Math.max(... nums);
// Traverse from the lowest to the highest digit
for (let exp = 1; exp <= m; exp *= 10) {
// Perform counting sort on the k-th digit of array elements
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// i.e., exp = 10^(k-1)
countingSortDigit(nums, exp);
}
}
```
=== "Dart"
```dart title="radix_sort.dart"
[class]{}-[func]{digit}
/* Get k-th digit of element _num, where exp = 10^(k-1) */
int digit(int _num, int exp) {
// Passing exp instead of k can avoid repeated expensive exponentiation here
return (_num ~/ exp) % 10;
}
[class]{}-[func]{countingSortDigit}
/* Counting sort (based on nums k-th digit) */
void countingSortDigit(List<int> nums, int exp) {
// Decimal digit range is 0~9, therefore need a bucket array of length 10
List<int> counter = List<int>.filled(10, 0);
int n = nums.length;
// Count the occurrence of digits 0~9
for (int i = 0; i < n; i++) {
int d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d
counter[d]++; // Count the occurrence of digit d
}
// Calculate prefix sum, converting "occurrence count" into "array index"
for (int i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// Traverse in reverse, based on bucket statistics, place each element into res
List<int> res = List<int>.filled(n, 0);
for (int i = n - 1; i >= 0; i--) {
int d = digit(nums[i], exp);
int j = counter[d] - 1; // Get the index j for d in the array
res[j] = nums[i]; // Place the current element at index j
counter[d]--; // Decrease the count of d by 1
}
// Use result to overwrite the original array nums
for (int i = 0; i < n; i++) nums[i] = res[i];
}
[class]{}-[func]{radixSort}
/* Radix sort */
void radixSort(List<int> nums) {
// Get the maximum element of the array, used to determine the maximum number of digits
// In Dart, int length is 64 bits
int m = -1 << 63;
for (int _num in nums) if (_num > m) m = _num;
// Traverse from the lowest to the highest digit
for (int exp = 1; exp <= m; exp *= 10)
// Perform counting sort on the k-th digit of array elements
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// i.e., exp = 10^(k-1)
countingSortDigit(nums, exp);
}
```
=== "Rust"
```rust title="radix_sort.rs"
[class]{}-[func]{digit}
/* Get the k-th digit of element num, where exp = 10^(k-1) */
fn digit(num: i32, exp: i32) -> usize {
// Passing exp instead of k can avoid repeated expensive exponentiation here
return ((num / exp) % 10) as usize;
}
[class]{}-[func]{counting_sort_digit}
/* Counting sort (based on nums k-th digit) */
fn counting_sort_digit(nums: &mut [i32], exp: i32) {
// Decimal digit range is 0~9, therefore need a bucket array of length 10
let mut counter = [0; 10];
let n = nums.len();
// Count the occurrence of digits 0~9
for i in 0..n {
let d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d
counter[d] += 1; // Count the occurrence of digit d
}
// Calculate prefix sum, converting "occurrence count" into "array index"
for i in 1..10 {
counter[i] += counter[i - 1];
}
// Traverse in reverse, based on bucket statistics, place each element into res
let mut res = vec![0; n];
for i in (0..n).rev() {
let d = digit(nums[i], exp);
let j = counter[d] - 1; // Get the index j for d in the array
res[j] = nums[i]; // Place the current element at index j
counter[d] -= 1; // Decrease the count of d by 1
}
// Use result to overwrite the original array nums
nums.copy_from_slice(&res);
}
[class]{}-[func]{radix_sort}
/* Radix sort */
fn radix_sort(nums: &mut [i32]) {
// Get the maximum element of the array, used to determine the maximum number of digits
let m = *nums.into_iter().max().unwrap();
// Traverse from the lowest to the highest digit
let mut exp = 1;
while exp <= m {
counting_sort_digit(nums, exp);
exp *= 10;
}
}
```
=== "C"
```c title="radix_sort.c"
[class]{}-[func]{digit}
/* Get the k-th digit of element num, where exp = 10^(k-1) */
int digit(int num, int exp) {
// Passing exp instead of k can avoid repeated expensive exponentiation here
return (num / exp) % 10;
}
[class]{}-[func]{countingSortDigit}
/* Counting sort (based on nums k-th digit) */
void countingSortDigit(int nums[], int size, int exp) {
// Decimal digit range is 0~9, therefore need a bucket array of length 10
int *counter = (int *)malloc((sizeof(int) * 10));
memset(counter, 0, sizeof(int) * 10); // Initialize to 0 to support subsequent memory release
// Count the occurrence of digits 0~9
for (int i = 0; i < size; i++) {
// Get the k-th digit of nums[i], noted as d
int d = digit(nums[i], exp);
// Count the occurrence of digit d
counter[d]++;
}
// Calculate prefix sum, converting "occurrence count" into "array index"
for (int i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// Traverse in reverse, based on bucket statistics, place each element into res
int *res = (int *)malloc(sizeof(int) * size);
for (int i = size - 1; i >= 0; i--) {
int d = digit(nums[i], exp);
int j = counter[d] - 1; // Get the index j for d in the array
res[j] = nums[i]; // Place the current element at index j
counter[d]--; // Decrease the count of d by 1
}
// Use result to overwrite the original array nums
for (int i = 0; i < size; i++) {
nums[i] = res[i];
}
// Free memory
free(res);
free(counter);
}
[class]{}-[func]{radixSort}
/* Radix sort */
void radixSort(int nums[], int size) {
// Get the maximum element of the array, used to determine the maximum number of digits
int max = INT32_MIN;
for (int i = 0; i < size; i++) {
if (nums[i] > max) {
max = nums[i];
}
}
// Traverse from the lowest to the highest digit
for (int exp = 1; max >= exp; exp *= 10)
// Perform counting sort on the k-th digit of array elements
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// i.e., exp = 10^(k-1)
countingSortDigit(nums, size, exp);
}
```
=== "Kotlin"
```kotlin title="radix_sort.kt"
[class]{}-[func]{digit}
/* Get the k-th digit of element num, where exp = 10^(k-1) */
fun digit(num: Int, exp: Int): Int {
// Passing exp instead of k can avoid repeated expensive exponentiation here
return (num / exp) % 10
}
[class]{}-[func]{countingSortDigit}
/* Counting sort (based on nums k-th digit) */
fun countingSortDigit(nums: IntArray, exp: Int) {
// Decimal digit range is 0~9, therefore need a bucket array of length 10
val counter = IntArray(10)
val n = nums.size
// Count the occurrence of digits 0~9
for (i in 0..<n) {
val d = digit(nums[i], exp) // Get the k-th digit of nums[i], noted as d
counter[d]++ // Count the occurrence of digit d
}
// Calculate prefix sum, converting "occurrence count" into "array index"
for (i in 1..9) {
counter[i] += counter[i - 1]
}
// Traverse in reverse, based on bucket statistics, place each element into res
val res = IntArray(n)
for (i in n - 1 downTo 0) {
val d = digit(nums[i], exp)
val j = counter[d] - 1 // Get the index j for d in the array
res[j] = nums[i] // Place the current element at index j
counter[d]-- // Decrease the count of d by 1
}
// Use result to overwrite the original array nums
for (i in 0..<n)
nums[i] = res[i]
}
[class]{}-[func]{radixSort}
/* Radix sort */
fun radixSort(nums: IntArray) {
// Get the maximum element of the array, used to determine the maximum number of digits
var m = Int.MIN_VALUE
for (num in nums) if (num > m) m = num
var exp = 1
// Traverse from the lowest to the highest digit
while (exp <= m) {
// Perform counting sort on the k-th digit of array elements
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// i.e., exp = 10^(k-1)
countingSortDigit(nums, exp)
exp *= 10
}
}
```
=== "Ruby"
```ruby title="radix_sort.rb"
[class]{}-[func]{digit}
### Get k-th digit of element num, where exp = 10^(k-1) ###
def digit(num, exp)
# Passing exp instead of k avoids expensive exponentiation calculations
(num / exp) % 10
end
[class]{}-[func]{counting_sort_digit}
### Counting sort (sort by k-th digit of nums) ###
def counting_sort_digit(nums, exp)
# Decimal digit range is 0~9, therefore need a bucket array of length 10
counter = Array.new(10, 0)
n = nums.length
# Count the occurrence of digits 0~9
for i in 0...n
d = digit(nums[i], exp) # Get the k-th digit of nums[i], noted as d
counter[d] += 1 # Count the occurrence of digit d
end
# Calculate prefix sum, converting "occurrence count" into "array index"
(1...10).each { |i| counter[i] += counter[i - 1] }
# Traverse in reverse, based on bucket statistics, place each element into res
res = Array.new(n, 0)
for i in (n - 1).downto(0)
d = digit(nums[i], exp)
j = counter[d] - 1 # Get the index j for d in the array
res[j] = nums[i] # Place the current element at index j
counter[d] -= 1 # Decrease the count of d by 1
end
# Use result to overwrite the original array nums
(0...n).each { |i| nums[i] = res[i] }
end
[class]{}-[func]{radix_sort}
### Radix sort ###
def radix_sort(nums)
# Get the maximum element of the array, used to determine the maximum number of digits
m = nums.max
# Traverse from the lowest to the highest digit
exp = 1
while exp <= m
# Perform counting sort on the k-th digit of array elements
# k = 1 -> exp = 1
# k = 2 -> exp = 10
# i.e., exp = 10^(k-1)
counting_sort_digit(nums, exp)
exp *= 10
end
end
```
=== "Zig"
!!! question "Why start sorting from the lowest digit?"
```zig title="radix_sort.zig"
[class]{}-[func]{digit}
In successive sorting rounds, the result of a later round will override the result of an earlier round. For example, if the first round result is $a < b$, while the second round result is $a > b$, then the second round's result will replace the first round's result. Since higher-order digits have higher priority than lower-order digits, we should sort the lower digits first and then sort the higher digits.
[class]{}-[func]{countingSortDigit}
## 11.10.2 &nbsp; Algorithm Characteristics
[class]{}-[func]{radixSort}
```
Compared to counting sort, radix sort is suitable for larger numerical ranges, **but the prerequisite is that the data must be representable in a fixed number of digits, and the number of digits should not be too large**. For example, floating-point numbers are not suitable for radix sort because their number of digits $k$ may be too large, potentially leading to time complexity $O(nk) \gg O(n^2)$.
!!! question "Why start sorting from the least significant digit?"
In consecutive sorting rounds, the result of a later round will override the result of an earlier round. For example, if the result of the first round is $a < b$ and the second round is $a > b$, the second round's result will replace the first round's result. Since higher-order digits take precedence over lower-order digits, it makes sense to sort the lower digits before the higher digits.
## 11.10.2 &nbsp; Algorithm characteristics
Compared to counting sort, radix sort is suitable for larger numerical ranges, **but it assumes that the data can be represented in a fixed number of digits, and the number of digits should not be too large**. For example, floating-point numbers are unsuitable for radix sort, as their digit count $k$ may be large, potentially leading to a time complexity $O(nk) \gg O(n^2)$.
- **Time complexity is $O(nk)$, non-adaptive sorting**: Assuming the data size is $n$, the data is in base $d$, and the maximum number of digits is $k$, then sorting a single digit takes $O(n + d)$ time, and sorting all $k$ digits takes $O((n + d)k)$ time. Generally, both $d$ and $k$ are relatively small, leading to a time complexity approaching $O(n)$.
- **Space complexity is $O(n + d)$, non-in-place sorting**: Like counting sort, radix sort relies on arrays `res` and `counter` of lengths $n$ and $d$ respectively.
- **Stable sorting**: When counting sort is stable, radix sort is also stable; if counting sort is unstable, radix sort cannot ensure a correct sorting order.
- **Time complexity of $O(nk)$, non-adaptive sorting**: Let the data volume be $n$, the data be in base $d$, and the maximum number of digits be $k$. Then performing counting sort on a certain digit uses $O(n + d)$ time, and sorting all $k$ digits uses $O((n + d)k)$ time. Typically, both $d$ and $k$ are relatively small, and the time complexity approaches $O(n)$.
- **Space complexity of $O(n + d)$, non-in-place sorting**: Same as counting sort, radix sort requires auxiliary arrays `res` and `counter` of lengths $n$ and $d$.
- **Stable sorting**: When counting sort is stable, radix sort is also stable; when counting sort is unstable, radix sort cannot guarantee obtaining correct sorting results.
+188 -40
View File
@@ -2,20 +2,20 @@
comments: true
---
# 11.2 &nbsp; Selection sort
# 11.2 &nbsp; Selection Sort
<u>Selection sort</u> works on a very simple principle: it uses a loop where each iteration selects the smallest element from the unsorted interval and moves it to the end of the sorted section.
<u>Selection sort (selection sort)</u> works very simply: it opens a loop, and in each round, selects the smallest element from the unsorted interval and places it at the end of the sorted interval.
Suppose the length of the array is $n$, the steps of selection sort is shown in Figure 11-2.
Assume the array has length $n$. The algorithm flow of selection sort is shown in Figure 11-2.
1. Initially, all elements are unsorted, i.e., the unsorted (index) interval is $[0, n-1]$.
2. Select the smallest element in the interval $[0, n-1]$ and swap it with the element at index $0$. After this, the first element of the array is sorted.
3. Select the smallest element in the interval $[1, n-1]$ and swap it with the element at index $1$. After this, the first two elements of the array are sorted.
4. Continue in this manner. After $n - 1$ rounds of selection and swapping, the first $n - 1$ elements are sorted.
5. The only remaining element is subsequently the largest element and does not need sorting, thus the array is sorted.
2. Select the smallest element in the interval $[0, n-1]$ and swap it with the element at index $0$. After completion, the first element of the array is sorted.
3. Select the smallest element in the interval $[1, n-1]$ and swap it with the element at index $1$. After completion, the first 2 elements of the array are sorted.
4. And so on. After $n - 1$ rounds of selection and swapping, the first $n - 1$ elements of the array are sorted.
5. The only remaining element must be the largest element, requiring no sorting, so the array sorting is complete.
=== "<1>"
![Selection sort process](selection_sort.assets/selection_sort_step1.png){ class="animation-figure" }
![Selection sort steps](selection_sort.assets/selection_sort_step1.png){ class="animation-figure" }
=== "<2>"
![selection_sort_step2](selection_sort.assets/selection_sort_step2.png){ class="animation-figure" }
@@ -47,7 +47,7 @@ Suppose the length of the array is $n$, the steps of selection sort is shown in
=== "<11>"
![selection_sort_step11](selection_sort.assets/selection_sort_step11.png){ class="animation-figure" }
<p align="center"> Figure 11-2 &nbsp; Selection sort process </p>
<p align="center"> Figure 11-2 &nbsp; Selection sort steps </p>
In the code, we use $k$ to record the smallest element within the unsorted interval:
@@ -57,14 +57,14 @@ In the code, we use $k$ to record the smallest element within the unsorted inter
def selection_sort(nums: list[int]):
"""Selection sort"""
n = len(nums)
# Outer loop: unsorted range is [i, n-1]
# Outer loop: unsorted interval is [i, n-1]
for i in range(n - 1):
# Inner loop: find the smallest element within the unsorted range
# Inner loop: find the smallest element within the unsorted interval
k = i
for j in range(i + 1, n):
if nums[j] < nums[k]:
k = j # Record the index of the smallest element
# Swap the smallest element with the first element of the unsorted range
# Swap the smallest element with the first element of the unsorted interval
nums[i], nums[k] = nums[k], nums[i]
```
@@ -74,15 +74,15 @@ In the code, we use $k$ to record the smallest element within the unsorted inter
/* Selection sort */
void selectionSort(vector<int> &nums) {
int n = nums.size();
// Outer loop: unsorted range is [i, n-1]
// Outer loop: unsorted interval is [i, n-1]
for (int i = 0; i < n - 1; i++) {
// Inner loop: find the smallest element within the unsorted range
// Inner loop: find the smallest element within the unsorted interval
int k = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[k])
k = j; // Record the index of the smallest element
}
// Swap the smallest element with the first element of the unsorted range
// Swap the smallest element with the first element of the unsorted interval
swap(nums[i], nums[k]);
}
}
@@ -94,15 +94,15 @@ In the code, we use $k$ to record the smallest element within the unsorted inter
/* Selection sort */
void selectionSort(int[] nums) {
int n = nums.length;
// Outer loop: unsorted range is [i, n-1]
// Outer loop: unsorted interval is [i, n-1]
for (int i = 0; i < n - 1; i++) {
// Inner loop: find the smallest element within the unsorted range
// Inner loop: find the smallest element within the unsorted interval
int k = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[k])
k = j; // Record the index of the smallest element
}
// Swap the smallest element with the first element of the unsorted range
// Swap the smallest element with the first element of the unsorted interval
int temp = nums[i];
nums[i] = nums[k];
nums[k] = temp;
@@ -113,75 +113,223 @@ In the code, we use $k$ to record the smallest element within the unsorted inter
=== "C#"
```csharp title="selection_sort.cs"
[class]{selection_sort}-[func]{SelectionSort}
/* Selection sort */
void SelectionSort(int[] nums) {
int n = nums.Length;
// Outer loop: unsorted interval is [i, n-1]
for (int i = 0; i < n - 1; i++) {
// Inner loop: find the smallest element within the unsorted interval
int k = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[k])
k = j; // Record the index of the smallest element
}
// Swap the smallest element with the first element of the unsorted interval
(nums[k], nums[i]) = (nums[i], nums[k]);
}
}
```
=== "Go"
```go title="selection_sort.go"
[class]{}-[func]{selectionSort}
/* Selection sort */
func selectionSort(nums []int) {
n := len(nums)
// Outer loop: unsorted interval is [i, n-1]
for i := 0; i < n-1; i++ {
// Inner loop: find the smallest element within the unsorted interval
k := i
for j := i + 1; j < n; j++ {
if nums[j] < nums[k] {
// Record the index of the smallest element
k = j
}
}
// Swap the smallest element with the first element of the unsorted interval
nums[i], nums[k] = nums[k], nums[i]
}
}
```
=== "Swift"
```swift title="selection_sort.swift"
[class]{}-[func]{selectionSort}
/* Selection sort */
func selectionSort(nums: inout [Int]) {
// Outer loop: unsorted interval is [i, n-1]
for i in nums.indices.dropLast() {
// Inner loop: find the smallest element within the unsorted interval
var k = i
for j in nums.indices.dropFirst(i + 1) {
if nums[j] < nums[k] {
k = j // Record the index of the smallest element
}
}
// Swap the smallest element with the first element of the unsorted interval
nums.swapAt(i, k)
}
}
```
=== "JS"
```javascript title="selection_sort.js"
[class]{}-[func]{selectionSort}
/* Selection sort */
function selectionSort(nums) {
let n = nums.length;
// Outer loop: unsorted interval is [i, n-1]
for (let i = 0; i < n - 1; i++) {
// Inner loop: find the smallest element within the unsorted interval
let k = i;
for (let j = i + 1; j < n; j++) {
if (nums[j] < nums[k]) {
k = j; // Record the index of the smallest element
}
}
// Swap the smallest element with the first element of the unsorted interval
[nums[i], nums[k]] = [nums[k], nums[i]];
}
}
```
=== "TS"
```typescript title="selection_sort.ts"
[class]{}-[func]{selectionSort}
/* Selection sort */
function selectionSort(nums: number[]): void {
let n = nums.length;
// Outer loop: unsorted interval is [i, n-1]
for (let i = 0; i < n - 1; i++) {
// Inner loop: find the smallest element within the unsorted interval
let k = i;
for (let j = i + 1; j < n; j++) {
if (nums[j] < nums[k]) {
k = j; // Record the index of the smallest element
}
}
// Swap the smallest element with the first element of the unsorted interval
[nums[i], nums[k]] = [nums[k], nums[i]];
}
}
```
=== "Dart"
```dart title="selection_sort.dart"
[class]{}-[func]{selectionSort}
/* Selection sort */
void selectionSort(List<int> nums) {
int n = nums.length;
// Outer loop: unsorted interval is [i, n-1]
for (int i = 0; i < n - 1; i++) {
// Inner loop: find the smallest element within the unsorted interval
int k = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[k]) k = j; // Record the index of the smallest element
}
// Swap the smallest element with the first element of the unsorted interval
int temp = nums[i];
nums[i] = nums[k];
nums[k] = temp;
}
}
```
=== "Rust"
```rust title="selection_sort.rs"
[class]{}-[func]{selection_sort}
/* Selection sort */
fn selection_sort(nums: &mut [i32]) {
if nums.is_empty() {
return;
}
let n = nums.len();
// Outer loop: unsorted interval is [i, n-1]
for i in 0..n - 1 {
// Inner loop: find the smallest element within the unsorted interval
let mut k = i;
for j in i + 1..n {
if nums[j] < nums[k] {
k = j; // Record the index of the smallest element
}
}
// Swap the smallest element with the first element of the unsorted interval
nums.swap(i, k);
}
}
```
=== "C"
```c title="selection_sort.c"
[class]{}-[func]{selectionSort}
/* Selection sort */
void selectionSort(int nums[], int n) {
// Outer loop: unsorted interval is [i, n-1]
for (int i = 0; i < n - 1; i++) {
// Inner loop: find the smallest element within the unsorted interval
int k = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[k])
k = j; // Record the index of the smallest element
}
// Swap the smallest element with the first element of the unsorted interval
int temp = nums[i];
nums[i] = nums[k];
nums[k] = temp;
}
}
```
=== "Kotlin"
```kotlin title="selection_sort.kt"
[class]{}-[func]{selectionSort}
/* Selection sort */
fun selectionSort(nums: IntArray) {
val n = nums.size
// Outer loop: unsorted interval is [i, n-1]
for (i in 0..<n - 1) {
var k = i
// Inner loop: find the smallest element within the unsorted interval
for (j in i + 1..<n) {
if (nums[j] < nums[k])
k = j // Record the index of the smallest element
}
// Swap the smallest element with the first element of the unsorted interval
val temp = nums[i]
nums[i] = nums[k]
nums[k] = temp
}
}
```
=== "Ruby"
```ruby title="selection_sort.rb"
[class]{}-[func]{selection_sort}
### Selection sort ###
def selection_sort(nums)
n = nums.length
# Outer loop: unsorted interval is [i, n-1]
for i in 0...(n - 1)
# Inner loop: find the smallest element within the unsorted interval
k = i
for j in (i + 1)...n
if nums[j] < nums[k]
k = j # Record the index of the smallest element
end
end
# Swap the smallest element with the first element of the unsorted interval
nums[i], nums[k] = nums[k], nums[i]
end
end
```
=== "Zig"
## 11.2.1 &nbsp; Algorithm Characteristics
```zig title="selection_sort.zig"
[class]{}-[func]{selectionSort}
```
- **Time complexity of $O(n^2)$, non-adaptive sorting**: The outer loop has $n - 1$ rounds in total. The length of the unsorted interval in the first round is $n$, and the length of the unsorted interval in the last round is $2$. That is, each round of the outer loop contains $n$, $n - 1$, $\dots$, $3$, $2$ inner loop iterations, summing to $\frac{(n - 1)(n + 2)}{2}$.
- **Space complexity of $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space.
- **Non-stable sorting**: As shown in Figure 11-3, element `nums[i]` may be swapped to the right of an element equal to it, causing a change in their relative order.
## 11.2.1 &nbsp; Algorithm characteristics
![Selection sort non-stability example](selection_sort.assets/selection_sort_instability.png){ class="animation-figure" }
- **Time complexity of $O(n^2)$, non-adaptive sort**: There are $n - 1$ iterations in the outer loop, with the length of the unsorted section starting at $n$ in the first iteration and decreasing to $2$ in the last iteration, i.e., each outer loop iterations contain $n$, $n - 1$, $\dots$, $3$, $2$ inner loop iterations respectively, summing up to $\frac{(n - 1)(n + 2)}{2}$.
- **Space complexity of $O(1)$, in-place sort**: Uses constant extra space with pointers $i$ and $j$.
- **Non-stable sort**: As shown in Figure 11-3, an element `nums[i]` may be swapped to the right of an equal element, causing their relative order to change.
![Selection sort instability example](selection_sort.assets/selection_sort_instability.png){ class="animation-figure" }
<p align="center"> Figure 11-3 &nbsp; Selection sort instability example </p>
<p align="center"> Figure 11-3 &nbsp; Selection sort non-stability example </p>
+19 -19
View File
@@ -2,28 +2,28 @@
comments: true
---
# 11.1 &nbsp; Sorting algorithms
# 11.1 &nbsp; Sorting Algorithm
<u>Sorting algorithms</u> are used to arrange a set of data in a specific order. Sorting algorithms have a wide range of applications because ordered data can usually be searched, analyzed, and processed more efficiently.
<u>Sorting algorithm (sorting algorithm)</u> is used to arrange a group of data in a specific order. Sorting algorithms have extensive applications because ordered data can usually be searched, analyzed, and processed more efficiently.
As shown in Figure 11-1, the data types in sorting algorithms can be integers, floating point numbers, characters, or strings, etc. Sorting criterion can be set according to needs, such as numerical size, character ASCII order, or custom criterion.
As shown in Figure 11-1, data types in sorting algorithms can be integers, floating-point numbers, characters, or strings, etc. The sorting criterion can be set according to requirements, such as numerical size, character ASCII code order, or custom rules.
![Data types and comparator examples](sorting_algorithm.assets/sorting_examples.png){ class="animation-figure" }
![Data type and criterion examples](sorting_algorithm.assets/sorting_examples.png){ class="animation-figure" }
<p align="center"> Figure 11-1 &nbsp; Data types and comparator examples </p>
<p align="center"> Figure 11-1 &nbsp; Data type and criterion examples </p>
## 11.1.1 &nbsp; Evaluation dimensions
## 11.1.1 &nbsp; Evaluation Dimensions
**Execution efficiency**: We expect the time complexity of sorting algorithms to be as low as possible, as well as a lower number of overall operations (lowering the constant term of time complexity). For large data volumes, execution efficiency is particularly important.
**Execution efficiency**: We expect the time complexity of sorting algorithms to be as low as possible, with a smaller total number of operations (reducing the constant factor in time complexity). For large data volumes, execution efficiency is particularly important.
**In-place property**: As the name implies, <u>in-place sorting</u> is achieved by directly manipulating the original array, without the need for additional helper arrays, thus saving memory. Generally, in-place sorting involves fewer data moving operations and is faster.
**In-place property**: As the name implies, <u>in-place sorting</u> achieves sorting by operating directly on the original array without requiring additional auxiliary arrays, thus saving memory. Typically, in-place sorting involves fewer data movement operations and runs faster.
**Stability**: <u>Stable sorting</u> ensures that the relative order of equal elements in the array does not change after sorting.
**Stability**: <u>Stable sorting</u> ensures that the relative order of equal elements in the array does not change after sorting is completed.
Stable sorting is a necessary condition for multi-key sorting scenarios. Suppose we have a table storing student information, with the first and second columns being name and age, respectively. In this case, <u>unstable sorting</u> might lead to a loss of order in the input data:
Stable sorting is a necessary condition for multi-level sorting scenarios. Suppose we have a table storing student information, where column 1 and column 2 are name and age, respectively. In this case, <u>unstable sorting</u> may cause the ordered nature of the input data to be lost:
```shell
# Input data is sorted by name
# Input Data Is Sorted by Name
# (name, age)
('A', 19)
('B', 18)
@@ -31,9 +31,9 @@ Stable sorting is a necessary condition for multi-key sorting scenarios. Suppose
('D', 19)
('E', 23)
# Assuming an unstable sorting algorithm is used to sort the list by age,
# the result changes the relative position of ('D', 19) and ('A', 19),
# and the property of the input data being sorted by name is lost
# Assuming We Use an Unstable Sorting Algorithm to Sort the List by Age,
# In the Result, the Relative Positions of ('D', 19) and ('A', 19) Are Changed,
# And the Property That the Input Data Is Sorted by Name Is Lost
('B', 18)
('D', 19)
('A', 19)
@@ -41,12 +41,12 @@ Stable sorting is a necessary condition for multi-key sorting scenarios. Suppose
('E', 23)
```
**Adaptability**: <u>Adaptive sorting</u> leverages existing order information within the input data to reduce computational effort, achieving more optimal time efficiency. The best-case time complexity of adaptive sorting algorithms is typically better than their average-case time complexity.
**Adaptability**: <u>Adaptive sorting</u> can utilize the existing order information in the input data to reduce the amount of computation, achieving better time efficiency. The best-case time complexity of adaptive sorting algorithms is typically better than the average time complexity.
**Comparison or non-comparison-based**: <u>Comparison-based sorting</u> relies on comparison operators ($<$, $=$, $>$) to determine the relative order of elements and thus sort the entire array, with the theoretical optimal time complexity being $O(n \log n)$. Meanwhile, <u>non-comparison sorting</u> does not use comparison operators and can achieve a time complexity of $O(n)$, but its versatility is relatively poor.
**Comparison-based or not**: <u>Comparison-based sorting</u> relies on comparison operators ($<$, $=$, $>$) to determine the relative order of elements, thereby sorting the entire array, with a theoretical optimal time complexity of $O(n \log n)$. <u>Non-comparison sorting</u> does not use comparison operators and can achieve a time complexity of $O(n)$, but its versatility is relatively limited.
## 11.1.2 &nbsp; Ideal sorting algorithm
## 11.1.2 &nbsp; Ideal Sorting Algorithm
**Fast execution, in-place, stable, adaptive, and versatile**. Clearly, no sorting algorithm that combines all these features has been found to date. Therefore, when selecting a sorting algorithm, it is necessary to decide based on the specific characteristics of the data and the requirements of the problem.
**Fast execution, in-place, stable, adaptive, good versatility**. Clearly, no sorting algorithm has been discovered to date that combines all of these characteristics. Therefore, when selecting a sorting algorithm, it is necessary to decide based on the specific characteristics of the data and the requirements of the problem.
Next, we will learn about various sorting algorithms together and analyze the advantages and disadvantages of each based on the above evaluation dimensions.
Next, we will learn about various sorting algorithms together and analyze the advantages and disadvantages of each sorting algorithm based on the above evaluation dimensions.
+25 -25
View File
@@ -4,50 +4,50 @@ comments: true
# 11.11 &nbsp; Summary
### 1. &nbsp; Key review
### 1. &nbsp; Key Review
- Bubble sort works by swapping adjacent elements. By adding a flag to enable early return, we can optimize the best-case time complexity of bubble sort to $O(n)$.
- Insertion sort sorts each round by inserting elements from the unsorted interval into the correct position in the sorted interval. Although the time complexity of insertion sort is $O(n^2)$, it is very popular in sorting small amounts of data due to relatively fewer operations per unit.
- Quick sort is based on sentinel partitioning operations. In sentinel partitioning, it's possible to always pick the worst pivot, leading to a time complexity degradation to $O(n^2)$. Introducing median or random pivots can reduce the probability of such degradation. Tail recursion effectively reduce the recursion depth, optimizing the space complexity to $O(\log n)$.
- Merge sort includes dividing and merging two phases, typically embodying the divide-and-conquer strategy. In merge sort, sorting an array requires creating auxiliary arrays, resulting in a space complexity of $O(n)$; however, the space complexity for sorting a list can be optimized to $O(1)$.
- Bucket sort consists of three steps: distributing data into buckets, sorting within each bucket, and merging results in bucket order. It also embodies the divide-and-conquer strategy, suitable for very large datasets. The key to bucket sort is the even distribution of data.
- Counting sort is a variant of bucket sort, which sorts by counting the occurrences of each data point. Counting sort is suitable for large datasets with a limited range of data and requires data conversion to positive integers.
- Radix sort processes data by sorting it digit by digit, requiring data to be represented as fixed-length numbers.
- Overall, we seek sorting algorithm that has high efficiency, stability, in-place operation, and adaptability. However, like other data structures and algorithms, no sorting algorithm can meet all these conditions simultaneously. In practical applications, we need to choose the appropriate sorting algorithm based on the characteristics of the data.
- Figure 11-19 compares mainstream sorting algorithms in terms of efficiency, stability, in-place nature, and adaptability.
- Bubble sort achieves sorting by swapping adjacent elements. By adding a flag to enable early return, we can optimize the best-case time complexity of bubble sort to $O(n)$.
- Insertion sort completes sorting by inserting elements from the unsorted interval into the correct position in the sorted interval each round. Although the time complexity of insertion sort is $O(n^2)$, it is very popular in small data volume sorting tasks because it involves relatively few unit operations.
- Quick sort is implemented based on sentinel partitioning operations. In sentinel partitioning, it is possible to select the worst pivot every time, causing the time complexity to degrade to $O(n^2)$. Introducing median pivot or random pivot can reduce the probability of such degradation. By preferentially recursing on the shorter sub-interval, the recursion depth can be effectively reduced, optimizing the space complexity to $O(\log n)$.
- Merge sort includes two phases: divide and merge, which typically embody the divide-and-conquer strategy. In merge sort, sorting an array requires creating auxiliary arrays, with a space complexity of $O(n)$; however, the space complexity of sorting a linked list can be optimized to $O(1)$.
- Bucket sort consists of three steps: distributing data into buckets, sorting within buckets, and merging results. It also embodies the divide-and-conquer strategy and is suitable for very large data volumes. The key to bucket sort is distributing data evenly.
- Counting sort is a special case of bucket sort, which achieves sorting by counting the number of occurrences of data. Counting sort is suitable for situations where the data volume is large but the data range is limited, and requires that data can be converted to positive integers.
- Radix sort achieves data sorting by sorting digit by digit, requiring that data can be represented as fixed-digit numbers.
- Overall, we hope to find a sorting algorithm that is efficient, stable, in-place, and adaptive, with good versatility. However, just like other data structures and algorithms, no sorting algorithm has been found so far that simultaneously possesses all these characteristics. In practical applications, we need to select the appropriate sorting algorithm based on the specific characteristics of the data.
- Figure 11-19 compares mainstream sorting algorithms in terms of efficiency, stability, in-place property, and adaptability.
![Sorting Algorithm Comparison](summary.assets/sorting_algorithms_comparison.png){ class="animation-figure" }
![Sorting algorithm comparison](summary.assets/sorting_algorithms_comparison.png){ class="animation-figure" }
<p align="center"> Figure 11-19 &nbsp; Sorting Algorithm Comparison </p>
<p align="center"> Figure 11-19 &nbsp; Sorting algorithm comparison </p>
### 2. &nbsp; Q & A
**Q**: When is the stability of sorting algorithms necessary?
**Q**: In what situations is the stability of sorting algorithms necessary?
In reality, we might sort based on one attribute of an object. For example, students have names and heights as attributes, and we aim to implement multi-level sorting: first by name to get `(A, 180) (B, 185) (C, 170) (D, 170)`; then by height. Because the sorting algorithm is unstable, we might end up with `(D, 170) (C, 170) (A, 180) (B, 185)`.
In reality, we may sort based on a certain attribute of objects. For example, students have two attributes: name and height. We want to implement multi-level sorting: first sort by name to get `(A, 180) (B, 185) (C, 170) (D, 170)`; then sort by height. Because the sorting algorithm is unstable, we may get `(D, 170) (C, 170) (A, 180) (B, 185)`.
It can be seen that the positions of students D and C have been swapped, disrupting the orderliness of the names, which is undesirable.
It can be seen that the positions of students D and C have been swapped, and the orderliness of names has been disrupted, which is something we don't want to see.
**Q**: Can the order of "searching from right to left" and "searching from left to right" in sentinel partitioning be swapped?
No, when using the leftmost element as the pivot, we must first "search from right to left" then "search from left to right". This conclusion is somewhat counterintuitive, so let's analyze the reason.
No. When we use the leftmost element as the pivot, we must first "search from right to left" and then "search from left to right". This conclusion is somewhat counterintuitive; let's analyze the reason.
The last step of the sentinel partition `partition()` is to swap `nums[left]` and `nums[i]`. After the swap, the elements to the left of the pivot are all `<=` the pivot, **which requires that `nums[left] >= nums[i]` must hold before the last swap**. Suppose we "search from left to right" first, and if no element larger than the pivot is found, **we will exit the loop when `i == j`, possibly with `nums[j] == nums[i] > nums[left]`**. In other words, the final swap operation will exchange an element larger than the pivot to the left end of the array, causing the sentinel partition to fail.
The last step of sentinel partitioning `partition()` is to swap `nums[left]` and `nums[i]`. After the swap is complete, the elements to the left of the pivot are all `<=` the pivot, **which requires that `nums[left] >= nums[i]` must hold before the last swap**. Suppose we first "search from left to right", then if we cannot find an element larger than the pivot, **we will exit the loop when `i == j`, at which point it may be that `nums[j] == nums[i] > nums[left]`**. In other words, the last swap operation will swap an element larger than the pivot to the leftmost end of the array, causing sentinel partitioning to fail.
For example, given the array `[0, 0, 0, 0, 1]`, if we first "search from left to right", the array after the sentinel partition is `[1, 0, 0, 0, 0]`, which is incorrect.
For example, given the array `[0, 0, 0, 0, 1]`, if we first "search from left to right", the array after sentinel partitioning is `[1, 0, 0, 0, 0]`, which is incorrect.
Upon further consideration, if we choose `nums[right]` as the pivot, then exactly the opposite, we must first "search from left to right".
Thinking deeper, if we select `nums[right]` as the pivot, then it's exactly the opposite - we must first "search from left to right".
**Q**: Regarding tail recursion optimization, why does choosing the shorter array ensure that the recursion depth does not exceed $\log n$?
**Q**: Regarding the optimization of recursion depth in quick sort, why can selecting the shorter array ensure that the recursion depth does not exceed $\log n$?
The recursion depth is the number of currently unreturned recursive methods. Each round of sentinel partition divides the original array into two subarrays. With tail recursion optimization, the length of the subarray to be recursively followed is at most half of the original array length. Assuming the worst case always halves the length, the final recursion depth will be $\log n$.
The recursion depth is the number of currently unreturned recursive methods. Each round of sentinel partitioning divides the original array into two sub-arrays. After recursion depth optimization, the length of the sub-array to be recursively processed is at most half of the original array length. Assuming the worst case is always half the length, the final recursion depth will be $\log n$.
Reviewing the original quicksort, we might continuously recursively process larger arrays, in the worst case from $n$, $n - 1$, ..., $2$, $1$, with a recursion depth of $n$. Tail recursion optimization can avoid this scenario.
Reviewing the original quick sort, we may continuously recurse on the longer array. In the worst case, it would be $n$, $n - 1$, $\dots$, $2$, $1$, with a recursion depth of $n$. Recursion depth optimization can avoid this situation.
**Q**: When all elements in the array are equal, is the time complexity of quicksort $O(n^2)$? How should this degenerate case be handled?
**Q**: When all elements in the array are equal, is the time complexity of quick sort $O(n^2)$? How should this degenerate case be handled?
Yes. For this situation, consider using sentinel partitioning to divide the array into three parts: less than, equal to, and greater than the pivot. Only recursively proceed with the less than and greater than parts. In this method, an array where all input elements are equal can be sorted in just one round of sentinel partitioning.
Yes. For this situation, consider partitioning the array into three parts through sentinel partitioning: less than, equal to, and greater than the pivot. Only recursively process the less than and greater than parts. Under this method, an array where all input elements are equal can complete sorting in just one round of sentinel partitioning.
**Q**: Why is the worst-case time complexity of bucket sort $O(n^2)$?
In the worst case, all elements are placed in the same bucket. If we use an $O(n^2)$ algorithm to sort these elements, the time complexity will be $O(n^2)$.
In the worst case, all elements are distributed into the same bucket. If we use an $O(n^2)$ algorithm to sort these elements, the time complexity will be $O(n^2)$.