我的一位同事使用的一个有趣的访谈问题:
假设您有一个非常长的未排序的无符号64位整数列表。您如何找到列表中不的最小非负整数?
后续行动:现在已经提出了通过排序的明显解决方案,你能否比O(n log n)更快地完成它?
FOLLOW-UP:您的算法必须在具有1GB内存的计算机上运行
澄清:列表在RAM中,但它可能会消耗大量的内容。你会提前给出列表的大小,比如N.
答案 0 :(得分:114)
如果数据结构可以就地变异并支持随机访问,那么您可以在O(N)时间和O(1)额外空间中进行。只需按顺序遍历数组,并为每个索引将索引处的值写入由value指定的索引,递归地将该位置的任何值放到其位置并丢弃值> N.然后再次遍历数组,寻找值与索引不匹配的点 - 这是不在数组中的最小值。这导致最多3N比较,并且仅使用一些值的临时空间。
# 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
答案 1 :(得分:85)
这是一个使用O(N)
空间的简单O(N)
解决方案。我假设我们将输入列表限制为非负数,并且我们想要找到列表中没有的第一个非负数。
N
。N
布尔值,初始化为所有false
。 X
,如果X
小于N
,请将数组的X'th
元素设置为true
。0
开始扫描数组,查找第一个false
元素。如果您在索引false
找到第一个I
,那么I
就是答案。否则(即当所有元素都是true
时),答案是N
。实际上,“N
布尔数组”可能会被编码为“位图”或“位集”,表示为byte
或int
数组。这通常占用较少的空间(取决于编程语言),并允许更快地完成对第一个false
的扫描。
这就是算法运作的方式/原因。
假设列表中的N
数字不明显,或者其中一个或多个数字大于N
。这意味着{em>至少在0 .. N - 1
范围内必须有一个不在列表中的数字。因此,找到最小缺失数的问题必须减少到找到最小缺失数小于N
的问题。这意味着我们不需要跟踪大于或等于N
的数字......因为它们不是答案。
上一段的替代方案是该列表是来自0 .. N - 1
的数字的排列。在这种情况下,第3步将数组的所有元素设置为true
,第4步告诉我们第一个“缺失”数字为N
。
算法的计算复杂度为O(N)
,具有相对较小的比例常数。它在列表中进行两次线性传递,或者如果已知列表长度,则只进行一次传递。没有必要表示将整个列表保存在内存中,因此算法的渐近内存使用正是表示布尔数组所需要的;即O(N)
位。
(相比之下,依赖于内存中排序或分区的算法假设您可以在内存中表示整个列表。在提出问题的形式中,这将需要O(N)
64位字。)
Xmax - Xmin
个计数器,其中Xmax
是列表中的最大数字,Xmin
是列表中的最小数字。每个计数器必须能够代表N个状态;即,假设二进制表示,它必须具有整数类型(至少)ceiling(log2(N))
位。Xmax
和Xmin
。ceiling(log2(N)) * (Xmax - Xmin)
位。相比之下,上面提到的算法在最差和最好的情况下只需要N
位。
然而,这种分析导致直觉,如果算法初始通过列表寻找零(并在需要时计算列表元素),如果找到它,它将给出更快的答案,根本不使用空格零。如果在列表中找到至少一个零的概率很高,那么绝对值得这样做。而这个额外的通行证不会改变整体的复杂性。
答案 2 :(得分:13)
由于OP现在已经指定原始列表保存在RAM中,并且计算机只有1GB的内存,所以我会想出答案是零。
1GB的RAM意味着该列表中最多可包含134,217,728个数字。但是有2个 64 = 18,446,744,073,709,551,616个可能的数字。因此零列在列表中的概率是137,438,953,472中的1。
相比之下,我struck by lightning this year的几率是700,000中的1。我getting hit by a meteorite的几率约为10万亿分之一。因此,由于天体过早死亡而不是零回答,我写在科学期刊上的可能性要高十倍。
答案 3 :(得分:10)
正如在其他答案中指出的那样,您可以进行排序,然后直接扫描直至找到差距。
您可以通过使用修改后的QuickSort将算法复杂度提高到O(N)并保留O(N)空间,从而消除不可能包含间隙的分区。
这节省了大量的计算。
答案 4 :(得分:8)
由于数字都是64位长,我们可以对它们使用radix sort,即O(n)。排序他们,然后扫描他们,直到找到你要找的东西。
如果最小数字为零,则向前扫描直至找到间隙。如果最小数字不为零,则答案为零。
答案 5 :(得分:8)
为了说明O(N)
思考的一个陷阱,这是一个使用O(N)
空间的O(1)
算法。
for i in [0..2^64):
if i not in list: return i
print "no 64-bit integers are missing"
答案 6 :(得分:5)
对于节省空间的方法,所有值都是不同的,您可以在空格O( k )
和时间O( k*log(N)*N )
中执行此操作。它节省空间,没有数据移动,所有操作都是基本的(增加减法)。
U = N; L=0
k
个区域中的数字空间进行分区。像这样:
0->(1/k)*(U-L) + L
,0->(2/k)*(U-L) + L
,0->(3/k)*(U-L) + L
... 0->(U-L) + L
count{i}
)。 (N*k
步骤)h
)。这意味着count{h} < upper_limit{h}
。 (k
步骤)h - count{h-1} = 1
你得到了答案U = count{h}; L = count{h-1}
使用散列可以改善这一点(感谢Nic这个想法)。
k
个区域中的数字空间进行分区。像这样:
L + (i/k)->L + (i+1/k)*(U-L)
inc count{j}
使用j = (number - L)/k
(if L < number < U)
h
)count{h} = 1
h是您的回答U = maximum value in region h
L = minimum value in region h
这将在O(log(N)*N)
。
答案 7 :(得分:3)
我只是对它们进行排序,然后按顺序运行,直到找到间隙(包括零点和第一个数字之间的间隙)。
就算法而言,这样的事情会这样做:
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
当然,如果你有比CPU grunt更多的内存,你可以创建一个所有可能的64位值的位掩码,只需为列表中的每个数字设置位。然后查找该位掩码中的第一个0位。这使得它在时间上变成了O(n)操作,但在内存要求方面非常昂贵: - )
我怀疑你可以改进O(n),因为我看不到这样做的方法,不涉及至少一次查看每个数字。
该算法的算法将是:
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")
答案 8 :(得分:2)
对列表进行排序,查看第一个和第二个元素,然后开始上升,直到出现间隙。
答案 9 :(得分:1)
你可以在O(n)时间和O(1)额外空间中进行,尽管隐藏因素非常大。这不是解决问题的实用方法,但它可能会很有趣。
对于每个无符号的64位整数(按升序)迭代列表,直到找到目标整数或到达列表的末尾。如果到达列表的末尾,则目标整数是不在列表中的最小整数。如果到达64位整数的末尾,则每个64位整数都在列表中。
这是一个Python函数:
def smallest_missing_uint64(source_list):
the_answer = None
target = 0L
while target < 2L**64:
target_found = False
for item in source_list:
if item == target:
target_found = True
if not target_found and the_answer is None:
the_answer = target
target += 1L
return the_answer
此功能故意低效保持O(n)。特别注意,即使在找到答案后,该函数仍会检查目标整数。如果在找到答案后立即返回该函数,则外循环运行的次数将受答案大小的约束,该答案的大小由n限制。这种改变会使运行时间为O(n ^ 2),即使它会快得多。
答案 10 :(得分:1)
感谢egon,swilden和Stephen C的灵感。首先,我们知道目标值的界限,因为它不能大于列表的大小。此外,1GB列表最多可包含134217728(128 * 2 ^ 20)个64位整数。
哈希部分
我建议使用散列来大大减少我们的搜索空间。首先,平方根的列表大小。对于1GB的列表,那是N = 11,586。设置一个大小为N的整数数组。遍历列表,并将您找到的每个数字的平方根*作为哈希值。在哈希表中,递增该哈希的计数器。接下来,遍历您的哈希表。您找到的第一个桶不等于它的最大尺寸定义了您的新搜索空间。
位图部分
现在设置一个等于新搜索空间大小的常规位图,并再次遍历源列表,在搜索空间中找到每个数字时填写位图。完成后,位图中的第一个未设置位将为您提供答案。
这将在O(n)时间和O(sqrt(n))空间内完成。
(*您可以使用像移位这样的东西来更有效地执行此操作,并相应地改变存储桶的数量和大小。)
答案 11 :(得分:1)
如果数字列表中只有一个缺失的数字,找到缺失数字的最简单方法是对系列求和并减去列表中的每个值。最终值是缺失的数字。
答案 12 :(得分:1)
我们可以使用哈希表来保存数字。完成所有数字后,从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;
最糟糕的情况是,如果数组中有n
个元素且{0, 1, ... n-1}
,则在n
时,答案将在O(n)
获得,并保持{{1}} }。
答案 13 :(得分:1)
int i = 0;
while ( i < Array.Length)
{
if (Array[i] == i + 1)
{
i++;
}
if (i < Array.Length)
{
if (Array[i] <= Array.Length)
{//SWap
int temp = Array[i];
int AnoTemp = Array[temp - 1];
Array[temp - 1] = temp;
Array[i] = AnoTemp;
}
else
i++;
}
}
for (int j = 0; j < Array.Length; j++)
{
if (Array[j] > Array.Length)
{
Console.WriteLine(j + 1);
j = Array.Length;
}
else
if (j == Array.Length - 1)
Console.WriteLine("Not Found !!");
}
}
答案 14 :(得分:1)
这是我用Java编写的答案:
基本理念: 1-循环通过阵列丢弃重复的正数,零和负数,同时总结其余数字,获得最大正数,并在地图中保留唯一的正数。
2-将总和计算为max *(max + 1)/ 2。
3-找出在步骤1和步骤1中计算的总和之间的差异。 2
4-再次从1循环到[sums difference,max]的最小值,并返回步骤1中填充的地图中不存在的第一个数字。
public static int solution(int[] A) {
if (A == null || A.length == 0) {
throw new IllegalArgumentException();
}
int sum = 0;
Map<Integer, Boolean> uniqueNumbers = new HashMap<Integer, Boolean>();
int max = A[0];
for (int i = 0; i < A.length; i++) {
if(A[i] < 0) {
continue;
}
if(uniqueNumbers.get(A[i]) != null) {
continue;
}
if (A[i] > max) {
max = A[i];
}
uniqueNumbers.put(A[i], true);
sum += A[i];
}
int completeSum = (max * (max + 1)) / 2;
for(int j = 1; j <= Math.min((completeSum - sum), max); j++) {
if(uniqueNumbers.get(j) == null) { //O(1)
return j;
}
}
//All negative case
if(uniqueNumbers.isEmpty()) {
return 1;
}
return 0;
}
答案 15 :(得分:0)
这可以帮助:
0- A is [5, 3, 2, 7];
1- Define B With Length = A.Length; (O(1))
2- initialize B Cells With 1; (O(n))
3- For Each Item In A:
if (B.Length <= item) then B[Item] = -1 (O(n))
4- The answer is smallest index in B such that B[index] != -1 (O(n))
答案 16 :(得分:0)
def solution(A):
A.sort()
j = 1
for i, elem in enumerate(A):
if j < elem:
break
elif j == elem:
j += 1
continue
else:
continue
return j
答案 17 :(得分:0)
使用python并不是最有效,但是正确
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import datetime
# write your code in Python 3.6
def solution(A):
MIN = 0
MAX = 1000000
possible_results = range(MIN, MAX)
for i in possible_results:
next_value = (i + 1)
if next_value not in A:
return next_value
return 1
test_case_0 = [2, 2, 2]
test_case_1 = [1, 3, 44, 55, 6, 0, 3, 8]
test_case_2 = [-1, -22]
test_case_3 = [x for x in range(-10000, 10000)]
test_case_4 = [x for x in range(0, 100)] + [x for x in range(102, 200)]
test_case_5 = [4, 5, 6]
print("---")
a = datetime.datetime.now()
print(solution(test_case_0))
print(solution(test_case_1))
print(solution(test_case_2))
print(solution(test_case_3))
print(solution(test_case_4))
print(solution(test_case_5))
答案 18 :(得分:0)
通过基本的javascript解决方案
var a = [1, 3, 6, 4, 1, 2];
function findSmallest(a) {
var m = 0;
for(i=1;i<=a.length;i++) {
j=0;m=1;
while(j < a.length) {
if(i === a[j]) {
m++;
}
j++;
}
if(m === 1) {
return i;
}
}
}
console.log(findSmallest(a))
希望这对某人有帮助。
答案 19 :(得分:0)
unordered_set可用于存储所有正数,然后我们可以从1迭代到unordered_set的长度,并查看第一个未出现的数字。
int firstMissingPositive(vector<int>& nums) {
unordered_set<int> fre;
// storing each positive number in a hash.
for(int i = 0; i < nums.size(); i +=1)
{
if(nums[i] > 0)
fre.insert(nums[i]);
}
int i = 1;
// Iterating from 1 to size of the set and checking
// for the occurrence of 'i'
for(auto it = fre.begin(); it != fre.end(); ++it)
{
if(fre.find(i) == fre.end())
return i;
i +=1;
}
return i;
}
答案 20 :(得分:0)
1)过滤否定和零
2)分类/不同
3)访问数组
复杂性:O(N)或O(N * log(N))
使用 Java8
^(?:[a-z]+[A-Z]|[A-Z]+[a-z])[a-zA-Z]+$
^(?=[A-Z]*[a-z])(?=[a-z]*[A-Z])[a-zA-Z]+$
^(?!(?:[a-z]+|[A-Z]+)$)[a-zA-Z]+$
答案 21 :(得分:0)
YYYY-MM-DD\THH:MM:SS
获得100%的上述解决方案。
答案 22 :(得分:0)
这是Java中的一个答案,它不会修改输入并使用O(N)时间和N位加上一个小的常量内存开销(其中N是列表的大小):
int smallestMissingValue(List<Integer> values) {
BitSet bitset = new BitSet(values.size() + 1);
for (int i : values) {
if (i >= 0 && i <= values.size()) {
bitset.set(i);
}
}
return bitset.nextClearBit(0);
}
答案 23 :(得分:0)
来自Ants答案的Dafny片段显示了就地算法失败的原因。 requires
前置条件描述了每个项的值不得超出数组的范围。
method AntsAasma(A: array<int>) returns (M: int)
requires A != null && forall N :: 0 <= N < A.Length ==> 0 <= A[N] < A.Length;
modifies A;
{
// Pass 1, move every value to the position of its value
var N := A.Length;
var cursor := 0;
while (cursor < N)
{
var target := A[cursor];
while (0 <= target < N && target != A[target])
{
var new_target := A[target];
A[target] := target;
target := new_target;
}
cursor := cursor + 1;
}
// Pass 2, find first location where the index doesn't match the value
cursor := 0;
while (cursor < N)
{
if (A[cursor] != cursor)
{
return cursor;
}
cursor := cursor + 1;
}
return N;
}
使用和不使用forall ...
子句将代码粘贴到验证程序中以查看验证错误。第二个错误是验证者无法为Pass 1循环建立终止条件的结果。证明这是留给更了解该工具的人。
答案 24 :(得分:0)
Ants Aasma干得好!我想了大约15分钟的答案,并以类似的思维方式独立提出答案:
#define SWAP(x,y) { numerictype_t tmp = x; x = y; y = tmp; }
int minNonNegativeNotInArr (numerictype_t * a, size_t n) {
int m = n;
for (int i = 0; i < m;) {
if (a[i] >= m || a[i] < i || a[i] == a[a[i]]) {
m--;
SWAP (a[i], a[m]);
continue;
}
if (a[i] > i) {
SWAP (a[i], a[a[i]]);
continue;
}
i++;
}
return m;
}
m表示“当前最大可能输出,根据我对第一个i输入的了解,并假设在m-1处输入之前没有任何其他值”。
仅当(a [i],...,a [m-1])是值(i,...,m-1)的置换时,才返回m的该值。因此,如果a [i]> = m或者如果a [i]&lt;我或者如果[i] == a [a [i]]我们知道m是错误的输出并且必须至少有一个元素更低。所以递减m并用a [m]交换[i]我们可以递归。
如果不是这样,那么[i]&gt;我知道a [i]!= a [a [i]]我们知道用[a [i]]交换a [i]会增加他们自己位置的元素数量。
否则a [i]必须等于i,在这种情况下我们可以增加i知道所有直到并包括该索引的值都等于它们的索引。
这不能进入无限循环的证据留给读者练习。 :)
答案 25 :(得分:0)
我喜欢“猜零”的评价。如果数字是随机的,那么很可能是零。如果“审查员”设置了非随机列表,则添加一个并再次猜测:
LowNum=0
i=0
do forever {
if i == N then leave /* Processed entire array */
if array[i] == LowNum {
LowNum++
i=0
}
else {
i++
}
}
display LowNum
最坏的情况是n * N,其中n = N,但实际上n很可能是一个很小的数字(例如1)
答案 26 :(得分:0)
正如Stephen C巧妙地指出的那样,答案必须是一个小于数组长度的数字。然后我会通过二分查找找到答案。这可以优化最坏的情况(因此面试官无法在'假设'病态情景中抓住你)。在一次采访中,请指出你这样做是为了在最坏的情况下进行优化。
使用二进制搜索的方法是从数组的每个元素中减去您要查找的数字,并检查否定结果。
答案 27 :(得分:0)
我不确定我是否得到了这个问题。但是如果列表1,2,3,5,6和缺失的数字是4,则可以在O(n)中找到缺失的数字: (N + 2)(N + 1)/ 2-(N + 1)N / 2
编辑:抱歉,我猜我昨晚想的太快了。无论如何,第二部分实际上应该用sum(list)代替,这是O(n)的来源。该公式揭示了它背后的想法:对于n个连续的整数,总和应该是(n + 1)* n / 2。如果缺少数字,则总和将等于(n + 1)个连续整数减去缺失数的总和。感谢你指出我在脑海里放了一些中间件。