[STL]为什么sort的自定义cmp函数中必须使用严格弱序(strict weak order)

8 篇文章 0 订阅

一、背景

之前在刷pat题时遇到过自定义sort中的cmp函数时,当排序数据过多时可能发生段错误。查询资料后发现是我写的自定义cmp函数中,当两个元素相等时返回的true,而产生了bug。当时只是大概知道只是因为sort的cmp函数要求 严格弱排序(strict weak order),具体的原因并未深究。
前几天在别人的博客中发现又有人在问这个问题,我又查了一点资料才算是大概了解了使用 严格弱序(strict weak order) 的原因。在此记录,愿对读者有所帮助。

二、严格弱序(strict weak order)

STL官方文档中对sort函数的cmp参数(文档中为comp)的描述如下:[1]

comp
Binary function that accepts two elements in the range as arguments, and returns a value convertible to bool. The value returned indicates whether the element passed as first argument is considered to go before the second in the specific strict weak ordering it defines.
The function shall not modify any of its arguments.
This can either be a function pointer or a function object.

翻译如下:

接受两个元素参数的二元函数,返回一个可以转为bool类型的值。返回值表明了传入的两个参数中按照该函数中定义的严格弱序第一个参数是否移动到第二个参数之后。
该函数不可以修改传入的参数。可以是一个函数指针或者是函数对象。

那么什么是严格弱序呢?
维基百科中对严格弱序的介绍如下:[2]

A strict weak ordering is a binary relation < on a set S that is a strict partial order (a transitive relation that is irreflexive, or equivalently, that is asymmetric) in which the relation “neither a < b nor b < a” is transitive.Therefore, a strict weak ordering has the following properties:

  1. For all x in S, it is not the case that x < x (irreflexivity).
  2. For all x, y in S, if x < y then it is not the case that y < x (asymmetry).
  3. For all x, y, z in S, if x < y and y < z then x < z (transitivity).
  4. For all x, y, z in S, if x is incomparable with y (neither x < y nor y < x hold), and y is incomparable with z, then x is incomparable with z (transitivity of incomparability).

翻译如下:

严格弱序是一个在集合S上的严格偏序关系">"(是一种非自反的传递关系,即非对称关系)。该关系在"a!<b"并且"b!<a"时具有传递性。因此,一个严格弱序满足如下几条性质:

  1. 对于集合S中的任意元素x,x!<x(非自反性)。
  2. 对于集合S中的任意元素x、y,如果x<y,那么y!<x(非对称性).
  3. 对于集合S中的任意元素x、y和z,如果x<y,并且y<z,那么x<z(传递性)。
  4. 对于集合S中的任意元素x、y和z,如果x与y不具备可比性(即x!<y且y!<x),y与z不具备可比性,那么x与z也不具备可比性(不可比性的传递性)。

严格弱序的定义中可以看出,"<=“和”>=“关系并不满足严格弱序。因为对于任意的x,有x<=x(x>=x),不满足性质1(非自反性)。
而”<“和”>“满足所有的四条性质,所以”<“和”>“关系是一种严格弱序关系。
因此,在sort的自定义cmp函数中,不可以使用”<=“或者”>=“关系确定返回值,可以使用”<“或”>"关系确定返回值。
至于为什么必须使用严格弱序的详细原因可以从sort的源代码中找到。

三、原因

首先给出stl/algorithm.h中sort函数的源代码:[3]

template<typename _RandomAccessIterator, typename _Compare>
    inline void
    sort(_RandomAccessIterator __first, _RandomAccessIterator __last, _Compare __comp)
    {
      typedef typename iterator_traits<_RandomAccessIterator>::value_type _ValueType;
      // concept requirements
      __glibcxx_function_requires(_Mutable_RandomAccessIteratorConcept<
	    _RandomAccessIterator>)
      __glibcxx_function_requires(_BinaryPredicateConcept<_Compare, _ValueType, _ValueType>)
      __glibcxx_requires_valid_range(__first, __last);
    if (__first != __last)
	{
	  std::__introsort_loop(__first, __last, std::__lg(__last - __first) * 2, __comp);
	  std::__final_insertion_sort(__first, __last, __comp);
	}
 }

主要代码为:

   if (__first != __last)
	{
	  std::__introsort_loop(__first, __last, std::__lg(__last - __first) * 2, __comp);
	  std::__final_insertion_sort(__first, __last, __comp);
	}

可以看到sort函数首先判断排序区间是否合法,如果不合法直接返回;合法则调用__introsort_loop()和__final_insertion_sort()函数。
__introsort_loop()函数执行内省排序算法[4],代码如下:

  /// This is a helper function for the sort routine.
  template<typename _RandomAccessIterator, typename _Size, typename _Compare>
    void
    __introsort_loop(_RandomAccessIterator __first,
		     _RandomAccessIterator __last,
		     _Size __depth_limit, _Compare __comp)
    {
      while (__last - __first > int(_S_threshold))
		{
		  if (__depth_limit == 0)
		    {
		      _GLIBCXX_STD_A::partial_sort(__first, __last, __last, __comp);
		      return;
		    }
		  --__depth_limit;
		  _RandomAccessIterator __cut = std::__unguarded_partition_pivot(__first, __last, __comp);
		  std::__introsort_loop(__cut, __last, __depth_limit, __comp);
		  __last = __cut;
		}
   }

__introsort_loop()函数首先判断待排序元素数量是否大于阈值_S_threshold。
如果小于阈值则返回到sort()函数中,继续执行之后的__final_insertion_sort()函数,__final_insert_sort()函数执行插入排序算法;
如果大于阈值,则执行内省排序算法。内省排序首先从快速排序开始,在递归深度达到__depth_limit时开始执行堆排序。快速排序即为代码中的__unguarded_partition_pivot(),堆排序为partial_sort();
快速排序的函数名为__unguarded_partition_pivot(),其中的"unguarded"代表无防备的、无检查的,即这个函数没有对first和last做边界检查,这样做是为了减少比较运算的开支(这就是cmp函数中必须使用严格弱序的原因)。
我们继续看__unguarded_partition_pivot()函数的代码,如下:

  /// This is a helper function...
  template<typename _RandomAccessIterator, typename _Tp, typename _Compare>
    _RandomAccessIterator
    __unguarded_partition(_RandomAccessIterator __first,
			  _RandomAccessIterator __last,
			  const _Tp& __pivot, _Compare __comp)
    {
      while (true)
	{
	  while (__comp(*__first, __pivot))
	    ++__first;
	  --__last;
	  while (__comp(__pivot, *__last))
	    --__last;
	  if (!(__first < __last))
	    return __first;
	  std::iter_swap(__first, __last);
	  ++__first;
	}
    }

代码中使用__comp(*first, __pivot)作为while循环的终止条件,如果两个相邻的元素相等而__comp()函数返回true时,while循环会继续执行++first,如果所有元素都相等,最终会导致指针first越界。因此__comp()函数中需要使用严格弱序,在两个元素相等时返回false。

四、总结

STL中sort()函数的执行过程大致如下:

元素个数大于_S_threshold
元素个数小于等于_S_threshold
迭代深度达到__depth_limit
sort
__introsort_loop
__final_insertion_sort
__unguarded_partition_pivot
partial_sort
执行插入排序
执行堆排序

__unguarded_partition_pivot()函数执行快速排序,为了减少比较运算的开销在该函数中没有对边间条件进行检查,因此如果相邻的两个元素相等时__comp函数返回true,而且待排序的元素全部相等时,会导致指针越界。

五、参考资料

[1] sort - C++ Reference. http://www.cplusplus.com/reference/algorithm/sort/
[2] Partially ordered set. https://en.wikipedia.org/wiki/Partially_ordered_set
[3] STL algorithm源码: stl_algo.h. https://blog.csdn.net/qq844352155/article/details/39346493
[4] 内省排序 - 维基百科. https://zh.wikipedia.org/wiki/内省排序
[5] 【决战西二旗】|理解Sort算法. https://zhuanlan.zhihu.com/p/95522832
[6] STL的sort函数浅析. https://www.cnblogs.com/FdWzy/p/12521238.html
[7] 知无涯之std::sort源码剖析. http://www.voidcn.com/article/p-xddyozky-n.html
[8] 进行std::sort的元素为什么要保证严格弱序?. https://www.cnblogs.com/RookieSuperman/p/12375563.html

  • 23
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值