快速排序及优化(三路划分等)

原创 2017年04月19日 00:36:02

快速排序, 是最经典的排序算法之一。快速排序拥有良好的时间复杂度,平均为O(nlog2n),最差为O(n2)。在这里,我们不妨略略深入讨论一下快速排序:

时间复杂度分析

首先说平均时间复杂度。以比较常用的从两头进行扫描的算法为例,算法主要分两步:
1. 是快排的核心:“分趟”。就是“每一趟”下来,找到某一个元素应该待的位置,这个元素一般被称为pivot;
2.再分别对pivot前后两部分进行递归排序。

#include <iostream>
using namespace std;
int partition(int *a, int left, int right)
{
    int key = a[left];
    while(left < right){
        while(left < right && a[right] >= key) right--;     //从右找到第一个比key小的
        a[left] = a[right];
        while(left < right && a[left] <= key) left++;       //从左找到第一个比key大的
        a[right] = a[left];   
    }
    a[left] = key;                                          //基准归位
    return left;    
}
void Qsort(int *a, int left, int right)
{
    if(left < right){                                       //元素长度>1时
        int pos = partition(a, left, right);
        Qsort(a, left, pos - 1);                            //pos本身不需要再动了
        Qsort(a, pos + 1, right);
    }
}
int main()
{
    int a[] = {57, 68, 59, 52, 72, 28, 96, 33, 24};

    Qsort(a, 0, sizeof(a) / sizeof(a[0]) - 1);

    for(int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
    {
        cout << a[i] << " ";
    }

    return 0;
}

显然,一趟下来,pivot被固定的位置越趋于中间,前后两部分子序列的递归调用就越均衡,这时候时间复杂度是最小的。

    T(n) <= n + 2T(n/2)
         <= 2n + 4T(n/4)
         <= 3n + 8T(n/8)
         ...
         <= (log n)n + nT(1) = O(nlog n)

因此,为O(nlog2n)

最差的情况下,也就是pivot被固定后的位置总是在最前面或最后面,导致前后两部分子序列实际只是一个子序列。这也就意味着,原代排序列本身就是有序的,要么从小到大,要么从大到小。比如从小到大:此时,第一趟经过n-1次比较,将第一个元素固定在首位;第二趟经过n-2次比较,将第二个元素固定在第二位,以此类推,n个元素总共要比较1+2+3+...+(n1)=n(n1)/2次,所以复杂度为O(n2)。当然,如果从简单形象的角度去理解,一般的快排执行过程大概是二叉树形结构,而最差情况则是退化成了链表。

优化

优化大致有三种比较有效的方法。

使用插入排序

在子序列比较小的时候,其实插排是比较快的,因为对于有序的序列,插排可以达到O(n)的复杂度,如果序列比较小,则和大序列比起来会更加有序,这时候使用插排效率要比快排高。其实现方法也很简单:快排是在子序列元素个数变成1是,才停止递归,我们可以设置一个阈值n,假设为5,则大于5个元素,子序列继续递归,否则选用插排。(其实在C++的STL中,归并算法就是采用了这个思路,当子序列小到一定程度的时候,直接选用插排对子序列进行排序)

快排是在待排数列越趋近于有序时变得越慢,复杂度越高,调用插排可以很好的解决这个问题。

pivot选用中位数

对于一般的快排,我们直接简单的就取最左或最右的数据作为pivot,这样的话很可能遇到比较极端的pivot,使得划分出来的左右子序列变得不均衡。如果选取最左、中间、最右这三个值的中位数的话,显然会使得pivot更加“不偏激”,这样划分出来的左右子序列也会更加均衡。

选用中位数和调用插排一样,都能避免数列比较有序时复杂度变高的问题。

三路划分

快排是二路划分的算法。如果待排序列中重复元素过多,也会大大影响排序的性能。这时候,如果采用三路划分,则会很好的避免这个问题。

如果一个带排序列重复元素过多,我们先随机选取一个pivot,设为T,那么数列可以分为三部分:小于T,等于T,大于T:

等于T的部分就无需再参与后续的递归调用了,速度自然就大大提升了。

但是问题在于怎么高效地将序列划分为三部分!

如下图,我们可以设置四个游标,左端a、b,右端c、d。b、c的作用跟之前两路划分时候的左右游标相同,就是从两端向中间遍历序列,并将遍历到的元素与pivot比较,如果等于pivot,则移到两端(b对应的元素移到左端,c对应的元素移到右端。移动的方式就是拿此元素和a或d对应的元素进行交换,所以a和d的作用就是记录等于pivot的元素移动过后的边界),反之,如果大于或小于pivot,还按照之前两路划分的方式进行移动。这样一来,中间部分就和两路划分相同,两头是等于pivot的部分,我们只需要将这两部分移动到中间即可。


参考算法如下,摘自http://blog.csdn.net/jlqCloud/article/details/46939703

private void quickSort(int[] a, int left, int right) {
    if (right <= left)
        return;

    /* 
     * 工作指针
     * p指向序列左边等于pivot元素的位置
     * q指向序列右边等于Pivot元素的位置
     * i指向从左向右扫面时的元素
     * j指向从右向左扫描时的元素
     */
    int p, q, i, j;
    int pivot;// 锚点
    i = p = left;
    j = q = right - 1;
    /*
     * 每次总是取序列最右边的元素为锚点
     */
    pivot = a[right];
    while (true) {
        /*
         * 工作指针i从右向左不断扫描,找小于或者等于锚点元素的元素
         */
        while (i < right && a[i] <= pivot) {
            /*
             * 找到与锚点元素相等的元素将其交换到p所指示的位置
             */
            if (a[i] == pivot) {
                swap(a, i, p);
                p++;
            }
            i++;
        }
        /*
         * 工作指针j从左向右不断扫描,找大于或者等于锚点元素的元素
         */
        while (left <= j && a[j] >= pivot) {
            /*
             * 找到与锚点元素相等的元素将其交换到q所指示的位置
             */
            if (a[j] == pivot) {
                swap(a, j, q);
                q--;
            }
            j--;
        }
        /*
         * 如果两个工作指针i j相遇则一趟遍历结束
         */
        if (i >= j)
            break;

        /*
         * 将左边大于pivot的元素与右边小于pivot元素进行交换
         */
        swap(a, i, j);
        i++;
        j--;
    }
    /*
     * 因为工作指针i指向的是当前需要处理元素的下一个元素
     * 故而需要退回到当前元素的实际位置,然后将等于pivot元素交换到序列中间
     */
    i--;
    p--;
    while (p >= left) {
        swap(a, i, p);
        i--;
        p--;
    }
    /*
     * 因为工作指针j指向的是当前需要处理元素的上一个元素
     * 故而需要退回到当前元素的实际位置,然后将等于pivot元素交换到序列中间
     */
    j++;
    q++;
    while (q <= right) {
        swap(a, j, q);
        j++;
        q++;
    }

    /*
     * 递归遍历左右子序列
     */
    quickSort(a, left, i);
    quickSort(a, j, right);
}

private void quick(int[] a) {
    if (a.length > 0) {
        quickSort(a, 0, a.length - 1);
    }
}

private void swap(int[] arr, int a, int b) {
    int temp = arr[a];
    arr[a] = arr[b];
    arr[b] = temp;
}

三路划分可以避免很多重复元素再次参与递归,对于有大量重复元素的待排序列,效率提高了不少。

以上只是理论上的总结,当然实践起来代码也不难写。在这里推荐一篇有码有实验数据的文章,看后也是更加直观形象,受益匪浅。

快排的优化其实对于一个计算机科学与技术的入门者来讲,是一个不错的思维上的砥砺,这种类型的东西多多探索,计算机科学“素养”自然慢慢就上去了。

版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

排序算法之三路划分的快速排序

当待排序元素序列中有大量的重复排序码时,简单的快速排序算法的效率将会降到非常之低。一种直接的想法就是将待排序列分成三个子序列:一部分是排序码比基准元素排序码小的;一部分是与基准元素排序码等值的;一部分...

算法之道(二):快速排序—三路快排 vs 双基准

快速排序被公认为是本世纪最重要的算法之一,这已经不是什么新闻了。对很多语言来说是实际系统排序,包括在Java中的Arrays.sort。 那么快速排序有什么新进展呢? 好吧,就像我刚才提到的那样(...

三种快速排序算法的实现(递归算法、非递归算法、三路划分快速排序)

快速排序的三个步骤: 1、分解:将数组A[l...r]划分成两个(可能空)子数组A[l...p-1]和A[p+1...r],使得A[l...p-1]中的每个元素都小于等于A(p),而且,小于等于A[...

C++面试100经典

1.    面向对象的程序设计思想是什么? 答:把数据结构和对数据结构进行操作的方法封装形成一个个的对象。 2.    什么是类? 答:把一些具有共性的对象归类后形成一个集合,也就是所谓...

快速排序的三者取中划分

使用一个尽可能在文件中间划分的元素。 一种选择划分元素的方法是从文件中取出三个元素,使用三个元素的中间元素作为划分元素。取数组中的左边元素、中间元素和右边元素,对这三个元素排序。以中间值为划分元素! ...

找工作知识储备(2)---数组字符串那些经典算法:最大子序列和,最长递增子序列,最长公共子串,最长公共子序列,字符串编辑距离,最长不重复子串,最长回文子串

0、前言         这一部分的内容原本是打算在之后的字符串或者数组专题里面写的,但看着目前火热进行的各家互联网公司笔试面试中,出现了其中的一两个内容,就随即将这些经典问题整理整理,单写一篇发...

<算法>基于三路划分的链表快速排序

此算法基于三路划分的快速排序的思想,如果不懂请先看我之前的博客: http://blog.csdn.net/gpwner/article/details/76039533但是又不完全和之前将的三路划...

快速排序(2)算法改进--小的子文件、三者取中、重复关键字三路划分

标签(空格分隔): 排序算法 1. 小的子文件由于快速排序会递归的调用自身的许多小文件,因而要对小的子文件尽可能使用高效的算法 三者取其中快速排序 重复关键字-三路划分快速排序

算法<基于三路划分的快速排序>

当待排序元素序列中有大量的重复排序码时,简单的快速排序算法的效率将会降到非常之低。一种直接的想法就是将待排序列分成三个子序列:一部分是排序码比基准元素排序码小的;一部分是与基准元素排序码等值的;一部分...

三路快速排序

核心实现/** * 三路快速排序 */ public class QuickSort3Ways { public static void sort(Integer[] arr){ ...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)