我正在leetcode.com上练习基本的编程技能(我强烈推荐它。这很棒),并且遇到了有趣的结果。
我试图找到给定数组A
的第一个不动点,该点具有严格的升序值。
我以两种方式做到这一点。首先,二进制搜索/线性时间混合
start, end = 0, len(A) - 1
while(start <= end):
middle = (start + end) / 2
if A[middle] == middle:
for i in range(start, middle + 1):
if A[i] == i:
return i
elif A[middle] > middle:
end = middle - 1
elif A[middle] < middle:
start = middle + 1
return -1
这是简单的二进制搜索,直到找到匹配项,然后依次遍历所有可能的第一个固定点,直到找到第一个匹配项。这是线性部分进入的地方。例如,假设len(A) = 1001
和A[500] = 500
是唯一的固定点,那么我在二分查找的一次迭代中找到了它,但是随后我必须从索引开始0至500,一个接一个,寻找第一个匹配项。那是n/2
或换句话说O(n)
第二种解决方案是纯二进制搜索
start, end = 0, len(A) - 1
fixed_point = -1
while(start <= end):
middle = (start + end) / 2
if A[middle] == middle:
fixed_point = middle
end = middle - 1
elif A[middle] > middle:
end = middle - 1
elif A[middle] < middle:
start = middle + 1
return fixed_point
我希望它会更好,但实际上会更糟。
第一个解决方案在运行时间方面击败了所有提交的97%,运行时间为40毫秒。
第二种解决方案仅击败所有提交的72%,运行时间为48毫秒。
我很好奇为什么会这样。第二种解决方案真的更好吗?
答案 0 :(得分:2)
在A[middle] == middle
处:“找到固定点的任何索引”的位置。第一个版本更有效地找到中点运行的起点,假设它发生在线性扫描的起点“ fsvo”附近(fsvo):每个二进制循环的成本“更昂贵”( C),而不考虑所需的循环数(O复杂度)。 对于特定的简并输入,应该有可能构造纯二进制搜索更好的情况: n的大小和输入的分布。
例如,[0, 0, 0 ..., midpoint=1, 1, 1 ...]
的简并情况在第一个版本中将是O(n)
,因为线性循环将超过start=0..midpoint=n/2
1 。 使用此退化数据以及足够大的n值,纯二进制搜索将在边界更好的情况下占主导地位。但是,假定它是“分布良好的随机数据”,则采用线性方法探针可以在非常小的线性扫描集中进行磨练(对于最终循环,C较小)。
这类似于为什么线性扫描对于小型阵列来说更快,以及为什么合并排序或快速排序(例如)可以切换到插入排序以进行近叶排序的原因。 Big-O将行为描述为n-> Inifinity,而没有考虑固定成本。
1 扫描也可能是midpoint=n/2..start=0
。在这种情况下,所示的简并情况是理想情况,而相应的简并情况将是[0, 1, 1, 1 ...]
。