• 交换排序算法之快速排序



    八种排序算法可以按照如图分类,本文主要介绍快速排序。


    交换排序

    所谓交换,就是序列中任意两个元素进行比较,根据比较结果来交换各自在序列中的位置,以此达到排序的目的。


    快速排序

    快速排序的思想很简单,就是先把待排序的数组拆成左右两个区间,左边都比中间的基准数小,右边都比基准数大。接着左右两边各自再做同样的操作,完成后再拆分再继续,一直到各区间只有一个数为止。

    举个例子,现在我要排序 4、9、5、1、2、6 这个数组。一般取首位的 4 为基准数,第一次排序的结果是:

    2、1、4、5、9、6

    可能有人觉得奇怪,2 和 1 交换下位置也能满足条件,为什么 2 在首位?这其实由实际的代码实现来决定,并不影响之后的操作。

    以 4 为分界点,对 2、1、4 和 5、9、6 各自排序,得到

    1、2、4、5、9、6

    不用管左边的 1、2、4 了,将 5、9、6 拆成 5 和 9、6,再排序,至此结果为

    1、2、4、5、6、9

    为什么把快排划到交换排序的范畴呢?因为元素的移动也是靠交换位置来实现的。


    算法实现

    大体思路已经有了,接下来就是具体实现。

    相信大家已经看出来了,算法的实现需要用到递归(拆分区间之后再对每个区间作同样的操作)。所有关于算法实现的阐述仅以一次排序为例。

    基准数一般取数组首位的数,而最终基准数是要放在中间位置的。我们使用一个临时变量 stard 来保存选取的基准值,其余元素根据比较结果决定是否交换位置。

    我们分别得到数组的首元素和尾元素的下标,作为头指针和尾指针。先尾指针开始,如果对应的数比基准值大,尾指针向前移动,直到遇到比基准值小的为止。此时将尾指针对应的数值赋给头指针的位置。轮到头指针,如果对应的数比基准值小,头指针向后移动,直到遇到比基准值大的为止。此时头指针对应的数值赋给尾指针的位置。再到尾指针,再到头指针,直到头尾指针重合为止。此时将 stard 的值赋给指针重合处下标,一趟排序完成。

    用 Java 实现的快速排序如下

    /**
     * 快速排序
     *
     * @param arr 待排序的整型数组
     * @param start 起始下标
     * @param end 末尾下标
     */
    public static void quicksort(int[] arr, int start, int end) {
    
        if (start < end) {
            // 把数组中第 0 个数字作为基准数
            int stard = arr[start];
            //  记录需要排序的下标
            int low = start;
            int high = end;
            // 循环找到比基准数大的数和比基准数小的数
            while (low < high) {
                // 右边的数字比基准数大
                while (low < high && arr[high] >= stard) {
                    high--;
                }
                // 使用右边的数替换左边的数
                arr[low] = arr[high];
                // 左边的数字比基准数小
                while (low < high && arr[low] <= stard) {
                    low++;
                }
                // 使用左边的数替换右边的数
                arr[high] = arr[low];
            }
            // 把标准值赋给下标重合的位置
            arr[low] = stard;
            // 处理所有小的数字
            quicksort(arr, start, low);
            // 处理所有大的数字
            quicksort(arr, low + 1, end);
        }
    }
    

    我还在网上看到另一种实现方式,个人觉得第二种虽然更符合交换这一概念,但在交换次数和空间消耗上有一丢丢逊色。

    /**
     * 快速排序
     *
     * @param arr 待排序的整型数组
     * @param start 起始下标
     * @param end 末尾下标
     */
    public static void quicksort(int[] arr, int start, int end) {
    
        if (start < end) {
            // 把数组中第 0 个数字作为基准数
            int stard = arr[start];
            //  记录需要排序的下标
            int low = start;
            int high = end;
            // 循环找到比基准数大的数和比基准数小的数
            while (low < high) {
                // 右边的数字比基准数大
                while (low < high && arr[high] >= stard) {
                    high--;
                }
                // 左边的数字比基准数小
                while (low < high && arr[low] <= stard) {
                    low++;
                }
                // 相等继续循环
                if (low < high && arr[low] == arr[high]) {       
                    low++;            
                } else {
                   	// 左边的数和右边的数交换位置
                    int temp = arr[low];                
                    arr[low] = arr[high];                
                    arr[high] = temp;            
                }  
            }
            // 处理所有小的数字
            quicksort(arr, start, low);
            // 处理所有大的数字
            quicksort(arr, low + 1, end);
        }
    }
    

    快速排序稳定性

    快速排序是不稳定的算法,只要举出一个实例,即可说明它的不稳定性。所谓不稳定,即序列中如果存在多个相同的元素,经过排序后它们的顺序位置发生变化,就是不稳定的。

    假设在数列中存在 a[i]=a[j],若在排序之前,a[i]在a[j]前面,并且排序之后,a[i] 仍然在 a[j] 前面,则这个排序算法是稳定的。


    快速排序性能

    快速排序的一次划分算法从两头交替搜索,直到 low 和 high 重合,因此其时间复杂度是 O(n)。这里要注意时间复杂度的定义,常数是忽略不计的,可能第二次划分只有原长度的一半,即 (1/2)*n,但还是认为是 O(n)。

    整个快速排序算法的时间复杂度与划分的趟数有关。理想的情况是,每次划分所选择的中间数恰好将当前序列几乎等分。第一次划分变成两个数组,第二次划分变成四个数组,最后一定会变成 n 个数组,划分次数即是 log2n 次,便可得到长度为1的子表。这样,整个算法的最优时间复杂度为 O(nlog2n)。

    最坏的情况是,每次所选的中间数是当前序列中的最大或最小元素,这使得每次划分所得的子表中一个为空表,另一子表的长度为原表的长度减一。这样,长度为 n 的数据表的快速排序需要经过 n 趟划分,使得整个排序算法的时间复杂度为 O(n2)。

    快速排序的平均时间复杂度也是 O(nlogn)。

    从空间性能上看,尽管快速排序需要的辅助空间也就是个常数级 O(1) ,但用到了递归,每一次划分都需要开辟一块辅助空间。

    因此最优的情况下空间复杂度为 O(log2n) ,最差的情况下空间复杂度为 O( n ) 。


  • 相关阅读:
    WIN10 激活
    unity 安装指定版本编辑器
    Object.assign 用于对象之间的合并
    mac 如何进入User 目录
    unity hub 个人版许可证获取失败
    [九、实用进阶]7使用Xcode的版本管理功能(SCM)
    [九、实用进阶]1隐藏顶部的状态栏
    [九、实用进阶]5正则表达式的使用
    [九、实用进阶]2列出设备上的所有字体
    [八、数据与网络]22使用URLSession上传图片至服务器
  • 原文地址:https://www.cnblogs.com/Yee-Q/p/13396644.html
Copyright © 2020-2023  润新知