排序算法6:快速排序

数据结构与算法

  • 快速排序为应用最多的排序算法,因为快速二字而闻名。快速排序和归并排序一样,采用的都是分治思想
  • 分治法的基本思想是:将原问题分解为若干个规模更小但结构与原问题相似的子问题。递归地解这些子问题,然后将这些子问题的解组合为原问题的解。
  • 我们只需关注最小问题该如何求解,和如何去递归既可以得到正确的算法实现。
  • 快速排序可以分为:单路快速排序,双路快速排序,三路快速排序,他们区别在于选取几个指针来对数组进行遍历下面我们依次来讲解。

1. 单路快速算法

1.1 单路快速算法的思想:

image

首先我们选取数组中的一个数,将其放在合适的位置,这个位置左边的数全部小于该数值,这个位置右边的数全部大于该数值 。

  1. 假设数组为 arr[l...r] 假设指定数值为数组第一个元素 int v = arr[l],假设 j 标记为比 v 小的最后一个元素, 即 arr[j+1] > v。当前考察的元素为 i 则有arr[l + 1 ... j] < v , arr[j+1,i) >= v 如上图所示。
  2. 假设正在考察的元素值为 e ,e >= v 的时候我们只需交将不动,直接 i++ 去考察下一个元素,
  3. 当e < v 由上述假设我们需要将 e 放在<v 的部分 ,此时我们只需将 arr[j] 和 arr[i] 交换一下位置即可。
  4. 最后一个元素考察完成以后,我们再讲 arr[l]和 arr[j]调换一下位置就可以了。
  5. 上述遍历完成以后 arr[l + 1 ... j] < v , arr[j+1,i) >= v 就满足了,接下来我们只需要递归的去考察 arr[l + 1 ... j] 和 arr[j+1,r] 即可。

1.2 单路快速排序的 Java 实现:


    public static void quickSortOneWay(int[] arr, int l, int r) {
        if (l >= r) {
            return;
        }
        int p = partition(arr, l, r);

        quickSortOneWay(arr, l, p - 1);
        quickSortOneWay(arr, p + 1, r);

    }

    private static int partition(int[] arr, int l, int r) {
        // 为了提高效率,减少造成快速排序的递归树不均匀的概率,
        // 对于一个数组,每次随机选择的数为当前 partition 操作中最小最大元素的可能性为 1/n
        int randomNum = (int) (Math.random() * (r - l + 1) + l);
        swap(arr, l, randomNum);


        //将小于v的数据放在索引为j的位置
        int v = arr[l];
        int j = l;
        for (int i = l + 1; i <= r; i++) {
            if (arr[i] < v) {
                swap(arr, j + 1, i);
                j++;
            }
        }
        
        swap(arr, l, j);
        return j;
    }

    //交换位置
    private static void swap(int[] arr, int l, int r) {
        int temp = arr[l];
        arr[l] = arr[r];
        arr[r] = temp;
    }

对于上述算法中为什么选取了当前排序数组中随机一个元素进行比较,假设我们在考察的数组已经为已经排序好的数组,那么我们递归树就会向右侧延伸 N 的深度,这种情况使我们不想要看到的,如果我们每次 partition 都随机从数组中取一个数,那么这个数是当前排序数组中最小元素可能性为 1/n 那么每次都取到最小的数的可能性就很低了。

2双路快速排序

2.1 双路快速排序算法思想:

  1. 跟单路一样,双路快速排序,同样选择数组的第一个元素当做标志位(经过随机选择后的)
  2. 双路快速排序要求有两个指针,指针 i j 分别指向 l+1 和 r 的位置然后两者同时向数组中间遍历 在遍历过程中要保证arr[l+1 ... i) <= v, arr(j....r] >= v 因此我们可以初始化 i = l+1 以保证左侧区间初始为空,j = r 保证右侧空间为空
  3. 遍历过程中要 i <= r 且 arr[i] <= v 的时候 i ++ 就可以了
    当 arr[i] > v 时表示遇到了 i 的值大于 v 数值 此刻能等待 j 角标的值,从右向左遍历数组 当 arr[i] < v 表示遇到了 j 的值小于 v 的元素,它不该在这个位置呆着,
  4. 得到了 i j 的角标后 先要判断是否到了循环结束的时候了,即 i 是否已经 大于 j 了。
  5. 否则 应该讲 i 位置的元素和 j 位置的元素交换位置,然后 i++ j-- 继续循环
  6. 遍历结束的条件是 i>j 此时 arr[j]为最后一个小于 v 的元素 arr[i] 为第一个大于 v 的元素 因此 j 这个位置 就应该是 v 所应该在数组中的位置 因此遍历结束后需要交换 arr[l] 与 arr[j]


    image

2.2 双路快速排序的 Java 实现:

    public static void quickSortOneWay(int[] arr, int l, int r) {
        if (l >= r) {
            return;
        }
        int p = partition(arr, l, r);

        quickSortOneWay(arr, l, p - 1);
        quickSortOneWay(arr, p + 1, r);

    }

    private static int partition(int[] arr, int l, int r) {
        // 为了提高效率,减少造成快速排序的递归树不均匀的概率,
        // 对于一个数组,每次随机选择的数为当前 partition 操作中最小最大元素的可能性为 1/n
        int randomNum = (int) (Math.random() * (r - l + 1) + l);
        swap(arr, l, randomNum);


        //将小于v的数据放在索引为j的位置
        int v = arr[l];
        int i = l;
        int j = r;

        while (true) {
            while (i < r && arr[i] <= v) i++;
            while (j > l + 1 && arr[j] >= v) j--;

            if (i > j) {
                break;
            }

            swap(arr, i, j);
            i++;
            j--;

        }


        swap(arr, l, j);
        return j;
    }

    //交换位置
    private static void swap(int[] arr, int l, int r) {
        int temp = arr[l];
        arr[l] = arr[r];
        arr[r] = temp;
    }

3 三路快速排序

3.1 三路快速排序算法思想

上述两种算法我们发现对于与标志位相同的值得处理总是,做了多余的交换处理,如果我们能够将数组分为> = <三部分的话效率可能会有所提高。

  1. 我们将数组划分为 arr[l+1...lt] <v arr[lt+1..i) =v arr[gt...r] > v三部分 其中 lt 指向 < v 的最后一个元素前一个元素,gt 指向>v的第一个元素的前一个元素,i 为当前考察元素
  2. 定义初始值得时候依旧可以保证这初始的时候这三部分都为空 int lt = l; int gt = r; int i = l + 1;
  3. 当 e > v 的时候我们需要将 arr[i] 与 arr[gt-1] 交换位置,并将 > v 的部分扩大一个元素 即 gt-- 但是此时 i 指针并不需要操作,因为换过过来的数还没有被考察。
  4. 当 e = v 的时候 i ++ 继续考察下一个
  5. 当 e < v 的时候我们需要将 arr[i] 与 arr[lt+1] 交换位置
  6. 当循环结束的时候 lt 位于小于 v 的最后一个元素位置所以最后我们需要将arr[l] 与 arr[lt] 交换一下位置。


    image
image

3.2 三路快速排序 Java 代码实现:

    @Override
    public void quickSort(int[] arr, int left, int right) {
        if (left >= right) {
            return;
        }

        // 为了提高效率,减少造成快速排序的递归树不均匀的概率,
        // 对于一个数组,每次随机选择的数为当前 partition 操作中最小最大元素的可能性 降低 1/n!

        int randomNum = (int) (Math.random() * (right - left + 1) + left);
        swap(arr, left, randomNum);

        int v = arr[left];
        // 三路快速排序即把数组划分为大于 小于 等于 三部分
        //arr[l+1...lt] <v  arr[lt+1..i) =v  arr[gt...r] > v 三部分
        // 定义初始值得时候依旧可以保证这初始的时候这三部分都为空
        int leftEnd = left;
        int rightStart = right;
        int i = left + 1;

        while (i <= rightStart) {
            if (arr[i] < v) {
                swap(arr, i, leftEnd + 1);
                i++;
                leftEnd++;
            } else if (arr[i] == v) {
                i++;
            } else {
                swap(arr, i, rightStart);
                rightStart--;
                //i++ 注意这里 i 不需要加1 因为这次交换后 i 的值仍不等于 v 可能小于 v 也可能等于 v 所以交换完成后 i 的角标不变
            }
        }

        swap(arr, left, leftEnd);
        leftEnd--;
        quickSort(arr, left, leftEnd);
        quickSort(arr, rightStart, right);

    }

4 快速排序时间复杂度空间复杂度

4.1 时间复杂度

  • 在最优的情况下,快速排序算法的时间复杂度为O(nlogn)。
  • 在最坏的情况下,待排序的序列为正序或者逆序最终其时间复杂度为O(n2)。
  • 平均的情况,设枢轴的关键字应该在第k的位置(1≤k≤n),那么:由数学归纳法可证明,其数量级为O(nlogn)。

4.2 空间复杂度

就空间复杂度来说,主要是递归造成的栈空间的使用

  • 最好情况,递归树的深度为log2n,其空间复杂度也就为O(logn)
  • 最坏情况,需要进行n‐1递归调用,其空间复杂度为O(n)
  • 平均情况,空间复杂度也为O(logn)。

可惜的是,由于关键字的比较和交换是跳跃进行的,因此,快速排序是一种不稳定的排序方法。

参考

搞懂基本排序算法
快速排序最好,最坏,平均复杂度分析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342