首页 > 代码库 > STL算法 — sort

STL算法 — sort

能使用STL的sort系列算法的前提是容器的迭代器必须为随机迭代器。所以,vector和deque天然适用。STL的sort算法采用了一些策略,在不同情况下采用不同的排序算法,以达到各种算法优势互补的效果。基本的原则是:数据量大时采用快速排序,数据量小时采用插入排序(这是对快排常用的一种优化策略),递归层次过深改用堆排序。

首先是插入排序。它的平均和最坏时间复杂度都为O(N2),量级小于千,那么插入排序还是一个不错的选择。SGI STL的插入排序默认以增序排列,另外还可以传入一个仿函数作为比较规则,这里只说明前者。

外层循环确定一个子区间,i位置的元素需要插入到已排序区间的正确位置上:
template <class RandomAccessIterator>
void __insertion_sort(RandomAccessIterator first, RandomAccessIterator last) {
  if (first == last) return;  // 容器为空,直接返回
  for (RandomAccessIterator i = first + 1; i != last; ++i)  // 确定子区间
    __linear_insert(first, i, value_type(first));
}


进入__linear_insert函数:
template <class RandomAccessIterator, class T>
inline void __linear_insert(RandomAccessIterator first, 
                            RandomAccessIterator last, T*) {
  T value = http://www.mamicode.com/*last;      // 新插入元素>

上面代码的注释已经说明了一种简单的情况,这是只需借用copy_backward算法将元素整体搬移,以腾出第一个位置给新插入元素。但其它情况下就要进入__unguarded_linear_insert函数了:
template <class RandomAccessIterator, class T>
void __unguarded_linear_insert(RandomAccessIterator last, T value) {
  RandomAccessIterator next = last; // 指向新插入元素
  --next; // 指向之前一个元素
  while (value < *next) { // 发现逆转对
    *last = *next;        // 较大的元素往后走
    last = next;
    --next;
  }
  *last = value;          // 新元素放入正确位置
}


上述函数的目标就是要找到value也就是新插入元素的正确插入位置。以value位置为起点,前向遍历已排序区间,遇到比value大的就后移一个位置,以消除逆转对的存在。注意,这里不需要进行越界判断,因为value肯定不会插入到区间头部。

当遇到大数据量时,效率为平方级的插入排序就不适用了。这时STL改用快速排序,平均效率为O(NlogN),最坏效率为O(N2)。

快排首先要选取枢轴,STL采用的方法是选取前、中、后三点的中值作为枢轴。接下来是分割,遍历整个迭代器,小于枢轴的放左边,大于枢轴的放右边。具体做法如下:
  • first迭代器向后移动,*first大于或等于枢轴时停止。
  • last迭代器向前移动,*last小于或等于枢轴时停止。
  • 若first仍在last的左边,则交换*first和*last,并调整两个迭代器指向下一个位置。
  • 重复上述行为直到first和last交叉,first即为右半部分起始位置。
分割代码如下:
template <class RandomAccessIterator, class T>  // 快排的分割函数
RandomAccessIterator __unguarded_partition(RandomAccessIterator first, 
                                           RandomAccessIterator last, 
                                           T pivot) {
  while (true) {
    while (*first < pivot) ++first;     // 向后移动直到*first大于或等于枢轴
    --last;
    while (pivot < *last) --last;       // 向前移动直到*last小于或等于枢轴
    if (!(first < last)) return first;  // 交叉后停止,返回的迭代器作为右子区间的开头
    iter_swap(first, last);             // 交换两个迭代器所指元素
    ++first;
  }
}    


现在来看看sort算法的对外接口:
template <class RandomAccessIterator>
inline void sort(RandomAccessIterator first, RandomAccessIterator last) {
  if (first != last) {
    __introsort_loop(first, last, value_type(first), __lg(last - first) * 2);
    __final_insertion_sort(first, last);
  }
}


要阅读__introsort_loop函数,先要了解introsort。以下是维基百科里的解释:
内省排序(英语:Introsort)是由David Musser在1997年设计的排序算法。这个排序算法首先从快速排序开始,当递归深度超过一定深度(深度为排序元素数量的对数值)后转为堆排序。采用这个方法,内省排序既能在常规数据集上实现快速排序的高性能,又能在最坏情况下仍保持O(nlogn)的时间复杂度。

个人理解,最坏情况发生在分割深度过深时,以中值作为枢轴的方法并不能选出合适的枢轴,导致分割时频繁地交换元素,使效率下降到O(N)。

上述代码中的__lg(last - first) * 2就是根据元素个数计算深度阈值,当分割深度超过阈值时,转而采用堆排序,是效率继续保持为O(nlogn)。
template <class Size>
inline Size __lg(Size n) {  // 控制分割恶化的情况,求2^k <= n的最大k值
  Size k;
  for (k = 0; n != 1; n >>= 1) ++k;
  return k;
}


例如,当元素个数为40时,最多允许分割10层。

有了上面内省排序的基础,下面的代码就很容易看了:
template <class RandomAccessIterator, class T, class Size>
void __introsort_loop(RandomAccessIterator first,
                      RandomAccessIterator last, T*,
                      Size depth_limit) {
  while (last - first > __stl_threshold) {  // 大于某个值才进行快速排序,这里__stl_threshold = 16
    if (depth_limit == 0) {
      partial_sort(first, last, last);      // 分割恶化,采用堆排序
      return;
    }
    --depth_limit;
    RandomAccessIterator cut = __unguarded_partition
      (first, last, T(__median(*first, *(first + (last - first)/2),
                               *(last - 1))));    // 采用三点中值作为枢轴进行快速排序
    __introsort_loop(cut, last, value_type(first), depth_limit);  // 对右半部分递归进行排序
    last = cut; // 调整last位置,下一次while循环将对左半部分排序
  }
}


partial_sort已经在另一篇文章中详细分析过了,实际上就是对整个区间进行堆排序。__median函数就是求取前、中、后三个元素中值作为枢轴,其它函数都已经在上面讲解过了。递归分段排序,典型的分治策略。当__introsort_loop函数返回时,要么完全已排序,要么大体已排序。大体已排序则要进行微调,即sort函数调用完__introsort_loop之后调用__final_insertion_sort进行微调:
template <class RandomAccessIterator>
void __final_insertion_sort(RandomAccessIterator first, 
                            RandomAccessIterator last) {
  if (last - first > __stl_threshold) { // 超过16个元素,则分割为两段
    __insertion_sort(first, first + __stl_threshold); // 前段使用插入排序
    __unguarded_insertion_sort(first + __stl_threshold, last);     // 剩余元素逐个插入
  }
  else
    __insertion_sort(first, last);  // 元素个数不超过16,直接使用插入排序
}


注意,为了效率,STL又开始折腾了...,再进入__unguarded_insertion_sort瞧瞧:
template <class RandomAccessIterator, class T>
void __unguarded_insertion_sort_aux(RandomAccessIterator first, 
                                    RandomAccessIterator last, T*) {
  for (RandomAccessIterator i = first; i != last; ++i)
    __unguarded_linear_insert(i, T(*i));  // 将i所指元素插入已排序的16个元素的序列中,前面已经介绍过了
}
 
template <class RandomAccessIterator>
inline void __unguarded_insertion_sort(RandomAccessIterator first, 
                                RandomAccessIterator last) {
  __unguarded_insertion_sort_aux(first, last, value_type(first));
}


为什么要这样?当从__introsort_loop函数返回时,要么完全有序,要么大体有序。大体有序时,前面的子区间整体是要小于后面的子区间的,这是introsort操作之后的结果。在__final_insertion_sort中,如果元素个数超过16,先对前16个元素进行插入排序,后面剩下的元素再逐个插入到此有序区间。刚才说了由于已经是大体有序了,所以逐个插入操作不会出现过多的逆转对,这正是插入排序的优势所在。

至此,STL的sort算法分析完了。要读懂STL的心思确实不是意见容易的事情啊。

参考:
《STL源码剖析》 P389.