排序算法:快速排序

快速排序的问题

快速排序是冒泡排序的加强版,学习交换排序算法中一定会接触的一个算法。改进的着眼点在于减少对关键字的比较次数和移动次数,因为冒泡排序每次交换的都是相邻位置的元素且一次只能后移一个位置。

而快排的思想就是找出序列中的一个轴值,(按升序)将比轴值小或等于的数都放在轴值的左边,比轴值大或等于的数都放在轴值的右边,我们把这个过程成为“一次划分”。要注意的是,一次划分完毕后,轴值左右两方的子序列不是完全有序的。然后我们分别对左右两方的序列分别进行一次划分的操作,在重复…..
直到整个序列有序为止。

从上面的说法可以看出,快排是一个递归的过程。

那么就有以下的问题了

  • ①轴值如何选择?
  • ②如何进行一次划分操作?
  • ③如何处理划分的两个待排子序列?
  • ④如何设定快排的结束条件?

    下面就来一个个问题击破。
    ①轴值如何选择?
    1.最简单的方法就是直接取待排序列的第一个元素作为轴值,但是呢如果待排序列是正序或者逆序,那么除了轴值外的所有元素都轴值的另一边,此时快排就退化成了冒泡,如果序列基本正序或逆序的话性能也会下降的厉害。
    2.可选的方法有我们直接取中间的元素作为轴值(和第一个元素交换,讲解编程实现时会讲解为什么要交换)
    3.我们也可以随机选择序列中的一个元素来作为初始调用的第一个元素。
    4.从递归的角度来分析,如果以一次划分的轴值作为一个树的根节点,左右子序列的轴值作为其左右子节点,可以看出快排的排序展开是呈树状的,如果快排的展开树是一个平衡的树的话,递归的层数就可以达到最低,以此达到最高的效率,毕竟函数调用的花销本身也不小。因此我们期望尽可能的在一次划分阶段将待排序列划分成两个大小相近或相等的子序列。因此,如果要用快排来排数据量比较大的数据,或者使用到一次划分的场景,有必要设计一些策略来寻找序列的中位数。(这里不展开讨论,只是点出快排不稳定的一个原因,但目前为止没本人没遇到一个成本很低找中位数的策略)。

    ②如何进行一次划分操作?
    1.初始化:选取第一个元素(已经和选定轴值的数交换后)作为轴值,设置i,j两个参数记录用来指示将要与轴值记录进行比较的左侧记录位置和右侧记录位置,也就是该次划分过程的区间。
    2.右侧扫描过程:将轴值与j指向的元素进行比较,如果j指向的元素比较大,则j往中间前进一步(j–)。重复比较,直到轴值大于j指向的记录,然后交换i和j的元素,由i指向轴值变为j指向轴值。进行左侧扫描
    3.左侧扫描过程:将轴值于i指向的元素进行比较,如果i指向的元素比较小,则i往中间前进一步(i++)。重复比较,知道轴值小于i指向的记录,然后交换i和j的元素,由j指向轴值变为i指向轴值。进行右侧扫描。
    4.重复2,3直到i和j相遇。
    下面贴出伪代码

    1. 将i和j分别之指向待划分区间的最左侧和最右侧记录的位置;
    2. 重复下述过程,直到i==j
      2.1 右侧扫描,知道记录j的元素小于轴值记录的元素;
      2.2 如果存在划分区间,则i和j的元素进行交换,执行i++
      2.3 左侧扫描,知道记录i的元素大于轴值记录的元素;
      2.4 如果存在划分取件,则将i和j的元素进行交换,执行j–
    3. 退出循环,说明i和j指向了最终轴值的位置,返回该位置

下面贴上代码,设first,end分别为该次待划分的区间位置

int partition(int r[],int first,int end)
{
    if(r == NULL ||first >end) return -1;//判断非法输入
    int i=first,j=end;

    while(i<j)
    {
        while(i<j && r[i]<=r[j]) j--; //右侧扫描过程
        if(i<j)
        {
            swap(r[i],r[j]);
            i++; //区间first~i的元素一定比轴值小的了,但仍然是无序的
        }

        while(i<j && r[i]<=r[j]) i++; //左侧扫描过程
        if(i<j)
        {
            swap(r[i],r[j]);
            j--; //区间j~end的元素一定比轴值大的了,但仍然是无序的
        }
    }

    return i;//返回轴值的位置

}

③如何处理划分的两个子序列?
有另外一次划分的操作后,就很明了嘛,一次划分后的轴值在整个序列中的位置,就是是整个序列有序后自己应该待的位置。所以我们就可以不动它了,假设一次划分后的轴值所在为值为index。

那么我们下一步就要分别对区间:[1,index-1],[index+1,end]再次进行一次划分操作(两个子问题),然后在….继续
④如何设定快排的结束条件?
显然当一个序列中只有一个元素的时候,就没有必要进行一次划分操作了,因此当我们递归到问题规模为1的子问题时就可以结束了。

下面是学习快速排序以来个人遇到过的认为最优雅的实现

void quicksort(int r[],int first,int end)
{
    if(r == NULL || first > end) return ; //非法输入检查
    if(first < end)//区间长度大于1的序列才进行一次划分
    {
        //进行一次划分操作
        int partition_result = partition(r,first,end);
        //对子问题的划分要注意区间是不包含轴值的哦
        //递归对左子序列进行快速排序
        quicksort(r,first,partition_result-1);
        //递归对右子序列进行快速排序
        quicksort(r,partition_result+1,end);
    }
}

//初始调用就要输入的区间是[1,n]或者[0,n-1]
//即quicksort(r,0,n-1);//符合数组区间的调用

至于时间复杂度的详细证明个人觉得还是看算法导论这些书籍好点,博客上撸公式也是个累活。
就简单说说
时间复杂度为o(n*logn)
空间复杂度为O(logn~n):根据递归调用树的深度而定
因此,对于一个局部有序或者非常有序的序列,会使得递归调用树的不平衡度非常大,最坏情况递归的深度可以达到n-1。因此在数据量较大元素随机排列的序列中,使用快排的表现是最佳的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值