基本排序知识

我们一步一步来理解排序。下面从基本概念、代码逻辑到具体示例详细解释。

1.快速排序的基本概念

快速排序采用分治法(Divide and Conquer)策略。简单来说,就是把一个大问题分解成多个小问题,解决了小问题,大问题也就解决了。快速排序具体步骤如下:

  1. 选基准(Pivot):从数组里挑出一个元素当作基准。
  2. 分区(Partition):对数组重新排序,让比基准小的元素放在基准左边,比基准大的元素放在基准右边。分区结束后,基准就处于它在排序好的数组里该在的位置。
  3. 递归排序:用同样的方法对基准左边和右边的子数组进行排序。

代码详细解释

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 = 0high = 5,选 pivot = 5

  • 开始时 i = -1j 从 0 开始遍历。
  • 当 j = 0arr[0] = 10 大于 pivot,不做交换。
  • 当 j = 1arr[1] = 7 大于 pivot,不做交换。
  • 当 j = 2arr[2] = 8 大于 pivot,不做交换。
  • 当 j = 3arr[3] = 9 大于 pivot,不做交换。
  • 当 j = 4arr[4] = 1 小于 pivoti 变为 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.其他排序

至于,冒泡和选择排序都是很基础的排序,笔者不做赘述了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值