添加到 MBo 的答案中。
“最优”在算法方面可能是一个模棱两可的术语,因为在算法运行速度和内存效率之间经常存在折衷。有时我们也可能对最坏情况下的资源消耗或平均资源消耗感兴趣。我们将在这里循环最坏的情况,因为它更简单并且大致相当于我们场景中的平均值。
让我们将数组的长度称为n,并考虑3个示例。
示例 1
我们从一个非常简单的算法开始解决我们的问题,两个嵌套循环遍历数组,并检查每两个不同索引的项目是否总和为目标数。
时间复杂度:最坏的情况(答案是False 或True,但我们在检查的最后一对项目中找到它)有n^2 循环迭代。如果你熟悉大 O 表示法,我们会说算法的时间复杂度是O(n^2),这基本上意味着就我们的输入大小n 而言,求解算法所需的时间会增长更多或不像n^2 带有乘法因子(好吧,从技术上讲,符号的意思是“最多像 n^2 带有一个乘法因子,但将其用作“或多或少像" 代替)。
空间复杂度(内存消耗):我们只存储一个数组,加上一组固定的对象,其大小不依赖于n(Python 需要运行的所有东西,调用堆栈,可能还有两个迭代器和/或一些临时变量)。因此,随着n 增长的内存消耗部分只是数组的大小,即n 乘以在数组中存储整数所需的内存量(我们称之为sizeof(int))。
结论:时间是O(n^2),内存是n*sizeof(int)(+O(1),也就是一个额外的常数因子,这对我们来说无关紧要,从现在开始我们将忽略它)。
示例 2
让我们考虑一下 MBo 答案中的算法。
时间复杂度:比示例 1 中的要好得多。我们从创建字典开始。这是在n 的循环中完成的。在字典中设置键是在适当条件下的恒定时间操作,因此第一个循环的每个步骤所花费的时间不依赖于n。因此,目前我们在时间复杂度方面使用了O(n)。现在我们在n 上只剩下一个循环了。访问我们字典中的元素所花费的时间与n 无关,因此总复杂度再次为O(n)。将我们的两个循环组合在一起,因为它们都像n 一样增长到一个乘法因子,它们的总和也是如此(达到一个不同的乘法因子)。总计:O(n)。
内存:基本和以前一样,加上一个n元素的字典。为了简单起见,让我们考虑这些元素是整数(我们可以使用布尔值),并忘记字典的某些方面,只计算用于存储键和值的大小。有n整数键和n整数值要存储,在内存方面使用2*n*sizeof(int)。再加上我们之前的,我们总共有3*n*sizeof(int)。
结论:时间是O(n),内存是3*n*sizeof(int)。当n 增长时,该算法要快得多,但使用的内存是示例 1 的三倍。在一些几乎没有可用内存的奇怪场景中(可能是嵌入式系统),这个3*n*sizeof(int) 可能太多了,你可能无法使用此算法(诚然,它可能永远不会成为真正的问题)。
示例 3
我们可以在示例 1 和示例 2 之间找到一个权衡吗?
一种方法是复制与示例 1 中相同类型的嵌套循环结构,但需要进行一些预处理以用更快的方式替换内部循环。为此,我们对初始数组进行排序。使用well-chosen algorithms 完成,时间复杂度为O(n*log(n)),内存使用量可以忽略不计。
一旦我们对数组进行了排序,我们就编写外循环(这是整个数组的常规循环),然后在外循环内,使用二分法搜索我们缺少的数字以达到我们的目标@ 987654355@。这种二分法的内存消耗为O(log(n)),时间复杂度也为O(log(n))。
结论:时间是O(n*log(n)),内存是n*sizeof(int) + O(log(n))。内存几乎和示例 1 一样小。时间复杂度略高于示例 2。在示例 2 因内存不足而无法使用的场景中,在速度方面下一个最好的事情实际上是示例 3,即几乎与示例 2 一样快,并且如果非常慢的示例 1 运行,可能有足够的空间运行。
总体结论
这个答案只是为了表明“最佳”在算法中是依赖于上下文的。在这个特定示例中,不太可能选择实现示例 3。通常,如果 n 太小以至于人们会选择最简单的设计和最快的编码方式,您会看到示例 1,或者示例2 如果n 有点大,我们想要速度。但是,如果您查看我为排序算法链接的维基百科页面,您会发现它们都不是最擅长的。他们都有可以用更好的东西代替的场景。