找到列表中不存在的最小整数

时间:2009-10-19 03:44:46

标签: arrays algorithm

我的一位同事使用的一个有趣的访谈问题:

假设您有一个非常长的未排序的无符号64位整数列表。您如何找到列表中的最小非负整数?

后续行动:现在已经提出了通过排序的明显解决方案,你能否比O(n log n)更快地完成它?

FOLLOW-UP:您的算法必须在具有1GB内存的计算机上运行

澄清:列表在RAM中,但它可能会消耗大量的内容。你会提前给出列表的大小,比如N.

28 个答案:

答案 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)解决方案。我假设我们将输入列表限制为非负数,并且我们想要找到列表中没有的第一个非负数。

  1. 查找列表的长度;我们说它是N
  2. 分配一组N布尔值,初始化为所有false
  3. 对于列表中的每个号码X,如果X小于N,请将数组的X'th元素设置为true
  4. 从索引0开始扫描数组,查找第一个false元素。如果您在索引false找到第一个I,那么I就是答案。否则(即当所有元素都是true时),答案是N
  5. 实际上,“N布尔数组”可能会被编码为“位图”或“位集”,表示为byteint数组。这通常占用较少的空间(取决于编程语言),并允许更快地完成对第一个false的扫描。


    这就是算法运作的方式/原因。

    假设列表中的N数字不明显,或者其中一个或多个数字大于N。这意味着{em>至少在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))位。
    • 要确定数组大小,计数排序需要首先通过列表以确定XmaxXmin
    • 因此,最小的最坏情况空间要求为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 )中执行此操作。它节省空间,没有数据移动,所有操作都是基本的(增加减法)。

  1. 设置U = N; L=0
  2. 首先对k个区域中的数字空间进行分区。像这样:
    • 0->(1/k)*(U-L) + L0->(2/k)*(U-L) + L0->(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
  8. 使用散列可以改善这一点(感谢Nic这个想法)。

    1. 相同
    2. 首先对k个区域中的数字空间进行分区。像这样:
      • L + (i/k)->L + (i+1/k)*(U-L)
    3. inc count{j}使用j = (number - L)/k (if L < number < U)
    4. 找到其中没有k个元素的第一个区域(h
    5. 如果count{h} = 1 h是您的回答
    6. 设置U = maximum value in region h L = minimum value in region h
    7. 这将在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)个连续整数减去缺失数的总和。

感谢你指出我在脑海里放了一些中间件。