我正在尝试解决这个采访问题:给定一个唯一的正整数数组,找到要插入其中的最小数,以便每个整数仍然唯一。该算法应为O(n),并且额外的空间复杂度应为常数。允许将数组中的值分配给其他整数。
例如,对于数组[5, 3, 2, 7]
,输出应为1。但是对于[5, 3, 2, 7, 1]
,答案应为4。
我的第一个想法是对数组进行排序,然后再次遍历数组以查找连续序列的中断点,但是排序需要的比O(n)还多。
任何想法将不胜感激!
答案 0 :(得分:4)
从问题描述中可以看出:“允许将数组中的值分配给其他整数。” 这是O(n)空间,不是常数。
遍历数组,并将A[ |A[i]| - 1 ]
的{{1}}乘以-1。第二次循环,并为第一个单元格输出非负数或数组长度+1的输出(索引+1)(如果它们都已标记)。这利用了以下事实:数组中的唯一整数不能超过(数组长度)。
答案 1 :(得分:4)
我的尝试
假设数组A
是1索引的。我们将一个 active 值称为非零且不超过n
的值。
扫描数组,直到找到一个有效值,让A[i] = k
(如果找不到,请停止);
A[k]
处于活动状态
A[k]
的同时将k
移至A[k]
; 从i
继续直到到达数组末尾。
通过此操作后,将清除与数组中某个整数对应的所有数组条目。
例如
[5, 3, 2, 7], clear A[3]
[5, 3, 0, 7], clear A[2]
[5, 0, 0, 7], done
答案是1
。
例如
[5, 3, 2, 7, 1], clear A[5],
[5, 3, 2, 7, 0], clear A[1]
[0, 3, 2, 7, 0], clear A[3],
[0, 3, 0, 7, 0], clear A[2],
[0, 0, 0, 7, 0], done
答案是4
。
第一遍的行为是线性的,因为每个数字都被同时查看(并立即清除),并且i
有规律地增加。
第二遍是线性搜索。
A= [5, 3, 2, 7, 1]
N= len(A)
print(A)
for i in range(N):
k= A[i]
while k > 0 and k <= N:
A[k-1], k = 0, A[k-1] # -1 for 0-based indexing
print(A)
[5, 3, 2, 7, 1]
[5, 3, 2, 7, 0]
[0, 3, 2, 7, 0]
[0, 3, 2, 7, 0]
[0, 3, 0, 7, 0]
[0, 0, 0, 7, 0]
[0, 0, 0, 7, 0]
更新:
基于גלעדברקן的想法,我们可以以不破坏值的方式标记数组元素。然后,您报告第一个未标记的索引。
print(A)
for a in A:
a= abs(a)
if a <= N:
A[a-1]= - A[a-1] # -1 for 0-based indexing
print(A)
[5, 3, 2, 7, 1]
[5, 3, 2, 7, -1]
[5, 3, -2, 7, -1]
[5, -3, -2, 7, -1]
[5, -3, -2, 7, -1]
[-5, -3, -2, 7, -1]
答案 2 :(得分:1)
我将使用基于1的索引。
这个想法是重用输入集合,如果当前位置大于i
,则安排在第i个位置交换整数i
。可以在O(n)中执行。
然后在第二次迭代中,找到不包含i
的第一个索引i
,它也是O(n)。
在Smalltalk中,以数组(本身就是数组)实现:
firstMissing
self size to: 1 by: -1 do: [:i |
[(self at: i) < i] whileTrue: [self swap: i with: (self at: i)]].
1 to: self size do: [:i |
(self at: i) = i ifFalse: [^i]].
^self size + 1
因此,在O(n)中有两个循环,但在第一个循环(whileTrue:
)中也有另一个循环。那么第一个循环真的是O(n)吗?
是的,因为每个元素最多可以交换一次,因为它们将到达正确的位置。我们看到交换的累计数量受数组大小限制,并且第一个循环的总成本最多为2 * n,包括最后一个座位的总成本最多为3 * n,仍然是O(n)。
您还看到我们不在乎交换(self at: i) > i and: [(self at:i) <= self size]
的大小写,为什么?因为我们确定在这种情况下将缺少较小的元素。
一个小测试用例:
| trial |
trial := (1 to: 100100) asArray shuffled first: 100000.
self assert: trial copy firstMissing = trial sorted firstMissing.
答案 3 :(得分: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))
答案 4 :(得分:0)
要点:
您搜索的值将是
所有大于N的项目都可以忽略
项目是唯一的,因此每个小于或等于N的项目都有其唯一的位置。
算法:
遍历数组
如果a [i]> N替换为-1,则继续i + 1。
如果a [i] == i或a [i] == -1,则继续i + 1
如果a [i]!=我将a [i]与a [a [i]]交换并且不递增i(这将该项放在正确的位置)。
现在再次遍历数组并搜索第一个-1。
复杂度为O(N),因为在每一步中,您都将一项放置在其位置。
答案 5 :(得分:0)
您可以执行以下操作。
不过,我不确定这可能不是线性时间。
答案 6 :(得分:0)
编辑:您应该使用候选对象加上一半的输入大小作为枢纽,以减少此处的常数因子-请参见Daniel Schepler的评论-但我还没有时间在示例代码中使它起作用。
这不是最佳选择-正在寻找聪明解决方案-但这足以满足标准:)
我认为这可行...?
'use strict';
const swap = (arr, i, j) => {
[arr[i], arr[j]] = [arr[j], arr[i]];
};
// dummy pivot selection, because this part isn’t important
const selectPivot = (arr, start, end) =>
start + Math.floor(Math.random() * (end - start));
const partition = (arr, start, end) => {
let mid = selectPivot(arr, start, end);
const pivot = arr[mid];
swap(arr, mid, start);
mid = start;
for (let i = start + 1; i < end; i++) {
if (arr[i] < pivot) {
mid++;
swap(arr, i, mid);
}
}
swap(arr, mid, start);
return mid;
};
const findMissing = arr => {
let candidate = 1;
let start = 0;
let end = arr.length;
for (;;) {
if (start === end) {
return candidate;
}
const pivotIndex = partition(arr, start, end);
const pivot = arr[pivotIndex];
if (pivotIndex + 1 < pivot) {
end = pivotIndex;
} else {
//assert(pivotIndex + 1 === pivot);
candidate = pivot + 1;
start = pivotIndex + 1;
}
}
};
const createTestCase = (size, max) => {
if (max < size) {
throw new Error('size must be < max');
}
const arr = Array.from({length: max}, (_, i) => i + 1);
const expectedIndex = Math.floor(Math.random() * size);
arr.splice(expectedIndex, 1 + Math.floor(Math.random() * (max - size - 1)));
for (let i = 0; i < size; i++) {
let j = i + Math.floor(Math.random() * (size - i));
swap(arr, i, j);
}
return {
input: arr.slice(0, size),
expected: expectedIndex + 1,
};
};
for (let i = 0; i < 5; i++) {
const test = createTestCase(1000, 1024);
console.log(findMissing(test.input), test.expected);
}
答案 7 :(得分:0)
我几乎是靠自己找到了正确的方法,但是我必须进行搜索,然后在这里找到它:https://www.geeksforgeeks.org/find-the-smallest-positive-number-missing-from-an-unsorted-array/
注意:此方法对原始数据具有破坏性
原始问题中没有任何内容表明您不会造成破坏。
我将解释您现在需要做什么。
这里的基本“ aha”是第一个缺失数字必须在前N个正数之内,其中N是数组的长度。
一旦您理解了这一点,并意识到可以将数组本身中的值用作标记,则只需要解决一个问题:数组中的数字是否小于1?如果是这样,我们需要处理它们。
处理0或负数可以在O(n)时间内完成。获取两个整数,一个为我们的当前值,另一个为数组末尾。扫描时,如果找到0或负数,则使用第三个整数与数组中的最终值执行交换。然后我们递减数组指针的结尾。我们继续直到当前指针超出数组指针的末尾为止。
代码示例:
while (list[end] < 1) {
end--;
}
while (cur< end) {
if (n < 1) {
swap(list[cur], list[end]);
while (list[end] < 1) {
end--;
}
}
}
现在我们有了数组的末尾,还有一个被截断的数组。在这里,我们需要了解如何使用数组本身。由于我们关心的所有数字都是正数,并且我们有一个指针指向存在多少个数字的位置,因此,如果数组中存在一个数字,我们可以简单地将数字乘以-1即可将该位置标记为存在。
例如[5,3,2,7,1]当我们读3时,我们将其更改为[5,3,-2,7,1]
代码示例:
for (cur = 0; cur <= end; begin++) {
if (!(abs(list[cur]) > end)) {
list[abs(list[cur]) - 1] *= -1;
}
}
现在,请注意:您需要读取该位置的整数的绝对值,因为它可能会更改为负数。还要注意,如果一个整数大于列表指针的末尾,则不要更改任何内容,因为该整数将无关紧要。
最后,一旦您读取了所有正值,就遍历它们以查找当前为正的第一个值。这个地方代表您的第一个失踪号码。
Step 1: Segregate 0 and negative numbers from your list to the right. O(n)
Step 2: Using the end of list pointer iterate through the entire list marking
relevant positions negative. O(n-k)
Step 3: Scan the numbers for the position of the first non-negative number. O(n-k)
Space Complexity: The original list is not counted, I used 3 integers beyond that. So
it is O(1)
我要提到的一件事是列表[5,4,2,1,3]会以[-5,-4,-2,-1,-3]结尾,因此在这种情况下,您可以选择列表末尾位置的第一个数字,或结果为6。
第3步的代码示例
for (cur = 0; cur < end; cur++) {
if (list[cur] > 0) {
break;
}
}
print(cur);