| 网站首页 | 业界新闻 | 小组 | 威客 | 人才 | 下载频道 | 博客 | 代码贴 | 在线编程 | 编程论坛
欢迎加入我们,一同切磋技术
用户名:   
 
密 码:  
共有 487 人关注过本帖
标题:算法进化历程之“快速排序”
只看楼主 加入收藏
巧若拙
Rank: 4
来 自:宁波余姚
等 级:业余侠客
威 望:1
帖 子:159
专家分:273
注 册:2014-8-24
结帖率:46.15%
收藏
已结贴  问题点数:2 回复次数:1 
算法进化历程之“快速排序”
算法进化历程之“快速排序”
巧若拙(欢迎转载,但请注明出处:http://blog.

快速排序是在实践中最快的已知排序算法,它的版本非常多,本文将给出常见的一些版本并分析其优化过程。
几乎所有的快速排序算法都要用到以下两个函数:
void QuickSort(ElemType a[], int n) //快速排序的驱动程序
{
    QSort(a, 0, n-1);
}

void Swap(ElemType *a, ElemType *b)//家喻户晓的交换函数
{
    ElemType c = *a;
    *a = *b;
    *b = c;
}

快速排序的主程序QSort()的实现根据其优化程度的不同有多个版本,最初级的版本是:
void QSort_1(ElemType a[], int left, int right)
{
    int pivot;
   
    if (left < right)
    {
        pivot = Partition_1(a, left, right); //快速排序分割函数
        QSort_1(a, left, pivot-1);
        QSort_1(a, pivot+1, right);
    }
}

由于分割函数的不同实现方式,导致的快速排序算法版本的多样性。
版本一:取最后一个元素为枢纽元
《算法导论》中给出的第一个版本是取最后一个元素为枢纽元,然后从左侧向右扫描数组,将比枢纽元pivotKey小的值放在左侧,最后把枢纽元交换到适合位置。代码如下:
int Partition_1(ElemType a[], int left, int right)
{
    ElemType pivotKey = a[right];
    int i, j;
   
    for (i=j=left; j<right; j++)//把比pivotKey小的值放在左侧
    {
        if (a[j] < pivotKey)
        {
            if (i != j)
                Swap(&a[i], &a[j]);
            i++;
        }
    }
    Swap(&a[right], &a[i]);
   
    return i;
}

版本二:取左侧第一个元素为枢纽元
因为更常见的办法是取第一个元素为枢纽元(之后本文版本2-5均采用此法),所以我把Partition_1()略作修改,得到Partition_2()。代码如下:
int Partition_2(ElemType a[], int left, int right)
{
    ElemType pivotKey = a[left];
    int i, j;
        
    for (i=j=right; j>left; j--)//把比pivotKey大的值放在右侧
    {
        if (a[j] > pivotKey)
        {
            if (i != j)
                Swap(&a[i], &a[j]);
            i--;
        }
    }
    Swap(&a[left], &a[i]);//可以确定a[i]<=a[left]
   
    return i;
}

版本三:从数组的两端交替向中间扫描,每趟做两次交换
    很明显,只向一个方向扫描,每遇到比枢纽元pivot小的值就交换到左侧适当位置,会造成很多低效的交换,即某个位置处的元素可能会交换两次。我们可以通过从数组的两端交替向中间扫描,每扫描一次就把“不合群”的元素交换到另一侧的方法,确保每个位置的元素最多只被交换一次,这样效率要高于版本二。
int Partition_3(ElemType a[], int left, int right)
{
    ElemType pivotKey = a[left];
        
    while (left < right)//从数组的两端交替向中间扫描,实际上每次被交换的值都是枢纽元
    {
        while (left < right && a[right] >= pivotKey)
            right--;
            
        if (left == right)
            break;
        Swap(&a[left++], &a[right]);//把比pivotKey t小的值放在左侧
        
        while (left < right && a[left] <= pivotKey)
            left++;
            
        if (left == right)
            break;
        Swap(&a[left], &a[right--]);//把比pivotKey大的值放在右侧
    }
   
    return left;//最后枢纽元在left所指处
}

版本四:从数组的两端交替向中间扫描,每趟只做一次交换
进一步观察Partition_3(),我们发现每趟扫描都要发生两次交换,而且每次交换都有枢纽元的参与,还是存在不少多余的交换。能不能每趟只做一次交换呢?可以。代码如下:
int Partition_4(ElemType a[], int left, int right)
{
    ElemType pivotKey = a[left];   
    int i = left;
            
    while (i < right)//从数组的两端交替向中间扫描,每趟只做一次交换,效率较高
    {
        while (i < right && a[right] >= pivotKey)
            right--;
        
        if (i == right)
            break;
        
        while (i < right && a[i] <= pivotKey)
            i++;
        
        if (i == right)
            break;
            
        Swap(&a[i], &a[right]);
    }
    a[left] = a[i]; //最终将枢纽元归位
    a[i] = pivotKey;
   
    return i;//最后枢纽元在i所指处
}

版本五:用新元素覆盖已备份的旧元素
Partition_4()的交换操作比Partition_3()少了一半,应该说效率提高了不少。但交换操作本身需要三个动作,能不能有更好的方法来代替Swap()函数呢?或者说能否一次性把所有的元素“分配”到适合的位置呢?有!那就是先保存好第一个需要替换的元素值,然后用新元素覆盖已保存的旧元素——即此时有新旧两个位置存储了新元素的值。如此依次覆盖替换,最后把第一个被替换的元素值存储到最后一个“新元素”的“旧位置”,即完成了所有元素的位置安排。代码如下:
int Partition_5(ElemType a[], int left, int right)
{
    ElemType pivotKey = a[left];
   
    while (left < right)//从数组的两端交替向中间扫描,用新元素替换已保存的“不合群”元素,比交换操作效率高
    {
        while (left < right && a[right] >= pivotKey)
            right--;
        if (left == right)
            break;
        //因为a[left]的值已经有备份,故可以被替代;同理,执行此操作后a[right]的值也已经备份了,原a[right]就可以被覆盖了
        a[left] = a[right];
        
        while (left < right && a[left] <= pivotKey)
            left++;
        if (left == right)
            break;
        a[right] = a[left];
    }
    a[left] = pivotKey;
   
    return left;//最后枢纽元在left所指处
}

版本六:随机分割
前面五个版本,通过改进扫描方式和交换元素的方法,提高了效率。但由于每趟扫描都取左边第一个元素为枢纽元,很可能造成划分的不平衡,而导致时间复杂度变坏。采用随机产生枢纽元位置的方法,可以更好的实现均衡分割。算法借鉴了Partition_4()的扫描和交换方式。代码如下:
int Partition_6(ElemType a[], int left, int right)
{
    ElemType pivotKey;   
    int i = rand() % (right-left+1) + left;//随机产生枢纽元位置
   
    Swap(&a[left], &a[i]); //把枢纽元存储到a[left]
    pivotKey = a[left];
    i = left;        
    while (i < right)//从数组的两端交替向中间扫描,每趟只做一次交换,效率较高
    {
        while (i < right && a[right] >= pivotKey)
            right--;
        
        if (i == right)
            break;
        
        while (i < right && a[i] <= pivotKey)
            i++;
        
        if (i == right)
            break;
            
        Swap(&a[i], &a[right]);
    }
    a[left] = a[i]; //最终将枢纽元归位
    a[i] = pivotKey;
   
    return i;//最后枢纽元在i所指处
}

版本七:三数中值分割
随机产生枢纽元位置固然不错,但生产随机数的成本本身就不低,所以更好的一个替代方案是使用数组左端,右端和中心位置上的三个元素的中值作为枢纽元。算法借鉴了Partition_5()的扫描和交换方式。
代码如下:
int Partition_7(ElemType a[], int left, int right)
{
    ElemType pivotKey = Median3(a, left, right);//三数中值分割,返回枢纽元
   
    while (left < right)//从数组的两端交替向中间扫描,用新元素替换已保存的“不合群”元素,比交换操作效率高
    {
        while (left < right && a[right] >= pivotKey)
            right--;
        if (left == right)
            break;
 
        a[left] = a[right];
        
        while (left < right && a[left] <= pivotKey)
            left++;
        if (left == right)
            break;
        a[right] = a[left];
    }
    a[left] = pivotKey;
   
    return left;//最后枢纽元在left所指处
}

ElemType Median3(ElemType a[], int left, int right)//三数中值分割,返回枢纽元a[left],并确保确保a[right]大于枢纽元  
{
    int mid = (left + right) / 2;

    //确保a[right]是最大的
    if (a[left] > a[right])
        Swap(&a[left], &a[right]);
    if (a[mid] > a[right])
        Swap(&a[mid], &a[right]);
        
    //把 a[left] 作为枢纽元返回 ,要求 a[left] >= a[mid]
    if (a[left] < a[mid])
        Swap(&a[left], &a[mid]);
        
    return a[left];
}

版本八:消除尾递归
分割函数的不同实现方式就介绍到这里,接下来我们对QSort()函数进行优化。
最简单的优化是消除尾递归。代码如下:
void QSort_2(ElemType a[], int left, int right)//消除尾递归,效率更高
{
    int pivot, i;
   
    while (left < right)
    {
        pivot = Partition_7(a, left, right);
        QSort_2(a, left, pivot-1);
        left = pivot+1; //使用迭代,消除尾递归
    }
}

版本九:用插入排序解决小数组
对于很小的数组(N<20),快速排序不如插入排序好。所以我们可以采取一个这样的策略:若数组长度小于MAXLENTH,采用直接插入排序,否则采用快速排序。
代码如下:
void QSort_3(ElemType a[], int left, int right)
{
    int pivot, i = left;
   
    while (i + MAXLENTH < right)
    {
        pivot = Partition_7(a, i, right);
        QSort_3(a, left, pivot-1);
        i = pivot + 1; //使用迭代,消除尾递归
    }
    InsertSort(a, i, right);
}

void InsertSort(ElemType a[], int left, int right)//直接插入排序
{
     int i, j;
     ElemType temp;
     
     for (i=left+1; i<=right; i++)
     {
         temp = a[i];
         for (j=i;(j>0) && (a[j-1]>temp); j--)
         {
             a[j] = a[j-1];
         }
         a[j] = temp;
     }
}

版本十:Hoare的原始版本
我在《数据结构与算法分析——C语言描述》(Mark Allen Weiss著)中看到了快速排序的一个好的实现方法(通过对比《算法导论》的描述,我认为这种方法很可能就是快速排序算法发明人Hoare最早提出的版本),该方法先利用Median3()函数,在实现三数分割的同时,对这三个元素也进行了排序(我的Median3()函数与书中的略有不同,但功能基本相似),这样就设置了边界哨兵,同时要求a[i] == pivotKey时也要停止扫描,这样就可以不必每次判断是否越界,非常适合元素相异的大型数组,效率可以提高一倍。
    代码如下:
void QSort_4(ElemType a[], int left, int right)
{
    ElemType pivotKey;
    int i, j;
   
    if (left + MAXLENTH < right)
    {
        pivotKey = Median3(a, left, right);//三数中值分割,返回枢纽元a[left],并确保确保a[right]大于枢纽元  
        i = left;
        j = right;
        while (1)//a[i] == pivotKey也交换,可以不设边界,效率较高
        {
            while (a[--j] > pivotKey) {}
            while (a[++i] < pivotKey) {}
            
            if (i < j)
                Swap(&a[i], &a[j]);
            else
                break;
        }
        Swap(&a[left], &a[j]);
   
        QSort_4(a, left, j-1);
        QSort_4(a, j+1, right);
    }
    else
    {
        InsertSort(a, left, right);
    }
}

消除尾递归的版本如下:
void QSort_5(ElemType a[], int left, int right)//a[i] == pivotKey也交换,可以不设边界(消除尾递归版本)
{
    ElemType pivotKey;
    int i, j;
   
    i = left;
    while (i + MAXLENTH < right)
    {
        pivotKey = Median3(a, i, right);//三数中值分割,返回枢纽元a[left],并确保确保a[right]大于枢纽元  
        left = i;
        j = right;
        while (1)//a[i] == pivotKey也交换,可以不设边界,效率较高
        {
            while (a[--j] > pivotKey) {}
            while (a[++i] < pivotKey) {}
            
            if (i < j)
                Swap(&a[i], &a[j]);
            else
                break;
        }
        Swap(&a[left], &a[j]);
        
        QSort_5(a, left, j-1);
        i = j + 1; //使用迭代,消除尾递归
    }
    InsertSort(a, i, right);
}

快速排序真的很快,但也很难正确实现,我上面列出的算法虽然都进行了小规模数据的测试,但不敢保证大规模数据时是否还能正确使用,请热心的您给测试一下吧。感谢您的阅读。
搜索更多相关主题的帖子: 主程序 
2014-10-15 23:18
dcl2014
Rank: 4
等 级:业余侠客
威 望:1
帖 子:58
专家分:273
注 册:2014-9-20
收藏
得分:2 
学习一下
2014-10-16 09:06
快速回复:算法进化历程之“快速排序”
数据加载中...
 
   



关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.071161 second(s), 7 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved