排序优化

在C++的STL中为我们提供了很多排序函数如sort、stable_sort等等,平时我们也会直接使用这些现成的排序函数,为了让我们更加深层的了解底层的排序函数,现在我们从如何实现一个通用的、高性能的排序函数这个问题出发,进而更加全面的理解排序算法。

https://pan.baidu.com/s/1izhWFEqCNhi2LK3MApk6ng?pwd=n4xy

线性排序算法的时间复杂度比较低,适用场景比较特殊。所以如果要写一个通用的排序函数,不能选择线性排序算法。如果对于小规模数据进行排序,可以选择时间复杂度是O(n²)的算法;如果对大规模数据进行排序,时间复杂度是O(nlogn)的算法更加高效。所以,为了兼顾任意规模数据的排序,一般都会首选时间复杂度为O(nlogn)的排序算法来实现排序函数。

时间复杂度是O(nlogn)的排序算法不止一个,我们已经知道的有归并排序、快速排序,后面还会了解一种叫堆排序的排序算法。其中堆排序和快速排序都有比较多的应用,比如Java语言采用堆排序实现排序函数,而C++中采用了快排实现排序函数(后面我们会具体讲解)。

但是我们发现,貌似同样时间复杂度为O(nlogn)的归并排序却不经常被使用,这是因为归并排序并不是原地排序算法,它的空间复杂度为O(n)。所以说,当我们在为数据量比较大的文件排序时,除了数据本身占用的内存之外,我们还需要额外占用很大内存空间,所需的空间耗费就翻倍了。因此,考虑到空间效率的问题,归并排序才不被大家“宠信”。

我们之前在学习快速排序的时候了解到,快速排序并不是在任何情况下的时间复杂度都是O(nlogn),在最坏的情况下它的时间复杂度甚至会恶化到O(n²),下面我们来分析如何优化快速排序的问题。

如何优化快速排序?

什么情况下快速排序的时间复杂度会恶化到O(n²),当要排序的数据本身就是有序的或者接近有序也就是说,有序度接近n(n-1)/2,并且我们每次的分区点都选取最后一个数据,那快速排序算法就会变得非常糟糕,这种O(n²)时间复杂度出现的主要原因还是因为我们分区点选的不够合理。

最理想的分区点:被分区点分开的两个分区中,数据的数量接近相等。

如果只是很粗暴的直接选取第一个或者最后一个数作为分区点,而不考虑数据的特点,那么肯定会出现像上述情况那样,因为数据本身的一些情况导致时间复杂度恶化。为了提高排序算法的性能,我们就是要尽可能的让每次分区都比较平均。下面给出两种比较常用、简单的分区算法:

  • 三数取中法

我们从区间的首、尾、中间,分别取一个数,然后对比大小,取这3个数的中间值作为分区点。这样每间隔某个固定的长度,取数据出来比较,将中间值作为分区点的分区算法,肯定要比单纯的取某一个数据更好。但是,如果要排序的数据比较大,那“三数取中”可能就不够了,可能要“五数取中”或者“十数取中”。

  • 随机法

随机法,顾名思义,就是每次从要排序的区间中,随机的选取一个元素作为分区点。这种方法并不能保证每次分区点都选的比较好,但是从概率的角度来看,也不大可能会出现每次分区点都选的很差的情况,所以平均下,这样选的分区点是比较好的。时间复杂度退化为最糟糕的O(n²)的情况,出现的可能性不大。

Glibc中qsort()排序函数的底层实现原理

为了让我们对如何实现一个排序函数有一个更直观的感受,这里以Glibc中的qsort()函数举例说明。虽然qsort()从名字上来看,很像是基于快速排序算法实现的,实际上它不仅仅使用了快排这一种算法。

如果去看源码,我们会发现,qsort()会优先使用归并排序来排序输入数据,因为归并排序的空间复杂度是O(n),所以对于小数据量的排序,比如1KB、2KB等,归并排序额外需要1KB、2KB的内存空间,这个内存消耗是我们完全可以接收的,此时我们更加着重考虑的是速度,这也是用空间换时间思想的一种体现。

但是如果数据量太大,这个时候qsort()函数就会改为用快速排序算法来排序。其中快排算法的分区点选取方法接收“三数取中”法,所以我们发现,其实这些内部的函数也并不是很复杂。还有我们之前提及的递归太深导致堆栈溢出的问题,qsort()是通过自己实现一个堆上的栈,手动模拟递归来解决的。实际上,qsort()并不仅仅用到了归并排序和快速排序,它还用到了插入排序。在快速排序的过程中,当要排序的区间中,元素的个数小于等于4时,qsort()就退化成为插入排序,不再继续用递归来做快速排序,因为我们之前也讲过,在小规模数据面前,O(n²)时间复杂度的算法并不一定O(nlogn)的算法执行时间长。

这里着重分析一下这个说法:我们最开始将复杂度分析的时候说过,算法的性能可以通过时间复杂度来分析,但是,这种分析是比较偏理论的,如果我们深究的话,实际上时间复杂度并不等于代码实际的运行时间。时间复杂度代表的是一个增长的趋势,如果画成增长曲线图,我们会发现O(n²)比O(nlogn)要陡峭,也就是说增长趋势更加猛一些。但是我们在讲解大O复杂度表示法的时候,会省略低阶、系数和常数,也就是说,O(nlogn)在有省略低阶、系数、常数之前可能是O(knlogn+c),而且k和c有可能还是一个比较大的数。假设k=1000,c=200,当我们对小规模数据(比如n=100)排序时,n²的值实际上比knlogn+c还要小:

knlogn + c = 1000 * 100 * log100 + 200 远大于10000
n * n = 100 * 100= 10000

所以说我们在理论层面分析算法的时间复杂度的时候会抛弃掉很多细节,但是在具体应用的过程中,就需要通过实践来选取更加合适算法来让我们的代码执行效率更高,因此对于小规模数据的排序,O(n²)的排序算法并不一定比O(nlogn)排序算法执行的时间长。对于小数据量的排序,我们选择比较简单、不需要递归的插入排序算法,通过这一改进策略实际上可以节省大约15%(相对于不用截止的做法而自始至终使用快速排序时)的运行时间。这种做法同时也避免了一些有害的退化情形,比如当只有一个或者两个元素的时候却取三个元素的中值这这样的情况。

这里给出实际的快速排序例程:

// 快速排序算法(驱动程序)
template <typename Comparable>
void quicksort(vector<Comparable> &a)
{quicksort(a, 0, a.size() - 1);
}// 执行三数中值分割的代码
template <typename Comparable>
const Comparable & median3(vector<Comparable> &a, int left, int right)
{int centor = (left + right) / 2;if (a[center] < a[left])std::swap(a[left], a[center]);if (a[right] < a[left])std::swap(a[left], a[right]);if (a[right] < a[centor])std::swap(a[centor], a[right]);// 将枢纽元置于right-1处std::swap(a[center], a[right - 1]);return a[right - 1];
}// 进行递归调用的内部快速排序方法
// 使用三数中值分割法,以及截止范围是10的截止技术
template <typename Comparable>
void quicksort(vector<Comparable> &a, int left, int right)
{if (left + 10 <= right){const Comparable &pivot = median3(a, left, right);// 开始分割int i = left, j = right - 1;for (; ; ){while (a[++i] < pivot) { }while (pivot < a[--j]) { }if (i < j)std::swap(a[i], a[j])elsebreak;}// 恢复枢纽元std::swap(a[i], a[right - 1]);// 将小于等于枢纽元的元素排序quicksort(a, left, i - 1);// 将大于等于枢纽元的元素排序quicksort(a, i + 1, right);}else// 对子数组进行一次插入排序insertionSort(a, left, right);
}

还记得之前在链表实现插入删除操作时,我们引入了哨兵的概念来简化我们的代码,提高执行效率。在qsort()插入排序的算法实现中,也利用了这种编程技巧。虽然哨兵可能只是少做一次判断,但是毕竟排序函数是非常常用、非常基础的函数,性能的优化要做到极致。

左程云 算法与数据结构基础班相关推荐

  1. 左程云算法笔记总结-基础篇

    基础01(复杂度.基本排序) 认识复杂度和简单排序算法 时间复杂度 big O 即 O(f(n)) 常数操作的数量写出来,不要低阶项,只要最高项,并且不要最高项的系数 一个操作如果和样本的数据量没有关 ...

  2. 左程云算法笔记总结-基础提升篇

    提升01(哈希) 认识哈希函数 哈希函数的输入一般需要是无穷尽的,没有限制:输出可以有一定的范围,比如MD5加密后产生的字符串可以有2的32次方-1种,用十六进制表示需要16个字符. 相同的输入对应相 ...

  3. 【笔记:左程云算法与数据结构】4.链表

    用快慢指针找到链表的中点 "定制"三种不同的情况 1.元素个数为偶数时,慢指针指向中间偏左的元素 2.元素个数为偶数时,慢指针指向中间偏右的元素 3.慢指针指向中间的前一个元素 代 ...

  4. 左程云算法笔记(四)哈希表和有序表的使用、链表

    左程云算法笔记(四) 哈希表的使用 有序表的使用 链表 单链表反转 (LC206) 双向链表反转 打印两个有序链表的公共部分 合并两个有序链表(LC21) 判断一个链表是否为回文结构 (LC234) ...

  5. LeetCode左程云算法课笔记

    左程云算法课笔记 剑指Offer 位运算 ^运算符理解 寻找出现双中的单数 取出一个数最右边1的位置 找所有双出现中的两个单数 整数二进制奇数位偶数位交换 数组中全部出现k次返回出现一次的数 链表 判 ...

  6. CSDN专访左程云,算法之道

    算法的庞大让很多人畏惧,程序员如何正确的学习并应用于面试.工作中呢?今天,CSDN邀请了IBM软件工程师.百度软件工程师.刷题5年的算法热爱者左程云,来担任CSDN社区问答栏目的第二十六期嘉宾,届时会 ...

  7. B站左程云算法视频高级班01

    题目1:给定一个数组,求如果排序之后,相邻两数的最大差值,要求时间复杂度O(N),且要不能用非基于比较的排序 [               ]假设是长度为9的数组 首先遍历数组 找出min 和 ma ...

  8. B站左程云算法视频基础提升02

    岛问题 一个矩阵中只有0和1两种值,每个位置都可以和自己的上下左右相连,如果一片1连在一起,这个部分叫做一个岛,求一个矩阵有多少个岛 思路:遍历,infect class Solution {publ ...

  9. B站左程云算法视频高级班05

    题目一:在一个无序数组中,求第k小的数 快排:荷兰国旗问题 BFPRT 有讲究的选择一个数 之后的步骤和快排一样 假设整个方法叫f函数 f(arr, k) 1)0~4一组 5~9一组 0~14一组 剩 ...

最新文章

  1. 【Android工具】Yandex!可以安装PCchrome插件的手机浏览器!更新网页剪辑插件测试情况...
  2. 在猜年龄的基础上编写登录、注册方法,并且把猜年龄游戏分函数处理
  3. Mybatis代码生成适配Oracle和Mysql数据库_01
  4. CodeIgniter 的数据安全过滤全解析
  5. 递归要素及太深导致堆栈溢出怎么办?
  6. 惊艳二重奏!专家这样用开源软件建立监控体系
  7. 【Linux】Linux下使用w命令和uptime命令查看系统负载
  8. 搬砖的也能学Python----if - elif 语句
  9. (转)distcp从ftp到hdfs拷贝文件
  10. Spark修炼之道——Spark学习路线、课程大纲
  11. js 如何判断数组元素是否存在重复项
  12. PHP面试100题汇总
  13. pycharm看php文件是乱码,Jetbrains-PhpStorm2019.2中文乱码问题
  14. 阅兵方阵 蓝桥杯 第九届JavaA
  15. vb.net产生随机数Random代码实例
  16. php去除换行(回车换行)的方法
  17. 求英国帝国理工学院的iFR研究算法代码
  18. Apollo在有赞的实践
  19. 公司常用的Project管理工具
  20. Android实现版本更新和自动安装

热门文章

  1. 1-3 jsp页面跳转时弹出小窗口的方法
  2. html页面禁止右键、禁止复制、禁止图片拖动、禁止复制和剪切禁止IOS长按复制粘贴实现
  3. 美国财政部长称勒索软件对经济构成威胁、谷歌警示20亿Chrome用户|10月22日全球网络安全热点
  4. 透过荣耀耳机的三重“炼金术”,重识TWS行业
  5. 数字孪生|全国10省今年出台30+元宇宙政策文件,释放何种信号?
  6. 使用cmd查看电脑上连接过的wifi密码
  7. 展现整数数字:0,千:1k,万:10k,十万:125k,百万:1mil,千万:10mil,亿:100mil展现整数%,四舍五入
  8. Framebuffer编程总结,希望人人都能学会
  9. 搜索一轮练zi习nve计划(CODEVS)
  10. intel cpu tick-tock