快速排序

一直都想写一篇关于快速排序的博客,但迟迟没有动笔,今天趁着这个机会,将我对于快速排序的认识进行一个总结,希望对阅读到这篇博客的你有所帮助。
说起快速排序,我头脑中的第一反应就是partition操作,因为partition操作是整个快速排序的核心所在。那到底什么是partition操作呢?先来看一张图:
这里写图片描述
partition操作简单地说,就是找到一个元素作为标定点(如图中的5),经过操作后,使得该标定点左边的元素是<5,标定点右边的元素是>=5,经过这次操作,5这个元素已经放置在它应该放的位置上。从上图中不难看出,如果将5左右两边的子数组都分别排好序后,那么整个数组就已经有序了,即,我们所要做的工作就是对5左右两边的子数组分别递归调用快速排序进行排序即可。
那么partition操作具体应该怎么做呢?再来看两张图:
这里写图片描述

这里写图片描述
为了方便,我们取第一个元素作为标定点,j指针指向小于v的最后一个元素,指针i指向当前考察的元素e,当e>=v时,直接将i指针右移,而当e小于v时,将i指向的元素和j的下一个位置的元素(即j+1位置的元素)进行一次交换,然后i指针和j指针都分别右移一个单位。以此类推,直到遍历完整个数组,最后只要将j所指向的元素和v元素进行一次交换,就完成了partition操作。
上述是基本的快速排序的方法,下面来探讨几个优化的方法。
优化1:当子数组的元素个数比较少时,采用插入排序替换快速排序。虽然快速排序是O(nlogn)的排序算法,而插入排序是O(n^2)的算法,但是插入排序前面的系数要小于快速排序,即插入排序:O(a*n^2),快速排序:O(b*nlogn),有a < b,则当排序的个数,即n较小时,插入排序反而比快速排序要快。
优化2:当遇到基本有序的数组时,使用上述的快排会使时间复杂度退化为O(n^2),原因如下图所示:
这里写图片描述
由于我们只是选取最左边的元素作为标定点,使得当数组基本有序时,递归的层数从原来的logn层退化为n层,而每层的partition操作又需要O(n)的时间复杂度,所以总的时间复杂度退化为O(n^n),解决方法是,随机化选取标定点,这样即使遇到基本有序的数组也不太可能退化为O(n^2)。
下面是增加了两个优化方案后的快速排序的代码:

//返回位置p,使得数组中在p之前的数都<arr[p],在P之后的数都>arr[p]

template<typename T>
int __partition(T arr[], int l, int r)
{
    //随机选取标定点
    swap(arr[rand()%(r-l+1)+l], arr[l]);
    T v = arr[l];
    int j = l;
    for(int i = l+1; i <= r; i++)
    {
        if(arr[i] < v)
        {
            swap(arr[j+1], arr[i]);
            j++;
        }
    }
    //最后交换v和指针j所指向的元素
    swap(arr[j], arr[l]);
    //将j的位置返回
    return j;
}

template<typename T>
void __quickSort(T arr[], int l, int r)
{
    //当元素格式小于等于15时,使用插入排序替换快速排序
    if( r - l <= 15 ){
        insertionSort(arr,l,r);
        return;
    }
    //设置时间种子,为随机化选取标定点做准备
    srand(time(NULL));
    //得到标定点的位置
    int p = __partition(arr, l, r);
    //对标定点左边的子数组进行排序
    __quickSort(arr, l, p-1);
    //对标定点右边的子数组进行排序
    __quickSort(arr, p+1, r);
}

上面的第二种优化方法虽然在数组基本有序时能够摆脱退化为O(n^2)的命运,但是还有一种常见的情况会让我们上面的快排退化为O(n^2),那就是在数组中包含大量重复元素时。接下来介绍一种十分高大上的优化方法,能够有效解决大量重复元素的问题,这种方法被广泛地应用,例如java标准库中的sort()方法就是使用了这种优化后的快排。这种方法称为三路快速排序,先来看图:
这里写图片描述
使用我们之前的快排,在包含大量重复元素时,partition操作会将数组分为两个大小极度不平衡的两部分,对右边那个大块头进行递归调用的话,调用的层数接近与n层,这便会使得时间复杂度退化为O(n^2),解决方法是下面的三路快排,将数组分为小于v,等于v和大于v三部分,对于中间等于v的部分,因为它们已经在正确的位置上,下一次递归将不对这部分进行操作,这就使得待处理的数据大大减少,如下图所示:
这里写图片描述
指针lt指向小于v的最后一个元素,指针i指向当前正在考察的元素e,指针gt指向大于v的最后一个元素,大于v部分从右向左扩展。当e==v时,指针i直接右移,当e小于v时,i指向的元素和lt+1指向的元素进行交换,并且i指针和lt指针都向右移动一个单位。当e大于v时,将i指向的元素和gt-1指向的元素进行交换,交换后,gt指针向左移动一个单位,i指针不需要动。上述就是整个三路快排的基本操作,经过这个操作后,只需要对小于v部分和大于v部分再递归调用快排即可。下面是三路快排的c++代码实现:

template<typename T>
void __quickSort3Ways( T arr[], int l, int r ){
    //当元素个数小于等于15时,使用插入排序替代快排
    if ( r - l <= 15 ){
        insertionSort( arr, l , r );
        return;
    }
    //随机化选取标定点
    swap(arr[l],arr[rand()%(r-l+1)+l]);
    T v = arr[l];
    int lt = l;//arr[l+1,lt] < v
    int gt = r + 1;//arr[gt,r] > v
    int i = l + 1;//arr[lt+1,i) == v
    while( i < gt ){
        if( arr[i] == v ){
            i++;
        }
        else if( arr[i] > v ){
            swap( arr[i] , arr[gt-1] );
            gt--;
        }
        else{
            swap( arr[lt+1] , arr[i] );
            i++;
            lt++;
        }
    }
    swap( arr[lt], arr[l] );
    //对小于v部分进行排序
    __quickSort3Ways( arr , l , lt - 1 );
    //对大于v部分进行排序
    __quickSort3Ways( arr , gt , r );
}

template<typename T>
void quickSort3Ways( T arr[] , int n ){
    //设置时间种子
    srand(time(NULL));
    __quickSort3Ways( arr, 0 , n - 1 );
}

三路快排是一种比较优秀的优化快速排序,很多系统级别的排序都是使用三路快排,掌握了三路快排,相信你已经对快速排序算法有了比较深入的理解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值