将最小可能的正整数插入唯一整数数组

时间:2019-06-10 12:24:44

标签: arrays algorithm big-o

我正在尝试解决这个采访问题:给定一个唯一的正整数数组,找到要插入其中的最小数,以便每个整数仍然唯一。该算法应为O(n),并且额外的空间复杂度应为常数。允许将数组中的值分配给其他整数。

例如,对于数组[5, 3, 2, 7],输出应为1。但是对于[5, 3, 2, 7, 1],答案应为4。

我的第一个想法是对数组进行排序,然后再次遍历数组以查找连续序列的中断点,但是排序需要的比O(n)还多。

任何想法将不胜感激!

8 个答案:

答案 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)

您可以执行以下操作。

  • 找到最大值(m),所有元素的总和,元素数(n)
  • 缺少m-n个元素,它们的和为q = sum(1..m)-s-对和有一个封闭形式的解决方案
  • 如果只缺少一个整数,就可以了-报告q
  • 如果缺少多个(m-n),您将意识到丢失的整数之和为q,并且其中至少一个小于q /(m-n)
  • 从顶部开始,除了只考虑小于q /(m-n)的整数-这将是新的m,只有低于该最大值的元素才影响新的s和n。这样做直到您只剩下一个缺失的整数。

不过,我不确定这可能不是线性时间。

答案 6 :(得分:0)

编辑:您应该使用候选对象加上一半的输入大小作为枢纽,以减少此处的常数因子-请参见Daniel Schepler的评论-但我还没有时间在示例代码中使它起作用。

这不是最佳选择-正在寻找聪明解决方案-但这足以满足标准:)

  1. 定义到目前为止可能的最小候选对象:1。
  2. 如果输入的大小为0,则可能的最小候选为有效候选,因此将其返回。
  3. 将输入划分为 pivot(具有中值枢纽的中位数,如quicksort)。
  4. 如果≤轴的大小小于轴本身,则其中有一个自由值,因此从第2步开始,仅考虑<轴分区。
  5. 否则(当它是=轴心时),新的最小候选对象是轴心+1。从第2步开始,仅考虑>轴心分区。

我认为这可行...?

'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);