STL算法之sort

        STL所提供的各式各样算法中,sort()是最复杂最庞大的一个。这个算法接受两个RandomAccessIterators(随机存取迭代器),然后将区间内的所有元素以渐增方式由小到大重新排列。还有一个版本则是允许用户指定一个仿函数代替operator<作为排序标准。STL的所有关联式容器(associative containers)都拥有自动排序功能(底层结构为RB-tree,见STL关联式容器介绍_stl 关联式容器-CSDN博客),所以不需要用到这个sort算法。至于序列式容器(sequence containers)中的stack,queue和priority-queue都有特别的入口,不允许用户对元素排序。剩下vector、dequeue和list,前两者的迭代器属于RandomAccessIterators,适合使用sort算法,list的迭代器则属于BidirectionalIterators,都不适合使用sort算法。如果要对list或slist排序,应该使用它们自己提供的成员函数sort()。稍后我们便可以看到为什么泛型算法sort()一定要求RandomAccessIterators。

排序有多么重要

        人类生活在一个有序的世界中。没有排序,很多事情无法进行。排过序的数据,特别容易查找。电话簿总是以人名为键值来排序,对人名而言,电话簿是有序的,对电话号码而言,电话簿是无序的。在电话簿里找一个人(从而得到他的电话号码)很容易,但我们能想象再电话簿里头不通过人名查找某个特定的电话号码吗?

        这类情况大量发生在日常生活中。字典需要排序,书籍索引需要排序,磁盘目录需要排序,名片需要排序,图书馆藏需要排序,户籍数据需要排序。任何数据只要你想快速查找,具需要排序。

        犹有进者,排序可能使其他工作更快更轻松。如果你要确定(或找出)一堆数据里头没有有重复的元素,先排序一遍再找,会比闷着头两两比对快快得多。换句话说,许多算法可能因为数据先行排序过而大幅改善效率。排序的成本,成为影响执行时间的关键因素。

        STL算法的sort算法,数据量大时采用Quik Sort,分段递归排序。一旦分段后的数据量小于某个门槛,为了避免Quik Sort的递归调用带来过多额外负担(overhead),就改用Insertion Sort。如果递归层次过深,还会改用Heap Sort参见STL序列式容器之heap(堆)_stl 堆-CSDN博客 。以下分别介绍Quick Sort和Insertion Sort,然后再整合起来介绍STL sort算法。

Insertion Sort

        Insertion Sort以双层循环的形式进行。外循环遍历整个序列,每次迭代决定出一个子区间;内循环遍历子区间,将子区间内的每一个“逆转对(inversion)”倒转过来。所谓“逆转对”是指任何两个迭代器i、j,i<j,而*i>*j。一旦不存在逆转对,序列即排序完毕。这个算法的时间复杂度为O(N^2),说起来并不理想,但是当数据量很少时,却有不错的效果,原因是实现上有一些技巧(稍后源代码可见),而且不像其它比较复杂的排序算法有着诸如递归调用等操作带来的额外负担。下图是Insertion Sort的详细步骤示意:

        图中左下部分的三角形内部,每一行都是有序的序列;每次往有序序列中增加一个元素,依次采用插入到正好使序列依然保持有序状态的方式进行;固而得名插入排序(Insertion Sort)。

        SGI STL的Insert Sort有两个不同的版本,一个使用默认的operator<,另个使用仿函数comp代替。以下列出版本一。由于STL规格并不开放Insert Sort,所以SGI将以下函数的名称都加上了双下划线,表示内部使用。

//版本一
template <class RandomAccessIterator>
void __intertion_sort(RandomAccessIterator first, RandomAccessIterator last) {
    if (first == last) return ;
    for (RandomAccessIterator I = first + 1, I != last; ++I) 
        __linear_insert(first, I, value_type(first));
}

template <class RandomAccessIterator, class T>
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_inserrt(last, value);
    
}


template <class RandomAccessIterator, class T>
void __unguarded_linear_inserrt(RandomAccessIterator last, T value) {
    RandomAccessIterator next = last;
    --next;
    while(value < *next) { // 新加入的元素比当前元素小,
        *last = *next;     // 则将当前元素往后挪,否则就再该位置填入新加入的值
        last = next;
        --next;
    }
    *last = value;
}

        上述函数之所以命名为unguarded_x是因为,一般的Insert Sort在内循环原本需要做两次判断,判断是否相邻两元素使“逆转对”,同时也判断循环是否超过边界。但由于上述所示的代码会导致最小值必然在内循环子区间的最边缘,所以两个判断可合为一个判断,所以成为unguarded_。省下一个判断操作,乍见之下无足轻重,但是在大数据量的情况下,影响还是可观的,毕竟这是一个非常根本的算法核心,在大数据量的情况下(大量调用?),提效会非常惊人。

        稍后出场的几个函数,也有以unguarded_为前缀命名者,同样是在特定情况下,边界条件的检验可以省略(或说已融入特定条件之内)。

Quick Sort

        如果我们拿Insertion Sort来处理大量数据,其O(N^2)的复杂度就令人摇头了。大数据量的情况下有许多更好的排序可供选择。正如其名称所昭示,Quik Sort是目前已知最快的排序法,平均复杂度为O(N LogN),最坏情况下将达到O(N^2)。不过IntroSort(极类似 median-of-three QuickSort的一种排序算法)可将最坏情况推进到O(N LogN)。早期的STL sort算法都采用Quick Sort,SGI STl已改用IntroSort。

        Quick Sort算法可以叙述如下。假设S代表将被排序的序列:

  1. 如果S的元素个数为0或1,结束。
  2. 取S中的任何一个元素,当做枢轴(pivot)v。
  3. 将S分割为L,R两段,使L内的每一个元素都小于或等于v,R内的每一个元素大于或等于v。
  4. 对L,R递归执行Quick Sort

        Quick Sort的精神在于将大区间分割为小区间,分段排序。每一个小区间排序完成后,串接起来的大区间也就完成了排序。最坏的情况下发生在分割(partition)时产生出一个空的子区间--那完全没有达到分割的预期效果。下图说明了Quick Sort的分段排序过程

  Median-of-Three(三点中值)

        注意任何一个元素度可以被选来当做枢轴(pivot),但是其合适与否却影响Quick Sort的效率,为了避免“元素当初输入时不够随机”所带来的恶化效应,最理想最稳当的方式就是取整个序列的头、尾、中央三个位置的元素,以其中值(median)作为枢轴。这种做法称为median-of-three partition,或称为mediun-of-three-QuickSort。为了能够快速取出中央位置的元素,显然随机迭代器必须能随机定位,亦即必须是个RandomAccessIterators。

        以下是SGI STL提供的三点中值决定函数:

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) 
            return c; // a  < b, b >= c, a < c
        else 
            return a;
    else if (a < c)    // c> a>= b
        return a;
    else if (b < c)    // a >= b, a>=c, b <c
        return c;
    else 
        return b;
}

Partition(分割)

        分割方法不只一种,以下叙述既简单又有良好成效的做法。令头端迭代器first向尾端移动,尾端迭代器last向头部移动。当*first大于或等于枢轴时就停下来,当*last小于或等于枢轴时也停下来,然后检验两个迭代器是否交错。如果first仍然在左而last仍然在右,就两者元素互换,然后各自调整一个位置(向中央逼近),再继续进行相同的行为。乳沟发现两个迭代器交错了(亦即!(first < last)),表示整个序列已经调整完毕,以此时的first为轴,将序列分为左右两半,左半部分所有元素值都小于或等于枢轴,右半部分所有元素值都大于或等于枢轴。

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

template<class RandomAcccessIterator, class T>
RandomAcccessIterator __unguarded_partion(RandomAcccessIterator first,
                                          RandomAcccessIterator last,
                                          T pivot) {
    while(true) {
        while(*first < pivot) ++first;
        --last;
        while(pivot < *last) --last;
        if (!(first < last)) return first;
        iter_swap(first, last);
        ++first;
    }
}

下图是分割实例的完整过程:

threshold(阈值)

        面对一个只有十来个元素的小型序列,使用Quick Sort这样复杂而(可能)需要大量运算的排序法,是否划算?不,不划算,在小数据量的情况下,甚至简单如Insert Sort者也可能快过Quick Sort--因为Quick Sort会为了极小的子序列产生许多的函数递归调用。

        鉴于这种情况,适度评估序列的大小,然后决定采用Quick Sort或Insertion Sort,是值得采纳的一种优化措施。然后究竟多小的序列才应该断然改用Insertion Sort?并无定论,5~20都可能导致差不多的结果,实际的最佳值因设备而异。

final insertion sort

        优化措施用不嫌多,只要我们不是贸然行事。如果我们令某个大小以下的序列滞留在"几近排序但尚未竟全功"的子序列做一次完整的排序,其效率一般认为会比“将所有子序列彻底排序”更好。这是以为Insertion Sort在面对“几近排序”的序列时,有很好的表现。

introsort

        不当的枢轴选择,导致不当的分割。导致Quick Sort恶化未O(N^2).David R. Musser于1996年提出易总混合式排序算法:Introspective Sorting(内省式排序),简称Intro Sort,其行为在大部分情况下几乎与median-of-3 Quick Sort 完全相同(当然也一样快)。但是当分割行为有恶化未二次行为的倾向时,能够自我侦测,转而改用Heap Sort,使效率维持在Heap Sort的O(N logN),又比一开始就使用Heap Sort来得好。稍后边可以看到SGI STL源代码中对IntroSort的实现。

SGI 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()用来控制分割恶化的情况

// 找出2^k <= n 的最大值k,例:n=7,得k=2;n=20的k=4;n=8,得k=3
template <class Size>
inline Size __lg(Size n) {
    Size k;
    for (k = 0; n>1; n >>= 1) ++k;
    return k;
}

__introsort_loop() 最后一个参数表示递归的最深层次不应超过2*log(N),代码如下:

template <class RandomAccessIterator, class T, class Size>
void __introsort_loop(RandomAccessIterator first, RandomAccessIterator last, T*, Size depth_limit) {
    while ((last - first) > __stl_threshold) { // __stl_threashold = 16,全局常量
        if (depth_limit == 0) {
            partial_sort(first, last, last); // 改用heapsort
        }   
        --depth_limit;
        RandomAccessIterator cut = __unguarded_partition(first, last, T(__median(*first,
                                                                                *(first + (last - first)/2),
                                                                                *(last-1))));
        // 右半段递归进行sort
        __introsort_loop(cut, last, value_type(first), depth_limit);
        last = cut;
        // 因为重置了last,所以左半段继续进行排序
    }   
}

        函数一开始判断序列的大小.__stl_threshold是个全局整型常数,定义如下

const int __stl_threshold = 16;

        通过元素个数检验后,再检查分割层次。如果分割层次超过指定值,就改用partital_sort(), 事实上调用的是Heap Sort。

        都通过了这些检验之后,便进入Quik Sort完全相同的程序:以median-of-3方法确定枢轴位置,然后调用__unguarded_partition()找出分割点,然后针对左右段递归进行IntroSort。

        当__introsort_loop()结束,[first, last)内有多个“元素个数少于16”的子序列,每个子序列都有相当程度的排序,但尚未排序(以为元素个数一旦小于__stl_threshold,就被中止进一步的排序操作了)。回到母函数sort(),再进入__final_insertion_sort():

template <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);
    }
}

        此函数首先判断元素个数是否大于16.如果答案为否,就调用__insertion_sort()加以处理。如果答案为是,就将[first, last)分割为长度为16的一段子序列,和另一段剩余子序列,再针对两个子序列分别调用__insert_sort()和__unguarded_insertion_sort().前者代码已于先前展示,后者源代码如下:

template <RandomAccessIterator>
inline void __unguarded_insertion_sort(RandomAccessIterator first, RandomAccessIterator last) {
    __unguarded_insertion_sort_aux(first, last, value_type(first));
}

template <RandomAccessIterator>
void __unguarded_insertion_sort_aux(RandomAccessIterator first, RandomAccessIterator last, T*) {
    for (RandomAccessIterator i = first, i != last; ++i)  
        __unguarded_linear_insert(i, T(*i));
}

        这就是SGI STL sort算法的完整过程。为了做个比较,我们再列出RW STL sort的部分源代码,RW版本用的是纯粹Quick Sort,不是Intro Sort

template <class RandomAccessIterator>
inline void sort(RandomAccessIterator first, RandomAccessIterator last) {
    if (!(first == last)) {
        __quick_sort_loop(first, last);
            __final_insertion_sort(first, last); //其内兼容于SGI STL完全相同
    }
}

template <class RandomAccessIterator>
inline void __quick_sort_loop(RandomAccessIterator first, RandomAccessIterator last) {
    __quick_sort_loop_aux(first, last, _RWSTD_VALUE_TYPE(first));
}

template <class RandomAccessIterator, class T>
inline void __quick_sort_loop_aux(RandomAccessIterator first, RandomAccessIterator last, T*) {
    while (last - first > __stl_threshold) {
        // median-of-3 partitioning
        RandomAccessIterator cut = __unguarded_partition(first, last, T(__median(*first, *(first + (last - first)/2), *(last - 1))));

        if (cut - first > last - cut) {
            __quick_sort_loop(cut, last);    // 较短段以递归方式处理
            last = cut;
        } else {
            __quick_sort_loop(first, cut);    // 较短段以递归方式处理
            first = cut;

        }
    }
}

参考文档《STL源码剖析--侯捷》

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

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

相关文章

Spring Shell如何与SpringBoot集成并快速创建命令行界面 (CLI) 应用程序

Spring Shell 介绍 Spring Shell 是一个强大的工具&#xff0c;可用于构建命令行应用程序&#xff0c;提供了简单的方式来创建和管理交互式 CLI。它适合那些希望通过命令行与 Java 应用程序进行交互的开发者&#xff0c;尤其是在需要自动化、交互式输入或与 Spring 生态系统集…

圣桥ERP queryForString.dwr SQL注入漏洞复现

0x01 产品描述: 杭州圣乔科技有限公司主要研发全套工业企业ERP系列软件产品,现在公司已经形成ERP 软件、OA办公管理、等四大系列二十小类软件产品。致力于为政府、教育、医疗卫生、文化事业、公共事业(电、水、气等)、交通、住建、应急、金融、电信运营商、企业等用户提供专…

SystemUI修改状态栏电池图标样式为横屏显示(以Android V为例)

SystemUI修改状态栏电池图标样式为横屏显示(以Android V为例) 1、概述 在15.0的系统rom产品定制化开发中&#xff0c;对于原生系统中SystemUId 状态栏的电池图标是竖着显示的&#xff0c;一般手机的电池图标都是横屏显示的 可以觉得样式挺不错的&#xff0c;所以由于产品开发…

【设计模式系列】备忘录模式(十九)

目录 一、什么是备忘录模式 二、备忘录模式的角色 三、备忘录模式的典型应用场景 四、备忘录模式在Calendar中的应用 一、什么是备忘录模式 备忘录模式&#xff08;Memento Pattern&#xff09;是一种行为型设计模式&#xff0c;它允许在不暴露对象内部状态的情况下保存和恢…

简单的动态带特殊符号敏感词校验

简单的动态带特殊符号敏感词校验 敏感词之前进行了简单了解&#xff0c;使用结巴分词自带词库可以实现&#xff0c;具体参考我的如下博文 敏感词校验 此次在此基础进行了部分优化&#xff0c;优化过程本人简单记录一下&#xff0c;具体优化改造步骤如下所示 1.需求 我们公司…

AJAX三、XHR,基本使用,查询参数,数据提交,promise的三种状态,封装-简易axios-获取省份列表 / 获取地区列表 / 注册用户,天气预报

一、XMLHttpRequest基本使用 XMLHttpRequest&#xff08;XHR&#xff09;对象用于与服务器交互。 二、XMLHttpRequest-查询参数 语法: 用 & 符号分隔的键/值对列表 三、XMLHttpRequest-数据提交 核心步骤 : 1. 请求头 设置 Content-Type 2. 请求体 携带 符合要求 的数…

S4 UPA of AA :新资产会计概览

通用并行会计&#xff08;Universal Parallel Accounting&#xff09;可以支持每个独立的分类账与其他模块集成&#xff0c;UPA主要是为了支持平行评估、多货币类型、财务合并、多准则财务报告的复杂业务需求 在ML层面UPA允许根据不同的分类账规则对物料进行评估&#xff0c;并…

CMD 介绍

CMD 介绍 CMD 是 Windows 操作系统中的命令提示符&#xff08;Command Prompt&#xff09;程序&#xff0c;它是一种命令行工具&#xff0c;可以让用户通过键入命令来与计算机进行交互。 DOS: disk operating system, 磁盘操作系统. 是利用命令行来操作计算机. DOS 不是 CMD…

Hadoop生态圈框架部署 伪集群版(六)- MySQL安装配置

文章目录 前言一、MySQL安装与配置1. 安装MySQL2. 安装MySQL服务器3. 启动MySQL服务并设置开机自启动4. 修改MySQL初始密码登录5. 设置允许MySQL远程登录6. 登录MySQL 卸载1. 停止MySQL服务2. 卸载MySQL软件包3. 删除MySQL配置文件及数据目录 前言 在本文中&#xff0c;我们将…

java基础语法光速入门

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文整理Java的基础语法部分 适合有编程基础的人快点掌握语法使用 没学过一两门语言的话。。还是不建议看了 极致的浓缩没有一点解释 注释 单行注释 // 多行注释 /**/ 数据类型 布尔型:true false 整型:int,lon…

「Mac玩转仓颉内测版41」小学奥数篇4 - 分数加减法

本篇将通过 Python 和 Cangjie 双语解决简单的分数加减法问题&#xff0c;帮助学生理解分数的运算规则&#xff0c;并学会用编程解决数学计算问题。 关键词 小学奥数Python Cangjie分数运算 一、题目描述 编写一个程序&#xff0c;接收两个分数并计算它们的和与差。输入的分…

基于频谱处理的音频分离方法

基于频谱处理的音频分离方法 在音频处理领域&#xff0c;音频分离是一个重要的任务&#xff0c;尤其是在语音识别、音乐制作和通信等应用中。音频分离的目标是从混合信号中提取出单独的音频源。通过频谱处理进行音频分离是一种有效的方法&#xff0c;本文将介绍其基本原理、公…

力扣-图论-1【算法学习day.51】

前言 ###我做这类文章一个重要的目的还是给正在学习的大家提供方向和记录学习过程&#xff08;例如想要掌握基础用法&#xff0c;该刷哪些题&#xff1f;&#xff09;我的解析也不会做的非常详细&#xff0c;只会提供思路和一些关键点&#xff0c;力扣上的大佬们的题解质量是非…

工业—使用Flink处理Kafka中的数据_ChangeRecord2

使用 Flink 消费 Kafka 中 ChangeRecord 主题的数据,每隔 1 分钟输出最近 3 分钟的预警次数最多的 设备,将结果存入Redis 中, key 值为

PortSwigger 原型污染

一、什么是原型污染 原型污染是一种 JavaScript 漏洞&#xff0c;它使攻击者能够向全局对象原型添加任意属性&#xff0c;然后这些属性可能被用户定义的对象继承。 二、JavaScript 原型和继承基础 1、原型 JavaScript 中的每个对象都链接到某种类型的另一个对象&#xff0c;称…

AMEYA360:上海永铭电子全新高压牛角型铝电解电容IDC3系列,助力AI服务器电源高效运转

随着数据中心和云计算的高速发展&#xff0c;AI服务器的能效要求日益提高。如何在有限空间内实现更高的功率密度和稳定的电源管理&#xff0c;成为AI服务器电源设计的一大挑战。永铭推出全新高压牛角型铝电解电容IDC3系列&#xff0c;以大容量、小尺寸的创新特性&#xff0c;为…

jmeter基础_打开1个jmeter脚本(.jmx文件)

课程大纲 方法1.菜单栏“打开” 菜单栏“文件” - “打开” &#xff08;或快捷键&#xff0c;mac为“⌘ O”&#xff09;&#xff0c;打开文件选择窗口 - 选择脚本文件&#xff0c;点击“open”&#xff0c;即可打开脚本。 方法2.工具栏“打开”图标 工具栏点击“打开”图标&…

基于微信小程序的教学质量评价系统

​ 私信我获取源码和万字论文&#xff0c;制作不易&#xff0c;感谢点赞支持。 基于微信小程序的教学质量评价系统 摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了基于微信小程序的教学质量评价系统的开发全过…

数据结构-最小生成树

一.最小生成树的定义 从V个顶点的图里生成的一颗树&#xff0c;这颗树有V个顶点是连通的&#xff0c;有V-1条边&#xff0c;并且边的权值和是最小的,而且不能有回路 二.Prim算法 Prim算法又叫加点法&#xff0c;算法比较适合稠密图 每次把边权最小的顶点加入到树中&#xff0…

增量预训练网络安全大模型的一次尝试

一、背景 探索使用网络安全知识&#xff0c;对开源基模型进行增强&#xff0c;评估是否能使基模型在网络安全领域表现出更好地专业度。 项目基于云起无垠SecGPT开源项目&#xff0c;在hugeface开源数据集的基础上&#xff0c;增加了自有预训练数据&#xff0c;进行增量预训练…