你看到的“相似”(?!)完全是虚幻的。
基本的 O(N 平方) 方法会一遍又一遍地重复它们的工作,而不会利用在“上一步”上完成的任何工作的“下一步”。所以第一步花费的时间与 N 成正比,第二步花费的时间与 N-1 成正比,依此类推——从 1 到 N 的整数总和与 N 的平方成正比。
例如,在选择排序中,您每次都在查找 I:N 部分中的最小元素,其中 I 首先是 0,然后是 1,依此类推。这是(并且必须)通过检查所有这些元素来完成的元素,因为以前没有注意通过利用以前的通道来减少后续通道的工作量。找到最小元素后,将其与第 I 个元素交换,增加 I,然后继续。当然是 O(N 平方)。
高级 O(N log N) 方法的结构巧妙,可以利用前面步骤中完成的后续步骤。与基本方法相比,这种差异是如此普遍和深刻,以至于如果一个人无法感知它,那主要是关于一个人的感知敏锐度,而不是方法本身:-)。
例如,在归并排序中,您在逻辑上将数组分成两部分,0 到半长,半长到长度。一旦对每一半进行排序(以相同的方式递归,直到长度足够短),两半就会合并,这本身就是一个线性子步骤。
由于您每次都减半,显然您需要与 log N 成比例的步骤数,并且由于每个步骤都是 O(N),因此显然您会得到非常理想的 O(N log N)。
Python 的“timsort”是一种“自然归并排序”,即归并排序的一种变体,它可以利用数组中已经排序(或反向排序)的部分,它可以快速识别这些部分并避免花费任何进一步的工作.这不会改变 big-O,因为这大约是 最坏-case 时间 - 但 expected 时间崩溃得更远,因为在许多现实生活中,一些部分排序 strong>存在。
(请注意,按照 big-O 的严格定义,快速排序一点也不快——最坏的情况是与 N 的平方成正比,当您每次都碰巧选择了一个糟糕的枢轴时.. . expected-time 明智,虽然没有 timsort 好,因为在现实生活中,您反复选择灾难支点的情况非常罕见......但是,最糟糕-case,他们可能发生!-)。
timsort 是如此,即使是非常有经验的程序员也会被吓到。我不算数,因为我是发明者 Tim Peters 的朋友,也是 Python 狂热者,所以我的偏见很明显。但是,考虑...
...我记得在 Google 的一次“技术演讲”中介绍了 timsort。坐在我旁边的前排是乔什·布洛赫,当时他也是一名 Google 员工,并且是杰出的 Java 专家。演讲进行到一半时,他再也无法抗拒——他打开笔记本电脑,开始进行黑客攻击,看看它是否可能像出色、敏锐的技术演示所显示的那样好。
因此,timsort 现在也是 Java 虚拟机 (JVM) 最新版本中的排序算法,尽管仅适用于用户定义的对象(基元数组仍以旧方式排序,quickersort [*]我相信——我不知道哪些 Java 特性决定了这种“拆分”设计选择,我的 Java-fu 相当弱:-)。
[*] 这本质上是快速排序,加上一些用于枢轴选择的技巧,以尝试避免中毒案例——这也是 Python 在蒂姆·彼得斯(Tim Peters)在他做出的许多重要贡献中做出这一不朽贡献之前使用的工具几十年。
结果有时会让具有 CS 背景的人感到惊讶(比如 Tim,我有幸拥有很久以前的学术背景,不是 CS,而是 EE,这很有帮助:-)。例如,您必须维护一个不断增长的数组,该数组始终在任何时间点进行排序,因为必须将新的传入数据点添加到数组中。
经典的方法是使用二等分 O(log N) 来为每个新的传入数据点找到合适的插入点——但是,为了将新数据放在正确的位置,您需要移动后面的内容一个槽,即 O(N)。
使用 timsort,您只需将新数据点附加到数组,然后对数组进行排序 - 在这种情况下,对于 timsort 来说是 O(N)(因为它在利用第一个 N- 1 件!-)。
您可以将 timsort 视为将“利用以前完成的工作”推向一个新的极端——其中不仅以前由算法本身完成的工作,而且还受到现实生活数据处理其他方面的其他影响(导致段被提前排序),都被利用到了极点。
然后我们可以进入桶排序和基数排序,它们通过利用项目的内部结构改变了话语平面——在传统排序中,这限制了一个人能够比较两个项目。
或者一个类似的例子——Bentley 在他的不朽著作“Programming Pearls”中提出——需要对一个包含数百万个唯一正整数的数组进行排序,每个正整数都被限制为 24 位长。
他用一个 16M 位的辅助数组解决了这个问题——毕竟只有 2M 字节——最初都是零:一个通过输入数组设置辅助数组中的相应位,然后一个通过辅助数组在找到1s 的地方再次形成所需的整数——然后砰,O(N) [并且非常快速:-)] 对这种特殊但重要的情况进行排序!-)