【STL源码阅读】std::sort(),十分钟了解msvc的stl的sort实现

本文详细解读了C++标准库sort函数背后的IntroSort算法,涉及快速排序、堆排序和插入排序的运用,以及如何通过中位数猜测优化分割过程。通过代码实例演示了关键部分的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一份简化的代码(可读性较强)

c++ 的sort用了很多年,一直不知道具体是怎么写的
决定看看代码,以下文章结构可能有点混乱,建议读者同时打开vs同步跳转
https://www.geeksforgeeks.org/internal-details-of-stdsort-in-c/
简化版本
sort() 使用的算法是IntroSort
IntroSort是一个混合排序算法使用三个排序算法

  1. 快速排序
  2. 堆排序
  3. 插入排序

当元素的个数少于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)
很多想法还是值得学习的

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值