.

  • 一份简化的代码(可读性较强)
  • 看看std::sort()是怎么做的
    • 打开vs,跳到sort的定义
    • 在首先在algorithm中找到sort
    • 可以看到这个sort是仅仅支持随机访问的迭代器
  • 跳到 带有 _Pred的sort默认情况是less<>{}
  • 跳转到_Sort_unchecked
    • _Partition_by_median_guess_unchecked
    • 跳到第五行 _Guess_median_unchecked
    • 跳到_Med3_unchecked
      • 写个小程序测试一下
  • 总结

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

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 sortedint 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 aregreater than key, to one position aheadof 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]; // pivotint i = (low - 1); // Index of smaller elementfor (int j = low; j <= high- 1; j++){// If current element is smaller than or// equal to pivotif (arr[j] <= pivot){// increment index of smaller elementi++;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 elementsint size = end - begin;// If partition size is low then do insertion sortif (size < 16){InsertionSort(arr, begin, end);return;}// If the depth is zero use heapsortif (depthLimit == 0){make_heap(begin, end+1);sort_heap(begin, end+1);return;}// Else use a median-of-three concept to// find a good pivotint * pivot = MedianOfThree(begin, begin+size/2, end);// Swap the values pointed by the two pointersswapValue(pivot, end);// Perform Quick Sortint * 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 IntrosortIntrosortUtil(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 elementIntrosort(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 quicksortauto _Mid = _Partition_by_median_guess_unchecked(_First, _Last, _Pred);_Ideal = (_Ideal >> 1) + (_Ideal >> 2); // allow 1.5 log2(N) divisionsif (_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 (;;) { // partitionfor (; _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 upwardif (_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 downwardif (--_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 middleusing _Diff        = _Iter_diff_t<_RanIt>;const _Diff _Count = _Last - _First;if (40 < _Count) { // Tukey's nintherconst _Diff _Step     = (_Count + 1) >> 3; // +1 can't overflow because range was made inclusive in callerconst _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 middleif (_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)
很多想法还是值得学习的

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

  1. C++ STL源码剖析 笔记

    写在前面 记录一下<C++ STL源码剖析>中的要点. 一.STL六大组件 容器(container): 各种数据结构,用于存放数据: class template 类泛型: 如vecto ...

  2. 【STL源码剖析】迭代器

    [STL源码剖析]迭代器 第3章 迭代器(iterators)与traits编程技法(<STL源码剖析> ) 3.1 迭代器设计思维--STL关键所在 3.2 迭代器(iterator)是 ...

  3. TiDB 源码阅读系列文章(十五)Sort Merge Join

    2019独角兽企业重金招聘Python工程师标准>>> 什么是 Sort Merge Join 在开始阅读源码之前, 我们来看看什么是 Sort Merge Join (SMJ),定 ...

  4. 源码阅读:AFNetworking(十六)——UIWebView+AFNetworking

    该文章阅读的AFNetworking的版本为3.2.0. 这个分类提供了对请求周期进行控制的方法,包括进度监控.成功和失败的回调. 1.接口文件 1.1.属性 /**网络会话管理者对象*/ @prop ...

  5. 源码阅读:SDWebImage(十九)——UIImage+ForceDecode/UIImage+GIF/UIImage+MultiFormat

    该文章阅读的SDWebImage的版本为4.3.3. 由于这几个分类都是UIImage的分类,并且内容相对较少,就写在一篇文章中. 1.UIImage+ForceDecode 这个分类为UIImage ...

  6. STL源码剖析——P142关于list::sort函数

    在list容器中,由于容器自身组织数据的特殊性,所以list提供了自己的排序函数list::sort, 并且实现得相当巧妙,不过<STL源码剖析>的原文中,我有些许疑问,对于该排序算法,侯 ...

  7. [转载]《STL源码剖析》阅读笔记之 迭代器及traits编程技法

    本文从三方面总结迭代器   迭代器的思想   迭代器相应型别及traits思想   __type_traits思想 一 迭代器思想 迭代器的主要思想源于迭代器模式,其定义如下:提供一种方法,使之能够依 ...

  8. 【STL源码学习】std::list类的类型别名分析

    有了点模板元编程的traits基础,看STL源码清晰多了,以前看源码的时候总被各种各样的typedef给折腾得看不下去, 将<list>头文件的类继承结构简化如下 #include < ...

  9. Soul网关源码阅读(十)自定义简单插件编写

    Soul网关源码阅读(十)自定义简单插件编写 简介     综合前面所分析的插件处理流程相关知识,此次我们来编写自定义的插件:统计请求在插件链中的经历时长 编写准备     首先我们先探究一下,一个P ...

最新文章

  1. Eclipse解决Ctrl+c很卡的方法
  2. java分装_Java ——Number Math 类 装箱 拆箱 代码块
  3. word一键生成ppt 分页_如何一键把Word转换为PPT?
  4. (原创) cocos2d-x 3.0+ lua 学习和工作(4) : 公共函数(8): 生成只读table
  5. 华为隐藏功能扩大内存代码大全_发现将华为手机这3个功能打开,竟然可以将手机性能极限发挥...
  6. [蓝桥杯]试题 基础练习 Sine之舞
  7. 【NetBeans IDE 8.2链接MySQL数据库教程】
  8. 夏敏捷 第39本著作《微信小程序游戏开发快速入门到实战》
  9. nc系统显示不能连接到服务器,用友U8 用友登录时提示不能连接到服务器。
  10. 隐藏APP图标并通过代码启动
  11. html thead作用,HTML thead 标签定义和用法详细介绍
  12. 一些免费的代理服务器
  13. unity隐藏鼠标光标的2种方法
  14. Redis 6 学习记录
  15. SASE 的核心能力:安全Web网关
  16. 介绍几种室内定位技术
  17. SpringBoot整合Shiro详细用法
  18. 韦根读卡电路c语言程序,RFID读卡模块电路图、PCB源文件 125K RFID读卡模块 - 下载 - 搜珍网...
  19. c语言教学知识,C语言的编程教程_入门教学知识
  20. Python入门---顺序与选择结构

热门文章

  1. 语音电话机器人的核心技术是什么
  2. php无极分类非递归_php实现无限级分类(递归方法)
  3. 2019-12-02 校内数模新手赛
  4. 一体化医学影像平台PACS源码,影像存档与传输系统源码
  5. Max Pooling核运算
  6. 服务器开机显示mm,【分享】mmrecov到新的服务器后,启动RPC服务的问题
  7. 手机打开网页显示500服务器错误,win10打开网页提示http 500 内部服务器错误怎么办...
  8. 深入理解SP、LR和PC
  9. com.qihoo.android.float window,全民枪战360版(com.crisisfire.android.qihoo) - 3.23.1 - 游戏 - 酷安...
  10. 爆笑!博士师兄分享科研人最婉转的提分手方式...