排序:快速排序

1 思路

假设对数组data进行排序,如果能够对data以元素v分割成左右两部分,

  • 对于左边所有元素都比v小,
  • 对于右边所有元素都比v要大。

那么只要我们不断的递归对左右两个子数组进行相同的过程,最终数组就都会有序。


快速排序

2 实现

因此,首先需要考虑的是,如何对一个数组进行分割?
如果我们要分割数组data[l, r]中的元素,首先需要选定一个标定点:这里我们选择最左边(l)的元素:
那么我们可以使用变量i循环遍历data[l+1, r]的区间,如果遇到比v小的,那么我们就放到j+1的位置,同时j要自增1,以表示<v的部分进行了扩容。如下图所示,操作i和j的目的是不断的缩小未处理部分,同时给<v和>=v的部分扩容。需要注意的是,需要考虑=v的元素,我们将其放在了右边部分。


partition过程

当整个未处理部分未空时,此时,我们只需要交换v和j处的元素,即完成了分割,分割方法实现如下:

// 将data[l, r]划分为三部分data[l, j-1]、data[j]、data[p+1, r],其中,data[l, j-1]所有元素都比data[j]小,data[j+1, r]所有元素都比data[j]要大
private static <E extends Comparable<E>> int partition(E[] data, int l, int r) {
    E v = data[l];
    int j = l;
    for (int i = l + 1; i <= r; i++) {
        if (data[i].compareTo(v) < 0) {
            j++;
            Utils.swap(data, i, j);
        }
    }
    Utils.swap(data, l, j);
    return j;
}

已经有了分割方法,接下来实现递归,思路如下:

  • 对数组以v进行分割
  • 对<v的子数组进行排序
  • 对>=v的子数组进行排序
  • 递归之

实现代码如下:

public static <E extends Comparable<E>> void sort(E[] data) {
    if (data == null || data.length <= 1) {
        return;
    }

    sort(data, 0, data.length - 1);
}

// 宏观语义:该函数的功能用于对数组data的区间[l, r]进行排序
private static <E extends Comparable<E>> void sort(E[] data, int l, int r) {
    // 最简单的情况
    if (l >= r) {
        return;
    }

    // 先解决更小的问题
    int p = partition(data, l, r);
    sort(data, l, p - 1);
    sort(data, p + 1, r);
}

3 有序数组的退化

有序数组会使快排退化为O(n^2)的复杂度,这是因为,当数组有序的时候,每次partition后,左半区间将为空,而右半区间将为除v外的所有元素,这就违背了一分为二的分治思想,树的层次将由O(logn)变为了O(n)层。
解决的办法是,随机选择数组中的一个元素作为v,然后和第0个元素交换位置。

// 将data[l, r]划分为三部分data[l, j-1]、data[j]、data[p+1, r],其中,data[l, j-1]所有元素都比data[j]小,data[j+1, r]所有元素都比data[j]要大
private static <E extends Comparable<E>> int partition(E[] data, int l, int r, Random random) {
    int p = random.nextInt(r - l + 1) + l;
    Utils.swap(data, l, p);
    
    E v = data[l];
    int j = l;
    for (int i = l + 1; i <= r; i++) {
        if (data[i].compareTo(v) < 0) {
            j++;
            Utils.swap(data, i, j);
        }
    }
    Utils.swap(data, l, j);
    return j;
}

4 双路快排

上面的算法还是有问题,当所有元素都相等时,还是会出现退化为O(n^2)复杂度的问题。这是因为,我们总是把等于v的元素都放到右边,因此还是会出现左半区间将为空,而右半区间将为除v外的所有元素这种情况。解决方法是把=v的元素分散在左右两半空间,具体思想如图示:


双路排序

做一些解释:

  • 我们需要遍历的是未处理部分的区间,并将该区间的元素分散到左右两个区间中,该区间用[i, j]表示。
  • 向后遍历i,一旦data[i]>=v的元素停止遍历
  • 向前遍历j,一旦data[j]<=v的元素停止遍历
  • i的起始值为l+1,此时未处理部分为除第l个元素的所有元素;终止值>j,此时表示没有未处理部分了。
  • j的初始值为r;终止值为<i,此时表示没有未处理部分了。
  • 如果,i和j处的元素需要交换,交换i和j处的元素。
private static <E extends Comparable<E>> int partition2(E[] data, int l, int r, Random random) {
    int p = random.nextInt(r - l + 1) + l;
    Utils.swap(data, l, p);
    int i = l + 1;
    int j = r;
    while (true) {
        while (i <= j && data[i].compareTo(data[l]) < 0) {
            i++;
        }
        while (j >= i && data[j].compareTo(data[l]) > 0) {
            j--;
        }
        if (i >= j) {
            break;
        }
        Utils.swap(data, i, j);
        i++;
        j--;
    }
    Utils.swap(data, l, j);
    return j;
}

5 复杂度分析

对于双路算法来说,其实还是存在某种情况,每次随机取得的值刚好可以使算法退化到O(n^2),但这样的概率非常小,可以忽略不记;对于算法复杂度分析,有下面三条原则:

  • 如果能找到一组数据能使算法100%恶化,此时是普通算法,看最差情况,例如单路排序。
  • 如果没有一组数据能使算法100%恶化,此时是随机算法,看期望,例如双路排序。
  • 如果算法中间会有多次调用,可以尝试均摊分析,例如数组的扩容

6 三路快排

如果一个数组中存在大量相同的元素,其实在遍历的时候就可以辨别出来,那么在partition过程中,就可以把那部分数据都放在中间,再一次递归就可以少递归很多元素,其大致思想如下图:


三路快排

做如下解释:

  • 将数组分为五个部分,v, <v的部分,=v的部分,未处理部分,>v的部分
  • 循环遍历未索引部分,由i来表示,如果i处的元素<v,则和lt后面的元素进行交换,扩容<v的部分,i后挪
  • 如果i的元素>v,则和gt前面的元素进行交换,此时,由于交换到i处的元素还未被考察,因此i不变,gt需要向前挪
  • i的终止条件是>=gt还要大,则表示没有可考察的元素了,未处理部分未空
private static <E extends Comparable<E>> void sort3ways(E[] data, int l, int r, Random random) {
    // 最简单的情况
    if (l >= r) {
        return;
    }

    int lt = l, gt = r + 1, i = l;
    // 维持循环不变量:data[l+1, lt] < v, data[lt+1, i-1]=v, data[gt, r]>v
    while (i < gt) {
        if (data[i].compareTo(data[l]) < 0) {
            lt++;
            Utils.swap(data, lt, i);
            i++;
        } else if (data[i].compareTo(data[l]) > 0) {
            gt--;
            Utils.swap(data, gt, i);
        } else {
            i++;
        }
    }
    Utils.swap(data, l, lt);
    //此时,data[l, lt-1] < v, data[lt, i-1]=v, data[gt, r]>v
    sort3ways(data, l, lt - 1, random);
    sort3ways(data, gt, r, random);
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容