STL排序

本文深入探讨了STL中的排序算法,包括全排序、部分排序及特定元素定位等应用场景,对比了不同算法的性能与适用场景。

排序一直是数据结构中的常用算法,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_sortnth_element,你只能间接使用。一个可选的方法是把list中的元素拷贝到带有随机迭代器的容器中,然后再使用这些算法;另一种是生成一个包含list::iterator的容器,直接对容器内的list::iterator进行排序,然后通过list::iterator得到所指的元素;第三种方法,借助一个包含iterator的 有序容器,然后反复把list中的元素连接到你想要链接的位置。看见了吧,你可以选择的方式还是比较多的。

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

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

  • 若需对vector, string, deque,  array容器进行全排序,你可选择sortstable_sort

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

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

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

  • 若使用的list容器,你可以直接使用partitionstable_partition算法,你可以使用list::sort代替sort和stable_sort排序。若你需要得到partial_sortnth_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。

<think>嗯,我现在要了解C++ STL中的sort函数的使用和实现。首先,我需要回顾一下自己之前学过的内容,或者有没有相关的资料可以参考。记得STL中的sort函数不是简单的快速排序,而是结合了多种排序算法来优化性能,比如内省排序和插入排序。不过具体的实现细节可能不太清楚,需要查证。 根据用户提供的引用资料,比如引用[1]提到STL的sort做了很多优化,避免最差情况退化成O(n²),而引用[5]则说明底层用了内省式排序和插入排序。内省排序结合了快速排序、堆排序和插入排序,这可能是在递归深度超过一定限制后切换到堆排序来避免快速排序的最坏情况。那具体是如何工作的呢? 然后是sort的使用方法。引用[2]中的例子显示可以对vector进行排序,包括整个容器或者部分范围。sort函数的参数是迭代器,比如vec.begin()和vec.end(),或者vec.begin() + 3来指定部分排序。这说明sort函数可以处理不同的容器,比如vector和deque,因为引用[4]指出只有vector和deque适用sort算法,其他如stack、queue等不适合。 接下来需要整理sort的使用方法,包括基本用法、自定义比较函数,以及适用的容器类型。然后是实现原理部分,要解释内省排序、插入排序的应用,以及如何避免快速排序的缺点。可能还要提到递归深度控制,比如当深度超过log2(n)时转为堆排序,这样即使遇到最坏情况,时间复杂度还是维持在O(n log n)。 另外,引用[3]提到用户自己实现的快速排序在单链表排序时超时,而STL的sort却能通过,这说明STL的实现确实更高效,可能是因为避免了递归过深的问题,或者对小数组用了插入排序优化。插入排序在小数据量时效率更高,所以可能在sort实现中,当元素数量小于某个阈值时切换到插入排序,这样可以减少递归调用的开销。 在回答用户的问题时,需要结构清晰,分点说明使用方法和实现原理,同时结合引用资料中的例子和解释。比如在用法部分,可以给出基本排序、部分排序、自定义比较的代码示例;在实现部分,解释内省排序、堆排序的切换条件,插入排序的应用场景,以及这些优化如何保证时间复杂度稳定在O(n log n)。 最后,生成相关问题时要围绕用户可能感兴趣的方面,比如性能优化方法、适用容器、自定义比较函数的使用,或者不同排序算法的比较等。例如,用户可能想知道为什么STL的sort比普通快排更好,或者如何自定义排序规则。</think>### C++ STL `sort` 使用与实现详解 #### 一、`sort` 的基本用法 1. **默认排序(升序)** 对容器(如 `vector`、`deque`)的全部或部分元素进行排序: ```cpp #include <algorithm> #include <vector> using namespace std; int main() { vector<int> vec = {5, 1, 6, 8, 2, 7, 9}; sort(vec.begin(), vec.end()); // 全范围排序,结果为 {1,2,5,6,7,8,9} sort(vec.begin(), vec.begin() + 3); // 仅对前3个元素排序,结果为 {1,5,6,8,2,7,9} [^2] return 0; } ``` 2. **自定义排序规则** 通过函数指针、Lambda 或函数对象定义比较逻辑: ```cpp // 降序排序 sort(vec.begin(), vec.end(), greater<int>()); // 按字符串长度排序 vector<string> strs = {"apple", "cat", "banana"}; sort(strs.begin(), strs.end(), [](const string& a, const string& b) { return a.size() < b.size(); }); ``` 3. **适用容器** - **支持容器**:`vector`、`deque`(需要随机访问迭代器)[^4] - **不适用容器**:`list`(需用 `list::sort` 方法)、关联式容器(如 `map` 已自动排序) --- #### 二、`sort` 的底层实现 STL `sort` 结合了三种算法优化性能,保证时间复杂度稳定为 $O(n \log n)$,避免快速排序的最坏情况 $O(n^2)$[^1][^5]。 1. **内省式排序(Introsort)** - **快速排序**:初始阶段使用快速排序,递归划分区间。 - **堆排序**:当递归深度超过 $\log_2 n$ 时,切换为堆排序,避免快排退化[^5]。 - **插入排序**:当区间元素数量 ≤ 16 时,改用插入排序(小数据效率更高)[^3]。 2. **关键优化点** - **基准值选择**:采用三点中值法(首、中、尾元素的中位数)避免快排最坏情况。 - **递归深度限制**:通过 $\log_2 n$ 限制深度,强制转为堆排序。 --- #### 三、性能对比 | 场景 | 普通快排 | STL `sort` | |---------------------|-------------|------------------| | 平均时间复杂度 | $O(n \log n)$ | $O(n \log n)$ | | 最坏时间复杂度 | $O(n^2)$ | $O(n \log n)$ | | 适用数据量 | 通用 | 通用(优化小数据)| ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值