排序算法总结——快速排序

版权声明:转载请注明出处!谢谢! https://blog.csdn.net/qq_28114615/article/details/86064412

1.算法步骤

快速排序的一般步骤如下:

1.先从数列中取出一个数作为基准数。

2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。

3.再对左右区间重复第二步,直到各区间只有一个数。

2.图解算法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
(图片来自于《啊哈!算法》)
这里需要解释一下为什么每次交换的时候都需要右边的哨兵先走:最主要是相遇点的问题,算法的最后,是需要将相遇点与基准数相互交换的,因此这里必须要让相遇点小于基准数。而相遇点要么是左边哨兵先到相遇点,然后右边哨兵走过去相遇的,要么就是右边哨兵先到相遇点,左边哨兵走过去相遇的。

在左边哨兵先走情况下:

这种情况下每一轮都是先左后右,如果左边的停了,该右边行动了,这说明左边的找到了比基准数大的数;如果右边的停了,左边的该行动了,说明上一轮交换已经结束了,可以进行新一轮的先左后右了,因此,在相遇前的最后一轮搜寻中

如果是左边哨兵先到相遇点,让右边哨兵去靠近,那么说明这个时候左边哨兵脚下踩的必定是比基准数大的数,然后右边哨兵靠近后相遇,那么相遇点的数就比基准数大了,这样显然是不行的;
如果是右边哨兵先到相遇点,让左边哨兵去靠近,那么说明这个时候右边哨兵脚下踩的必定是已经在上一轮交换后的比基准数大的数,然后左边哨兵靠近后相遇,那么相遇点的数就比基准数大,这样也是不行的。
因此,不管是左边哨兵先站好位置还是右边哨兵先站好位置,在左边哨兵先走的情况下,最终相遇点的数是必定大于基准数的,因此不能让左边哨兵先走。

再来看右边哨兵先走的情况:

这种情况下每一轮都是先右后左,如果左边的停了,该右边行动了,说明上一轮已经结束了,可以进行新一轮的先右后左了。如果右边的停了,该左边行动了,说明右边的找到了比基准数小的数,因此,在相遇前的最后一轮搜寻中

如果是左边哨兵先站好位置,让右边哨兵去靠近,那么说明这个时候左边哨兵脚下踩的已经是上一轮交换结束后的小于基准数的数,然后右边哨兵靠近再相遇,相遇点就不大于基准数了,这样是符合要求的;
如果是右边哨兵先站好位置,让左边哨兵去靠近,说明这个时候右边哨兵脚下踩的是比基准数小的数,然后左边哨兵靠近后相遇,那么相遇点的数就比基准数小,这样也是符合要求的。

因此,不管是左边哨兵先站好位置还是右边哨兵先站好位置,在右边哨兵先走的情况下,最终相遇点的数必定是小于基准数的,这是符合要求的,因此必须让右边哨兵先走。

同样,如果基准数是数组最右端的数,那么相应的就应该先左后右了。

3.代码实现

#include <iostream>
//#include <stdio.h>
#include <vector>

using namespace std;
int getIndex(vector<int>& nums,int left,int right)   //获取相遇点
{
    int base=nums[left];
    int start=left;    //存储基准数的位置和大小
    while(left<right)  //左右哨兵未相遇
    {
        while(left<right&&nums[right]>=base)right--;  //从右边开始往左寻找小于基准数的数
        while(left<right&&nums[left]<=base)left++;   //从左边开始往右寻找大于基准数的数
        //两数交换位置
        int temp=nums[right];
        nums[right]=nums[left];
        nums[left]=temp;
    }
    //哨兵相遇,则将基准数换到相遇点来
    nums[start]=nums[left];
    nums[left]=base;
    
    return left; //返回相遇点

}
void QSort(vector<int>& nums,int left,int right)
{
    if(left>=right)return;    
    int index=getIndex(nums,left,right);   //获得相遇点,将原区间分为两个子区间进行递归分治
    QSort(nums,left,index-1);  //左子区间递归
    QSort(nums,index+1,right); //右子区间递归
    return;
}
int main()
{
    cin.clear();
    vector<int>nums;

    int num;
    while(cin>>num)nums.push_back(num);

    int left=0;
    int right=nums.size()-1;

    QSort(nums,left,right);

    for(int i=0;i<=right;i++)cout<<nums[i]<<" ";
    return 0;
}

运行结果:
在这里插入图片描述

4.时间复杂度分析

4.1 最好情况时间复杂度

最好情况是指每一次相遇点恰好二分区间,这样就对数组起到了一个二分的作用,是效率最高的,最终就需要分logn次了,而每一次都需要将整个区间内的数组遍历一边,因此时间复杂度就是n*logn了,时间复杂度分析如下:
在这里插入图片描述

4.2 最坏情况时间复杂度

容易想到,如果分治的两个子区间其中有一个为0,另一个就是n-1,此时即为最坏情况,这个情况主要出现在数组原本就是处于升序或降序状态,对于升序,每一次右边哨兵都需要从右往左跑到头,对于降序,每一次左边哨兵都需要跑到最右边去,这两种情况都是的快速排序退化成类似冒泡排序,时间复杂度自然就是O(n)了,时间复杂度分析如下:
在这里插入图片描述

综上可知,快速排序的最好情况下时间复杂度为O(nlogn),最坏情况下时间复杂度为O(n2)。

5.稳定性分析

要分析稳定性,那么就需要关注快速排序中对与基准数相等的数(简称相等数)的处理方式。在快速排序中,左右哨兵进行搜寻时对于相等数并不敏感,两边遇到相等数后都会选择无视,然后继续搜寻下一个数,这样问题就出现了:

以 基准数为第一个数为例,那么相等数是位于基准数右边的,此时如果右边哨兵发现了相等数,不对其进行操作,那么相遇点肯定就在相等数的左边了,最终基准数交换后依旧处于相等数右边,与原先的基准数、相等数二者之间次序相同,是稳定的;
另一方面,如果左边哨兵发现了相等数,也不对其进行操作,那么相遇点肯定就在相等数的右边了,最终基准数被交换到相等数的右边,此时相等数与基准数二者之间的次序改变了,是不稳定的。

综上所述,快速排序是一种不稳定的排序算法。

猜你喜欢

转载自blog.csdn.net/qq_28114615/article/details/86064412