没有实际测试任何东西(一个冒险的举动!),我将声称 O(n log n)-time, O(1)-space 算法可能比 O(n)-time 快, O(n)-空间算法,但仍然可能不是最优算法。
首先,让我们从忽略您所描述的算法的特定细节的高级角度来讨论这个问题。要记住的一个细节是,尽管 O(n) 时间算法比 O(n log n) 时间算法渐近地快,但它们的速度只是对数因子。请记住,宇宙中的原子数约为 1080(感谢物理学!),宇宙中原子数的以 2 为底的对数约为 240。从实际角度来看,这意味着您可以将额外的 O(log n) 因子视为一个常数。因此,要确定在特定输入上 O(n log n) 算法是否比 O(n) 算法更快或更慢,您需要更多地了解大 O 表示法隐藏了哪些常量。例如,对于任何适合宇宙的 n,在 600n 时间运行的算法将比在 2n log n 时间运行的算法慢。因此,就挂钟性能而言,要评估哪种算法更快,您可能需要对算法进行一些分析以查看哪种算法更快。
然后是缓存和引用位置的影响。计算机内存中有大量缓存,这些缓存针对读取和写入相邻的情况进行了优化。缓存未命中的代价可能是巨大的——比命中慢数百或数千倍——所以你想尽量减少这种情况。如果算法使用 O(n) 内存,那么随着 n 变大,您需要开始担心内存访问的密集程度。如果它们分散开,那么缓存未命中的成本可能会开始很快增加,从而显着提高隐藏在时间复杂度的大 O 表示法中的系数。如果它们更连续,那么您可能不需要太担心这一点。
您还需要注意可用的总内存。如果您的系统上有 8GB 的 RAM 并获得一个包含十亿个 32 位整数的数组,那么如果您需要 O(n) 辅助空间以及一个合理的常数,那么您将无法容纳辅助内存进入主内存,它将开始被操作系统分页,真的会杀死你的运行时。
最后,还有随机性的问题。基于散列的算法预期运行速度很快,但如果你得到一个糟糕的散列函数,算法就有可能会变慢。生成好的随机位是困难的,所以大多数哈希表只使用“相当好的”哈希函数,冒着最坏情况输入的风险,这会使算法的性能退化。
那么这些问题在实践中是如何实际发挥出来的呢?好吧,让我们看看算法。 O(n)-time, O(n)-space 算法通过构建数组中所有元素的哈希表来工作,以便您可以轻松检查给定元素是否存在于数组中,然后扫描数组并看看是否有一对总和。考虑到上述因素,让我们考虑一下这个算法是如何工作的。
内存使用量为 O(n),并且由于散列的工作方式,对散列表的访问不太可能是顺序的(理想的散列表将具有相当多的随机访问模式)。这意味着您将有很多缓存未命中。
高内存使用意味着对于大型输入,您必须担心内存被分页进出,从而加剧上述问题。
由于上述两个因素,O(n) 运行时中隐藏的常数项可能比看起来要高得多。
哈希不是最坏情况下的效率,因此可能存在导致性能显着下降的输入。
现在,考虑 O(n log n) 时间,O(1) 空间算法,该算法通过进行就地数组排序(例如堆排序),然后从左右向内走并查看如果您能找到总和为目标的一对。此过程的第二步具有出色的引用局部性——几乎所有数组访问都是相邻的——并且几乎所有您将要获得的缓存未命中都将出现在排序步骤中。这将增加隐藏在大 O 符号中的常数因子。然而,该算法没有退化的输入,并且它的低内存占用可能意味着引用的局部性将优于哈希表方法。因此,如果我不得不猜测,我会把钱花在这个算法上。
...嗯,实际上,我会把钱花在第三种算法上:O(n log n)-time, O(log n)-space 算法,基本上就是上面的算法,但是使用 introsort 而不是堆排序。 Introsort 是一种 O(n log n) 时间、O(log n) 空间算法,它使用随机快速排序对数组进行主要排序,如果快速排序看起来即将退化,则切换到堆排序,并进行最后的插入排序传递清理一切。快速排序具有惊人的参考局部性——这就是它如此之快的原因——并且插入排序在小输入上更快,所以这是一个很好的折衷方案。另外,O(log n) 额外的内存基本上没什么 - 请记住,实际上,log n 最多为 240。该算法具有您可以获得的最佳参考局部性,给出了 O( n log n) 项,因此在实践中它可能会优于其他算法。
当然,我也必须限定这个答案。我在上面所做的分析假设我们正在讨论算法的相当大的输入。如果您只查看少量输入,那么整个分析就会消失,因为我考虑的影响不会开始出现。在这种情况下,最好的选择就是分析这些方法,看看哪种方法最有效。从那里,您可能能够构建一种“混合”方法,在这种方法中,您对一个大小范围内的输入使用一种算法,而对不同大小范围内的输入使用一种不同算法。这很有可能会提供一种优于任何一种方法的方法。
也就是说,套用 Don Knuth 的话说,“请注意上述分析 - 我只是证明它是正确的,并没有实际尝试过。”最好的选择是分析所有内容并查看它是如何工作的。我没有这样做的原因是分析需要注意哪些因素,并强调比较两种算法的纯大 O 分析的弱点。我希望实践能证明这一点!如果没有,我很想看看我哪里弄错了。 :-)