posts - 111, comments - 35, trackbacks - 0, articles - 0
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理

公告

理解你的排序操作(stable_sort,sort,partial_sort,nth_element,stable_partition,partition)

Posted on 2011-05-25 20:48 李大嘴 阅读(...) 评论(...) 编辑 收藏

排序一直是数据结构中的常用算法,STL提供的排序算法非常丰富,如何有效使用就值得探讨。在网上没有找到条款31的翻译,于是我自己翻译了。--Winter

如何进行排序?让我数数有几种方法。

一旦程序员需对容器元素进行排序,sort算法马上就会出现在他的脑海(可能有些程序员会想到qsort,但详细阅读条款46后,他们会放弃使用qsort的想法,转而使用sort算法)。

sort是一个非常优秀的算法,但并当你并不真正需要它的时候,其实就是一种浪费。有时你并不需要一个完整的排序(简称为全排序)。例如,你现有一 个包含Widget对象(Widget意为“小挂件”)的vector容器,并且你想把其中质量最好的20个Widget送给你最好的客户,那么你需做的 只是找出这20个质量最好的Widget元素,剩下的并不需关心它们的顺序。这时你需要的是部分排序(相对于全排序),恰好在算法中就有一个名副其实的部 分排序函数函数:partial_sort:

bool qualityCompare(const Widget& lhs, const Widget& rhs)
{
            // lhs的质量不比rhs的质量差时返回true,否则返回false
}

partial_sort (widgets.begin(),                         // 把质量最好的20元素
                    widgets.begin() + 20,                 // 顺序放入widgets容器中
                    widgets.end(),                            
                    qualityCompare);
…                                                                 // 使用 widgets...

通过调用partial_sort,容器中开始的20个元素就是你需要的质量最好的20个Widget,并按顺序排列,质量排第一的就是 widgets[0], 紧接着就是widgets[1],依次类推。这样你就可以把质量第一的Widget送给你最好的顾客,质量第二的Widget就可以送给下一个顾客,很方 便吧,更方便的还在后面呢。

如果你只是想把这20个质量最好的Widget礼物送给你最好的20位顾客,而不需要给他们一一对应,partial_sort在这里就有些显得大 材小用了。因为在这里,你只需要找出这20个元素,而不需要对它们本身进行排序。这时你需要的不是partial_sort,而是 nth_element。

nth_element排序算法只是对一个区间进行排序,一直到你指定的第n个位置上放上正确的元素为止,也就是说,和你进行全排序和 nth_element排序相比,其共同点就是第n个位置是同一个元素。当nth_element函数运行后,在全排序中应该在位置n之后的元素不会出现 在n的前面,应该在位置n前面的元素也不会出现在n的后面。听起来有些费解,主要是我不得不谨慎地使用我的语言来描述nth_element的功能。别着 急,呆会儿我会给你解释为什么,现在先让我们来看看nth_element是如何让质量最好的20个widget礼物排在vector容器的前面的:

nth_element (widgets.begin(),             // 把质量最好的20元素放在
                      widgets.begin() + 20,     // widgets容器的前面,
                      widgets.end(),                // 但并不关心这20个元素
                      qualityCompare);            //本身内部的顺序

你可以看出,调用nth_element函数和调用partial_sort函数在本质上没有区别,唯一的不同在于partial_sort把前 20个元素还进行排列了,而nth_element并不关系他们内部的顺序。两个算法都实现了同样的功能:把质量最好的20个元素放在vector容器的 开始部分。

这样引起了一个重要的问题:要是质量一样的元素,排序算法将会如何处理呢?假设有12个元素的质量都为1(最好等级),15个元素的质量等级为 2(质量次之),如果要选择20个最好的Widget,则先选12个质量为1的元素,然后从15个中选出8个质量为2的元素。到底nth_element 和partial_sort如何从15个中选出8个,依据何在?换句话说,当多个元素有同样的比较值时,排序算法如何决定谁先谁后?

对于partial_sort和nth_element算法来说,你无法控制它们,对于比较值相同的元素它们想怎么排就怎么排(查看条款19,了解 两个值相等的定义)。在我们上面的例子中,面对需要从15个等级为2的元素选出8个增加到top 20中去,他们会任意选取。这样做也有它的道理:如果你要求得到20个质量最好的Widget,同时有些Widget的质量又一样,当你得到20个元素至 少不比剩下的那些质量差,这已经达到你的要求,你就不能抱怨什么了。

假如对于全排序,你倒是可以得到更多的控制权。一些排序算法是“稳定的”(stable),在一个“稳定”的排序算法中,如果两个元素有相同的值, 它们的相对位置在排序后也会保持不变。例如:如果在未排序时Widget A在Widget B之前,而且都有相同的质量等级,那么“稳定”排序算法就可以保证在排序之后,Widget A仍然在Widget B之前。而非“稳定”排序算法就不能保证这一点。

partial_sort和nth_element都不是“稳定”排序算法,真正的“稳定”排序算法是stable_sort,从名字上看就知道它 是“稳定”的。如果你在排序的时候需要保证相同元素的相对位置,你最好使用stable_sort,在STL中没有为partial_sort和 nth_element算法提供对应的“稳定”版本。

说到nth_element,名字确实很怪,但是功能却是不少,除了让你找到无关顺序的top n个元素外,它还能找到某个范围的中值,或者找到在某个特定百分点的值。

    vector<Widget>::iterator begin(widgets.begin());     // widgets的第一个
    vector<Widget>::iterator end(widgets.end());           //和最后一个迭代器
                                                                                      //
    vector<Widget>::iterator goalPosition;                      // 需要定位的那个迭代器

      //以下代码用来得到质量排在中间的那个元素的迭代器
    goalPosition = begin + widgets.size() / 2;             // 要找的那个元素应该
                                                                                 //在vector的中部。
    nth_element(begin, goalPosition, end,         // 找到容器widgets元素的中值
                        qualityCompare);                      //
    …                                                                     // goalPosition现在指向中值元素
   
    //以下代码用来得到质量排在75%的元素
    vector<Widget>::size_type goalOffset =               // 计算出要找的值
                                        0.25 * widgets.size();         //离begin迭代器的距离。
                                                                                  //
    nth_element( begin, begin + goalOffset, end,       // 得到质量排在75%的元素
                            qualityCompare);                           //

    …                                                 // goalPosition 现在指向质量排在75%的元素。

当你需要把一个集合由无序变成有序时,可选用sort, stable_sort或partial_sort,当你只需得到top n或者在某个特定位置的元素,你就可以使用nth_element。或许有时你的需求比nth_element提供的还要少,例如:你并不需要得到质量最 好的前20个Widget,而只需要识别那些质量等级为1或者等级为2的Widget。当然,你可以对整个vector按照Widget的质量等级进行全 排序,然后查找到第一个质量等级低于等级2的元素。

问题就在于全排序太耗费资源,而且大部分工作都是无用功。这种情况下最好选择partition算法,partition只是给你确定一个区间,把 符合特定条件的元素放到这个区间中。举例来说,要把质量等级好于等于等级2的Widget的元素放在widget容器的前端,我们可以定义一个用于识别 Widget质量等级的函数:

    bool hasAcceptableQuality(const Widget& w)
    {
        //如果w的质量等于或好于2,返回true,否则返回false
    }

然后把这个判断函数传递给partion算法:
    vector<Widget>::iterator goodEnd =   // 把所有满足hasAcceptableQuality
                    partition(widgets.begin(),     // 条件的放在widgets容器的前面,
                    widgets.end(),                     // 返回第一个不满足条件的
                    hasAcceptableQuality);       //元素的位置

这样一来,所有在迭代器widgets.begin()和迭代器goodEnd之间的元素都是满足需求的元素:其质量等级好于或等于2。而在 goodEnd 到 widgets.end() 之间的元素的质量等级都会低于质量等级2。如果你对质量等级相同元素的相对位置很关心的话,你可以选择stable_partition算法来代替 partition。

需要注意的是sort, stable_sort, partial_sort, 和nth_element算法都需要以随机迭代器(random access
iterators)为参数,因此这些算法能只能用于vector, string, deque, 和array等容器,对于标准的关联容器map、set、multmap、multset等,这些算法就有必要用了,这些容器本身的比较函数使得容器内所 有元素一直都是有序排列的。对于容器list,看似可以用这些排序算法,其实也是不可用的(其iterator的类型并不是随机迭代器),不过在需要的时 候可以使用list自带的排序函数sort(有趣的是list::sort函数和一个“稳定”排序函数的效果一样)。如果你想对一个list容器使用 partial_sort或nth_element,你只能间接使用。一个可选的方法是把list中的元素拷贝到带有随机迭代器的容器中,然后再使用这些 算法;另一种是生成一个包含list::iterator的容器,直接对容器内的list::iterator进行排序,然后通过 list::iterator得到所指的元素;第三种方法,借助一个包含iterator的有序容器,然后反复把list中的元素连接到你想要链接的位 置。看见了吧,你可以选择的方式还是比较多的。

partition 和stable_partition函数与sort、stable_sort、partial_sort、nth_element不一样,要求没有那么严 格,输入参数只需是双向迭代器(bidirectional iterator)。因此你可以对所有的标准序列容器使用partition和stable_partition算法。

让我们来总结一下你的排序操作:

若需对vector, string, deque, 或 array容器进行全排序,你可选择sort或stable_sort;

若只需对vector, string, deque, 或 array容器中取得top n的元素,部分排序partial_sort是首选.

若对于vector, string, deque, 或array容器,你需要找到第n个位置的元素或者你需要得到top n且不关系top n中的内部顺序,nth_element是最理想的;

若你需要从标准序列容器或者array中把满足某个条件或者不满足某个条件的元素分开,你最好使用partition或stable_partition;

若使用的list容器,你可以直接使用partition和stable_partition算法,你可以使用list::sort代替sort和 stable_sort排序。若你需要得到partial_sort或nth_element的排序效果,你必须间接使用。正如上面介绍的有几种方式可以 选择。

另外,你可以使用标准的关联容器来保证容器中所有元素在操作过程中一直有序。你还可考虑非标准的STL容器priority_queue,它同样可 以保证其元素在所有操作过程中一直有序(priority_queue在传统意义上属于STL的一部分,但根据“STL”定义,需要STL容器支持迭代 器,而priority_queue并不支持迭代器,故不能能称为标准STL容器)。

这时你或许会问:“性能如何?”非常好的问题。广义的讲,算法做的工作越多,花的时间越长,“稳定”性排序算法比“非稳定”性排序算法要耗时。我们可以按照其消耗的资源(时间和空间)的多少,对本文中讨论的排序算法作个排序,消耗资源越少的排在前面:

1. partition

2. stable_partition

3. nth_element

4. partial_sort

5. sort

6. stable_sort

选择这些算法的依据是你的需求而不是它们的性能。若你能选择一个算法恰好满足你的需求(如用部分排序代替全排序),不仅会清晰表达你的意图,而且能高效的使用STL。

附上一个小例子:

#include<iostream>
#include<vector>
#include<iterator>
#include<cstdlib>
#include<algorithm>
#include<functional>

using namespace std;

bool less5(int a)
{
return a<5;
}

int main()
{
    const size_t n=10;
    int a[10]={3,6,9,2,5,8,1,4,7,0};
    vector<int> ive(a,a+n);
    cout<<"After sort;\n";
    sort(a,a+n);
    for(int i=0;i<n;++i) cout<<a[i]<<" ";
    cout<<endl;
   
    cout<<"The top 4 number: "<<endl;
    partial_sort(ive.begin(),ive.begin()+4,ive.end(),greater<int>());
    copy(ive.begin(),ive.begin()+4,ostream_iterator<int>(cout," "));
   
    cout<<"\nThe 4th number: "<<endl;
    nth_element(ive.begin(),ive.begin()+3,ive.end(),greater<int>());   //注意是 ive.begin()+3
    cout<<ive[3];
   
    cout<<"\nThe top 4 number: "<<endl;
    copy(ive.begin(),ive.begin()+4,ostream_iterator<int>(cout," "));
   
    cout<<"\nThe numbers are divided less or greater 5:"<<endl;
    partition(ive.begin(),ive.end(),less5);
    copy(ive.begin(),ive.end(),ostream_iterator<int>(cout," "));
    cout<<endl;
   
    system("pause");
    return 0;
}