前言
当你第一眼看到这道面试题是不是心里在暗喜,一问算法题就比问排序算法,一问排序算法就问快速排序。
如果你回答:
STL里的sort算法肯定用的是快速排序啊?难不成还是冒泡排序么?
如果你只是回答快速排序,那么恭喜你只答对了33.333%,离正确答案还差一大截。
回答完,接着会引来一堆问题轰炸:
- 数据量大和数据量小都适合用快速排序吗?
- 快速排序的时间复杂度不是稳定的nlogn,最坏情况会变成n^2,怎么解决复杂度恶化问题?
- 快速排序递归实现时,怎么解决递归层次过深的问题?
- 递归过深会引发什么问题?
- 怎么控制递归深度?如果达到递归深度了还没排完序怎么办?
首先,回答用到哪种排序算法,正确答案是:
毫无疑问是用到了快速排序,但不仅仅只用了快速排序,还结合了插入排序和堆排序。
是不是很惊喜,很意外?
为什么?直接看STL源码实现,来源于侯捷老师翻译的鼎鼎大名的《STL源码剖析》关于sort算法实现的细节,实现细节有很多精彩的地方。
并非所有容器都使用sort算法
既然问的是STL的sort算法实现,那么先确认一个问题,哪些STL容器需要用到sort算法?
首先,关系型容器拥有自动排序功能,因为底层采用RB-Tree,所以不需要用到sort算法。
其次,序列式容器中的stack、queue和priority-queue都有特定的出入口,不允许用户对元素排序。
剩下的vector、deque,适用sort算法。
实现逻辑
STL的sort算法,数据量大时采用QuickSort快排算法,分段归并排序。一旦分段后的数据量小于某个门槛(16),为避免QuickSort快排的递归调用带来过大的额外负荷,就改用Insertion Sort插入排序。如果递归层次过深,还会改用HeapSort堆排序。
结合快速排序-插入排序-堆排序 三种排序算法。
具体代码
源文件:https://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-html-USERS-4.4/a01347.html
template<typename _RandomAccessIterator>
inline void
sort(_RandomAccessIterator __first, _RandomAccessIterator __last)
{
typedef typename iterator_traits<_RandomAccessIterator>::value_type
_ValueType;
// concept requirements
__glibcxx_function_requires(_Mutable_RandomAccessIteratorConcept<
_RandomAccessIterator>)
__glibcxx_function_requires(_LessThanComparableConcept<_ValueType>)
__glibcxx_requires_valid_range(__first, __last);
if (__first != __last)
{
//快速排序+插入排序
std::__introsort_loop(__first, __last,
std::__lg(__last - __first) * 2);
//插入排序
std::__final_insertion_sort(__first, __last);
}
}
其中__lg
函数是计算递归深度,用来控制分割恶化,当递归深度达到该值改用堆排序,因为堆排序是时间复杂度恒定为nlogn:
template<typename _Size>
inline _Size
__lg(_Size __n)
{
_Size __k;
for (__k = 0; __n != 1; __n >>= 1)
++__k;
return __k;
}
先来看,__introsort_loop
快排实现部分:对于区间小于16
的采用快速排序,如果递归深度恶化改用堆排序
。
template<typename _RandomAccessIterator, typename _Size>
void
__introsort_loop(_RandomAccessIterator __first,
_RandomAccessIterator __last,
_Size __depth_limit)
{
typedef typename iterator_traits<_RandomAccessIterator>::value_type
_ValueType;
//_S_threshold=16,每个区间必须大于16才递归
while (__last - __first > int(_S_threshold))
{
//达到指定递归深度,改用堆排序
if (__depth_limit == 0)
{
std::partial_sort(__first, __last, __last);
return;
}
--__depth_limit;
_RandomAccessIterator __cut =
std::__unguarded_partition(__first, __last,
_ValueType(std::__median(*__first,
*(__first
+ (__last
- __first)
/ 2),
*(__last
- 1))));
std::__introsort_loop(__cut, __last, __depth_limit);
__last = __cut;
}
}
再来看插入排序
部分:
template<typename _RandomAccessIterator>
void
__final_insertion_sort(_RandomAccessIterator __first,
_RandomAccessIterator __last)
{
if (__last - __first > int(_S_threshold))
{
//先排前16个
std::__insertion_sort(__first, __first + int(_S_threshold));
//后面元素遍历插入到前面有序的正确位置
std::__unguarded_insertion_sort(__first + int(_S_threshold), __last);
}
else
std::__insertion_sort(__first, __last);
}
为什么用插入排序?因为插入排序在面对“几近排序”的序列时,表现更好。
结束语
最好的理解方式还是看书再结合源码。