STL源码剖析 6.7 其他算法

53 篇文章 1 订阅
#include <iostream>
#include <algorithm>
#include <iterator>
#include <set>
#include <vector>

template<class T>
struct display{
    void operator()(const T&x){
        std::cout << x << ' ';
    }
};

struct even{
    bool operator()(int x)const{
        return x%2 ? false : true;
    }
};

int main(int argc,char* argv[]) {
    int ia[] = {12,17,20,22,23,30,33,40};
    std::vector<int>iv{ia,ia+sizeof(ia)/sizeof(int)};

    std::cout << *std::lower_bound(iv.begin(),iv.end(),21) <<std::endl; //22
    std::cout << *std::upper_bound(iv.begin(),iv.end(),21) << std::endl;//22

    //面对有序区间(sorted range), 可以二分查找法寻找某个元素
    std::cout << std::binary_search(iv.begin(),iv.end(),33) << std::endl;// 1 (true)
    std::cout << std::binary_search(iv.begin(),iv.end(),34) << std::endl;// 0 (false)

    //下一个排列组合
    std::next_permutation(iv.begin(),iv.end());
    std::copy(iv.begin(),iv.end(),std::ostream_iterator<int>(std::cout," "));
    std::cout << std::endl;
    //12 17 20 22 23 30 40 33

    //上一个排列组合
    std::prev_permutation(iv.begin(),iv.end());
    std::copy(iv.begin(),iv.end(),std::ostream_iterator<int>(std::cout," "));
    std::cout << std::endl;
    //12 17 20 22 23 30 33 40

    // 随机重排
    std::random_shuffle(iv.begin(),iv.end());
    std::copy(iv.begin(),iv.end(),std::ostream_iterator<int>(std::cout," "));
    std::cout << std::endl;
    //33 12 30 20 17 23 22 40

    //将 iv.begin () +4 - iv.begin ()个元素排序,放进
    //[iv.begin() , iv.begin()+4)区间内。剩余兀素不保证维持原相对次序
    std::partial_sort(iv.begin(),iv.begin()+4,iv.end());
    std::copy(iv.begin(),iv.end(),std::ostream_iterator<int>{std::cout," "});
    std::cout << std::endl;
    //12 17 20 22 33 30 23 40

    //排序(缺省为递增排序)
    std::sort(iv.begin(),iv.end());
    std::copy(iv.begin(),iv.end(),std::ostream_iterator<int>(std::cout," "));
    std::cout << std::endl;
    //12 17 20 22 23 30 33 40

    //排序(指定为递减排序)
    std::sort(iv.begin(),iv.end(),std::greater<int>());
    std::copy(iv.begin(),iv.end(),std::ostream_iterator<int>{std::cout," "});
    std::cout << std::endl;
    //40 33 30 23 22 20 17 12

    // 在 iv尾端附加新元素,使成为40 33 30 23 22 20 17 12 22 30 17
    iv.push_back(22);
    iv.push_back(30);
    iv.push_back(17);

    //排序,并保持“原相对位置”
    std::stable_sort(iv.begin(),iv.end());
    std::copy(iv.begin(),iv.end(),std::ostream_iterator<int>{std::cout," "});
    std::cout << std::endl;
    //12 17 17 20 22 22 23 30 30 33 40

    // 面对一个有序区间,找出其中的一个子区间,其内每个元素都与某特定元素值相同;
    // 返回该子区间的头尾迭代器
    // 如果没有这样的子区间,返回的头尾迭代器均指向该特定元素可插入
    // (并仍保持排序)的地点
    std::pair<std::vector<int>::iterator,std::vector<int>::iterator>pairIte{};
    pairIte = std::equal_range(iv.begin(),iv.end(),22);
    std::cout << *(pairIte.first) << std::endl; //22
    std::cout << *(pairIte.second) << std::endl;//23

    pairIte = std::equal_range(iv.begin(),iv.end(),25);
    std::cout << *(pairIte.first) << std::endl; //30
    std::cout << *(pairIte.second) << std::endl;//30

    std::random_shuffle(iv.begin(),iv.end());
    std::copy(iv.begin(),iv.end(),std::ostream_iterator<int>{std::cout," "});
    std::cout << std::endl;

    // 将小于*(iv.begin()+5)(本例为40) 的元素置于该元素之左
    // 其余置于该元素之右。不保证维持原有的相对位置
    std::nth_element(iv.begin(),iv.begin()+5,iv.end());
    std::copy(iv.begin(),iv.end(),std::ostream_iterator<int>{std::cout," "});
    std::cout << std::endl;
    // 20 12 22 17 17 22 23 30 30 33 40

    // 将大于* (iv.begin() +5)(本例为22 ) 的元素置于该元素之左
    // 其余置于该元素之右。不保证维持原有的相对位置
    std::nth_element(iv.begin(),iv.begin()+5,iv.end(),std::greater<int>{});
    std::copy(iv.begin(),iv.end(),std::ostream_iterator<int>{std::cout," "});
    std::cout << std::endl;
    // 40 33 30 30 23 22 17 17 22 12 20

    // 以 “是否符合even()条件”为依据,将符合者置于左段,不符合者置于右段
    //保证维持原有的相对位置•如不需要“维持原有的相对位置”,可改用partition ()
    std::stable_partition(iv.begin(),iv.end(),even());
    std::copy(iv.begin(),iv.end(),std::ostream_iterator<int>(std::cout," "));
    std::cout << std::endl;
    // 40 30 30 22 22 12 20 33 23 17 17


    return 0;
}

6.7.2 lower_bound (应用于有序区间)

  • 这是二分查找(binary search)的一种版本,试图在已排序的[first, last)中 寻找兀素value ;如果[first, last)具有与value相等的元素(s), 便返回一个迭 代器,指向其中第一个元素。
  • 如果没有这样的元素存在,便返回 “假设这样的元素存在时应该出现的位置”。也就是说,它会返回一个迭代器,指向第一个“不小于value”的兀素。如果value大于[first, last)内的任何一个元素,则返 回last  以稍许不同的观点来看lower_bound,其返回值是“在不破坏排序状态 的原则下,可插入value的第一个位置”。见图6-7。

  •  这个算法有两个版本,版本一采用operator<进行比较,版本二采用仿函数 comp 
  • 更正式地说,版本一返回 [first,last) 中 最 远 的 迭 代 器 使 得 【first, i) 中的每个迭代器j 都满足 *j < value
  •  版本二返回[first, last)中最远的迭 代器i , 使 [first, i ) 中的每个迭代器j 都 满 足 comp(*j , value)为真”。

 6.7.3 upper_bound (应用于有序区间)

  • 算 法 upper_bound是二分查找(binary search) 法的一个版本。它试图在已 排序的[first, last)中寻找value.更明确地说,它 会 返 回 “在不破坏顺序的 情况下,可插入value的最后一个合适位置”。见图6-7。
  • 由于STL规范"区间圈定”时的起头和结尾并不对称(是的, [first,last) 包 含 first:但不包含last), 所 以 upper_bound与 lower_bound的返回值意 义大有不同。如果你查找某值,而它的确出现在区间之内,则 lower_bound返回 的是一个指向该元素的迭代器。但是upper_bound 所返回的是在不破坏排序状态的情况下 , value 可被插入的“最后一个”合适位 置 .如 果 value存在,那么它返回的迭代器将指向value的下一位置,而非指 向value本身。
  • upper_bound有两个版本,版本一采用operator<进行比较,版本二采用仿 函 数 compo更正式地说,版本一返回[first,last)区间内最远的迭代器1 , 使 (firsts)内的每个迭代器j 都 满 足 Mvalue < * j 不为真” • 版本二返回 [first.last)区间内最远的迭代器i , 使 【first, i ) 中的每个迭代器j 都满足 comp (value, * j ) 不为 真 "。 

 6.7.4 binary_search (应用于有序区间)

  • 算法binary ^search是一种二分查找法,试图在已排序的[first, last)中 寻找兀素value..如 果 [first, last)内有等同于value的兀素,便返回true, 否则返回false。
  • 返回单纯的 bool或许不能满足你,前 面 所 介 绍 的 lower_bound和upper_bound能够提供额外的信息。事实上bin&ry_search便是利用lower_bound先找出“假设value存在的话,应该出现的位置”,然后再对比该位置上的值是 否为我们所要查找的目标,并返回对比结果

 6.7.5 next_perm utation

  • S T L 提供了两个用来计算排列组合关系的算法,分别是 next_permucation prev_permutation。
  • 首先我们必须了解什么是“下一个”排列组合,什么是“前 一个”排列组合。考虑三个字符所组成的序列{a,b,c}。这个序列有六个可能的排 列组合:abc, acb, bac, bca, cab, cba。这些排列组合根据less-than操作符 做字典顺序(lexicographical)的排序。也就是说,a b c 名列第一,因为每一个元 素都小于其后的元素。acb是次~个排列组合,因为它是固定了 a (序列内最小元 素)之后所做的新组合。
  • 同样道理,那些固定b (序列内次小元素)而做的排列组 合,在次序上将先于那些固定c而做的排列组合。以bac和b c a 为例,bac在bca 之前,因为序列a c 小于序列ca。面 对 bca,我们可以说其前一个排列组合是 bac,而其后一个排列组合是cab.
  • 序 列 abc没 有 “前一个”排列组合,cba没 有 “后一个”排列组合。
  • next_permutation ( ) 会 取 得 [first, last)所标25之序列的下一个排列组 合。如果没有下一个排列组合,便返回false;否则返回true。 

random_shuffle

  • 这个算法将[first z last)的兀素次序随机重排。也就是说,在 N! 种可能的 元素排列顺序中随机选出一种,此处 N 为 last-first。
  • N 个元素的序列,其排列方式有N ! 种,random_shuffle会产生一个均匀分 布,因此任何一个排列被选中的机率为1/N!o这很重要,因为有不少算法在其第 一阶段过程中必须获得序列的随机重排,但如果其结果未能形成“在 N!个可能排列上均匀分布(uniform distribution) , 便很容易造成算法的错误。
  • random_shuffle有两个版本,差别在于随机数的取得.版本一使用内部随机 数产生器,版本二使用一个会产生随机随机数的仿函数。特别请你注意,该仿函数的传递方式是by reference而非一般的by value,这是因为随机随机数产生器有一 个重要特质:它拥有局部状态(local state), 每次被调用时都会有所改变,并因此 保障产生出来的随机数能够随机。

 6.7.8 partial_sort / partial_sort_copy

  • 本算法接受一个middle迭代器(位于序列[first,last) 之内),然后重新安排[first,last), 使序列中的middle-first个最小元素以递增顺序排序, 置于[first .middle) 内。其余 last-middle 个元素安置于[middle , last)中,不保证有任何特定顺序。
  • 使 用 sort算法,同样能够保证较小的N 个元素以递增顺序置于[first, f irst+N) 之内。选 择 partial_sort而 非 sort的唯一理由是效率。是的,如果只是挑出前N 个最小元素来排序,当然比对整个序列排序快上许多。
  • partial_sort的任务是找出middle-first个最小兀素,因此,首先界定 出 区 间 [first, middle), 并 利 用 4.7.2节 的 make_heap () 将它组织成一个max-heap,然后就可以将(middle, last)中的每一个元素拿来与max-heap的最大值比较(tnax-heap的最大值就在第一个元素身上,轻松可以获得);如果小于该 最大值,就互换位置并重新保持max-heap的状态。如此一来,当我们走遍整个[middle, last)时,较大的元素都已经被抽离出[first,middle), 这时候再以 sort_heap () 将[first, middle) 做一次排序,即功德圆满。见图6-11的步骤详解。

  • first 到 middle 采用 堆排序
  • first到middle 是需要排序的元素的范围,如果 middle到last中存在比 first到middle中小的元素,将小的元素 替换 堆中的最大值,重新进行堆排序

 

  •  oite 递减排序

6.7.9 sort

  • STL所提供的各式各样算法中,Sort是最复杂最庞大的一个。这个算法接 受 两 个 RandomAccessIterators (随机存取迭代器),然后将区间内的所有元素以 渐增方式由小到大重新排列。第二个版本则允许用户指定一个仿函数(functor),作为排序标准9。
  • STL的所有关系型容器(associative containers) 都拥有自动排序功能 (底层结构采用R B -tree,见第5 章),所以不需要用到这个s o rt算法。
  • 至于序 列式容器(sequence containers) 中的 sta ck、queue 和 p rio rity -q u e u e 都有特别的出入口,不允许用户对元素排序
  • 剩下vector、deque和 l i s t , 前两者的迭代 器 属 于 RandomAccessIterators, 适 合 使 用 s o r t算法l i s t 的迭代器则属于 Bidireciioinaltterators , 不 在 S T L 标 准 之 列 的 si 1 st , 其 迭 代 器 更 属 于 Forwardlterators,都不适合使用s o rt算法。如 果 要 对 l i s t 或 s l i s t 排序,应该使用它们自己提供的member functions sort ( ) 。稍后我们便可看到为什么泛型算法
    s o r t()—定要求 RandomAccessIterators.
  • STL的sort算法,数据量大时采用Quick Sort,分段递归排序° 一旦分段后 的数据量小于某个门槛,为避免Quick Sort的递归调用带来过大的额外负荷(overhead), 就改用Insertion S ort.如果递归层次过深,还会改用Heap Sort (已 于 4.7.2节介绍). 以下分别介绍Quick Sort和 Insertion S o rt,然后再整合起来介 绍 STL sort算法。 

In s e rtio n S ort

  • Insertion Sort以双层循环的形式进行。外循环遍历整个序列,每次迭代决定 出一个子区间;内循环遍历子区间,将子区间内的每一个“逆转对(inversion) ” 倒转过来。所 谓 “逆转对”是指任何两个迭代器i,j, i < j 而 *i > *j " 一旦 不存在“逆转对”,序列即排序完毕。这个算法的复杂度为O(N2),说起来并不理想,但是当数据量很少时,却有不错的效果,原因是实现上有一些技巧(稍后源代码可见),而且不像其它较为复杂的排序算法有着诸如递归调用等操作带来的额外负荷。图 6-12是 Insertion S ort的详细步骤示意。
  • 所以SGI将以下函数的名称都加上双下划线,表示内部使用

  •  p每次 向外扩充一个区间
  • first 每次负责将新扩充的区间内的元素 进行排序

 1. 如果S 的元素个数为0 或 1 , 结束。
2. 取 S 中的任何一个元素,当作枢轴(pivot) vo 3. 将 S 分割为L, R 两段,使L 内的每一个元素都小于或等于v, R 内的每一个 元素都大于或等于V
4. 对 L, R 递归执行Quick Sort。

  • Quick Sort的精神在于将大区间分割为小区间,分段排序。每一个小区间排序 完成后,串接起来的大区间也就完成了排序。最坏的情况发生在分割(partitioning) 时产生出一个空的子区间一 K完全没有达到分割的预期效果。图 6-13说明了Quick S ort的分段排序.

 P a rtitio in in g ( 分 割 )

  • 分割方法不只一种,以下叙述既简单又有良好成效的做法• 令头端迭代器first向尾部移动,尾端迭代器last向头部移动• 当 *first大于或等于枢轴时 就停下来,当 *last小于或等于枢轴时也停下来,然后检验两个迭代器是否交错。 如果 first仍然在左而last仍然在右,就将两者元素互换,然后各自调整一个 位置(向中央逼近),再继续进行相同的行为。如果发现两个迭代器交错了(亦即 !(first < last)) , 表示整个序列已经调整完毕,以此时的first为轴,将 序列分为左右两半,左半部所有元素值都小于或等于枢轴,右半部所有元素值都大于或等于枢轴。

 

 threshold (阈 值 )

  • 面对一个只有十来个元素的小型序列,使用像Quick Sort这样复杂而(可能) 需要大量运算的排序法,是否划算?不,不划算,在小数据量的情况下,甚至简单如 Insertion S o rt者也可能快过Quick Sort 因为Quick S o rt会为了极小的子序列而产生许多的函数递归调用。
  • 鉴于这种情况,适度评估序列的大小,然后决定采用Quick Sort或Insertion Sort,是值得采纳的一种优化措施.然而究竟多小的序列才应该断然改用Insertion Sort 呢?唔,并无定论,5~20都可能导致差不多的结果,实际的最佳值因设备而异。

 finalinsertion_sort

  • 优化措施永不嫌多,只要我们不是贸然行事(Donald Knuth说过一件名言:贸然实施优化,是所有恶果的根源,premature optimization is the root of all evil) .如果我们令某个大小以下的序列滞留在“几近排序但尚未完成”的状态,最后再以一次Insertion Sort将所有这些“几近排序但尚未竟全功”的子序列做一次完整 的排序,其效率一般认为会比“将所有子序列彻底排序”更好。这是因为Insertion Sort在 面 对 “几近排序”的序列时,有很好的表现。

introsort

  • 不当的枢轴选择,导致不当的分割,导致Quick S ort恶化为O(N2) 。David R.Musser (此 君 于 S TL领域大大有名)于 1996年提出一种混合式排序算法:Introspective Sorting (内省式排序)1。,简 称 IntroSort,其行为在大部分情况下几乎 与 median-of-3 Quick S o rt完全相同(当然也就一样快).但是当分割行为(partitioning) 有恶化为二次行为的倾向时,能够自我侦测,转而改用Heap Sort, 使效率维持在Heap S o rt的O(NlogN) , 又比一开始就使用Heap S ort来得好。稍 后便可看到SG ISTL源代码中对IntroSort的实现。

 6.7.10 equal_range (应用于有序区间)

  • 算法equal_range是二分查找法的一个版本,试图在已排序的 [first,last)中 寻找value.它返回一对迭代器i 和 j,其中i 是在不破坏次序的前提下 , value 可插入的第一个位置(亦即lower_bound) , j 则是在不破坏次序的前提下,value 可插入的最后一个位置(亦即upper_bound)= 因此,[i,j)内的每个兀素都等同于value,而 且 [i,j)是 [first, last)之中符合此一性质的最大子区间。
  • 如 果 以 稍 许 不 同 的 角 度 来 思 考 equal_range , 我们可把它想成是 [first.last)内 “与 value等同”之所有元素所形成的区间A 。由于 [first, last) 有 序 (sorted) , 所 以 我 们 知 道 "与 value等同”之所有兀素一 定都相邻。于是,算 法 lower_bound返回区 间 A 的第一个迭代器,算法upper_bound返回区间A 的最后兀素的下一位置,算 法 equal_range 则是以pair的形式将两者都返回。
  • 即 使 [first,last)并 未 含 有 “与 value等同”之任何元素,以上叙述仍 然合理.这种情况下“与 value等同”之所有元素所形成的,其实是个空区间。 在不破坏次序的前提下,只有一个位置可以插入value,而 equal_range所返回 的 p a ir ,其第一和第二元素(都是迭代器)皆指向该位置。 

 6.7.11 inplace_merge (应用于有序区间)

  • 如果两个连接在一起的序列[first,middle) 和 [middle, last) 都已排序,那 么 inplace_merge可将它们结合成单一一个序列,并仍保有序性(sorted). 如果原先两个序列是递增排序,执行结果也会是递增排序,如果原先两个序列是递减排序,执行结果也会是递减排序。
  • 和 merge 一 样 ,inplace_merge 也是一种稳定(sfaMe) 操作。每个作为数据来源的子序列中的元素相对次序都不会变动;如果两个子序列有等同的元素,第一序列的元素会被排在第二序列元素之前。
  • inplace_merge 有两个版本,其差别在于如何定义某元素小于另一个元素• 第一版本使用operator进行比较,第二版本使用仿函数(functor) comp进行比 较• 以下列出版本一的源代码:

  •  上述辅助函数首先判断缓冲区是否足以容纳inplace.merge所接受的两个 序列中的任何一个-如果空间充裕(源代码中标示easel和 case2之处),工作逻 辑很简单:把两个序列中的某一个copy到缓冲区中,再使用merge完成其余工作。是的,merge足堪胜任,它的功能就是将两个有序但分离(sorted and separated)的区间合并,形成一个有序区间,因此,我们只需将merge的结果置放处(迭代器 result) 指定为 inplace_merge 所接受之序列起始点(迭代器first) 即可。

  •  但是当缓冲区不足以容纳任何 个序列时(源代码中标示case3之处),情况 就棘手多了。面对这种情况,我们的处理原则是,以递归分割(recursive partitioning) 的方式,让处理长度减半,看看能否容纳于缓冲区中(如果能,才好办事儿)。例如,沿用图6-16的输入状态、并假设缓冲区大小为3 , 小于序列一的长度4 和序列二的长度5 , 于是,拿较长的序列二开刀,计算出first_cut和 second_cut如下:

6.7.12 nth_element

  • 这个算法会重新排列[first, last),使迭代器nth所指的元素,与 “整个[first, last)完整排序后,同一位置的元素”同值。此外并保证[nth, last) 内没有任何一个元素小于(更精确地说是不大于)[first, nth)内的元素,但对于[first,nth)和 [nth, last)两个子区间内的元素次序则无任何保证 这一点 也 是 它 与 partial_sort很大的不同处。以此观之,nth_element比较近似 partition 而非 sort 或 partial_sort.

  •  nth_element有两个版本,其差异在于如何定义某个兀素小于另一个元素。 第一版本使用operator<进行比较,第二个版本使用仿函数comp 进行比较。注意,这个算法只接受RandomAccessIterotor
  • nth element的做法是,不断地以median-of-3 partitioning (以首、尾、中央三点中值为枢轴之分割法,见 6.7.9节)将整个序列分割为更小的左(L ) 、右 (R)子序列。如果n th 迭代器落于左子序列,就再对左子序列进行分割,否则就再对 右子序列进行分割• 依此类推,直到分割后的子序列长度不大于3 (够小了),便对最后这个待分割的子序列做Insertion S ort,大功告成。 

 6.7.13 merge sort

  • 虽然SGI S T L所采用的排序法是IntroSort (一种比Quick Sort考虑更周详的算法,见6.7.9节 ),不过,另一个很有名的排序算法Merge S o rt,很轻易就可以利 用 STL算 法 inplace_merge (6.7.11节)实现出来。
  • Merge Sort的概念是这样的:既然我们知道,将两个有序(sorted)区间归并 成一个有序区间,效果不错,那么我们可以利用“分而治之”(devide and conquer)” 的概念,以各个击破的方式来对一个区间进行排序。首先,将区间对半分割,左右两段各自排序,再利用 inplace_merge 重新组合为一个完整的有序序列。对半分 割的操作可以递归进行,直到每一小段的长度为0 或 1 (那么该小段也就自动完成了排序)。下面是一份实现代码:

 

  • Merge S ort的复杂度为O(NlogN)。虽然这和Quick Sort是一样的,但因为Merge Sort需借用额外的内存,而且在内存之间移动(复制)数据也会耗费不少时间,所以Merge S ort的效率比不上Quick Sort=实现简单、概念简单,是 Merge Sort的两大优点。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值