我同事在面试中提出了一个有趣的问题:
假设你被给予一个未排序的、非常长的无符号64位整数列表。你如何找到列表中不存在的最小的非负整数?
后续问题:既然已经提出了排序的显而易见的解决方案,你能否比O(n log n)更快地完成任务?
后续问题:你的算法必须在计算机上运行,例如1GB的内存。
澄清:这个列表在RAM中,尽管它可能占用大量内存。你预先知道列表的大小N。
我同事在面试中提出了一个有趣的问题:
假设你被给予一个未排序的、非常长的无符号64位整数列表。你如何找到列表中不存在的最小的非负整数?
后续问题:既然已经提出了排序的显而易见的解决方案,你能否比O(n log n)更快地完成任务?
后续问题:你的算法必须在计算机上运行,例如1GB的内存。
澄清:这个列表在RAM中,尽管它可能占用大量内存。你预先知道列表的大小N。
# Pass 1, move every value to the position of its value
for cursor in range(N):
target = array[cursor]
while target < N and target != array[target]:
new_target = array[target]
array[target] = target
target = new_target
# Pass 2, find first location where the index doesn't match the value
for cursor in range(N):
if array[cursor] != cursor:
return cursor
return N
这里有一个简单的 O(N)
解法,使用 O(N)
空间。假设我们将输入列表限制为非负数,并且要找到第一个不在列表中的非负数。
N
。N
个布尔值的数组,初始化为全部为 false
。X
,如果 X
小于 N
,则将数组的第 X
个元素设置为 true
。0
开始扫描数组,查找第一个为 false
的元素。如果您在索引 I
处找到第一个 false
,则 I
是答案。否则(即当所有元素都为 true
时),答案是 N
。实际上,"包含 N
个布尔值的数组" 可能会被编码为作为 byte
或 int
数组表示的 "位图" 或 "bitset"。这通常使用更少的空间(取决于编程语言),并允许更快地扫描第一个 false
。
这就是算法的工作原理和原因。
假设列表中的 N
个数字不是不同的,或者其中一个或多个大于 N
。这意味着在范围 0 .. N - 1
中必须至少有一个数字不在列表中。因此,查找最小缺失数的问题必须减少到查找最小缺失数 小于 N
的问题。这意味着我们不需要跟踪大于或等于 N
的数字……因为它们不会是答案。
与前一段相反的是,列表是从 0 .. N - 1
中数字的排列。在这种情况下,步骤 3 将数组的所有元素设置为 true
,并且步骤 4 告诉我们第一个“缺失”的数字是 N
。
该算法的计算复杂度为 O(N)
,比例系数相对较小。它通过列表进行两次线性通行,或者如果已知列表长度则只需要一次通行。没有必要将整个列表保存在内存中,因此算法的渐进空间使用量仅为表示布尔值数组所需的量;即 O(N)
位。
O(N)
64位字)。@Jorn指出,步骤1到3是计数排序的一个变体。从某种意义上说,他是对的,但差异很大:
Xmax-Xmin
个计数器的数组,其中Xmax
是列表中最大的数字,而 Xmin
是列表中最小的数字。每个计数器必须能够表示N个状态;即,假设使用二进制表示,它必须具有(至少) ceiling(log2(N))
位的整数类型。Xmax
和 Xmin
。 ceiling(log2(N)) * (Xmax-Xmin)
位。相比之下,上面展示的算法在最坏情况和最佳情况下仅需要N
位。
然而,这种分析使人产生一种直觉,即如果算法通过列表进行初始遍历以查找零(如果需要,计算列表元素),则在找到零时不使用任何空间即可更快地给出答案。如果有很大的可能性在列表中至少找到一个零,则绝对值得这样做。这个额外的遍历不会改变总体复杂度。
编辑:我已更改算法描述,以使用“布尔数组”,因为显然有人发现我最初使用位和位图的描述令人困惑。
bool []
还是位图来实现该数组,与通用解决方案无关。 - Joren正如其他答案所指出的,您可以进行排序,然后简单地扫描直到找到一个间隙。
你可以通过使用改进的快速排序算法将算法复杂度优化为O(N),同时保持O(N)的空间利用率,其中你会消除那些不可能包含间隙的分区。
这样可以节省大量计算。
for i in [0..2^64):
if i not in list: return i
print "no 64-bit integers are missing"
由于所有数字长度均为64位,我们可以使用基数排序,其时间复杂度为O(n)。对它们进行排序,然后扫描它们,直到找到你要的数字。
如果最小的数字是零,则向前扫描,直到找到空隙。如果最小的数字不是零,则答案为零。
O(k)
和时间复杂度为O(k*log(N)*N)
的方法。它具有高效的空间利用率,没有数据移动,所有操作都是基本操作(加减)。U=N;L=0
。
2.首先将数字空间划分为k
个区域。像这样:0->(1/k)*(U-L) + L
,0->(2/k)*(U-L) + L
,0->(3/k)*(U-L) + L
... 0->(U-L) + L
3.找到每个区域中有多少个数字(count{i}
)。(N*k
步)
4.找到第一个不满的区域(h
)。这意味着count{h} < upper_limit{h}
。(k
步)
5.如果h-count{h-1}=1
,则找到了答案。
6.设置U=count{h};L=count{h-1}
。
7.返回第2步。k
个区域。像这样:L+(i/k)->L+(i+1/k)*(U-L)
3.使用j=(number-L)/k
(如果L<number<U)
来增加count{j}
。
4.找到第一个没有k个元素的区域(h
)。
5.如果count{h}=1
,则找到了答案。
6.设置U=区域h中的最大值
,L=区域h中的最小值
。
7.返回第2步。O(log(N)*N)
。我会先将它们排序,然后按顺序遍历,直到找到一个空缺(包括零和第一个数字之间的空缺)。
就算法而言,可以使用以下算法:
def smallest_not_in_list(list):
sort(list)
if list[0] != 0:
return 0
for i = 1 to list.last:
if list[i] != list[i-1] + 1:
return list[i-1] + 1
if list[list.last] == 2^64 - 1:
assert ("No gaps")
return list[list.last] + 1
def smallest_not_in_list(list):
bitmask = mask_make(2^64) // might take a while :-)
mask_clear_all (bitmask)
for i = 1 to list.last:
mask_set (bitmask, list[i])
for i = 0 to 2^64 - 1:
if mask_is_clear (bitmask, i):
return i
assert ("No gaps")
我们可以使用哈希表来存储这些数字。当所有数字处理完毕后,从0开始计数直到找到最小值。一个合理的哈希函数将会在常数时间内进行哈希和存储,并在常数时间内检索。
for every i in X // One scan Θ(1)
hashtable.put(i, i); // O(1)
low = 0;
while (hashtable.get(i) <> null) // at most n+1 times
low++;
print low;
对列表进行排序,查看第一个和第二个元素,并开始向上移动直到出现间隔。