我的一位同事使用的一个有趣的采访问题:
假设您得到了一个很长的,未排序的无符号64位整数列表。您将如何找到列表中未出现的最小非负整数?
跟进:现在已经提出了明显的排序解决方案,您可以比O(n log n)更快吗?
跟进:您的算法必须在具有1GB内存的计算机上运行
澄清:该列表位于RAM中,尽管它可能会消耗大量的内存。预先给您列表的大小,例如N。
我的一位同事使用的一个有趣的采访问题:
假设您得到了一个很长的,未排序的无符号64位整数列表。您将如何找到列表中未出现的最小非负整数?
跟进:现在已经提出了明显的排序解决方案,您可以比O(n log n)更快吗?
跟进:您的算法必须在具有1GB内存的计算机上运行
澄清:该列表位于RAM中,尽管它可能会消耗大量的内存。预先给您列表的大小,例如N。
Answers:
如果数据结构可以在适当位置进行突变并支持随机访问,则可以在O(N)时间和O(1)额外空间中进行操作。只需依次遍历数组,并为每个索引将索引处的值写入由value指定的索引,然后将该位置的任何值递归地放置到该位置,并丢弃> N的值即可。然后再次遍历该数组以查找该点其中value与索引不匹配-这是不在数组中的最小值。这样最多可以进行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
这是一个O(N)
使用O(N)
空间的简单解决方案。我假设我们将输入列表限制为非负数,并且我们要查找列表中未包含的第一个非负数。
N
。N
布尔数组,初始化为全部false
。 X
列表中的每个数字,如果X
小于N
,则将X'th
数组的元素设置为true
。0
,寻找第一个元素false
。如果您false
在index处找到第一个I
,那么I
答案就是。否则(即当所有元素都存在时true
)答案是N
。实际上,“N
布尔数组”可能会被编码为表示为byte
或int
数组的“位图”或“位集” 。通常,这会使用较少的空间(取决于编程语言),并允许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[]
通过位图或通过位图实现此数组与常规解决方案无关。
正如其他答案中指出的那样,您可以进行排序,然后简单地向上扫描直到找到空白为止。
通过使用修改后的QuickSort,您可以将算法复杂度提高到O(N),并保留O(N)空间,在其中您可以消除那些不是包含间隙的潜在候选对象的分区。
这样可以节省大量计算。
为了说明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"
由于数字都是64位长,我们可以使用 基数排序对它们,即O(n)。对它们进行排序,然后对它们进行扫描,直到找到所需的内容。
如果最小数字为零,则向前扫描直到找到间隙。如果最小数字不为零,则答案为零。
对于节省空间的方法,所有值都是不同的,您可以在空间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
其中没有k个元素的第一个区域()count{h} = 1
h是你的答案U = maximum value in region h
L = minimum value in region h
这将在中运行O(log(N)*N)
。
U-L < k
我将对它们进行排序,然后遍历序列,直到找到一个间隙(包括零与第一个数字之间的起始间隙)。
在算法方面,可以这样做:
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占用的内存大得多,则可以创建所有可能的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")
对列表进行排序,查看第一个和第二个元素,然后开始向上直到出现间隙。
您可以在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),即使它要快得多。
感谢egon,swilden和Stephen C的启发。首先,我们知道目标值的界限,因为它不能大于列表的大小。同样,一个1GB的列表最多可以包含134217728(128 * 2 ^ 20)个64位整数。
散列部分
我建议使用散列来大大减少我们的搜索空间。首先,平方根为列表的大小。对于1GB的列表,则为N = 11,586。设置大小为N的整数数组。遍历列表,并将找到的每个数字的平方根*作为哈希。在哈希表中,增加该哈希的计数器。接下来,遍历哈希表。您发现的第一个不等于最大大小的存储桶定义了新的搜索空间。
位图部分
现在,设置一个与新搜索空间大小相等的常规位图,然后再次遍历源列表,在搜索空间中找到每个数字时填写位图。完成后,位图中的第一个未设置位将为您提供答案。
这将在O(n)时间和O(sqrt(n))空间中完成。
(*您可以使用移位之类的方法来更有效地执行此操作,并相应地更改存储桶的数量和大小。)
好吧,如果数字列表中只有一个缺失的数字,找到缺失数字的最简单方法是对序列求和,然后减去列表中的每个值。最终值是缺少的数字。
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 !!");
}
}
这是我用Java写的答案:
基本思想:1-遍历数组,丢弃重复的正数,零数和负数,同时对其余的数求和,获得最大的正数,并将唯一的正数保留在Map中。
2-计算总和为max *(max + 1)/ 2。
3-找到在步骤1和2计算得出的总和之间的差
4-再次从1循环到[sum sum,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;
}
正如Stephen C聪明地指出的那样,答案必须是小于数组长度的数字。然后,我将通过二进制搜索找到答案。这样可以优化最坏的情况(因此,面试官无法在“假设的情况”下抓住您)。在采访中,一定要指出您正在这样做,以针对最坏的情况进行优化。
使用二进制搜索的方法是从数组的每个元素中减去您要查找的数字,并检查是否为负数。
我不确定是否有这个问题。但是如果列表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)个连续整数的总和减去数字。
感谢您指出我正在考虑一些中间问题。
干得好蚂蚁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] <i或a [i] == a [a [i]],我们知道m是错误的输出,并且必须至少低一个元素。因此,递减m并将a [i]与a [m]交换就可以递归了。
如果这不是真的,而是a [i]> i,那么知道a [i]!= a [a [i]],我们知道将a [i]与a [a [i]]交换会增加元素数量在自己的位置。
否则,a [i]必须等于i,在这种情况下,我们可以知道已知直到该索引(包括该索引)的所有值都等于它们的索引,从而可以递增i。
这不能进入无限循环的证明留给读者练习。:)
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循环建立终止条件的结果。证明这一点的人是对工具更了解的人。
def solution(A):
index = 0
target = []
A = [x for x in A if x >=0]
if len(A) ==0:
return 1
maxi = max(A)
if maxi <= len(A):
maxi = len(A)
target = ['X' for x in range(maxi+1)]
for number in A:
target[number]= number
count = 1
while count < maxi+1:
if target[count] == 'X':
return count
count +=1
return target[count-1] + 1
得到了上述解决方案的100%。
1)过滤负数和零
2)排序/不同
3)访问数组
复杂度:O(N)或O(N * log(N))
使用Java8
public int solution(int[] A) {
int result = 1;
boolean found = false;
A = Arrays.stream(A).filter(x -> x > 0).sorted().distinct().toArray();
//System.out.println(Arrays.toString(A));
for (int i = 0; i < A.length; i++) {
result = i + 1;
if (result != A[i]) {
found = true;
break;
}
}
if (!found && result == A.length) {
//result is larger than max element in array
result++;
}
return result;
}
可以使用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;
}
使用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))
这可以帮助:
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))