【问题标题】:Fastest way to find smallest missing integer from list of integers从整数列表中找到最小缺失整数的最快方法
【发布时间】:2019-04-27 06:06:15
【问题描述】:

我有一个包含 100 个随机整数的列表。每个随机整数都有一个从 0 到 99 的值。允许重复,因此列表可能类似于

56, 1, 1, 1, 1, 0, 2, 6, 99...

我需要找到列表中包含的最小整数(>= 0)。

我最初的解决方案是这样的:

vector<int> integerList(100); //list of random integers
...
vector<bool> listedIntegers(101, false);
for (int theInt : integerList)
{
    listedIntegers[theInt] = true;
}
int smallestInt;
for (int j = 0; j < 101; j++)
{
    if (!listedIntegers[j])
    {
        smallestInt = j;
        break;
    }
}

但这需要一个用于记账的辅助数组和第二个(可能是完整的)列表迭代。我需要执行这个任务数百万次(实际应用程序是在一个贪婪的图形着色算法中,我需要使用顶点邻接列表找到最小的未使用颜色值),所以我想知道是否有一个聪明的方法来获得没有那么多开销也能得到同样的结果?

【问题讨论】:

  • 有错别字吗? O(2 * n)O(n) 相同。
  • 糟糕,你是对的。
  • 您必须为您的特定任务修改和优化优先级来实现堆。
  • 由于您希望此任务执行很多次,您可以利用以前的迭代(因此您可能需要指定它的工作方式)。但是在一个孤立的迭代中,恐怕你找不到比你的算法更好的东西了,尤其是对于这么小的列表(如果你总是只有 100 个整数)。好吧,除了不使用vector&lt;bool&gt;
  • 如果只有 100 个整数,确定最快的方法将归结为基准测试和微优化。从理论上的渐近角度来看,你所拥有的代码是解决问题的最快方法。

标签: c++ arrays algorithm vector time-complexity


【解决方案1】:

我相信没有更快的方法可以做到这一点。你可以做的是重用vector&lt;bool&gt;,每个线程只需要一个这样的向量。

虽然更好的方法可能是重新考虑整个算法以完全消除这一步。也许您可以在算法的每一步更新最少未使用的颜色?

【讨论】:

    【解决方案2】:

    既然你无论如何都要扫描整个列表,那么你的算法已经很不错了。我可以建议的唯一改进(这肯定会加快速度)是摆脱您的vector&lt;bool&gt;,并将其替换为堆栈分配的 4 个 32 位整数或 2 个 64 位整数数组。

    这样你就不必每次都在堆上分配数组的成本,而且你可以更快地得到第一个未使用的数字(第一个 0 位的位置)。要找到包含第一个 0 位的单词,您只需要找到第一个不是最大值的单词,并且可以使用一些 bit twiddling hack 来快速获取该单词中的第一个 0 位。

    【讨论】:

    • 我想过简单地标记无符号整数中的位,但不确定如何在不迭代这些整数的情况下找到第一个 0 位。有更快的方法吗?
    • @Tyson 您必须遍历整数,但其中只有 4 个!然后,要找到x 中的第一个 0 位,您只需要使用以下众多方法之一找到~x 中的第一个 1 位:stackoverflow.com/questions/757059/…
    【解决方案3】:

    你的程序已经非常高效了,在 O(n) 中。只能找到边际收益。 一种可能性是将可能值的数量划分为大小为block 的块,然后注册 不是在 bool 数组中,而是在 int 数组中,在这种情况下,记住取模 block 的值。
    在实践中,我们将大小为 N 的循环替换为大小为 N/block 的循环加上大小为 block 的循环。
    理论上,我们可以选择block = sqrt(N) = 12,以尽量减少N/block + block的数量。
    在以后的程序中,选择大小为 8 的块,假设整数除以 8 并计算模 8 的值应该很快。
    然而,很明显,如果有增益的话,只有相当大的最小值才能获得!

    constexpr int N = 100;
    int find_min1 (const std::vector<int> &IntegerList) {
        constexpr int Size = 13;    //N / block
        constexpr int block = 8;
        constexpr int Vmax = 255;   // 2^block - 1
    
        int listedBlocks[Size] = {0};
        for (int theInt : IntegerList) {
            listedBlocks[theInt / block] |= 1 << (theInt % block);
        }
        for (int j = 0; j < Size; j++) {
            if (listedBlocks[j] == Vmax) continue;
            int &k = listedBlocks[j];
            for (int b = 0; b < block; b++) {
                if ((k%2) == 0) return block * j + b;
                k /= 2;
            }
        }
        return -1;
    }
    

    【讨论】:

      【解决方案4】:

      可能您可以通过使用一些位操作将最后一步减少到 O(1),在您的情况下为 __int128,在循环一中设置相应的位并调用类似 __builtin_clz 或使用适当的 bit hack

      【讨论】:

        【解决方案5】:

        我能找到的从集合中找到最小整数的最佳解决方案是https://codereview.stackexchange.com/a/179042/31480

        这里是c++版本。

        int solution(std::vector<int>& A)
        {
            for (std::vector<int>::size_type i = 0; i != A.size(); i++) 
            {
                while (0 < A[i] && A[i] - 1 < A.size()
                    && A[i] != i + 1
                    && A[i] != A[A[i] - 1]) 
                {
                    int j = A[i] - 1;
        
                    auto tmp = A[i];
                    A[i] = A[j];
                    A[j] = tmp;
                }
            }
        
            for (std::vector<int>::size_type i = 0; i != A.size(); i++)
            {
                if (A[i] != i+1)
                {
                    return i + 1;
                }
            }
            return A.size() + 1;
        }
        

        【讨论】:

          【解决方案6】:

          一年过去了,但是……

          想到的一个想法是在迭代列表时跟踪未使用值的间隔。例如,为了实现高效查找,您可以将区间作为元组保留在二叉搜索树中。

          因此,使用您的示例数据:

          56, 1, 1, 1, 1, 0, 2, 6, 99...

          您最初将拥有未使用的区间 [0..99],然后,随着每个输入值的处理:

          56: [0..55][57..99]
          1: [0..0][2..55][57..99]
          1: no change
          1: no change
          1: no change
          0: [2..55][57..99]
          2: [3..55][57..99]
          6: [3..5][7..55][57..99]
          99: [3..5][7..55][57..98]
          

          结果(最低剩余间隔中的最低值):3

          【讨论】:

          • 鉴于列表中每个数字的大量内存写入,我看不出这将如何更快。但是,如果可能的数字范围比问题中的多得多,那么您的方法显然可以节省内存。
          • 我基本同意你的分析。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2014-01-31
          • 2018-02-10
          • 1970-01-01
          • 1970-01-01
          • 2015-07-03
          • 2011-02-07
          • 1970-01-01
          相关资源
          最近更新 更多