引言
排序(Sorting)是计算机科学中最基础、最重要的算法之一,广泛应用于数据处理、搜索优化等领域。本文将详细讲解几种常见排序算法的原理、实现,并通过可视化方式帮助理解排序过程。
排序算法分类
排序算法可以根据不同的方式进行分类:
- 比较排序:冒泡排序、快速排序、插入排序、Shell排序、选择排序、堆排序、归并排序。
- 非比较排序:桶排序、基数排序。
图片来源
基础比较排序算法
1. 冒泡排序(Bubble Sort)
1.1 原理
冒泡排序是一种简单的交换排序,通过重复遍历数组,每次比较相邻元素并进行交换,使较大的元素逐步上浮。
1.2 代码实现(Java)
public class BubbleSort {
public static void bubbleSort(int[] arr) {
int n = arr.length;
boolean swapped;
for (int i = 0; i < n - 1; i++) {
swapped = false;
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 交换元素
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = true;
}
}
// 如果未发生交换,则说明已经排序完成
if (!swapped) break;
}
}
}
算法原理:
在 冒泡排序(Bubble Sort) 中,排序的趟数取决于数组的长度 n,一般情况下,需要 (n-1) 趟 排序才能完全有序。
适用场景:教学示例、小数据集验证
计算方式:
- 第一趟:将最大的数“冒泡”到最后一位。
- 第二趟:将第二大的数“冒泡”到倒数第二位。
- 以此类推,经过 n−1n-1n−1 趟后,整个数组变得有序。
2. 快速排序(Quick Sort)
2.1 原理
快速排序采用分治策略,每次选择一个基准元素,将数组分成两部分,使得左边小于基准,右边大于基准,然后递归排序。
public class QuickSort {
/**
* 对数组进行快速排序
* @param arr 要排序的数组
* @param low 数组的起始索引
* @param high 数组的结束索引
*/
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
// 找到分区的枢轴位置
int pi = partition(arr, low, high);
// 递归地对枢轴左侧的子数组进行快速排序
quickSort(arr, low, pi - 1);
// 递归地对枢轴右侧的子数组进行快速排序
quickSort(arr, pi + 1, high);
}
}
/**
* 对数组进行分区操作,返回枢轴的位置
* @param arr 要分区的数组
* @param low 数组的起始索引
* @param high 数组的结束索引
* @return 枢轴的最终位置
*/
private static int partition(int[] arr, int low, int high) {
// 选择最后一个元素作为枢轴
int pivot = arr[high];
// i 指向比枢轴小的元素的最后一个位置
int i = low - 1;
// 遍历数组,将小于枢轴的元素交换到左侧
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
// 交换 arr[i] 和 arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 将枢轴元素放到正确的位置
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
// 返回枢轴的位置
return i + 1;
}
}
快速排序是不稳定的算法,它不满足稳定算法的定义。
算法稳定性
– 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!
优化策略
1. 三数取中法选择基准
2. 尾递归优化
3. 小数组切换插入排序
4. 双轴快排(Dual-Pivot)
3. 插入排序(Insertion Sort)
3.1 原理
插入排序的核心思想是将数组分为已排序部分和未排序部分,每次从未排序部分取出一个元素,插入到正确位置。
3.2 代码实现(Java)
public class InsertionSort {
// 插入排序方法,对整型数组进行排序
public static void insertionSort(int[] arr) {
// 从数组的第二个元素开始遍历数组
for (int i = 1; i < arr.length; i++) {
// 将当前元素存储在变量key中
int key = arr[i];
// 初始化j为当前元素的前一个元素的索引
int j = i - 1;
// 当j不小于0且前一个元素大于当前元素时,进行循环
while (j >= 0 && arr[j] > key) {
// 将前一个元素向后移动一位
arr[j + 1] = arr[j];
// 将j减1,继续向前比较
j--;
}
// 将当前元素插入到正确的位置
arr[j + 1] = key;
}
}
}
4. Shell排序(Shell Sort)
4.1 原理
Shell排序是插入排序的改进版,使用**步长(gap)**对数组进行分组排序,逐步缩小步长,最终变为插入排序。
4.2 代码实现(Java)
public class ShellSort {
// 希尔排序方法,对整型数组进行排序
public static void shellSort(int[] arr) {
// 获取数组长度
int n = arr.length;
// 初始化间隔 gap 为数组长度的一半,每次循环后 gap 减半,直到 gap 为 0
for (int gap = n / 2; gap > 0; gap /= 2) {
// 从间隔 gap 开始,对每个元素进行排序
for (int i = gap; i < n; i++) {
// 将当前元素存储在临时变量 temp 中
int temp = arr[i];
// 初始化 j 为当前元素的索引
int j;
// 当 j 不小于 gap 且 j-gap 位置的元素大于当前元素时,进行循环
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
// 将 j-gap 位置的元素移动到 j 位置
arr[j] = arr[j - gap];
}
// 将当前元素插入到正确的位置
arr[j] = temp;
}
}
}
}
5. 选择排序(Selection Sort)
5.1 原理
选择排序每次从未排序部分选择最小(或最大)的元素,放到已排序部分的末尾。
5.2 代码实现(Java)
public class SelectionSort {
// 选择排序方法,对整型数组进行排序
public static void selectionSort(int[] arr) {
// 获取数组长度
int n = arr.length;
// 遍历数组,i表示当前要排序的位置
for (int i = 0; i < n - 1; i++) {
// 初始化最小元素的索引为当前位置i
int minIndex = i;
// 从i+1位置开始,寻找剩余数组中的最小元素
for (int j = i + 1; j < n; j++) {
// 如果找到比当前最小元素还小的元素,更新最小元素的索引
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 交换当前位置i的元素与找到的最小元素
int temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
}
}
适合闪存等写入成本高的场景
6. 堆排序(Heap Sort)
6.1 原理
堆排序使用堆数据结构,首先建立最大堆,然后依次取出堆顶元素进行排序。
6.2 代码实现(Java)
public class SelectionSort {
// 选择排序方法,对整型数组进行排序
public static void selectionSort(int[] arr) {
// 获取数组长度
int n = arr.length;
// 遍历数组,i表示当前要排序的位置
for (int i = 0; i < n - 1; i++) {
// 初始化最小元素的索引为当前位置i
int minIndex = i;
// 从i+1位置开始,寻找剩余数组中的最小元素
for (int j = i + 1; j < n; j++) {
// 如果找到比当前最小元素还小的元素,更新最小元素的索引
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 交换当前位置i的元素与找到的最小元素
int temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
}
}
适用场景
- 选择排序(需要快速寻找最大/最小元素)
- 任务线程排序(尽量减少元素之间的交换)
7. 归并排序 (Merge Sort)
7.1 原理
归并排序是一种基于分治思想的排序算法,将数组分为更小的部分,分别排序后合并。
7.2 代码实现(Java)
public class MergeSort {
// 归并排序的主方法,对数组arr的从left到right的部分进行排序
public void sort(int arr[], int left, int right) {
// 如果左索引小于右索引,说明至少有两个元素,可以进行排序
if (left < right) {
// 找到中间索引,避免直接使用(left + right) / 2可能导致的整数溢出
int mid = left + (right - left) / 2;
// 递归地对左半部分进行排序
sort(arr, left, mid);
// 递归地对右半部分进行排序
sort(arr, mid + 1, right);
// 将排序好的左右两半部分合并
merge(arr, left, mid, right);
}
}
// 合并两个已排序的子数组的方法
void merge(int arr[], int left, int mid, int right) {
// 计算左子数组的长度
int n1 = mid - left + 1;
// 计算右子数组的长度
int n2 = right - mid;
// 创建临时数组存储左子数组
int L[] = new int[n1];
// 创建临时数组存储右子数组
int R[] = new int[n2];
// 复制数据到临时数组L
for (int i = 0; i < n1; ++i) L[i] = arr[left + i];
// 复制数据到临时数组R
for (int j = 0; j < n2; ++j) R[j] = arr[mid + 1 + j];
// 初始化左子数组的索引
int i = 0;
// 初始化右子数组的索引
int j = 0;
// 初始化归并后数组的索引
int k = left;
// 当两个子数组都没有遍历完时,进行比较合并
while (i < n1 && j < n2) {
// 如果左子数组的当前元素小于或等于右子数组的当前元素,将其放入原数组,并移动索引
if (L[i] <= R[j]) arr[k++] = L[i++];
// 否则,将右子数组的当前元素放入原数组,并移动索引
else arr[k++] = R[j++];
}
// 如果左子数组还有剩余元素,将其全部放入原数组
while (i < n1) arr[k++] = L[i++];
// 如果右子数组还有剩余元素,将其全部放入原数组
while (j < n2) arr[k++] = R[j++];
}
}
适用场景
- 需要稳定排序
- 数据规模较大,适用于链表排序
8. 桶排序 (Bucket Sort)
8.1 原理
假设待排序的数组a中共有N个整数,并且已知数组a中数据的范围[0, MAX)。在桶排序时,创建容量为MAX的桶数组r,并将桶数组元素都初始化为0;将容量为MAX的桶数组中的每一个单元都看作一个"桶"。
桶排序是一种利用哈希映射的排序算法,将数据分布到不同的桶中,然后对每个桶进行排序。
8.2 代码实现(Java)
import java.util.*; // 导入java.util包下的所有类,包括List, ArrayList和Collections等
public class BucketSort {
/**
* 对浮点数数组进行桶排序
* @param arr 待排序的浮点数数组
*/
public void sort(float arr[]) {
int n = arr.length; // 获取数组长度
// 创建一个List类型的数组,作为桶,每个桶是一个ArrayList
List<Float>[] buckets = new List[n];
// 初始化每个桶
for (int i = 0; i < n; i++) {
buckets[i] = new ArrayList<>(); // 为每个桶分配一个新的ArrayList
}
// 将数组中的元素分配到各个桶中
for (float num : arr) {
// 计算元素应该放入的桶的索引,假设数组中的元素都在0到1之间
int bucketIndex = (int) (n * num);
// 将元素添加到对应的桶中
buckets[bucketIndex].add(num);
}
// 对每个桶中的元素进行排序
for (List<Float> bucket : buckets) {
Collections.sort(bucket); // 使用Collections的sort方法对桶中的元素进行排序
}
// 将排序后的元素放回原数组
int index = 0; // 用于追踪原数组的索引
for (List<Float> bucket : buckets) {
for (float num : bucket) {
arr[index++] = num; // 将桶中的元素按顺序放回原数组
}
}
}
}
适用场景
- 数据分布均匀的情况下
- 适用于浮点数排序
9. 基数排序 (Radix Sort)
9.1 原理
它的基本思想是: 将整数按位数切割成不同的数字,然后按每个位数分别比较。 具体做法是: 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列.
9.2 代码实现(Java)
import java.util.*; // 导入java.util包下的所有类
public class RadixSort {
/**
* 对数组进行基数排序
* @param arr 待排序的数组
*/
public void sort(int[] arr) {
// 找到数组中的最大值
int max = Arrays.stream(arr).max().getAsInt();
// 对每一位进行计数排序,exp表示当前位数的权重(1, 10, 100, ...)
for (int exp = 1; max / exp > 0; exp *= 10) {
countSort(arr, exp); // 对数组按照当前位数进行计数排序
}
}
/**
* 根据指定位数进行计数排序
* @param arr 待排序的数组
* @param exp 当前位数的权重
*/
void countSort(int[] arr, int exp) {
int output[] = new int[arr.length]; // 输出数组,存储排序后的结果
int count[] = new int[10]; // 计数数组,存储每个数字出现的次数
// 计算每个数字在当前位数下出现的次数
for (int num : arr) {
count[(num / exp) % 10]++;
}
// 将计数数组转换为累加数组
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// 构建输出数组
for (int i = arr.length - 1; i >= 0; i--) {
output[--count[(arr[i] / exp) % 10]] = arr[i];
}
// 将排序后的数组复制回原数组
System.arraycopy(output, 0, arr, 0, arr.length);
}
}
结论
个数字在当前位数下出现的次数
for (int num : arr) {
count[(num / exp) % 10]++;
}
// 将计数数组转换为累加数组
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// 构建输出数组
for (int i = arr.length - 1; i >= 0; i--) {
output[--count[(arr[i] / exp) % 10]] = arr[i];
}
// 将排序后的数组复制回原数组
System.arraycopy(output, 0, arr, 0, arr.length);
}
}
## 结论
不同排序算法在**时间复杂度**、**空间复杂度**和**适用场景**上各有优劣,开发时需要根据具体需求选择合适的排序方法。