STL源码分析:sort函数

2023-10-31

       

目录

支持sort的容器

几种涉及到的排序算法

插入排序

快速排序

堆排序

sort函数的策略

sort函数的实现


STL的sort函数非常常用,不同的STL版本有不同的实现方式,本文就来说一下SGI STL中是如何实现sort函数的。

       sort函数所采用的排序方法并非是“一种”,而是“多种”排序算法的“混合物”。在SGI STL的版本中,对一个序列调用sort函数,那么这个序列可能会经历插入排序、快速排序和堆排序,其中最为核心的是快速排序。每种排序方式都是在特定情况下使用的,搭配使用从而更加高效。

支持sort的容器

       由于需要使用到这么多的排序方式,因此调用sort函数的容器必须支持“Random access iterator”(如果不能随机访问,那么快排选取枢轴会受到极大限制),即随机访问迭代器(即支持迭代器++、--和+=、-=)。在众多STL容器中,由于红黑树的迭代器是一个双向迭代器(仅支持++、--),hashtable的迭代器是一个前向迭代器(仅支持++),因此set、map(二者本身就有序)、hash_set和hash_map以及相应的multixxx都不适用于sort函数;queue和stack不允许排序,而list的迭代器是一个双向迭代器,slist的迭代器是一个前向迭代器,因此STL容器中适用sort函数的只有vector和deque。

几种涉及到的排序算法

插入排序

       插入排序的思想,就是在排序过程中把整个序列分为有序子序列和无序子序列,初始状况下有序子序列中只含有第一个元素。接着从第二个元素开始遍历,对于每个当前遍历的元素X,都会在其左侧的有序子序列中寻找到第一个小于X的元素Y,在寻找过程中,比X大的元素会与X交换位置,找到Y后把X插入Y的后面。整个排序过程中,当前遍历的元素的左边就是有序子序列,右边及它自身则为无序子序列。插入排序过程如下所示:

        从插入排序的过程来看可以知道,插入排序的平均时间复杂度为O(N^2),它取决于每个元素往回遍历时“何时才能找到第一个小于它的元素”。如果元素的数量比较少,那么显然插入排序会趋向于O(N)。

快速排序

       快速排序的思想,就是在序列中寻找一个枢轴,每一次遍历时可以让小于或等于枢轴的元素放在一边,而大于或等于枢轴的元素放在另外一边。然后再进行递归,对子区间进行快速排序,最终得到有序序列。 快速排序的平均时间复杂度为O(NlogN),影响其效率的关键在于枢轴的选取,如果枢轴选取不当,那么可能最终分隔出来的两个区间一个太长一个太短,此时快速排序就会趋向于O(N^2)。实际上,序列无序比有序更有益于快速排序,序列本身越有序,那么快速排序退化为O(N^2)的概率就更大,序列越随机无序越好。

堆排序

       堆排序也是一种平均时间复杂度为O(NlogN)的排序算法。不过相较于快速排序的一般情况,堆排序每插入一个数据都需要进行调整堆,这样就使得堆排序过程中元素的比较、交换过程比快速排序更多,虽然也是O(NlogN)级别,但在平均情况下还是略逊于快速排序。不过堆排序的好处是:可以实时插入数据来排序,并且最差情况也是O(NlogN)。

 

        从工业应用上来说,如果只是为了排序,那么快速排序在大多数情况下都是表现最好的一种排序算法,也是最常用的排序算法。不过为了精益求精,SGI STL在使用快速排序的同时,还使用插入排序和堆排序来尽量避免快速排序可能出现的问题。

sort函数的策略

       基于以上三种排序算法的特点,SGI STL中的sort函数有以下策略:

1.如果序列元素数量较少,那么应该采用插入排序。

       如前所述,插入排序在元素较少的情况下时间复杂度会趋向于O(N),而快速排序在元素较少的情况下,也更容易出现O(NlogN)向O(N^2)退化的情况,除此之外,快速排序内部是会递归排序的,这是无可避免的,在元素较少的情况下,多次递归调用所影响的效率降低也是值得考虑的。介于此,SGI STL认为,在元素较少的情况下,插入排序会比快速排序表现更好。

       那么,如何去衡量“元素较少”呢?这在不同版本的STL中有不同的规定。

       在SGI STL中,通过const变量__stl_threshold来定义这个值为16:

       而在VS2013 STL中,则通过const变量_ISORT_MAX来定义这个值为32:

       可见,在不同版本的STL中,对于这个值的定义是不同的。当序列元素数量小于该值,就会使用插入排序。

2.在快速排序过程中,如果递归子区间的元素个数较少,那么就停止快速排序,转而使用插入排序。

       这一点实际上和第1点是类似的。个人认为这里主要是考虑到对于小数组来说,插入排序与快速排序的差别不会很大,而快排由于本身还需要有递归的消耗,所以插入排序在小数组的情况下效果会好一些。

3.快速排序递归深度不宜过深,过深则用堆排序来替代。

       快速排序是一个递归的过程,越往下递归,子区间越小,递归消耗就越大,还可能造成栈溢出。但是如果此时子区间还没有达到插入排序的阈值该怎么办呢?既然此时不能再从子区间元素数量来衡量,那么就可以从递归深度来衡量,这样还可以避免递归栈溢出。

       那么如何去衡量“递归深度”呢?SGI STL用2*log(N)来作为快速排序的最大递归深度。举个例子,40个元素,那么快速排序递归的最大深度就是2*log(40)=2*5=10,也就是说,如果快速排序递归了10次,还没达到插入排序的阈值,那么就不应当再使用快速排序了,此时就会转为使用堆排序,对该子区间的元素进行排序。使用堆排序来代替的好处是可以让时间复杂度保持在O(NlogN)。

 

       下面来看看sort函数内部是如何实现的。

sort函数的实现

       sort函数源码如下:(以默认的升序为例,不考虑比较器的sort)

template <class _RandomAccessIter>
inline void sort(_RandomAccessIter __first, _RandomAccessIter __last) {
  ......
  if (__first != __last) {
    __introsort_loop(__first, __last,
                     __VALUE_TYPE(__first),
                     __lg(__last - __first) * 2);//此时序列已经变为基本有序
    __final_insertion_sort(__first, __last);
  }
}

        在sort函数中,调用了两个函数,一个是introsort_loop,一个是final_insertion_sort。

        这里的introsort就是内省排序,是David R.Musser于1996年提出的一种混合式排序算法。它也是上面第3点策略的体现。该函数定义如下:

template <class _RandomAccessIter, class _Tp, class _Size>
void __introsort_loop(_RandomAccessIter __first,
                      _RandomAccessIter __last, _Tp*,
                      _Size __depth_limit)
{
  while (__last - __first > __stl_threshold) {
    if (__depth_limit == 0) {//到达递归深度限制,就使用堆排序
      partial_sort(__first, __last, __last);
      return;
    }
    --__depth_limit;
    _RandomAccessIter __cut =
      __unguarded_partition(__first, __last,
                            _Tp(__median(*__first,
                                         *(__first + (__last - __first)/2),
                                         *(__last - 1))));//通过快排找到分割点
    __introsort_loop(__cut, __last, (_Tp*) 0, __depth_limit);//对分割点右边的部分继续快排
    __last = __cut;//分割点左边的部分进入下一个循环,左边的部分一定小于等于右边的部分
  }
}

       这个函数的第四个参数是depth_limit,也就是递归深度限制,可以看到,每次调用该函数的时候,depth_limit都会-1,然后作为递归调用introsort_loop的第四个参数,如果depth_limit为0,那么说明到达了最大的递归深度,此时就会调用partial_sort,就是堆排序来对该区间的元素进行排序。introsort的核心思想就是如此,sort函数的第3点策略也体现于此。再来说下其它部分。

       __unguarded_partition就是快速排序的体现,传入的前两个参数是排序区间,第三个参数是枢轴值,SGI STL选用区间第一个、中间的和最后一个元素三者大小排序中间的那个元素作为枢轴,这样的好处是避免枢轴直接是最大值或最小值而导致快速排序退化为冒泡排序。__unguarded_partition函数的返回值交给cut,它是分割点,即在cut左边的元素都不大于刚才选取的枢轴值,cut右边的元素都不小于刚才选取的枢轴值。

       得到了cut,就相当于快速排序结束了一轮,此时再次递归调用introsort_loop来对右子区间进行快速排序。而左子区间的快排则直接通过当前的循环进行,把cut作为左子区间的右端点,再次进行introsort_loop。

        值得注意的是这个循环的条件,是__last-first>stl_threshold,也就是说区间中的元素要超过stl_threshold才能继续进行快速排序,这个stl_threshold就是前面说的插入排序的阈值,如果小于或等于这个阈值,就会退出该循环。

 

       退出这个循环后会做什么呢?

       接下来就是调用__final_insertion_sort函数,从函数名就知道,插入排序就执行于此,该函数定义如下:

template <class _RandomAccessIter>
void __final_insertion_sort(_RandomAccessIter __first, 
                            _RandomAccessIter __last) {
  if (__last - __first > __stl_threshold) {//将序列分为两段来插入排序效果会更好
    __insertion_sort(__first, __first + __stl_threshold);
    __unguarded_insertion_sort(__first + __stl_threshold, __last);
  }
  else
    __insertion_sort(__first, __last);
}

       先来想一下什么时候会调用该函数:一种是序列本身元素数量就少于stl_threshold,那么直接执行__final_insertion_sort,这种情况属于序列元素较少,此时应该直接进行插入排序,属于sort函数的第1条策略;第二种是快速排序递归后子区间元素数量少于stl_threshold,此时相当于对“基本有序”的序列进行插入排序,这种属于sort函数的第2条策略

       在第一种情况下,显然就会直接执行else下的__insertion_sort,这个函数实际上就是进行插入排序,没别的。

       值得注意的是在第二种情况下,进入if中,会把序列分为两段来进行插入排序。之所以这样,是因为在经过快速排序后,序列中的前面stl_threshold元素整体必定是小于后面的元素的,此时对这两段分别排序肯定比一起排序更好(序列越短越利于插入排序,但是这也要建立在分开插入排序后整体依然是有序的前提,而这里的stl_threshold就保证了这一点)。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

STL源码分析:sort函数 的相关文章

随机推荐

  • 数据集市的概念

    目录 一 数据集市简介 1 1 数据集市与数据仓库 二 数据集市的类型 2 1 依赖数据仓库 2 2 独立数据集市 2 3 混合数据集市 三 数据集市的特点 四 实施数据集市的步骤 一 数据集市简介 数据集市就是企业级数据仓库的一个子集 它
  • Linux应用开发基础

    一 安装Pocy交叉编译工具链 将fsl imx x11 glibc x86 64 metatoolchain qt5 cortexa7hf neon toolchain 4 1 15 2 1 0 sh 拷贝到 Ubuntu 虚拟机 修改使
  • 用群晖NAS搭建个人音乐库

    安装教程 勾选启动NTP服务 1 群晖安装Audio Station 2 filestation会生成一个music文件夹 把下载好的音乐丢进music即可 音乐平台听不到的歌也顺带通过下载解决了 这时候你就可以在audio station
  • No.7软件需求规格说明书及UML

    软件需求规格说明书 SRS 是需求开发活动的产物 编制该文档的目的是使项目干系人与开发团队对系统的初始规定有一个共同的理解 使之成为整个开发工作的基础 软件需求规格说明书 国家标准BG T 8567 2006中 提供了SRS的文档模版和编写
  • 微信公众号第三方登录,简单易懂

    1 准备工作 1 登录微信公众号接口测试平台设置信息 地址 http mp weixin qq com debug cgi bin sandbox t sandbox login 登录成功后可以看到测试用的appid和appsecret 这
  • Premiere Pro cc 2019 全面使用教程(非常简单)

    视频剪辑工具 对于youtuber vloger 抖音播客都是必不可少的工具 一直关注pr终于有机会尝试一下 比较全面地记录一部短片的制作操作 一 安装 1 安装软件 2 crack 将此dll替换C Program Files Adobe
  • 如何微调医疗大模型llm:llama2学习笔记

    三个微调方向 简单医疗问答 临床问答 影像学 一般流程 1 数据集准备 2 模型基座选择 3 微调 4 案例拆解 1 数据集准备 两种类型 一种文本一种影像 扩展 多模态 2 模型基座选择 多模态处理所有视频 文本 数字人将会受到威胁 数字
  • 21款网页版html5小游戏源码

    html5魅族创意的贪食蛇游戏源码下载 html5网页版打砖块小游戏源码下载 html5 3D立体魔方小游戏源码下载 html5网页版飞机躲避游戏源码下载
  • Arduino简单实例之三_土壤湿度传感器

    1 说明 用于土壤的湿度检测 可通过电位器调节土壤湿度的阀值 顺时针调节 控制的湿度会越大 逆时针越小 湿度低于设定值时 DO输出高电平 模块提示灯亮 湿度高于设定值时 DO输出低电平 模块提示灯灭 工作电压3 3V 5V 3V时 在空气中
  • 2023年新自采集壁纸网页源码+简约大气

    正文 一款壁纸网页源码 但这款源码是有网友改了一下接口 但还是挺不错的 有兴趣的可以自己去搭建体验 我也搭建了演示站 感觉挺行 界面美观大气 壁纸也很多 类型也比较多 程序 wwidsu lanzout com i3ZHQ0f7dqli 图
  • Hibernate --- hibernate.cfg.xml核心配置文件详解

    一 Hibernate配置文件加载流程 1 通过Configuration config new Configuration configure 加载默认配置文件 2 Configuration的configure 方法 注意 hibern
  • LeetCode总结 -- 图篇

    图的算法跟树一样是准备面试中必不可少的一块 不过图的方法很容易概括 面试中考核的无非就是两种搜索算法 深度优先搜索和广度优先搜索 LeetCode中关于图的问题有以下几个 Clone Graph Word Ladder Word Ladde
  • 【华为OD机试真题】不含101的数(python版)100%通过率 超详细代码注释 代码解读

    华为OD机试真题 2022 2023 真题目录 点这里 华为OD机试真题 信号发射和接收 试读 点这里 华为OD机试真题 租车骑绿道 试读 点这里 不含101的数 时间限制 1s空间限制 256MB限定语言 不限 题目描述 小明在学习二进制
  • 堪称一绝,阿里技术人都用的Nginx笔记手册,应用到架构齐全

    有人调侃我们说 程序员不如送外卖 送外卖是搬运食物 自己是搬运代码 都不产出新的东西 透支体力 又消耗健康 可替代性极强 30岁之后就要面临被优化的危险 想跳槽 但是更高的平台难进 同级别的平台又是重复 想利用业余时间学习提升 但是自己能力
  • mininet+pox+poxdesk使用入门

    mininet pox poxdesk使用入门 前提是安装好mininet以及pox和poxdesk模块 接下来就是如何使用 笔者环境是Win7环境下VMware Workstation新建虚拟机上运行Ubuntu 12 0 首先在新打开的
  • 面试题(vue,react,前端)

    目录 1 说说React生命周期中有哪些坑 如何避免 2 说说Real diff算法是怎么运作的 3 调和阶段setState干了什么 4 说说redux的实现原理是什么 写出核心代码 5 React合成事件的原理 6 React组件之间如
  • java后端生成图形验证码、前端接收并展示

    1 工具类 import java awt Color import java awt Font import java awt Graphics import java awt Graphics2D import java awt Ren
  • PHP中的数据类型有哪些?

    嗨 大家好 今天 我们来了解一下PHP中的数据类型 首先 让我们来介绍一下PHP中的基本数据类型 在PHP中 有六种基本数据类型 它们分别是 整数型 int 用于存储整数 例如 123 456等 浮点型 float 用于存储带有小数点的数
  • Linux删除文件后,发现磁盘空间没有释放-lsof

    最近碰到磁盘快满了 原因是程序错误导致日志爆炸性增长 于是直接删除日志文件 然后df h 发现磁盘空间一点都没下降 还是原来的90 使用率 有点奇怪 百度了解到 日志文件被删除之前文件处于被其他进程占用状态 即使删除 依然占用空间 通过ls
  • STL源码分析:sort函数

    目录 支持sort的容器 几种涉及到的排序算法 插入排序 快速排序 堆排序 sort函数的策略 sort函数的实现 STL的sort函数非常常用 不同的STL版本有不同的实现方式 本文就来说一下SGI STL中是如何实现sort函数的 so