算法学习之不那么简单的排序(1)

也不知道与简单排序对应的应该叫什么, 就叫不那么简单的排序好了.

本篇博客主要学习了希尔排序、归并排序and快速排序。

注: 这一篇和上一篇简单排序都算是学习白话算法系列的学习笔记吧

希尔排序

希尔排序是基于插入排序而来, 插入排序的最好时间复杂度是O(n), 当数组基本有序时, 效率是很高的. 而希尔排序, 设定一个增量, 按增量将数组分组.

例如数组{1,2,3,4}, 增量是2, 那么数组就可以分为{1,3}, {2,4}两组, 增量是1那就是1,2,3,4四组.

分组之后在组内进行插入排序, 再缩小增量重新分组再次排序, 直到增量是1(等同于正常的插入排序), 再插入排序一次, 排序完成.

void shellSort(int arr[], int n){

    for (int gap = n/2; gap>0; gap/=2) {
        for (int i = gap; i<n; i++) {
            if (arr[i] < arr[i - gap]) {
                int temp = arr[i];
                int j;
                // 思路与插入排序相同, 用临时变量保存要插入的数, 向数组前面查找插入的位置, 一边查找, 一边将前面较大的数字后移
                // 临时变量不小于前面的某数时, 说明找到了正确的位置, 只要放在那个数后面就可以了
                for (j = i-gap; j>=0 && temp<arr[j]; j-=gap) {
                    arr[j+gap] = arr[j];
                }
                arr[j+gap] = temp;
            }
        }
    }
}

归并排序

归并二字就是递归&合并

归并排序的关键在于合并有序数组, 合并两个有序数组的方式是先比较两数组的第一个元素, 更小的取出放入新数组, 再依次向后比较, 直到某个数组的元素取光, 把另一个数组的元素依次放入新数组既可.

//先来演示合并数组
void mergeArray(int a[], int m, int b[], int n){
    int c[m+n];

    int i, j, k;
    //必须初始化, 否则会有残值
    i = j = k = 0;

    // 此处不能用for循环, 除非只写第二个表达式, 否则ijk哪个做自增都不合适
    // 其中k看似合适, 但for循环最后会执行一次第三个表达式, k会+1
    while (i < m && j < n) {
        if (a[i] < b[j]) {
            c[k++] = a[i++];
        }else{
            c[k++] = b[j++];
        }
    }

    while (i < m) {
        c[k++] = a[i++];
    }

    while (j < n) {
        c[k++] = b[j++];
    }

    printfArray(c, m+n);
}

下面开始撸正式的归并排序

// 合并有序序列
void mergearray(int arr[], int first, int last, int mid, int temp[]){
    int tempIndex = 0;

    int firstSequenceIndex = first;
    int secondSequeceIndex = mid + 1;

    // 因为这里用的是数组角标, 而不是长度, 所以用<= 而不是<
    while (firstSequenceIndex <= mid && secondSequeceIndex <= last) {
        // 取较小值放入临时数组
        if (arr[firstSequenceIndex] < arr[secondSequeceIndex] ) {
            temp[tempIndex++] = arr[firstSequenceIndex++];
        }else{
            temp[tempIndex++] = arr[secondSequeceIndex++];
        }
    }
    // 如果前一个序列还有值, 依次放入临时数组
    while (firstSequenceIndex <= mid) {
        temp[tempIndex++] = arr[firstSequenceIndex++];
    }
    // 如果后一个序列还有值, 依次放入临时数组
    while (secondSequeceIndex <= last) {
        temp[tempIndex++] = arr[secondSequeceIndex++];
    }
    // 将排好序的部分赋值给原数组
    for (int i = 0; i < tempIndex; i++) {
        arr[first++] = temp[i];
    }

}

// 搞清归并排序, 主要搞清以下两点
// 1. 递归到只有一个数时, 递归函数开始出栈, 一个数肯定是有序序列
// 2. 合并两个有序序列, 可以形成新的有序序列
void mergeSort(int arr[], int first, int last, int temp[]){
    if(first < last){
        // 将数组分成两部分
        int mid = (first + last)/2;
        // 前一半排序
        mergeSort(arr, first, mid, temp);
        // 后一半排序
        mergeSort(arr, mid+1, last, temp);
        // 合并有序序列
        mergearray(arr, first, last, mid, temp);
    }
}

快速排序

快速排序是时间复杂度O(logN*N)的排序算法中比较出名的, 面试算法常常会问, 而手写出来是很有难度的事情. 这里非常感谢白话经典算法系列的作者, 讲解通俗易懂.

快速排序的基本思想一句话概括就是挖坑填数+分治法, 下面详细描述:

  1. 先取左边第一个数作为基准数
  2. 与基准数比较, 比基准数大的换到右边, 小的换到左边
  3. 左右两边分成两个部分, 再进行一次前两步的操作. 重复对左右两边拆分, 进行前两步操作, 直到只剩一个数.

这样说还是太抽象, 举个栗子吧

数组a = {3, 1, 4, 2, 0}

  1. 取a[0]作为基准数, 使用新变量baseNumber存储
  2. 从右向左比较, 比基准数小的放在基准数的位置上, 数组变成{0, 1, 4, 2, 0}, 此时出现一个坑a[4]
  3. 从左往右比较, 比基准数大的填入上一个坑a[4], 数组变成{0, 1, 4, 2, 4}, 此时的新坑是a[2]
  4. 再从右向左比较, 比基准数小的填入上一个坑a[2], 数组变成{0, 1, 2, 2, 4}, 此时的坑是a[3]
  5. 再从左向右比较时, 发现左右相遇了, 将baseNumber赋值给a[3], 数组变成{0, 1, 2, 3, 4}

因为数组元素较少, 这样就排序完成了, 但足够大家了解挖坑填数的思路了.

有一点需要说明, 为什么左右相遇了就可以把baseNumber赋值给那个元素? 因为左右两边相遇时, 所有数字都已经比较了一遍, 已经做到”比基准数大的都在右边, 比基准数小的都在左边”.

根据上面的分析, 可以很容易写出挖坑填数的代码:

void changeArray(int arr[], int left, int right){
    int i = left;
    int j = right;

    // 使用变量存储最左边的数做基准数
    // 基准数也可不使用最左边的, 中间和最后一个当然都可以
    int baseNumber = arr[left];

    // 当i=j时意味着数列中所有数都与基准数比较过了, 故结束比较
    while (i < j) {

        // 从右往左比较, 找到比基准数小的数的下标
        while (arr[j] > baseNumber && i < j) {
            j--;
        }
        arr[i] = arr[j];

        // 从左往右比较, 找到比基准数大的数的下标
        while (arr[i] < baseNumber && i < j) {
            i++;
        }
        arr[j] = arr[i];
    }
    // 将基准数赋值给a[i](也可以是a[j], 此时i=j)
    arr[i] = baseNumber;

}

最后baseNumber赋值,arr[i] = baseNumber,可能会有人对这句疑惑, 为何可以直接赋值, 不会少一个数吗?

答案是不会, 从上面的代码看出, 即便while (arr[i] < baseNumber && i < j)这个循环没有走, arr[i]的值也会赋值给arr[j], 这样arr[i]的值必定有两个, 当然可以直接赋值.

接下来彻底完成递归调用:

void quickSort(int arr[], int left, int right){
    // 递归的结束条件, left=right, 也就是只剩一个数的时候
    if (left < right) {

        int i = left;
        int j = right;
        int baseNumber = arr[left];

        while (i < j) {  
            while (arr[j] > baseNumber && i < j) {
                j--;
            }
            arr[i] = arr[j];

            while (arr[i] < baseNumber && i < j) {
                i++;
            }
            arr[j] = arr[i];
        }

        arr[i] = baseNumber;

        // 递归调用
        quickSort(arr, left, i - 1);
        quickSort(arr, i + 1, right);
    } 
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值