快速排序实现及pivot的选取

coursera上斯坦福的算法专项在讲到快速排序时,称其为最优雅的算法之一。快速排序确实是一种比较有效的排序算法,很多类库中也都采用了这种排序算法,其最坏时间复杂度为O(n^2),平均时间复杂度为O(nlogn),且其不需要额外的存储空间。

基本步骤

快速排序主要使用了分治的思想,通过选取一个pivot,将一个数组划分为两个子数组。其步骤为:
1.从数组中选择一个元素作为pivot
2.重新排列数组,小于pivot的在pivot的左边,大于pivot的在其右边。
3.递归地对划分后的左右两部分重复上述步骤。

简单的伪代码如下:

<img src="http://ou5lyiz64.bkt.clouddn.com/img/qsort%E9%80%89%E5%8C%BA_001.png" style="zoom: 50%" />

其中最主要的就是partition划分过程了。

划分过程

partition过程需要首先选择一个pivot,然后将小于pivot的元素放到左半部分,大于pivot的放到右半部分,并且最终pivot的位置及为其在排序好的数组中的最终位置。

这里使用第一个元素作为pivot,若选择其他元素作为pivot,则将其交换到第一个元素,这样可以保证代码的一致性及容易实现。示意图如下:

<img src="http://ou5lyiz64.bkt.clouddn.com/img/qsort%E9%80%89%E5%8C%BA_002.png" style="zoom:50%" />

这里使用i和j,i和j最初为p+1的位置,在遍历的过程中i始终指向>p的第一个元素,j始终指向当前待遍历的元素,若a[j] < p,则将其与a[i]进行交换。相关过程如下:

<img src="http://ou5lyiz64.bkt.clouddn.com/img/qsort%E9%80%89%E5%8C%BA_003.png" style="zoom:50%" />

基本实现如下:

    /**
    * a[l+1],...,a[i-1] < p
    * a[i],...,a[j-1] > p
    */
    private static int partition(int[] a, int l, int r) {
        int p = a[l];

        int i = l + 1;
        for (int j=l+1; j<=r; j++) {
            if (a[j] < p) {
                swap(a, j, i);
                i++;
            }
        }
        swap(a, l, i-1);
        return i-1;
    }

基本实现

public class QuickSort {
    public static void qSort(int[] a) {
        if (a == null || a.length <= 1) {
            return;
        }
    
        qSort(a, 0, a.length-1);
    }

    private static void qSort(int[] a, int l, int r)    {
        if (l >= r) {
            return;
        }

        int pos = partition(a, l, r);

        qSort(a, l, pos - 1);
        qSort(a, pos + 1, r);
    }

    /**
    * a[l+1],...,a[i-1] < p
    * a[i],...,a[j-1] > p
    */
    private static int partition(int[] a, int l, int r) {
        int p = a[l];
        int i = l + 1;
        for (int j=l+1; j<=r; j++) {
            if (a[j] < p) {
                swap(a, j, i);
                i++;
            }
        }
        swap(a, l, i-1);
        return i-1;
    }

    //返回pivot下标 选择第一个元素
    private static int choosePivotFirst(int[] a, int l, int r) {
        return l;
    }
    
   private static void swap(int[] a, int x, int y) {
        int temp = a[x];
        a[x] = a[y];
        a[y] = temp;
    }

pivot的选取

根据斯坦福算法专项课,然我们实现三种不同的pivot选取方式,并计算相应比较次数,分别为choose first, choose last, median of three, 还可以进行随机选取,这也是快速排序为什么是一种随机化算法。

pivot的选取决定了快速排序的运行时间,下面对几种特殊情况进行分析:

1.最坏情况

假设我们始终选取第一个元素作为pivot, 并且输入数组是有序的,那么每次划分后面所有元素都大于pivot, 每次只能将问题规模减少1,所以运行时间为n+n-1+n-2+...+1 = O(n^2).

2.最好情况

最好情况为每次选取的pivot都能将数组平均地划分为两部分,由于划分的过程为O(n),所以总的运行时间为T(n) = 2T(n/2) + O(n)根据主方法,时间复杂度为O(nlogn)。

3.随机选取

每次运行过程中,随机选取pivot, 通常能得到比较好的结果。

选取方式及实现

斯坦福算法专项课上让我们实现三种不同的选取方式,选取第一个,最后一个,以及三数取中。

1.choose first

该种方式最为简单,只需返回子数组的第一个元素下标即可,下面为其实现:

//返回pivot下标 选择第一个元素
private static int choosePivotFirst(int[] a, int l, int r) {
    return l;
}

2.choose last

选择最后一个元素,实现如下:

//选择最后一个元素作为pivot
private static int choosePivotLast(int[] a, int l, int r) {
        return r;
}

3.median-of-three

选取第一个、最后一个以及中间的元素的中位数,如4 5 6 7, 第一个4, 最后一个7, 中间的为5, 这三个数的中位数为5, 所以选择5作为pivot,8 2 5 4 7, 三个元素分别为8 5 7, 中位数为7, 所以选择最后一个元素7作为pivot,其实现如下:

//median-of-three pivot rule
private static int choosePivotMedianOfThree(int[] a, int l, int r) {    
    int mid = 0;
    if ((r-l+1) % 2 == 0) {
        mid = l + (r-l+1)/2 - 1;
    } else {
        mid = l + (r-l+1)/2;
    }

    //只需要找出中位数即可,不需要交换
    //有的版本也可以进行交换
    if (((a[l]-a[mid]) * (a[l]-a[r])) <= 0) {
        return l;
    } else if (((a[mid]-a[l]) * (a[mid]-a[r])) <= 0)    {
        return mid;
    } else {
        return r;
    }
}

最后的划分过程如下:

private static int partition(int[] a, int l, int r) {
    //pivot选择方式
    //int pi = choosePivotFirst(a, l, r);
    //int pi = choosePivotLast(a, l, r);
    int pi = choosePivotMedianOfThree(a, l, r);

    //始终将第一个元素作为pivot, 若不是, 则与之交换
    if (pi != l) {
        swap(a, pi, l);
    }
    int p = a[l];

    int i = l + 1;
    for (int j=l+1; j<=r; j++) {
        if (a[j] < p) {
            swap(a, j, i);
            i++;
        }
    }
    swap(a, l, i-1);
    return i-1;
}

注意最后的划分过程相比于之前增加的pivot的选取方式,而不是单纯地将第一个元素作为pivot, 可以看到,若第一个元素不是pivot, 需要将pivot与第一个元素进行交换,这样保证代码的统一性。

总结与感想

1.学会体会这些算法背后的思想,为什么要这样设计

2.对于比较复杂的算法,学会使用特殊情况进行分析

参考资料:

(1) coursera斯坦福算法专项课part1

(2) 维基百科快速排序

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

推荐阅读更多精彩内容