快速排序

背景

快速排序 (Quicksort) 由 C.A.R. Hoare (著名计算机科学家,1980年图灵奖得主) 于1962年提出。他在 Computer Journal 中发表的经典论文 “Quicksort” 中描述了这一算法。采用了分治法的策略。

基本思想

  • 先从数列中取出一个数作为基准数
  • 将比这个数小的放到它的左边,比这个数大的放到它的右边
  • 对左右区间重复上一步,直到各个区间只有一个数

算法复杂度

  • 最坏情况复杂度
    O(n2)

每个内点只有一个叶子结点。 比较次数为:

0+1+2+...+n=n(n+1)2

  • 最好情况复杂度
    O(nlog2n)

每个内点有两个叶子结点,若叶子数为n,内点数为 n - 1 ,顶点数为 2n - 1, 叶子到根的距离为

O(log2n)
(上下取整)。

顶点到根的距离之和 = 2 * 叶子到跟的距离之和 - 顶点数 + 1

比较次数约为

2nlog2n(n1)nlog2n2n+1

  • 平均时间复杂度
    O(nlog2n)

平均情况下,Tn为n个对象平均比较次数,若取第k个数为分割标准,则一个序列有 k - 1 个数,另一个序列有 n - k 个数。

Tn=1nk=1n(n1+Tk1+Tnk)=n1+2nk=0n1TkT2=1,T1=0,T0=0

求解:

nTn=n(n1)+2k=0n1Tk(n+1)Tn+1=(n+1)n+2k=0nTk

相减:

(n+1)Tn+1=(n+2)Tn+2n

Tn+1n+2=Tnn+1+2n(n+1)(n+2)

变换:

Sn=Tnn+1

Sn+1Sn=2n(n+1)(n+2)S0=0,S1=0,S2=13

Sn=k=0n12k(k+1)(k+2)=4k=0n11k+22k=0n11k+1=2k=2n1k+4n+12

估计和,用求和进行逼近,只需上界

k=2n1k<n11xdx=ln(n)ln(1)=ln(n)

Sn<2ln(n)+2n+1+2=2ln(n)+O(1)

调和极数求和:

1+12+13+...+1n=ln(n+1)+r

这里r是欧拉常数约为0.57721。

则:

Tn=2(n+1)ln(n)+O(n)=1.3863nlog2n+O(n)

 

算法实现

quicksort 1

通过从左到右的扫描完成排序。容易忽略swap(l,m)这一步 (移动哨兵),而当t是子数组中严格最大的元素时,会导致死循环。
在一些常见输入下,可能退化为平方时间算法。

void quick_sort(int l, int r)
{
    if (l < r)
    {
        int i = l, j = r, m = l;
        for( i = l + 1; i < r; i++)
        {
            if(x[i] < x[l])
            swap(++m,i);
        }
        swap(l, m);

        quick_sort(l, m - 1); // 递归调用 
        quick_sort(m + 1, r);
    }
}

quicksort 2

将划分方案改为从右向左进行。循环终止时,x[m] == x[l],直接使用参(l,m-1)和(m+1,r)递归,不再需要swap操作。

void quick_sort(int l, int r)
{
    if (l < r)
    {
        int i,m = r + 1;
        for( i = r ; i >= l; i--)
        {
            if(x[i] >= x[l])
            swap(--m,i);
        }

        quick_sort(l, m - 1); // 递归调用 
        quick_sort(m + 1, r);
    }
}

考虑极端情况,如n个相同元素组成的数组,对于这种输入,插入排序的性能非常好,每个元素移动距离都为0。 而quicksort 1的性能却非常糟糕,n - 1 次划分每次都要 O(n) 的时间来去掉一个元素。 总时间是

O(n2)
。采用双向划分可以避免这个问题。
下标i 和 j 初始化为数组的两端,主循环中有两个内循环,第一个循环向右移动遇到较大元素时停止,第二个循环向左移动遇到较小元素时停止。然后主循环测试两个下标是否交叉并替换值。当元素相同时,停止扫描并交换i和j的值,这样虽然交换次数增加了,但是将所有元素相同的最坏情况变成了差不多
O(nlog2n)
的最好情况。如下:

quicksort 3

void quick_sort(int l, int r)
{
    if (l < r)
    {
        int i = l, j = u + 1, t = x[l];
        while(1)
        {
            while(i <= r && x[i] < t) i++;
            while(x[j] > t) j--;
            if(i > j)
                break;
            swap(i,j);
        }
        swap(i,j);

        quick_sort(l, j - 1); // 递归调用 
        quick_sort(j + 1, r);
    }
}

quicksort 4

下面是一种常见的双向写法。

void quick_sort(int s[], int l, int r)
{
    if (l < r)
    {
        int i = l, j = r, x = s[l];
        while (i < j)
        {
            while(i < j && s[j] >= x) // 从右向左找第一个小于x的数
                j--;  
            if(i < j) 
                s[i++] = s[j];

            while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数
                i++;  
            if(i < j) 
                s[j--] = s[i];
        }
        s[i] = x;
        quick_sort(s, l, i - 1); // 递归调用 
        quick_sort(s, i + 1, r);
    }
}

算法优化

  • 上面的快速排序都是围绕第一个元素进行的。对于随机的输入,这样没有问题,但是对于某些常见的输入,这样做需要的时间和空间更多, 如数组已经升序时。随机选择哨兵可以得到更好的性能。
  • 快排花费了很多时间来排序小的子数组,如果用插入排序之类的简单方法实现小数组的排序,程序速度会更快。

不妨令 r - l < cutoff 时采用插入排序,cutoff取值为50比较理想。

  • 代码调优时可以将循环体内的swap改为内联代码(另外的swap调用次数少,不计)。

quicksort 5

void quick_sort(int l, int r)
{
    if (r - l < cutoff) return;
    else
    {
        swap(l, randint(l,u));
        int i = l, j = r + 1, t = x[l];
        while(1)
        {
            while(i <= r && x[i] < t) i++;
            while(x[j] > t) j--;
            if(i > j)
                break;
            int temp = x[i];
            x[i] = x[j];
            x[j] = temp;
        }
        swap(i,j);

        quick_sort(l, j - 1); // 递归调用 
        quick_sort(j + 1, r);
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值