深入理解快速排序算法的稳定性

在初次接触排序算法稳定性这个概念时,我一直认为复杂度为O(n2)的算法是稳定的,复杂度为O(nlogn)的算法是不稳定的。当时是这样理解的,复杂度为O(n2)的算法不可能再坏,而复杂度为O(nlogn)的算法在极端情况下可能会退化为O(n2),例如快速排序。但其实这是错误的,稳定性的概念远没有这么复杂,它只表示两个值相同的元素在排序前后是否有位置变化。如果前后位置变化,则排序算法是稳定的,否则是不稳定的。稳定性的定义符合常理,两个值相同的元素无需再次交换位置,交换位置是做了一次无用功。

之前对稳定性这个概念认识很浅,只停留在知道这个名词的层次上,直到最近写代码遇到一个诡异的现象才让我对稳定性有了更深刻的认识。我现在需要对大量数据进行排序,数据范围有限。针对数据量的大小,可以选择快速排序和基数排序(可参考:基数排序的性能优化)。在此基础上,每一个数据都还伴随有一个属性,在排序过程中也要随之移动这个属性数据。一种可行方案是将数据和属性构成一个结构体,然后对结构体进行排序。在排序的过程中只有指针的移动,没有实际结构体的移动。这种方案涉及间接寻址,对现有代码改动较大,没有采用。

我采用的方案是在原始快速排序和基数排序基础上做修改,每当出现元素交换时,就将对应的属性也进行交换。按这种思路实现的快速排序代码如下:

void q_sort_with_time(int* a,int *b,int left,int right)
{
    if(left>=right) return;

    int i=left,j=right+1;
    int pivot=a[left];
    int b_pivot=b[left];

    while(true)
    {
        do
        {
            i++;
        }while(i<=right&&a[i]<pivot);

        do
        {
            j--;
        }while(j>left&&a[j]>pivot);

        if(i>=j) break;

        swap(&a[i],&a[j]);
        swap(&b[i],&b[j]);
    }

    a[left]=a[j];
    a[j]=pivot;

    b[left]=b[j];
    b[j]=b_pivot;

    q_sort_with_time(a,b,left,j-1);
    q_sort_with_time(a,b,j+1,right);
}

void quick_sort_with_time(int* a,int* b,int n)
{
    q_sort_with_time(a,b,0,n-1);
}

上述代码的结构很简单,就是在快排基础上增加属性元素的交换。基数排序代码如下:

const static int radix=1024;
static int p[]={0,10,20,30};

inline int get_part(int n,int i)
{
    return n>>p[i]&(radix-1);
}

void radix_sort_with_time(int* a,int* b,int n)
{
    int* bucket=(int*)malloc(sizeof(int)*n);
    int* b_bucket=(int*)malloc(sizeof(int)*n);
    int count[radix];

    for(int i=0;i<2;++i)
    {
        memset(count,0,sizeof(int)*radix);

        for(int j=0;j<n;++j)
        {
            count[get_part(a[j],i)]++;
        }

        for(int j=1;j<radix;++j)
        {
            count[j]+=count[j-1];
        }

        for(int j=n-1;j>=0;--j)
        {
            int k=get_part(a[j],i);
            bucket[count[k]-1]=a[j];
            b_bucket[count[k]-1]=b[j];
            count[k]--;
        }

        memcpy(a,bucket,sizeof(int)*n);
        memcpy(b,b_bucket,sizeof(int)*n);
    }

    free(bucket);
    free(b_bucket);
}

基数排序代码稍微有点复杂,需要重新开辟一个新的数组存放属性数据,但是思想还是在交换原始元素的同时交换属性元素。虽然思想很简单,但是还是对自己的实现心存疑虑,同时写了一个验证函数,验证最后的结果是否一致(最初的想法是如果正确实现则原始元素是排好序的,同时两种排序算法的属性元素也应该一致):

int equals(int* a,int* b,int* c,int* d,int n)
{
    for(int i=0;i<n;i++)
    {
        if(a[i]!=b[i])
        {
            printf("1---%d: %d is not equal with %d\n",i,a[i],b[i]);
            return 0;
        }
        if(c[i]!=d[i])
        {
            printf("2---%d: %d is not equal with %d,%d:%d\n",i,c[i],d[i],a[i],b[i]);
            return 0;
        }
    }

    return 1;
}

完成上面的代码,就可以开始测试。首先测试数组长度小于100时,没有问题,很沾沾自喜。然后测试1000,第一次没问题,重复测试就报错了。很奇怪,多次测试时有正确有错误,一时搞不清楚到底怎么回事。开始认为应该代码有bug,但是仔细debug了一天也没有搞明白到底哪个地方出错了。后来,换了一个思路:快排和基数排序都属于比较高级的排序,我找个最笨的排序先保证正确性,然后去和快排、基数排序比较。之后按照这个思路把冒泡排序的算法实现了:

void bubble_sort_with_time(int* a,int* b,int n)
{
    for(int i=n-1;i>0;i--)
    {
        for(int j=0;j<i;j++)
        {
            if(a[j]>a[j+1])
            {
                swap(&a[j],&a[j+1]);
                swap(&b[j],&b[j+1]);
            }
        }
    }
}

对于快排和基数排序,我开始认为是基数排序的算法实现错了,毕竟这个算法是首次使用。但是结果令我吃惊:冒泡排序的结果和基数排序的结果完全一致,反倒是快速排序和冒泡排序的结果不一致!太出乎意料了,问题找了半天没想到是出在自己擅长的快排上面。快排怎么可能会出错呢,毕竟用了这么久。很长时间内我也没有找到问题所在。后来我将equals方法修改了下,去掉return,把所有不匹配的数据都打印出来,一打印终于发现问题所在!

打印结果显示,错误总是出在两个值相同的元素间,一瞬间就明白怎么回事了:快速排序是不稳定排序,它可能会交换两个值相同的元素,所以这根本就不是一个bug。没想到debug两天原来是做的无用功。

为了能汲取教训,我又仔细分析了一下为什么快排是不稳定排序。出现不稳定的关键是两个do-while循环:

do
{
    i++;
}while(i<=right&&a[i]<pivot);

do
{
    j--;
}while(j>left&&a[j]>pivot);

两个循环在进行元素比较时,分别用了小于和大于操作(也可以改用小于等于和大于等于,但是对性能没有影响)。这就意味着如果出现和pivot值相同的元素,它都会被作为交换对象而移动到pivot的前面或者后面,这就出现了值相同的元素会交换顺序的问题,因而是不稳定的。

深入理解快速排序算法的稳定性

时间: 2024-10-27 18:12:36

深入理解快速排序算法的稳定性的相关文章

递归方法理解快速排序算法

快速排序是对冒泡排序的一种改进.它的基本思想是:通过一躺排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要 小,然后再按次方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列.最坏情况的时间复杂度为O(n2),最好 情况时间复杂度为O(nlog2n). 另外 java没指针概念 可以认为是句柄 假设要排序的数组是A[1]……A[N],首先任意选取一个数据(通常选用第一个数据)作为关键数据,然后将所有比它的数都放到它前面,所

算法的稳定性的理解

一.稳定排序的解释 首先,排序算法的稳定性大家应该都知道,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同.在简单形式化一下,如果Ai = Aj, Ai原来在位置前,排序后Ai还是要在Aj位置前. 二.稳定排序的好处 排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用.基数排序就 是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的. 举个例子: 这么说吧,一个班的

快速排序算法的简单理解

快速排序算法的简单理解 本文用的编程语言为python,简单阐释了作者对快速排序算法的学习心得,尽量用通俗易懂的方式向读者表达.如果文章中有什么纰漏与错误,请读者指正. 在了解快速排序之前,我们先来了解一下递归 递归 递归调用自己的函数 先来看一个函数 def (i): print(i) countdown(i-1) 这是一个不断递减的函数,如果调用这个函数,它会无限循环下去.这可不是一件好事.我们应该给予它一些限制,告诉它什么时候停止调用自己,什么时候调用自己.我们把这种限制分别叫做基线条件与

算法的稳定性

经常提到老师提到说到算法的稳定性,其实我并不是很理解算法的稳定性,所以今天准备进行一下总结. 通常所说的算法的稳定性,是指 假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的:否则称为不稳定的. 下面介绍常见的算法的稳定性: (1)冒泡排序  冒泡排序就是把小的元素往前调或者把大的元素往后调.比较是相邻的两个元素比较,交换也发生在这两个元素之间

iOS算法(一)置快速排序算法

快速排序是当遇到较大数据时,排序快,高效的方法(公司面试时,基本上会被问到...) 该方法的基本思想是: 1.先从数列中取出一个数作为基准数. 2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边. 3.再对左右区间重复第二步,直到各区间只有一个数. 简单地理解就是,找一个基准数(待排序的任意数,一般都是选定首元素),把比小于等于基准数的元素放到基准数的左边,把大于基准数的元素放在基准数的右边.排完之后,在把基准数的左边和右边各看成一个整体, 左边:继续选择基准数把小于等

数据结构中各种排列三平台搭建序算法的稳定性比较

1.简排列三平台搭建论坛:haozbbs.com Q1446595067单选择排序 2.堆排序 (1和2是属于选择排序) 3.直接插入排序 4.希尔排序 (3和4属于插入排序,有时把改进后的直接插入排序叫做二分插入) 5.冒泡排序 6.快速排序 (5和6属于交换排序.交换排序顾名思义是不停的交换数据位置.但实际上选择排序也在不停的交换元素,但次数较少,只有找到最大值才一次交换.侧重点还是在通过遍历或堆来选择出最值.而冒泡排序就是通过不停交换相邻元素得出最大值,快速排序也在不停交换元素使序列一步步

快速排序算法

快速排序的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的. 稳定性:快速排序是不稳定的排序 时间复杂度: 最好:O(nlogn) 最差:O(n^2) 辅助空间:O(logn) ~ O(n) /* 快速排序算法 */ #include <cstdio> /* 对给定的闭区间进行快速排序 */ void QSort(int* L, int low, int high){ if (low

关于快速排序算法(一个90%的人都不懂其原理、99.9%的人都不能正常写出来的算法.)

一.奇怪的现象 研究快速排序很久了,发现一个古怪的实情:这算法描述起来很简单,写一个正确的出来实在不容易.写一个优秀的快速排序算法更是难上加难. 也难怪该算法提出来过了很久才有人写出一个正确的算法,过了很久才优秀的版本出来. 二.原理描述 从数列中挑出一个元素,称为 "基准"(pivot), 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边).在这个分区退出之后,该基准就处于数列的中间位置.这个称为分区(partition)操作

快速排序算法(Quicksort)

快速排序算法是对集合中元素进行排序最通用的算法,俗称快排,其算法的时间复杂度为O(nlgn),空间复杂度为O(1). 我们举例来对其算法思路进行理解,譬如数组 A = { 4, 8, 1, 2, 9, 7, 3, 0, 5, 6 }; 第一步,以最后一个数6为基准,把小于等于6的数挪到数组左边,把大于6的数挪到数组右边. 那么结果为 { 4, 1, 2, 3, 0, 5, 8, 9, 7, 6 },这个时候再做一步,把8和6进行交换,得到{ 4, 1, 2, 3, 0, 5, 6, 9, 7,