STL sort 分析

前言

STL 中提供了很多算法,sort 是我们经常使用的,那它究竟是如何实现的呢?

STL 的 sort 算法,数据量大时采用快速排序,分段递归。一旦分段的数据量小于某个门槛,为避免快速排序的递归调用带来过大的额外负荷,就改用插入排序。如果递归层次过深,还会改用堆排序

这个算法接受两个随机迭代器,然后将区间内的所有元素以升序排列。第二个版本则允许用户指定一个彷函数作为排序标准,我们暂时不考虑这个版本。

注意本文并不教具体的排序算法,只是分析 sort 中的行为,对于插入排序和快速排序需自行掌握。

插入排序

插入排序以双层循环的形式进行。外层循环遍历整个序列,每次迭代决定一个子区间;内循环遍历子区间,将当前值插入到合适的位置。

template <class RandomAccessIterator>
void __insertion_sort(RandomAccessIterator first, RandomAccessIterator last) {
  if (first == last) {
    return;
  } 
  for (RandomAccessIterator j = first + 1; j != last; ++j) {
    // 外循环,形成 [first, j) 的子区间
    // j 迭代器指向当前循环要插入的数据
    __linear_insert(first, j, value_type(first));
  }
}

插入排序

template <class RandomAccessIterator, class T>
inline void __linear_insert(RandomAccessIterator first, 
                            RandomAccessIterator last, T*) {
  T value = *last;
  if (value < *first) {	
    copy_backward(first, last, last + 1);	// 令元素整体向后移动一个位置
    *first = value;	                      // 将尾元素插在头部
  } else {
    // 尾不小于头
    __unguarded_linear_insert(last, value);
  }
}
template <class RandomAccessIterator, class T>
void __unguarded_linear_insert(RandomAccessIterator last, T value) {
  RandomAccessIterator next = last;	// 指向尾元素
  --next;								            // 调整指向前一元素
  while (value < *next) {   		    // 尾元素更小,插入位置应在前面
    *last = *next;                  // 将 next 指向的元素值向后移动一个位置
    last = next;		
    --next;	                        // 向前移动迭代器,继续寻找
  }
  *last = value;                    // value >= *next 即为正确的插入位置
}

上面的代码看起来好像没什么特别之处,其实暗藏玄机。在一般的插入排序中,我们除了要判断两元素是否逆序外,还需要判断 next 是否超出了边届

为什么上述代码不需要判断呢?

原因就在于上述代码的左边届一定是最小值,这在调用上一层 __linear_insert 时做的,该函数首先将要插入的值于左边届比较,更小就直接插入。此时,进入到 __unguarded_linear_insert 循环体的待插入值,一定大于等于左边届,就不可能出现越届的情况,也就可以把边届检查省略了。

快速排序

快速排序过程如下,假设 S 代表将被处理的序列:

  1. 如果 S 的元素个数为 0 或 1,结束
  2. 取 S 中的任何一个元素,当作枢轴 v
  3. 将 S 分隔为 L、R 两段,使 L 内的每一个元素都小于或等于 v,R 内的每一个元素都大于或等于 v
  4. 对 L,R 递归执行快速排序

快速排序的精髓在于将大区间分割为小区间,分段排序。每一个小区间排序完成后,和起来的大区间也就完成了排序。

三点中值

理论上,任何一个元素都可以被选做枢轴,但合适与否却会影响快速排序的效率。比如一个已经升序的序列,我们每次都选其左端点作为枢轴,效率将退化为 O ( N 2 ) O(N^2) O(N2)

最理想的方式是取整个序列的头、尾和中间三个位置的元素以其中值作为枢轴。为了能够快速取出中央位置的元素,显然迭代器必须是随机迭代器。

template <class T>
inline const T& __median(const T& a, const T& b, const T& c) {
  if (a < b) {
    if (b < c) {        // a < b < c
      return b;
    } else if (a < c) {	// a < b,b >= c,a < c
      return c;
    } else {            // a < b,a >= c
      return a;
    }
  } else if (a < c) {   // a >= b,a < c
    return a;
  } else if (b < c) {   // a >= b,a >= c,b < c
    return c;
  } else {              // a >= b,a >= c,b >= c
    return b;
  }
}

分隔

令头迭代器 first 向尾部移动,尾端迭代器 last 向头部移动。当 *first 大于或等于枢轴时就停下来当 *last 小于或等于枢轴时也停下来,然后检验两个迭代器是否交错。如果 first 仍然在左,last 仍然在右,就将两者元素交换,然后各自调整一个位置。

下面是 SGI STL 提供的分隔函数,其返回值是分隔后右段第一个位置:

template <class RandomAccessIterator, class T>
RandomAccessIterator __unguarded_partition(RandomAccessIterator first, 
                                           RandomAccessIterator last, 
                                           T pivot) {
  while (true) {
    while (*first < pivot) {	// 向后寻找大于等于枢轴的值
      ++first;
    }
    --last;
    while (pivot < *last) {		// 向前寻找小于等于枢轴的值
      --last;
    }

    if (!(first < last)) {		// first 在右,last 在左就退出
      return first;
    }
    iter_swap(first, last);
    ++first;
  }
}

[外链图片转存中…(img-8EZS6qnS-1706626051753)]

阈值

面对一个数据量很小的序列,使用快速排序产生递归调用是不划算的。鉴于这种情况,适当评估序列的大小,然后决定采用快速排序或插入排序,是值得采纳的一种优化措施。不过对于多小的序列采用插入排序,暂时没有定论,5~20 都可能导致差不多的结果。

STL sort

下面是 SGI STL 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);
  }
}

__lg

__lg 是用来控制分隔恶化的情况:找出 2 k ≤ n 2^k \leq n 2kn 的最大值 k。

例:n = 10,得 k = 3;n = 2 得 k = 1;n = 20 得 k = 4。

template <class Size>
inline Size __lg(Size n) {
  Size k;
  // k 即为 n 最高位的 1 右边的位数
  for (k = 0; n != 1; n >>= 1) {
    ++k;
  }
  return k;
}

__introsort_loop

当元素个数为 20 时,__introsort_loop() 的最后一个参数是 4 * 2,意思是最多允许分割 8 层。

template <class RandomAccessIterator, class T, class Size>
void __introsort_loop(RandomAccessIterator first,
                      RandomAccessIterator last, T*,
                      Size depth_limit) {
  // __stl_threshold 是个全局常量,SGI STL 中定义为 16
  while (last - first > __stl_threshold) {
    // 分割恶化,改用堆排序
    if (depth_limit == 0) {	 
      partial_sort(first, last, last);
      return;
    }
    
    --depth_limit;
    // 枢轴通过 __median 获取
    T val = __median(*first, *(first + (last - first) / 2), *(last - 1));
    RandomAccessIterator cut = __unguarded_partition(first, last, val);
    // 对右半段递归调用 __introsort_loop
    __introsort_loop(cut, last, value_type(first), depth_limit);	
    // 调整边届,左半段在当前循环解决
    last = cut;
  }
}

上述代码中并没对左右两段递归调用,只是对右段递归调用,左半段调整边届后在循环内处理。这样的可读性较差,但可以减少一次递归调用的消耗。

__final_insertion_sort

如果我们令某个大小以下的序列停留的「接近排序但尚未完成」的状态,最后再用一次插入排序将所有这些「接近排序但尚未完成」的子序列做一次完整的排序,其效率一般认为会比「将所有子序列彻底排序」更好。这时因为插入排序在面对「接近排序」的序列时,有很好的表现。

template <class RandomAccessIterator>
void __final_insertion_sort(RandomAccessIterator first, 
                            RandomAccessIterator last) {
  // 判断元素个数
  if (last - first > __stl_threshold) {	
    __insertion_sort(first, first + __stl_threshold);	      
    // 保证最小元素在最左侧,然后调用不用判断边届的插入排序
    __unguarded_insertion_sort(first + __stl_threshold, last); 
  } else {
    __insertion_sort(first, last);
  }
}

上述代码非常奇怪,首先判断元素的个数。

为什么要这样做呢?

首先,快速排序是以 __stl_threshold 为最小阈值,也就是说最小值一定在最左边 __stl_threshold 个元素内。先将最小元素放在最左侧,再调用不用判断边届的插入排序,可以提高效率。

否则,插入排序过程中每段要先调用 __linear_insert,与左端点值比较,再调用 __unguarded_linear_insert。这样的话,只有最左边呢段需要调用 __linear_insert。

template <class RandomAccessIterator>
inline void __unguarded_insertion_sort(RandomAccessIterator first, 
                                      RandomAccessIterator last) {
  __unguarded_insertion_sort_aux(first, last, value_type(first));
}
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));	// 调用不用判断边届的插入排序
  }
}
  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值