STL sort 分析

前言

STL 中提供了很多算法,sort 是我们经常使用的,那它究竟是如何实现的呢?

STL 的 sort 算法,数据量大时采用快速排序,分段递归。一旦分段的数据量小于某个门槛,为避免快速排序的递归调用带来过大的额外负荷,就改用插入排序。如果递归层次过深,还用改用堆排序。这个算法接受两个随机迭代器,然后将区间内的所有元素以升序排列。第二个版本则允许用户指定一个彷函数作为排序标准,我们暂时不考虑这个版本。

注意本文并不教具体的排序算法,只是分析 sort 中的行为,对于插入排序和快速排序需自行掌握。

插入排序

插入排序以双层循环的形式进行。外层循环遍历整个序列,每次迭代决定一个子区间;内循环遍历子区间,将当前值插入到合适的位置。

template <class RandomAccessIterator>
    void __insertion_sort(RandomAccessIterator first, RandomAccessIterator last) {
    if (first == last) {
        return;
    } 
    for (RandomAccessIterator i = first + 1; i != last; ++i) {
        // 外循环,形成 [first, i) 的子区间
        __linear_insert(first, i, value_type(first));
    }
}

插入排序

// 版本一辅助函数
template <class RandomAccessIterator, class T>
    inline void __linear_insert(RandomAccessIterator first, 
                                RandomAccessIterator last, T*) {
    T value = *last;			// 记录尾元素
    if (value < *first) {		// 尾比头还小
        copy_backward(first, last, last + 1);	// 令元素整体向后移动一个位置
        *first = value;							// 将尾元素插在头部
    } else {					// 尾不小于头
        __unguarded_linear_insert(last, value);
    }
}
// 版本一辅助函数
template <class RandomAccessIterator, class T>
    void __unguarded_linear_insert(RandomAccessIterator last, T value) {
    RandomAccessIterator next = last;	// 指向尾元素
    --next;							// 调整指向前一元素
    while (value < *next) {			// 尾元素更小,插入位置应在前面
        *last = *next;					// 将 next 指向的元素值向后移动一个位置
        last = next;		
        --next;							// 向前移动迭代器,继续寻找
    }
    *last = value;					// value >= *next 即为正确的插入位置
}

上面的代码看起来好像没什么特别之处,其实暗藏玄机。在一般的插入排序中,我们除了要判断两元素是否逆序外,还需要判断 next 是否超出了边届

为什么上述代码不需要判断呢?

原因就在于上述代码的左边届一定是最小值,这在调用上一层 __linear_insert 时做的,该函数首先将要插入的值于左边届比较,更小就直接插入。此时,进入到 __unguarded_linear_insert 循环体的待插入值,一定大于等于左边届,就不可能出现越届的情况,也就可以把边届检查省略了。

快速排序

快速排序过程如下,假设 S 代表将被处理的序列:

  1. 如果 S 的元素个数为 0 或 1,结束
  2. 取 S 中的任何一个元素,当作枢轴 v
  3. 将 S 分隔为 L、R 两段,使 L 内的每一个元素都小于或等于 v,R 内的每一个元素都大于或等于 v
  4. 对 L,R 递归执行快速排序

快速排序的精神在于将大区间分割为小区间,分段排序。每一个小区间排序完成后,和起来的大区间也就完成了排序。

三点中值

理论上,任何一个元素都可以被选做枢轴,但合适与否却会影响快速排序的效率。比如一个已经升序的序列,我们每次都选其左端点作为枢轴,效率将退化为 O ( N 2 ) O(N^2) O(N2)

最理想的方式是取整个序列的头、尾和中间三个位置的元素以其中值作为枢轴。为了能够快速取出中央位置的元素,显然迭代器必须是随机迭代器。

template <class T>
    inline const T& __median(const T& a, const T& b, const T& c) {
    if (a < b) {
        if (b < c) {		// a < b < c
            return b;
        } else if (a < c) {	// a < b,b >= c,a < c
            return c;
        } else {			// a < b,a >= c
            return a;
        }
    } else if (a < c) {	// a >= b,a < c
        return a;
    } else if (b < c) {	// a >= b,a >= c,b < c
        return c;
    } else {				// a >= b,a >= c,b >= c
        return b;
    }
}

分隔

令头迭代器 first 向尾部移动,尾端迭代器 last 向头部移动。当 *first 大于或等于枢轴时就停下来当 *last 小于或等于枢轴时也停下来,然后检验两个迭代器是否交错。如果 first 仍然在左,last 仍然在右,就将两者元素交换,然后各自调整一个位置。

下面是 SGI STL 提供的分隔函数,其返回值是分隔后右段第一个位置:

template <class RandomAccessIterator, class T>
    RandomAccessIterator __unguarded_partition(RandomAccessIterator first, 
                                               RandomAccessIterator last, 
                                               T pivot) {
    while (true) {
        while (*first < pivot) {	// 向后寻找大于等于枢轴的值
            ++first;
        }
        --last;
        while (pivot < *last) {		// 向前寻找小于等于枢轴的值
            --last;
        }

        if (!(first < last)) {		// first 在右,last 在左就退出
            return first;
        }
        iter_swap(first, last);
        ++first;
    }
}  

分隔

阈值

面对一个数据量很小的序列,使用快速排序产生递归调用是不划算的。鉴于这种情况,适当评估序列的大小,然后决定采用快速排序或插入排序,是值得采纳的一种优化措施。不过对于多小的序列采用插入排序,暂时没有定论,5~20 都可能导致差不多的结果。

STL sort

下面是 SGI STL sort() 源代码:

template <class RandomAccessIterator>
    inline void sort(RandomAccessIterator first, RandomAccessIterator last) {
    if (first != last) {
        __introsort_loop(first, last, value_type(first), __lg(last - first) * 2);
        __final_insertion_sort(first, last);
    }
}

__lg

__lg 是用来控制分隔恶化的情况:找出 2 k ≤ n 2^k \leq n 2kn 的最大值 k。

例:n = 10,得 k = 3;n = 2 得 k = 1;n = 20 得 k = 4。

template <class Size>
    inline Size __lg(Size n) {
    Size k;
	// k 即为 n 最高位的 1 右边的位数
    for (k = 0; n != 1; n >>= 1) {
        ++k;
    }
    return k;
}

__introsort_loop

当元素个数为 20 时,__introsort_loop() 的最后一个参数是 4 * 2,意思是最多允许分割 8 层。

template <class RandomAccessIterator, class T, class Size>
    void __introsort_loop(RandomAccessIterator first,
                          RandomAccessIterator last, T*,
                          Size depth_limit) {
	// __stl_threshold 是个全局常量,SGI STL 中定义为 16
    while (last - first > __stl_threshold) {
        if (depth_limit == 0) {				// 分割恶化,改用堆排序
            partial_sort(first, last, last);
            return;
        }
        --depth_limit;						// 分割一次就 --
        // 枢轴通过 __median 获取
        RandomAccessIterator cut = __unguarded_partition
            (first, last, T(__median(*first, *(first + (last - first)/2),
                                     *(last - 1))));
        // 对右半段递归调用 __introsort_loop
        __introsort_loop(cut, last, value_type(first), depth_limit);	
        // 调整边届,左半段在当前循环解决
        last = cut;
    }
}

上述代码中并没对左右两段递归调用,只是对右段递归调用,左半段调整边届后在循环内处理。这样的可读性较差,但可以减少一次递归调用的消耗。

__final_insertion_sort

如果我们令某个大小以下的序列停留的「接近排序但尚未完成」的状态,最后再用一次插入排序将所有这些「接近排序但尚未完成」的子序列做一次完整的排序,其效率一般认为会比「将所有子序列彻底排序」更好。这时因为插入排序在面对「接近排序」的序列时,有很好的表现。

template <class RandomAccessIterator>
    void __final_insertion_sort(RandomAccessIterator first, 
                                RandomAccessIterator last) {
    if (last - first > __stl_threshold) {	// 元素个数大于 16 个
        __insertion_sort(first, first + __stl_threshold);			// 保证最小元素在最左侧
        __unguarded_insertion_sort(first + __stl_threshold, last);	// 调用不用判断边届的插入排序
    } else {
        __insertion_sort(first, last);
    }
}

上述代码非常奇怪,首先判断元素的个数。

为什么要这样做呢?

首先,快速排序是以 __stl_threshold 为最小阈值,也就是说最小值一定在最左边 __stl_threshold 个元素内。先将最小元素放在最左侧,再直接调用不用判断边届的插入排序,可以提高效率。否则,要先调用 __linear_insert,再与左端点值比较,再调用 __unguarded_insertion_sort。

template <class RandomAccessIterator>
    inline void __unguarded_insertion_sort(RandomAccessIterator first, 
                                           RandomAccessIterator last) {
    __unguarded_insertion_sort_aux(first, last, value_type(first));
}
template <class RandomAccessIterator, class T>
    void __unguarded_insertion_sort_aux(RandomAccessIterator first, 
                                        RandomAccessIterator last, T*) {
    for (RandomAccessIterator i = first; i != last; ++i) {
        __unguarded_linear_insert(i, T(*i));	// 调用不用判断边届的插入排序
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/353.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

三天吃透计算机网络面试八股文

本文已经收录到Github仓库&#xff0c;该仓库包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享等核心知识点&#xff0c;欢迎star~ Github地址&#xff1a;https://github.com/…

Linux常用命令

个人简介&#xff1a;云计算网络运维专业人员&#xff0c;了解运维知识&#xff0c;掌握TCP/IP协议&#xff0c;每天分享网络运维知识与技能。座右铭&#xff1a;海不辞水&#xff0c;故能成其大&#xff1b;山不辞石&#xff0c;故能成其高。个人主页&#xff1a;小李会科技的…

C++STL 容器案例 员工分组 实现步骤与代码分析与展示 实现步骤的注意事项

STL容器 员工分组案例 文章目录STL容器 员工分组案例1 案例描述2 实现步骤3 案例代码与分析1 案例描述 公司今天招聘了10个员工&#xff08;ABCDEFGHIJ&#xff09;&#xff0c;10名员工进入公司之后&#xff0c;需要指派员工在哪个部门工作员工信息有: 姓名 工资组成&#xf…

CANoe中使用CAPL刷写流程详解(Trace图解)(CAN总线)

&#x1f345; 我是蚂蚁小兵&#xff0c;专注于车载诊断领域&#xff0c;尤其擅长于对CANoe工具的使用&#x1f345; 寻找组织 &#xff0c;答疑解惑&#xff0c;摸鱼聊天&#xff0c;博客源码&#xff0c;点击加入&#x1f449;【相亲相爱一家人】&#x1f345; 玩转CANoe&…

史上最全最详细的Java架构师成长路径图,程序员必备

从新手码农到高级架构师&#xff0c;要经过几步&#xff1f;要多努力&#xff0c;才能成为为人倚重的技术专家&#xff1f;本文将为你带来一张程序员发展路径图&#xff0c;但你需要知道的是&#xff0c;天下没有普适的道理&#xff0c;具体问题还需具体分析&#xff0c;实践才…

Verilog实现组合逻辑电路

在verilog 中可以实现的数字电路主要分为两类----组合逻辑电路和时序逻辑电路。组合逻辑电路比较简单&#xff0c;仅由基本逻辑门组成---如与门、或门和非门等。当电路的输入发生变化时&#xff0c;输出几乎&#xff08;信号在电路中传递时会有一小段延迟&#xff09;立即就发生…

马上要面试了,还有八股文没理解?让ChatGPT来给你讲讲吧——如何更好使用ChatGPT?

最近这段时间 ChatGPT 掀起了一阵 AI 热潮&#xff0c;目前来看网上大部分内容都是在调戏 AI&#xff0c;很少有人写如何用 ChatGPT 做正事儿。 作为一个大部分知识都是从搜索引擎和 GitHub 学来的程序员&#xff0c;第一次和 ChatGPT 促膝长谈后&#xff0c;基本认定了一个事…

AI又进化了,突破性革命来了

大家好&#xff0c;我是 Jack。 2023 年&#xff0c;AI 真的杀疯了。短短不到一年的时间&#xff0c;当我们还在感慨 AI 一键生成的二次元画作精美万分的时候&#xff0c;它已经进化到了写实美照也能手到擒来的地步。 更多的效果&#xff0c;可以看刚刚发布的视频&#xff0c;…

爽,我终于掌握了selenium图片滑块验证码

因为种种原因没能实现愿景的目标&#xff0c;在这里记录一下中间结果&#xff0c;也算是一个收场吧。这篇文章主要是用selenium解决滑块验证码的个别案列。 思路&#xff1a; 用selenium打开浏览器指定网站 将残缺块图片和背景图片下载到本地 对比两张图片的相似地方&#x…

十大经典排序算法(上)

目录 1.1冒泡排序 1. 算法步骤 3.什么时候最快 4. 什么时候最慢 5.代码实现 1.2选择排序 1. 算法步骤 2. 动图演示 3.代码实现 1.3 插入排序 1. 算法步骤 2. 动图演示 3. 算法实现 1.4 希尔排序 1. 算法步骤 2. 动图演示 3.代码实现 1.5 归并排序 1. 算法步骤 2…

2023年中国高校计算机大赛-团队程序设计天梯赛(GPLT)上海理工大学校内选拔赛(同步赛) A — E

2023年中国高校计算机大赛-团队程序设计天梯赛&#xff08;GPLT&#xff09;上海理工大学校内选拔赛&#xff08;同步赛) 文章目录A -- A Xor B Problem题目分析codeB -- 吃苹果题目分析codeC -- n皇后问题题目分析codeD -- 分苹果题目分析codeE -- 完型填空题目分析codeA – A…

图像缩放对相机内外参矩阵的影响

参考资料&#xff1a;https://zhuanlan.zhihu.com/p/87185139 一、3D空间中点到图像的投影 设3D空间中的点(x,y,z)(x,y,z)(x,y,z)投影到图像上的像素坐标&#xff08;连续值&#xff0c;以左上角像素的左上角为原点的坐标系&#xff0c;注意与整数值的图像像素索引相区别&…

HTTPS的加密原理(工作机制)

现在很多网站使用的都是HTTPS协议,比如CSDN他们为什么要使用HTTPS协议而不是继续使用HTTP协议呢?以及HTTPS都做了些什么?HTTP协议与HTTPS有哪些区别? 下面我来 讲解这些问题?(篇幅可能有些长,请求耐心观看,我以0基础的角度去讲解这些东西, 如果你有一定的基础前面的跳过就好…

docker安装elasticsearch与head教程完整版—.NET Core Web Api与elasticsearch打造全站全文搜索引擎

默认已经有docker环境 下载与安装 elasticsearch &#xff0c;从hub.docker里面可以看到最新版本的镜像&#xff0c;选择你想要的版本 本教程是以 7.17.7 为案例&#xff0c;为啥不适用最新的&#xff0c;首先个人一般需用最新的版本&#xff0c;如果有亢很难填&#xff0c;其次…

三体到底是啥?用Python跑一遍就明白了

文章目录拉格朗日方程推导方程组微分方程算法化求解画图动图绘制温馨提示&#xff0c;只想看图的画直接跳到最后一节拉格朗日方程 此前所做的一切三体和太阳系的动画&#xff0c;都是基于牛顿力学的&#xff0c;而且直接对微分进行差分化&#xff0c;从而精度非常感人&#xf…

如何用Python求解微分方程组

文章目录odeint简介示例odeint简介 scipy文档中将odeint函数和ode, comples_ode这两个类称为旧API&#xff0c;是scipy早期使用的微分方程求解器&#xff0c;但由于是Fortran实现的&#xff0c;尽管使用起来并不方便&#xff0c;但速度没得说&#xff0c;所以有的时候还挺推荐…

Vite4 + Vue3 + vue-router4 动态路由

动态路由&#xff0c;基本上每一个项目都能接触到这个东西&#xff0c;通俗一点就是我们的菜单是根据后端接口返回的数据进行动态生成的。表面上是对菜单的一个展现处理&#xff0c;其实内部就是对router的一个数据处理。这样就可以根据角色权限或者一些业务上的需求&#xff0…

机器学习入门——线性回归

线性回归什么是线性回归&#xff1f;回归分析&#xff1a;线性回归&#xff1a;回归问题求解单因子线性回归简单实例评估模型表现可视化模型展示多因子线性回归什么是线性回归&#xff1f; 回归分析&#xff1a; 根据数据&#xff0c;确定两种或两种以上变量间相互依赖的定量…

自学大数据第六天~HDFS命令(一)

HDFS常用命令 查看hadoop版本 version hadoop version注意,没有 ‘-’ [hadoopmaster ~]$ hadoop version Hadoop 3.3.4 Source code repository https://github.com/apache/hadoop.git -r a585a73c3e02ac62350c136643a5e7f6095a3dbb Compiled by stevel on 2022-07-29T12:3…

【电赛MSP430系列】GPIO、LED、按键、时钟、中断、串口、定时器、PWM、ADC

文章目录MSP430一、GPIO二、点亮LED三、按键控制LED四、更改主时钟五、串口通信六、串口中断七、外部中断八、定时器九、定时器中断十、PWM十一、ADCMSP430 MSP430 是德州仪器&#xff08;TI&#xff09;一款性能卓越的超低功耗 16 位单片机&#xff0c;自问世以来&#xff0c…