用铅笔和纸,甚至只看纸上的数字行来思考问题总是有帮助的。然而,更好的解决方案一开始可能看起来过于复杂,而且它们的优势乍一看可能并不那么明显——请参阅gnibbler 的解决方案(他的回答是我个人的赢家,见下文)。
首先,您需要将一个数字与其他所有数字进行比较。然后是第二个数字,以此类推。当使用天真的方法时,在使用单个处理器时无法避免两个嵌套循环。那么时间复杂度总是 O(n^2) 其中 n 是序列的长度。事实是,一些循环可能隐藏在操作中,in
或者list.index()
原则上不会使解决方案更好。
想象一下这些数字的笛卡尔积——它由一对数字组成。这样的对有 n^2 个,但就加法运算的交换性质而言,大约有一半是相同的,其中 n 个是有自身的对。这意味着您只需要检查n^2 / 2 - n
对。如果它们适合测试,最好避免循环遍历不必要的对,而不是稍后进行测试:
for each first element in theList:
for each second element in the rest of theList from the checked one on:
if the first and the second elements give the solution:
report the result
possibly early break if only the first should be reported
对选中的列表的其余部分使用切片,在enumerate()
第一个(也可能在第二个)循环中使用 来了解索引。
尽量减少循环中的操作总是一个好主意。想想内循环体是做的次数最多的。这样您就可以在进入内部循环之前计算搜索到的数字:searched = sum - first
. 然后第二个循环加上if
可以替换为if searched in the rest of theList:
[在此处出现更完整的解决方案后编辑]
这是找到第一次出现或无的 O(n^2) 解决方案(纯 Python,简单,没有库,内置函数和切片,几行):
def sumPair(theList, n):
for index, e in enumerate(theList): # to know the index for the slicing below
complement = n - e # we are searching for the complement
if complement in theList[index+1:]: # only the rest is searched
return e, complement
print sumPair([6,3,6,8,3,2,8,3,2], 11)
[在 gnibbler 对切片和复制的评论之后添加]
gnibbler 关于切片是正确的。切片是副本。(问题是切片是否没有使用“写入时复制”技术进行优化——我不知道。如果是,那么切片将是一种廉价的操作。)为了避免复制,可以使用该list.index()
方法进行测试允许传递起始索引。唯一奇怪ValueError
的是,当找不到该项目时它会引发异常。这样,if complement...
必须替换为try ... except
:
def sumPair2(theList, n):
for ind, e in enumerate(theList):
try:
theList.index(n - e, ind + 1)
return e, n - e
except ValueError:
pass
Gnibbler 的评论让我更多地思考这个问题。事实是,set
可以接近 O(1) 来测试它是否包含元素和 O(n) 来构造集合。对于非数字元素(其中集合类型无法实现为位数组),这不是很清楚。当哈希数组发挥作用并且应该使用其他技术解决可能的冲突时,那么质量取决于实现。
如有疑问,请测量。这里 gnibbler 的解决方案稍作修改,与其他解决方案相同:
import timeit
def sumPair(theList, n):
for index, e in enumerate(theList):
if n - e in theList[index+1:]:
return e, n - e
def sumPair2(theList, n):
for ind, e in enumerate(theList):
try:
theList.index(n - e, ind + 1)
return e, n - e
except ValueError:
pass
def sumPair_gnibbler(theList, n):
# If n is even, check whether n/2 occurs twice or more in theList
if n%2 == 0 and theList.count(n/2) > 1:
return n/2, n/2
theSet = set(theList)
for e in theSet:
if n - e in theSet:
return e, n - e
问题中的原始数字用于第一次测试。n = 1
无法找到解决方案时的最坏情况的原因:
theList = [6,3,6,8,3,2,8,3,2]
n = 11
print '---------------------', n
print sumPair(theList, n),
print timeit.timeit('sumPair(theList, n)', 'from __main__ import sumPair, theList, n', number = 1000)
print sumPair2(theList, n),
print timeit.timeit('sumPair2(theList, n)', 'from __main__ import sumPair2, theList, n', number = 1000)
print sumPair_gnibbler(theList, n),
print timeit.timeit('sumPair_gnibbler(theList, n)', 'from __main__ import sumPair_gnibbler, theList, n', number = 1000)
n = 1
print '---------------------', n
print sumPair(theList, n),
print timeit.timeit('sumPair(theList, n)', 'from __main__ import sumPair, theList, n', number = 1000)
print sumPair2(theList, n),
print timeit.timeit('sumPair2(theList, n)', 'from __main__ import sumPair2, theList, n', number = 1000)
print sumPair_gnibbler(theList, n),
print timeit.timeit('sumPair_gnibbler(theList, n)', 'from __main__ import sumPair_gnibbler, theList, n', number = 1000)
它在我的控制台上产生以下输出:
--------------------- 11
(3, 8) 0.00180958639191
(3, 8) 0.00594907526295
(8, 3) 0.00124991060067
--------------------- 1
None 0.00502748219333
None 0.026334041968
None 0.00150958864789
从那短的数字序列和一个特殊情况来看,就时间复杂度而言,不可能说任何质量。无论如何,gnibbler 的解决方案赢了。
在序列包含唯一值的情况下,gnibbler 的解决方案使用最多的内存。让我们尝试更长的序列,包含 0、1、2、...、9999。n 等于 11 和 3000 代表有解决方案的任务。对于 n 等于 30000 的情况,无法找到这对数字。必须检查所有元素——最坏的情况:
theList = range(10000)
n = 11
print '---------------------', n
print sumPair(theList, n),
print timeit.timeit('sumPair(theList, n)', 'from __main__ import sumPair, theList, n', number = 100)
print sumPair2(theList, n),
print timeit.timeit('sumPair2(theList, n)', 'from __main__ import sumPair2, theList, n', number = 100)
print sumPair_gnibbler(theList, n),
print timeit.timeit('sumPair_gnibbler(theList, n)', 'from __main__ import sumPair_gnibbler, theList, n', number = 100)
n = 3000
print '---------------------', n
print sumPair(theList, n),
print timeit.timeit('sumPair(theList, n)', 'from __main__ import sumPair, theList, n', number = 100)
print sumPair2(theList, n),
print timeit.timeit('sumPair2(theList, n)', 'from __main__ import sumPair2, theList, n', number = 100)
print sumPair_gnibbler(theList, n),
print timeit.timeit('sumPair_gnibbler(theList, n)', 'from __main__ import sumPair_gnibbler, theList, n', number = 100)
n = 30000
print '---------------------', n
print sumPair(theList, n),
print timeit.timeit('sumPair(theList, n)', 'from __main__ import sumPair, theList, n', number = 100)
print sumPair2(theList, n),
print timeit.timeit('sumPair2(theList, n)', 'from __main__ import sumPair2, theList, n', number = 100)
print sumPair_gnibbler(theList, n),
print timeit.timeit('sumPair_gnibbler(theList, n)', 'from __main__ import sumPair_gnibbler, theList, n', number = 100)
请注意,序列要长得多。该测试仅重复 100 次,以便在合理的时间内获得结果。(除非您将时间除以 ,否则无法将时间与之前的测试进行比较number
。)它在我的控制台上显示以下内容:
--------------------- 11
(0, 11) 0.00840137682165
(0, 11) 0.00015695881967
(0, 11) 0.089894683992
--------------------- 3000
(0, 3000) 0.0166750746034
(0, 3000) 0.00966040735374
(0, 3000) 0.12532849753
--------------------- 30000
None 180.328006493
None 163.651082944
None 0.204691100723
在非最坏情况下,gnibbler 的解决方案似乎很慢。原因是它需要经历所有序列的准备阶段。幼稚的解决方案在大约三分之一的第一遍中找到了数字。能说明一切的是最坏的情况。gnibbler 的解决方案大约快 1000 倍,并且对于更长的序列,差异会增加。 Gnibbler 的解决方案显然是赢家。