排序算法
线面的表格列出了常用的10种排序算法,包括它们的时间复杂度、空间复杂度、用处和适用情况。并且在后面给出了每种排序方法的Rust示例代码
| 算法名称 | 时间复杂度 | 空间复杂度 | 用处 | 适用情况 |
|---|---|---|---|---|
| 冒泡排序 | O(n^2) | O(1) | 简单排序 | 数据量小 |
| 选择排序 | O(n^2) | O(1) | 简单排序 | 数据量小 |
| 插入排序 | O(n^2) | O(1) | 简单排序,序列基本有序时效率较高 | 数据量小 |
| 希尔排序 | O(nlogn) ~ O(n^2) | O(1) | 改进的插入排序,序列基本有序时效率较高 | 数据量中等 |
| 快速排序 | O(nlogn) ~ O(n^2) | O(logn) ~ O(n) | 高效的通用排序算法,处理大数据集效率较高 | 数据量大 |
| 归并排序 | O(nlogn) | O(n) | 高效的通用排序算法,处理大数据集效率较高,稳定性好,适用于链表结构的排序。 | 数据量大 |
| 堆排序 | O(nlogn) | O(1) | 高效的选择排序,处理大数据集效率较高。 | 数据量大 |
| 计数排序 | O(n+k) | O(k) | 线性时间复杂度,适用于数据范围不大且分布均匀的整数序列。 | 数据范围不大且分布均匀的整数序列 |
| 桶排序 | O(n+k) | O(n+k) | 线性时间复杂度,适用于数据范围不大且分布均匀的浮点数序列。 | 数据范围不大且分布均匀的浮点数序列 |
| 基数排序 | O(n*k) | O(n+k) | 线性时间复杂度,适用于数据位数固定且范围不大的整数序列。 | 数据位数固定且范围不大的整数序列 |
冒泡排序:
冒泡排序是一种简单的排序算法。它重复地遍历要排序的数列,每次比较两个相邻的元素,如果它们的顺序错误就把它们交换过来。下面是一个用Rust语言实现的冒泡排序算法示例:
fn bubble_sort(mut nums: Vec<i32>) -> Vec<i32> {
let n = nums.len();
for i in 0..n {
for j in 0..n-i-1 {
if nums[j] > nums[j+1] {
nums.swap(j, j+1);
}
}
}
nums
}
这段代码中,我们定义了一个bubble_sort函数,它接受一个整数向量nums作为输入,并返回一个排好序的整数向量。在函数内部,我们使用两层循环来遍历数列中的每一个元素,并进行比较和交换。
选择排序:
选择排序是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。下面是一个用Rust语言实现的选择排序算法示例:
fn selection_sort(mut nums: Vec<i32>) -> Vec<i32> {
let n = nums.len();
for i in 0..n {
let mut min_index = i;
for j in i+1..n {
if nums[j] < nums[min_index] {
min_index = j;
}
}
nums.swap(i, min_index);
}
nums
}
这段代码中,我们定义了一个selection_sort函数,它接受一个整数向量nums作为输入,并返回一个排好序的整数向量。在函数内部,我们使用两层循环来遍历数列中的每一个元素,并找到最小值所在的位置,然后将其与当前位置进行交换。
插入排序:
插入排序是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。下面是一个用Rust语言实现的插入排序算法示例:
fn insertion_sort(mut nums: Vec<i32>) -> Vec<i32> {
let n = nums.len();
for i in 1..n {
let mut j = i;
while j > 0 && nums[j-1] > nums[j] {
nums.swap(j-1, j);
j -= 1;
}
}
nums
}
这段代码中,我们定义了一个insertion_sort函数,它接受一个整数向量nums作为输入,并返回一个排好序的整数向量。在函数内部,我们使用两层循环来遍历数列中的每一个元素,并将其插入到已排好序的序列中。
快速排序:
快速排序是一种高效的排序算法。它采用了分治法(Divide and Conquer)的思想,把一个序列分成两个子序列,然后递归地对子序列进行快速排序。下面是一个用Rust语言实现的快速排序算法示例:
fn quick_sort(nums: &mut [i32]) {
if nums.len() <= 1 {
return;
}
let pivot = partition(nums);
quick_sort(&mut nums[0..pivot]);
quick_sort(&mut nums[pivot + 1..]);
}
fn partition(nums: &mut [i32]) -> usize {
let pivot = nums.len() - 1;
let mut i = 0;
for j in 0..pivot {
if nums[j] < nums[pivot] {
nums.swap(i, j);
i += 1;
}
}
nums.swap(i, pivot);
i
}
这段代码中,我们定义了一个quick_sort函数和一个partition函数。quick_sort函数接受一个可变引用的整数切片nums作为输入,并对其进行原地排序。在函数内部,我们首先判断切片的长度是否小于等于1,如果是则直接返回。然后我们调用partition函数来对切片进行分区,得到一个枢轴位置pivot。接着我们递归地对枢轴左右两边的子序列进行快速排序。
partition函数接受一个可变引用的整数切片nums作为输入,并返回一个枢轴位置。在函数内部,我们首先定义一个枢轴位置pivot为切片的最后一个元素。然后我们使用一个循环来遍历切片中除了枢轴位置以外的所有元素,并将小于枢轴元素的元素移动到枢轴左边。最后我们将枢轴元素移动到正确的位置,并返回该位置。
归并排序:
归并排序是一种高效稳定的排序算法。它采用了分治法(Divide and Conquer)的思想,把一个序列分成两个子序列,然后递归地对子序列进行归并排序,最后再将两个排好序的子序列合并成一个有序序列。下面是一个用Rust语言实现的归并排序算法示例:
fn merge_sort(nums: &[i32]) -> Vec<i32> {
let n = nums.len();
if n <= 1 {
return nums.to_vec();
}
let mid = n / 2;
let left = merge_sort(&nums[0..mid]);
let right = merge_sort(&nums[mid..]);
merge(&left, &right)
}
fn merge(left: &[i32], right: &[i32]) -> Vec<i32> {
let mut result = Vec::new();
let mut i = 0;
let mut j = 0;
while i < left.len() && j < right.len() {
if left[i] < right[j] {
result.push(left[i]);
i += 1;
} else {
result.push(right[j]);
j += 1;
}
}
while i < left.len() {
result.push(left[i]);
i += 1;
}
while j < right.len() {
result.push(right[j]);
j += 1;
}
result
}
这段代码中,我们定义了一个merge_sort函数和一个merge函数。merge_sort函数接受一个整数切片nums作为输入,并返回一个排好序的整数向量。在函数内部,我们首先判断切片的长度是否小于等于1,如果是则直接返回。然后我们将切片分成两半,并递归地对两半进行归并排序,最后再调用merge函数将两个排好序的子序列合并成一个有序序列。
merge函数接受两个整数切片left和right作为输入,并返回一个排好序的整数向量。在函数内部,我们使用两个指针分别指向两个切片的起始位置,并比较两个指针所指向的元素,将较小的元素加入结果向量中,并将指针后移一位。当其中一个指针到达切片末尾时,我们将另一个切片剩余的元素全部加入结果向量中。
堆排序:
堆排序是一种高效的排序算法。它利用了堆这种数据结构来实现排序。堆是一种完全二 叉树,它满足父节点的值总是大于(或小于)它的子节点。堆排序算法首先将待排序序列构建成一个大根堆(或小根堆),然后每次将堆顶元素与堆尾元素交换,并调整堆,直到堆中只剩下一个元素为止。下面是一个用Rust语言实现的堆排序算法示例:
use std::cmp::Ordering;
pub fn heap_sort<T: Ord>(arr: &mut [T]) {
let len = arr.len();
for i in (0..len / 2).rev() {
heapify(arr, i, len);
}
for i in (1..len).rev() {
arr.swap(0, i);
heapify(arr, 0, i);
}
}
fn heapify<T: Ord>(arr: &mut [T], i: usize, len: usize) {
let left = 2 * i + 1;
let right = 2 * i + 2;
let mut largest = i;
if left < len && arr[left].cmp(&arr[largest]) == Ordering::Greater {
largest = left;
}
if right < len && arr[right].cmp(&arr[largest]) == Ordering::Greater {
largest = right;
}
if largest != i {
arr.swap(i, largest);
heapify(arr, largest, len);
}
}
这段代码中,我们定义了一个heap_sort函数和一个heapify函数。heap_sort函数接受一个可变引用的切片arr作为输入,并对其进行原地排序。在函数内部,我们首先使用heapify函数将切片构建成一个大根堆。然后我们使用一个循环,每次将堆顶元素与堆尾元素交换,并调用heapify函数调整堆,直到堆中只剩下一个元素为止。
heapify函数接受一个可变引用的切片arr、一个下标i和一个长度len作为输入。在函数内部,我们首先计算出当前节点的左右子节点的下标,并找到三者中最大值所在的位置。如果最大值不在当前节点,则将其与当前节点交换,并递归地对子节点进行调整。
希尔排序(Shell Sort):
希尔排序是一种改进的插入排序算法。它通过将序列分成若干个子序列,然后对每个子序列进行插入排序,最后再对整个序列进行一次插入排序来实现排序。下面是一个用Rust语言实现的希尔排序算法示例:
fn shell_sort(nums: &mut [i32]) {
let n = nums.len();
let mut gap = n / 2;
while gap > 0 {
for i in gap..n {
let mut j = i;
while j >= gap && nums[j - gap] > nums[j] {
nums.swap(j - gap, j);
j -= gap;
}
}
gap /= 2;
}
}
这段代码中,我们定义了一个shell_sort函数,它接受一个可变引用的整数切片nums作为输入,并对其进行原地排序。在函数内部,我们首先定义一个间隔gap为切片长度的一半。然后我们使用一个循环来不断缩小间隔,每次将间隔减半。在每个间隔下,我们对切片中相隔间隔距离的元素进行插入排序。
计数排序(Counting Sort):
计数排序是一种线性时间复杂度的排序算法。它适用于数据范围不大且数据分布比较均匀的场景。它的基本思想是对每一个输入元素x,确定小于x的元素个数,然后根据这个信息直接把x放到它在输出数组中的位置上。下面是一个用Rust语言实现的计数排序算法示例:
fn counting_sort(nums: &[i32]) -> Vec<i32> {
let min = *nums.iter().min().unwrap();
let max = *nums.iter().max().unwrap();
let range = (max - min + 1) as usize;
let mut count = vec![0; range];
for &num in nums {
count[(num - min) as usize] += 1;
}
for i in 1..range {
count[i] += count[i - 1];
}
let mut result = vec![0; nums.len()];
for &num in nums.iter().rev() {
result[count[(num - min) as usize] as usize - 1] = num;
count[(num - min) as usize] -= 1;
}
result
}
这段代码中,我们定义了一个counting_sort函数,它接受一个整数切片nums作为输入,并返回一个排好序的整数向量。在函数内部,我们首先计算出切片中的最小值和最大值,并根据它们计算出数据范围range。然后我们定义一个计数数组count,并遍历切片中的每一个元素,统计每个元素出现的次数。接着我们对计数数组进行累加,使得每个元素表示小于等于该元素的元素个数。最后我们再次遍历切片中的每一个元素,并根据计数数组中的信息将其放到正确的位置上。
基数排序
基数排序是一种非比较型整数排序算法。它的原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。下面是一个用 Rust 语言实现的基数排序算法的例子
fn radix_sort(arr: &mut [u64]) {
let max: usize = match arr.iter().max() {
Some(&x) => x as usize,
None => return,
};
let radix = arr.len().next_power_of_two();
let mut place = 1;
while place <= max {
let digit_of = |x| x as usize / place % radix;
let mut counter = vec![0; radix];
for &x in arr.iter() {
counter[digit_of(x)] += 1;
}
for i in 1..radix {
counter[i] += counter[i - 1];
}
for &x in arr.to_owned().iter().rev() {
counter[digit_of(x)] -= 1;
arr[counter[digit_of(x)]] = x;
}
place *= radix;
}
}
这段代码中,radix_sort 函数接受一个可变的 u64 类型的数组切片作为参数。首先,它找到数组中的最大值 max。然后,它计算出基数 radix,并初始化变量 place 为 1。接下来,当 place 小于等于 max 时,循环执行以下操作:定义一个闭包 digit_of 来计算每个元素在当前位上的数字;创建一个计数器数组 counter;遍历数组中的每个元素,统计每个数字出现的次数;累加计数器数组中的每个元素;倒序遍历数组中的每个元素,并将其放入正确的位置;最后,将 place 的值乘以基数。当循环结束时,数组已经排好序了。
桶排序
桶排序是一种排序算法,它的工作原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后依次把各个桶中的记录列出来记得到有序序列。下面是一个用 Rust 语言实现的桶排序算法的例子
pub fn bucket_sort(arr: &[usize]) -> Vec<usize> {
if arr.is_empty() {
return vec![];
}
let max = *arr.iter().max().unwrap();
let len = arr.len();
let mut buckets = vec![vec![]; len + 1];
for x in arr {
buckets[len * *x / max].push(*x);
}
for bucket in buckets.iter_mut() {
super::insertion_sort(bucket);
}
let mut result = vec![];
for bucket in buckets {
for x in bucket {
result.push(x);
}
}
result
}
这段代码中,bucket_sort 函数接受一个 usize 类型的数组切片作为参数,并返回一个排好序的 Vec<usize>。首先,它检查数组是否为空,如果为空,则返回一个空向量。然后,它找到数组中的最大值 max 并计算出数组的长度 len。接下来,它创建一个向量 buckets,其中包含 len + 1 个空向量。然后,它遍历数组中的每个元素,并将其放入正确的桶中。接着,它对每个桶中的元素进行插入排序。最后,它创建一个空向量 result,并将各个桶中的元素依次放入其中。当循环结束时,返回排好序的向量。 from刘金,转载请注明原文链接。感谢!