我们一步一步来理解排序。下面从基本概念、代码逻辑到具体示例详细解释。
1.快速排序的基本概念
快速排序采用分治法(Divide and Conquer)策略。简单来说,就是把一个大问题分解成多个小问题,解决了小问题,大问题也就解决了。快速排序具体步骤如下:
- 选基准(Pivot):从数组里挑出一个元素当作基准。
- 分区(Partition):对数组重新排序,让比基准小的元素放在基准左边,比基准大的元素放在基准右边。分区结束后,基准就处于它在排序好的数组里该在的位置。
- 递归排序:用同样的方法对基准左边和右边的子数组进行排序。
代码详细解释
1. partition
函数
c
// 分区函数,它的作用是把数组以基准为界分成两部分
// arr 是要排序的数组,low 是当前子数组的起始索引,high 是当前子数组的结束索引
int partition(int arr[], int low, int high) {
// 选择当前子数组的最后一个元素作为基准
int pivot = arr[high];
// i 用来标记小于等于基准的元素应该存放的位置,初始化为 low - 1
int i = (low - 1);
// 遍历从 low 到 high - 1 的所有元素
for (int j = low; j <= high - 1; j++) {
// 如果当前元素小于等于基准
if (arr[j] <= pivot) {
// 让 i 加 1,指向新的可以存放小于等于基准元素的位置
i++;
// 交换 arr[i] 和 arr[j],把小于等于基准的元素放到左边
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 最后把基准元素放到正确的位置,也就是 i + 1 处
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
// 返回基准元素最终所在的位置
return (i + 1);
}
示例解释:
假设数组 arr = [10, 7, 8, 9, 1, 5]
,初始 low = 0
,high = 5
,选 pivot = 5
。
- 开始时
i = -1
,j
从 0 开始遍历。 - 当
j = 0
,arr[0] = 10
大于pivot
,不做交换。 - 当
j = 1
,arr[1] = 7
大于pivot
,不做交换。 - 当
j = 2
,arr[2] = 8
大于pivot
,不做交换。 - 当
j = 3
,arr[3] = 9
大于pivot
,不做交换。 - 当
j = 4
,arr[4] = 1
小于pivot
,i
变为 0,交换arr[0]
和arr[4]
,数组变为[1, 7, 8, 9, 10, 5]
。 - 遍历结束后,把
pivot
(即arr[5]
)和arr[i + 1]
(即arr[1]
)交换,数组变为[1, 5, 8, 9, 10, 7]
,返回基准位置 1。
2. quickSort
函数
c
// 快速排序函数,通过递归对数组进行排序
// arr 是要排序的数组,low 是当前子数组的起始索引,high 是当前子数组的结束索引
void quickSort(int arr[], int low, int high) {
// 当 low 小于 high 时,说明子数组里至少有两个元素,需要排序
if (low < high) {
// 调用 partition 函数进行分区,得到基准元素的最终位置
int pi = partition(arr, low, high);
// 递归地对基准元素左边的子数组进行排序
quickSort(arr, low, pi - 1);
// 递归地对基准元素右边的子数组进行排序
quickSort(arr, pi + 1, high);
}
}
示例解释:
- 第一次分区后,基准位置
pi = 1
,数组是[1, 5, 8, 9, 10, 7]
。 - 对左边子数组
[1]
调用quickSort(arr, 0, 0)
,因为low = high
,不做处理。 - 对右边子数组
[8, 9, 10, 7]
调用quickSort(arr, 2, 5)
,继续分区排序。
3. printArray
函数
c
// 打印数组函数,用来输出数组里的元素
// arr 是要打印的数组,size 是数组的元素个数
void printArray(int arr[], int size) {
// 遍历数组中的每个元素
for (int i = 0; i < size; i++)
// 输出元素,元素之间用空格分隔
printf("%d ", arr[i]);
// 换行
printf("\n");
}
这个函数很简单,就是遍历数组,把每个元素打印出来,元素间用空格分隔,最后换行。
4. main
函数
c
// 主函数,程序的入口
int main() {
// 定义一个待排序的数组
int arr[] = {10, 7, 8, 9, 1, 5};
// 计算数组的元素个数
int n = sizeof(arr) / sizeof(arr[0]);
// 输出原始数组
printf("Original array: \n");
printArray(arr, n);
// 调用 quickSort 函数对数组进行排序
quickSort(arr, 0, n - 1);
// 输出排序后的数组
printf("Sorted array: \n");
printArray(arr, n);
return 0;
}
在 main
函数里,先定义数组,算出元素个数,打印原始数组,然后调用 quickSort
函数排序,最后打印排序后的数组。
总结
快速排序通过不断地分区和递归排序,把一个大数组排序问题转化为多个小数组排序问题。你可以手动在纸上模拟一下排序过程,这样会更容易理解。
2.插入排序
我们可以很快在网上找到插入排序的代码,所以我不想在这重复写网上的标准代码浪费时间,我在这想让读者来分析一下下面两段代码:
public static void charu(int[]a){
for(int i=1;i<a.length;i++){
int j;
for(j=i;j>0&&a[i]<a[j-1];j--){
a[j]= a[j-1];
}
a[j]=a[i];
}
}
和
public static void charu(int[] a) {
for (int i = 1; i < a.length; i++) {
int current = a[i];
int j;
for (j = i; j > 0 && current < a[j - 1]; j--) {
a[j] = a[j - 1];
}
a[j] = current;
}}
分析两段代码好像差不多,但是后者才是正确的,为啥呢?
第一段代码出现错误的核心原因在于在内层循环移动元素的过程中,原数组 a
中用于比较的 a[i]
的值被覆盖,进而导致后续比较和插入操作使用了错误的数据,使得排序结果出错。下面详细分析:
代码回顾
java
public static void charu(int[] a) {
for (int i = 1; i < a.length; i++) {
int j;
for (j = i; j > 0 && a[i] < a[j - 1]; j--) {
a[j] = a[j - 1];
}
a[j] = a[i];
}
}
错误详细分析
1. 内层循环的比较和元素移动
内层 for
循环的条件是 j > 0 && a[i] < a[j - 1]
,在满足条件时会执行 a[j] = a[j - 1]
,此操作会将 a[j - 1]
的值覆盖到 a[j]
的位置。
当 j
从 i
开始递减时,随着元素的不断前移,a[i]
及其后面位置的值会被依次覆盖。如果 i
之后有元素需要移动到 i
之前的位置,那么 a[i]
的原始值就会被覆盖掉。
2. 后续比较使用被覆盖的值
由于 a[i]
的值可能在元素移动过程中被覆盖,后续内层循环继续使用 a[i]
与 a[j - 1]
进行比较时,比较的就不再是原始的 a[i]
值,而是被覆盖后的错误值,这会导致比较结果不准确,进而影响元素的移动和插入位置。
3. 最终插入错误
循环结束后,执行 a[j] = a[i]
,将此时 a[i]
的值插入到 a[j]
位置。但由于 a[i]
的值已经被改变,插入的就不是原本要插入的元素,从而使得排序结果出错。
示例
假设有数组 {5, 3, 4, 1, 2}
,当 i = 1
时,a[i]
为 3
。内层循环会将 5
后移到 a[1]
位置,此时 a[1]
的值变为 5
,而后续比较仍使用 a[i]
(此时 a[i]
已经变成 5
),这就导致后续比较和插入逻辑混乱。
正确做法
正确的是像第二段代码那样,先将 a[i]
的值保存到一个临时变量(如 current
)中,在移动元素时使用这个临时变量进行比较,最后将临时变量的值插入到正确的位置,避免原始值被覆盖
java
public static void charu(int[] a) {
for (int i = 1; i < a.length; i++) {
int current = a[i];
int j;
for (j = i; j > 0 && current < a[j - 1]; j--) {
a[j] = a[j - 1];
}
a[j] = current;
}
}
这样可以保证在移动元素过程中,要插入的元素的值不会被改变,从而确保排序结果的正确性。
3.其他排序
至于,冒泡和选择排序都是很基础的排序,笔者不做赘述了。