为啥冒泡排序没有效率?

Posted

技术标签:

【中文标题】为啥冒泡排序没有效率?【英文标题】:Why bubble sort is not efficient?为什么冒泡排序没有效率? 【发布时间】:2020-09-11 19:48:03 【问题描述】:

我正在使用 node.js 开发后端项目,并打算实现排序产品功能。 我研究了一些文章,有几篇文章说冒泡排序效率不高。 冒泡排序在我以前的项目中使用过,我很惊讶它为什么不好。 谁能解释为什么它效率低下? 如果您能用c编程或汇编命令解释,将不胜感激。

【问题讨论】:

有很多 O(n^2) 排序算法(冒泡排序、插入排序、选择排序等)主要用于教学目的,因为它们往往比人们实际使用的更好的排序算法。基于比较的快速排序算法是 O(n*log(n)),这是该类型排序所能做到的最好的。快速排序、归并排序、堆排序等都是O(n*log(n))。 嗯,永远不明白为什么人们使用 O(n^2) 的冒泡排序而不是使用 O(1) 的快速QB sort。 ?????? 【参考方案1】:

冒泡排序的时间复杂度为 O(N^2),因此与 O(N log N) 排序相比,对于大型数组来说它是垃圾。

在 JS 中,如果可能,请使用 JS 运行时可能能够使用预编译的自定义代码处理的内置排序函数,而不必 JIT 编译排序函数。标准库排序应该(通常?)进行良好调整,以便 JS 解释器/JIT 高效处理,并使用高效算法的高效实现。

这个答案的其余部分是假设一个用例,比如用预先编译的语言(如 C 编译为本机 asm)对整数数组进行排序。如果您以一个成员为键对结构数组进行排序,则变化不大,尽管如果您对char* 字符串与包含int 的大型结构进行排序,比较与交换的成本可能会有所不同。 (冒泡排序对于所有这些交换的情况都是不利的。)


请参阅Bubble Sort: An Archaeological Algorithmic Analysis,了解更多关于为什么它是“流行”(或广泛教授/讨论)的原因,尽管它是最糟糕的 O(N^2) 类型之一,包括一些历史/教育学事故。还包括一个有趣的定量分析,它是否实际上(有时声称)是使用几个代码指标最容易编写或理解的一种。

对于简单的 O(N^2) 排序是合理选择的小问题(例如,快速排序或合并排序的 N

冒泡排序(对未进行任何交换的传球进行早期淘汰)在某些几乎排序的情况下也并不可怕,但比插入排序更糟糕。但是一个元素每次只能向列表的前面移动一步,所以如果最小的元素接近末尾但完全排序,它仍然需要冒泡排序 O(N^2) 的工作 .***解释Rabbits and turtles。

插入排序没有这个问题:接近末尾的小元素一旦到达就会被有效地插入(通过复制较早的元素来打开一个间隙)。 (达到它只需要比较已经排序的元素来确定它并继续进行零实际插入工作)。靠近开始的大元素最终会快速向上移动,只需要稍微多做一点工作:每个要检查的新元素都必须插入到该大元素之前之前,在所有其他元素之后。所以这是两次比较,实际上是一次交换,不像冒泡排序在它的“好”方向上每一步交换一次。尽管如此,插入排序的坏方向比冒泡排序的“坏”方向要好得多。

有趣的事实:在真实 CPU 上进行小数组排序的最先进技术可以包括使用压缩最小/最大指令的 SIMD 网络排序,以及并行执行多个“比较器”的向量洗牌。


为什么冒泡排序在真实 CPU 上不好:

交换的模式可能比插入排序更随机,并且对于 CPU 分支预测器来说更难预测。从而导致比插入排序更多的分支错误预测。

我自己还没有测试过,但是想想插入排序是如何移动数据的:内部循环的每次完整运行都会将一组元素向右移动,以便为新元素打开一个间隙。该组的大小可能在外循环迭代中保持相当恒定,因此有合理的机会预测该内循环中循环分支的模式。

但是冒泡排序并没有做太多的部分排序组的创建;交换模式不太可能重复1

我搜索了我刚刚编造的这个猜测的支持,并确实找到了一些:Insertion sort better than Bubble sort? 引用***:

冒泡排序与现代 CPU 硬件的交互也很差。它产生的写入次数至少是插入排序的两倍,缓存未命中次数的两倍,以及越来越多的分支错误预测。

(IDK,如果“写入次数”是基于源的天真分析,或者查看经过体面优化的 asm):

这带来了另一点:冒泡排序可以很容易地编译成低效的代码。 交换的概念实现实际上存储到内存中,然后重新读取它刚刚写入的元素。根据您的编译器的智能程度,这实际上可能发生在 asm 中,而不是在下一次循环迭代中重用寄存器中的该值。在这种情况下,您将在内部循环内有存储转发延迟,从而创建一个循环承载的依赖链。而且还会造成缓存读取端口/加载指令吞吐量的潜在瓶颈。


脚注 1: 除非您重复对同一个小数组进行排序;我在我的 Skylake CPU 上尝试过一次,它使用了我为 this code golf question 编写的简化的 x86 asm 冒泡排序实现(代码高尔夫版本故意在性能方面很糟糕,仅针对机器代码大小进行了优化;IIRC 我进行基准测试的版本避免存储- 转发 stallslocked 指令,如 xchg mem,reg)。

我发现每次使用相同的输入数据(在重复循环中复制一些 SIMD 指令),Skylake 中的 IT-TAGE 分支预测器“学习”了特定 ~13 元素气泡的整个分支模式排序,导致perf stat 报告低于 1% 的分支错误预测,IIRC。所以它并没有证明我对冒泡排序的大量错误预测,直到我增加了一些数组大小。 :P

【讨论】:

【参考方案2】:

冒泡排序的时间复杂度为 O(n^2)。归并排序平均需要 O(n*log(n)) 时间,而快速排序平均需要 O(n*log(n)) 时间,因此比冒泡排序表现更好。

请参考:complexity of bubble sort。

【讨论】:

这也是最差的 O(n^2) 排序,包括对几乎排序的数组不利。对于简单的 O(n^2) 排序是合理选择的小问题(例如快速排序或合并排序的基本情况),通常使用插入排序。见Bubble Sort: An Archaeological Algorithmic Analysis

以上是关于为啥冒泡排序没有效率?的主要内容,如果未能解决你的问题,请参考以下文章

快速排序和冒泡排序

为啥插入排序比快速排序和冒泡排序更快?

为啥在我的情况下快速排序总是比冒泡排序慢?

冒泡排序编程中 j为啥要减1

冒泡排序

为啥会这样? (冒泡排序)[JavaScript]