.
一份简化的代码(可读性较强)
c++ 的sort用了很多年,一直不知道具体是怎么写的
决定看看代码,以下文章结构可能有点混乱,建议读者同时打开vs同步跳转
https://www.geeksforgeeks.org/internal-details-of-stdsort-in-c/
简化版本
sort() 使用的算法是IntroSort
IntroSort是一个混合排序算法使用三个排序算法
- 快速排序
- 堆排序
- 插入排序
当元素的个数少于16个的时候,使用的插入排序
log是以e为底数的对数
排序算法调用有一个层数限制,2 * log(end-begin);
如果调用到2 * log(end-begin);层之后还没有排好
就直接调用堆排序
其他情况就继续递归调用IntroSort
值得注意的是,快速排序用的是 首尾以及中间的三个位置的中位数
作为快排分割左右两个子问题的标准
/* A Program to sort the array using Introsort.
The most popular C++ STL Algorithm- sort()
uses Introsort. */
#include<bits/stdc++.h>
using namespace std;
// A utility function to swap the values pointed by
// the two pointers
void swapValue(int *a, int *b)
{
int *temp = a;
a = b;
b = temp;
return;
}
/* Function to sort an array using insertion sort*/
void InsertionSort(int arr[], int *begin, int *end)
{
// Get the left and the right index of the subarray
// to be sorted
int left = begin - arr;
int right = end - arr;
for (int i = left+1; i <= right; i++)
{
int key = arr[i];
int j = i-1;
/* Move elements of arr[0..i-1], that are
greater than key, to one position ahead
of their current position */
while (j >= left && arr[j] > key)
{
arr[j+1] = arr[j];
j = j-1;
}
arr[j+1] = key;
}
return;
}
// A function to partition the array and return
// the partition point
int* Partition(int arr[], int low, int high)
{
int pivot = arr[high]; // pivot
int i = (low - 1); // Index of smaller element
for (int j = low; j <= high- 1; j++)
{
// If current element is smaller than or
// equal to pivot
if (arr[j] <= pivot)
{
// increment index of smaller element
i++;
swap(arr[i], arr[j]);
}
}
swap(arr[i + 1], arr[high]);
return (arr + i + 1);
}
// A function that find the middle of the
// values pointed by the pointers a, b, c
// and return that pointer
int *MedianOfThree(int * a, int * b, int * c)
{
if (*a < *b && *b < *c)
return (b);
if (*a < *c && *c <= *b)
return (c);
if (*b <= *a && *a < *c)
return (a);
if (*b < *c && *c <= *a)
return (c);
if (*c <= *a && *a < *b)
return (a);
if (*c <= *b && *b <= *c)
return (b);
}
// A Utility function to perform intro sort
void IntrosortUtil(int arr[], int * begin,
int * end, int depthLimit)
{
// Count the number of elements
int size = end - begin;
// If partition size is low then do insertion sort
if (size < 16)
{
InsertionSort(arr, begin, end);
return;
}
// If the depth is zero use heapsort
if (depthLimit == 0)
{
make_heap(begin, end+1);
sort_heap(begin, end+1);
return;
}
// Else use a median-of-three concept to
// find a good pivot
int * pivot = MedianOfThree(begin, begin+size/2, end);
// Swap the values pointed by the two pointers
swapValue(pivot, end);
// Perform Quick Sort
int * partitionPoint = Partition(arr, begin-arr, end-arr);
IntrosortUtil(arr, begin, partitionPoint-1, depthLimit - 1);
IntrosortUtil(arr, partitionPoint + 1, end, depthLimit - 1);
return;
}
/* Implementation of introsort*/
void Introsort(int arr[], int *begin, int *end)
{
int depthLimit = 2 * log(end-begin);
// Perform a recursive Introsort
IntrosortUtil(arr, begin, end, depthLimit);
return;
}
// A utility function ot print an array of size n
void printArray(int arr[], int n)
{
for (int i=0; i < n; i++)
printf("%d ", arr[i]);
printf("\n");
}
// Driver program to test Introsort
int main()
{
int arr[] = {3, 1, 23, -9, 233, 23, -313, 32, -9};
int n = sizeof(arr) / sizeof(arr[0]);
// Pass the array, the pointer to the first element and
// the pointer to the last element
Introsort(arr, arr, arr+n-1);
printArray(arr, n);
return(0);
}
看看std::sort()是怎么做的
打开vs,跳到sort的定义
因为标准库都是使用模板写的,所以读起来多少有点晦涩
在首先在algorithm中找到sort
可以看到这个sort是仅仅支持随机访问的迭代器
不是随机访问的迭代器是不支持的,
随机访问的迭代器:就是在O(1)时间内可以任意访问元素
即支持a[n]这类的常数访问
比如list这类的是不可以用它排序的
list有一个类内的sort,使用归并排序实现
跳到 带有 _Pred的sort默认情况是less<>{}
_Pred: 比较规则的sort,你可以指定两个元素怎么比大小
常用lamda表达式给出
template <class _RanIt, class _Pr>
_CONSTEXPR20 void sort(const _RanIt _First, const _RanIt _Last, _Pr _Pred) { // order [_First, _Last)
_Adl_verify_range(_First, _Last);
const auto _UFirst = _Get_unwrapped(_First);
const auto _ULast = _Get_unwrapped(_Last);
_Sort_unchecked(_UFirst, _ULast, _ULast - _UFirst, _Pass_fn(_Pred));
}
跳转到_Sort_unchecked
template <class _RanIt, class _Pr>
_CONSTEXPR20 void _Sort_unchecked(_RanIt _First, _RanIt _Last, _Iter_diff_t<_RanIt> _Ideal, _Pr _Pred) {
// order [_First, _Last)
for (;;) {
if (_Last - _First <= _ISORT_MAX) { // small
_Insertion_sort_unchecked(_First, _Last, _Pred);
return;
}
if (_Ideal <= 0) { // heap sort if too many divisions
_Make_heap_unchecked(_First, _Last, _Pred);
_Sort_heap_unchecked(_First, _Last, _Pred);
return;
}
// divide and conquer by quicksort
auto _Mid = _Partition_by_median_guess_unchecked(_First, _Last, _Pred);
_Ideal = (_Ideal >> 1) + (_Ideal >> 2); // allow 1.5 log2(N) divisions
if (_Mid.first - _First < _Last - _Mid.second) { // loop on second half
_Sort_unchecked(_First, _Mid.first, _Ideal, _Pred);
_First = _Mid.second;
} else { // loop on first half
_Sort_unchecked(_Mid.second, _Last, _Ideal, _Pred);
_Last = _Mid.first;
}
}
}
这里的_Ideal,就是说理想情况下的最差情况
源代码的注释其实已经写上了
_Ideal = (_Ideal >> 1) + (_Ideal >> 2);
// allow 1.5 log2(N) divisions
最差允许1.5 log2(层)子问题划分
超过这个限度就要用堆排序了
和上面的参考代码还是有些不一样的上面对数是以e(自然对数)为底的
这里的_ISORT_MAX=32,也就是说元素个数少于32的时候采用的是插入排序
其他情况就是用_Sort_unchecked,但是只是在一边
与简化本不同的是,这里的分割之后仅仅在一边继续调用_Sort_unchecked
使用了if else 并不是两个子问题都调用_Sort_unchecked
这就需要看一下_Partition_by_median_guess_unchecked是怎么写的了
这里可以先看下面的
//假设你已经看完了_Partition_by_median_guess_unchecked的分析
_Partition_by_median_guess_unchecked:
返回的_Mid的[_Mid.first,_Mid.second)这个区间的元素都是相等的
就是说 分割线不是一个元素,而是几个相等的元素,暂时叫做分割块了
那么处于分割块内的元素就不需要排序了
只拍分割块两侧就行了 根据两侧元素的多少不同
让一边继续大循环,而另一边调用_Sort_unchecked
那么这两者有什么区别吗,递归不还是进入了这个for(;;)循环吗?
让元素少的一侧进行_Sort_unchecked,元素多的直接for
莫非是想减小递归的开销?欢迎评论区探讨了
_Partition_by_median_guess_unchecked
我们把用来分割子问题的元素叫做标杆
这部分是寻找子问题的标杆
我们知道快速排序的复杂度恶化
是因为如果标杆元素找的不是很合适
比如刚好找到了最小的或者最大的
导致左右两个部分相差很大,这样复杂度就会恶化为O(n^2)
然后看一下标准库是怎么做的
可以先看下面的_Guess_median_unchecked是怎么做的
//假设你已经看完了
经过了_Guess_median_unchecked,在_Mid 的位置,是一个比较靠谱的标杆了
template <class _RanIt, class _Pr>
_CONSTEXPR20 pair<_RanIt, _RanIt> _Partition_by_median_guess_unchecked(_RanIt _First, _RanIt _Last, _Pr _Pred) {
// partition [_First, _Last)
_RanIt _Mid = _First + ((_Last - _First) >> 1); // shift for codegen
_Guess_median_unchecked(_First, _Mid, _Prev_iter(_Last), _Pred);
_RanIt _Pfirst = _Mid;
_RanIt _Plast = _Next_iter(_Pfirst);
while (_First < _Pfirst && !_DEBUG_LT_PRED(_Pred, *_Prev_iter(_Pfirst), *_Pfirst)
&& !_Pred(*_Pfirst, *_Prev_iter(_Pfirst))) {
--_Pfirst;
}
while (_Plast < _Last && !_DEBUG_LT_PRED(_Pred, *_Plast, *_Pfirst) && !_Pred(*_Pfirst, *_Plast)) {
++_Plast;
}
_RanIt _Gfirst = _Plast;
_RanIt _Glast = _Pfirst;
for (;;) { // partition
for (; _Gfirst < _Last; ++_Gfirst) {
if (_DEBUG_LT_PRED(_Pred, *_Pfirst, *_Gfirst)) {
continue;
} else if (_Pred(*_Gfirst, *_Pfirst)) {
break;
} else if (_Plast != _Gfirst) {
_STD iter_swap(_Plast, _Gfirst);
++_Plast;
} else {
++_Plast;
}
}
for (; _First < _Glast; --_Glast) {
if (_DEBUG_LT_PRED(_Pred, *_Prev_iter(_Glast), *_Pfirst)) {
continue;
} else if (_Pred(*_Pfirst, *_Prev_iter(_Glast))) {
break;
} else if (--_Pfirst != _Prev_iter(_Glast)) {
_STD iter_swap(_Pfirst, _Prev_iter(_Glast));
}
}
if (_Glast == _First && _Gfirst == _Last) {
return pair<_RanIt, _RanIt>(_Pfirst, _Plast);
}
if (_Glast == _First) { // no room at bottom, rotate pivot upward
if (_Plast != _Gfirst) {
_STD iter_swap(_Pfirst, _Plast);
}
++_Plast;
_STD iter_swap(_Pfirst, _Gfirst);
++_Pfirst;
++_Gfirst;
} else if (_Gfirst == _Last) { // no room at top, rotate pivot downward
if (--_Glast != --_Pfirst) {
_STD iter_swap(_Glast, _Pfirst);
}
_STD iter_swap(_Pfirst, --_Plast);
} else {
_STD iter_swap(_Gfirst, --_Glast);
++_Gfirst;
}
}
}
跳到第五行 _Guess_median_unchecked
如果区间的元素个数小于等于40个
那么和简化版的一样,找到头尾以及中间元素三个元素的中位数作为标杆
如果是40个以及以上
就要认真对待了,我不知道这个40是怎么定出来的,神奇
// Tukey’s ninther这个注释又是什么意思呢,可能和这个40的来源有关系
_Med3_unchecked看名字就知道应该是找到3个数字的中间数
暂且这么理解 然后看它怎么做呢
_Step 是元素总数的8分之一 _Two_step 是元素总数的4分之一
然后进行了_Med3_unchecked操作
我的理解是就像采样一样,以不同的间隔进行寻找那个可以作为标杆
同样也不知道这样的做法的来源是什么
但是至少已经考虑了9个元素了,r如下图,标杆选错的概率就小很多
最终处于M的位置的至少是这九个元素的中位数
//这里可以先看一下_Med3_unchecked具体的做法
既然_Med3_unchecked是直接交换了元素就还理解了
也就是说 _Guess_median_unchecked
····做了元素的位置的调整
····元素个数在大于40的时候,进行了四轮位置调整
…^^^^ 在first+step以及其左右1个step的位置进行调整
…^^^^ 在last-step以及其左右1个step的位置进行调整
…^^^^ 在mid以及其左右1个step的位置进行调整
…^^^^ 在last-step、first+step以及mid这三个位置进行调整
以期望猜一个中位数,即使猜不到,也弄个八九不离十
····元素个数小于40就行行一次调整
经过这个函数之后,处于数组中间的那个元素就可以作为标杆了
这时候 让我们回到_Partition_by_median_guess_unchecked
template <class _RanIt, class _Pr>
_CONSTEXPR20 void _Guess_median_unchecked(_RanIt _First, _RanIt _Mid, _RanIt _Last, _Pr _Pred) {
// sort median element to middle
using _Diff = _Iter_diff_t<_RanIt>;
const _Diff _Count = _Last - _First;
if (40 < _Count) { // Tukey's ninther
const _Diff _Step = (_Count + 1) >> 3; // +1 can't overflow because range was made inclusive in caller
const _Diff _Two_step = _Step << 1; // note: intentionally discards low-order bit
_Med3_unchecked(_First, _First + _Step, _First + _Two_step, _Pred);
_Med3_unchecked(_Mid - _Step, _Mid, _Mid + _Step, _Pred);
_Med3_unchecked(_Last - _Two_step, _Last - _Step, _Last, _Pred);
_Med3_unchecked(_First + _Step, _Mid, _Last - _Step, _Pred);
} else {
_Med3_unchecked(_First, _Mid, _Last, _Pred);
}
}
跳到_Med3_unchecked
这个部分也比较容易理解
就是三个元素的冒泡排序
然后调整这三个数字的顺序
注意这里是iter_swap,直接将原数组的元素位置给换了
写个程序测试一下
template <class _RanIt, class _Pr>
_CONSTEXPR20 void _Med3_unchecked(_RanIt _First, _RanIt _Mid, _RanIt _Last, _Pr _Pred) {
// sort median of three elements to middle
if (_DEBUG_LT_PRED(_Pred, *_Mid, *_First)) {
_STD iter_swap(_Mid, _First);
}
if (_DEBUG_LT_PRED(_Pred, *_Last, *_Mid)) { // swap middle and last, then test first again
_STD iter_swap(_Last, _Mid);
if (_DEBUG_LT_PRED(_Pred, *_Mid, *_First)) {
_STD iter_swap(_Mid, _First);
}
}
}
写个小程序测试一下
猜想是对的,果然把元素的位置给换了
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
#define debug(x) cout<<#x<<": "<<(x)<<endl;
int main()
{
vector<int> a = {5,6,7,8,9,1,2,3,4};
vector<int>::iterator beg = a.begin();
vector<int>::iterator en = a.end()-1;
vector<int>::iterator mid = beg+a.size()/2;
cout << *beg << " " << *mid << " "<< * en << endl;
_Med3_unchecked(beg, mid, en, less<int>{});
cout << *beg << " " << *mid << " " << *en << endl;
return 0;
}
总结
整体来说,sort的源码里面的内容不算多,但是因为用了模板搞得代码的可读性很差
这也是c++目前遭受诟病的一大原因
不过标准库这样的设计使得排序算法的复杂度最差也是O(nlogn)
很多想法还是值得学习的