【数据结构与算法】->算法->排序(四)->排序优化->如何实现高性能的通用排序函数

Ⅰ 前言

前面三节,我已经把基本的排序算法都讲完了,第四节作为一个总结,分析一下排序算法的优化。

【数据结构与算法】->算法->排序(一)->冒泡排序&插入排序&选择排序

【数据结构与算法】->算法->排序(二)->归并排序&快速排序

【数据结构与算法】->算法->排序(三)->线性排序->桶排序&计数排序&基数排序

几乎所有的编程语言都会提供排序函数,比如 C 语言中 qsort(),C++ STL 中的sort()stable_sort(),还有 Java 语言中的 Collections.sort()。在平时的开发中,我们也都是直接使用这些现成的函数来实现业务逻辑中的排序功能。这篇文章我们就来简单探讨一下这些排序函数的底层逻辑。这也由此引出了这篇文章的主题,如何实现一个通用的、高性能的排序函数?

Ⅱ 如何选择合适的排序算法

如果要实现一个通用的、高效率的排序函数,我们应该选择哪种排序算法?让我们先回顾一下前三篇文章里讲过的几种排序算法。

算法名称时间复杂度是稳定排序?是原地排序?
冒泡排序O(n2)
插入排序O(n2)
选择排序O(n2)×
快速排序O(nlog2n)×
归并排序O(nlog2n)×
计数排序O(n+k) k是数据范围×
桶排序O(n)×
基数排序O(d*n) d是维度×

在第三节我讲过,线性排序算法的时间复杂度比较低,适用场景比较特殊。所以如果要写一个通用的排序函数,不能选择线性排序算法。

如果对小规模数据进行排序,可以选择时间复杂度是 O(n2) 的算法;如果对大规模数据进行排序,时间复杂度是 O(nlog2n) 的算法更加高效。所以为了兼顾任意规模数据的排序,一般都会首选时间复杂度是 O(nlog2n) 的排序算法来实现排序函数。

时间复杂度是 O(nlog2n) 的排序算法不止一个,我在前面的文章里讲过归并排序、快速排序,在后面讲解堆的时候我还会写到堆排序。堆排序和快速排序都有比较多的应用,比如 Java 采用堆排序实现排序函数,C 语言使用快速排序实现排序函数。

如果你留心的话,会发现,使用归并排序的情况其实不多。我们知道,快排在最坏情况下时间复杂度是 O(n2) ,而归并排序可以做到平均情况、最坏情况下的时间复杂度都是 O(nlog2n) ,这点上归并排序是很优秀的,可是为什么还是得不到秃头程序员的宠信呢?

其实就是因为归并的一个弱点,就是归并排序并不是原地排序算法,空间复杂度是 O(n)。所以粗略和夸张地说,如果要排序 100MB 的数据,除了数据本身占用的内存之外,排序算法还要额外再占用 100MB 的内存空间,空间耗费就翻倍了。

虽然我们说快排比较适合实现排序函数,但是快排在最坏情况下的时间复杂度也有 O(n2),如何解决这个 “复杂度恶化” 的问题呢?

Ⅲ 如何优化快速排序

快速排序的最坏情况,就是数据原来就是有序的或者接近有序的,每次分区点都选择最后一个数据,那么快速排序算法就会变得非常糟糕,时间复杂度会退化到 O(n2)。实际上,这种 O(n2) 时间复杂度出现的主要原因是因为我们分区点选得不够合理。

最理想的分区点是:被区分点分开的两个区中,数据的数量差不多。

如果很粗暴地直接选择最后一个或者第一个数据作为区分点,不考虑数据的特点,肯定会出现上述的情况,所以为了提高排序算法的性能,我们也要尽可能地让每次分区都比较平均。

那该如何选择分区点呢?我这里给出两个方法。

  • 三数取中法

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

  • 随机法

随机法就是每次从要排序的区间中,随机选择一个元素作为分区点。这种方法不能保证每次分区点都选得比较好,但是从概率上看,不大可能会出现每次分区点都选得很差的情况。

我们知道,快排是用递归来实现的。递归是非常要警惕堆栈溢出的。所以为了避免快速排序里,递归过深而堆栈过小,导致堆栈溢出,我们有两种解决方法:第一种是限制递归深度,一旦递归过深,超过了我们设定的阈值,就停止递归;第二种办法是通过在堆上模拟实现一个函数调用栈,手动模拟递归压栈、出栈的过程,这样就没有了系统栈大小的限制。

Ⅳ 举例分析排序函数

这里我们分析 C 语言中的函数 qsort()。虽说qsort()从名字上看,很像是基于快速排序算法实现的,实际上它用了很多算法组合而成。

如果你去看源码,就会发现,qsort() 会优先使用归并排序来排序输入数据,因为归并排序的空间复杂度是 O(n) ,所以对于小数据量的排序,比如 1KB、2KB 等,归并排序多占用的这点空间问题不大。有时候是需要用空间换时间的。

但是数据量太大的时候,就不可以这样做了,所以,要排序的数据量比较大的时候,qsort()会改为用快速排序算法来排序。

qsort()是如何选择快速排序算法的分区点呢?你看源码就会发现,它用的正是我在上面写的“三数取中法”。

还有我们前面提到的递归太深会导致堆栈溢出的问题,qsort()是通过自己实现一个堆上的栈,手动模拟递归来解决的。

实际上,qsort()并不仅仅用到了归并排序和快速排序,它还用到了插入排序。在快速排序的过程中,当要排序的区间中,元素的个数小于等于 4 时,qsort()就退化为插入排序,不再继续用递归做快速排序。 这是因为在小规模的数据面前,O(n2) 时间复杂度的算法并不一定比 O(nlog2n) 的算法执行时间长。

所以你看,一个排序函数也没有很复杂,一个 C 语言的排序函数差不多就被我们分析完了。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值