【问题标题】:Generate a new element different from 1000 elements of an array生成一个不同于数组的 1000 个元素的新元素
【发布时间】:2011-11-16 19:28:43
【问题描述】:

我在一次采访中被问到这个问题。考虑打孔卡的场景,其中每张打孔卡具有 64 位模式。有人建议我将每张卡片设为int,因为每个 int 都是位的集合。

另外,考虑到我有一个已经包含 1000 张此类卡片的数组。我每次都必须生成一个与之前的 1000 张卡片不同的新元素。数组中的整数(又名卡片)不一定是排序的。

更重要的是,这怎么可能是 C++ 的问题,64 bit int 来自哪里以及如何从要生成的元素不同于所有元素的数组中生成这张新卡已经存在在数组中?

【问题讨论】:

  • 也许我理解错了。每个新生成的数字都必须与之前的 all 不同,还是每次都删除第一个元素?在这种情况下,O(1000) = O(1) 的解可以简单地公式化。
  • @larsmans:要求新生成的整数与之前的所有整数不同。

标签: c++ arrays algorithm


【解决方案1】:

有 264 个 64 位整数,一个数不胜数 大于 1000,最简单的解决方案就是生成一个 随机 64 位数字,然后验证它不在表中 已经生成的数字。 (它的概率是 无穷小,但你不妨确定一下。)

由于大多数随机数生成器不生成 64 位值,因此您 剩下要么自己写,要么(更简单),结合 值,比如生成 8 个随机字节,然后 memcpying 将它们放入 uint64_t.

至于验证号码不存在,std::find 是 一两个新号码就好了;如果你必须做很多 查找,对表进行排序并使用二进制搜索将是 值得。或者某种哈希表。

【讨论】:

  • 但是在排序表中插入一个新元素需要 O(n) 时间,就像 std::find 一样,所以在这两种情况下你都被 O(n) 困住了。
  • @larsmans But O(n) 具有非常小的常数因子和良好的局部性通常优于具有较大的常数因子和不良局部性的 O(lg n)
  • @James 1000 在大量操作中并不是那么小。 lg 1000 是 10,所以在 O(n) 与 O(lg N) 竞争之前,你必须有一个相当大的常数
  • @quasiverse 请记住,O(n) 仅适用于插入,而不适用于查找。插入map 需要分配,这对于常数因子会产生非常非常大的差异。 (当然,如果性能真的成为问题,可以使用明显更好的解决方案。但它们更复杂,因此更容易出错。)
【解决方案2】:

我可能遗漏了一些东西,但大多数其他答案在我看来过于复杂。 只需对原始数组进行排序,然后从零开始计数:如果当前计数在数组中,则跳过它,否则您将获得下一个数字。这个算法是 O(n),其中 n 是新生成的数字的数量:排序数组和跳过现有数字都是常数。这是一个例子:

#include <algorithm>
#include <iostream>
unsigned array[] = { 98, 1, 24, 66, 20, 70, 6, 33, 5, 41 };

unsigned count = 0;
unsigned index = 0;

int main() {
  std::sort(array, array + 10);
  while ( count < 100 ) {
    if ( count > array[index] )
      ++index;
    else {
      if ( count < array[index] )
        std::cout << count << std::endl;
      ++count;
    }
  }
}

【讨论】:

  • 过于复杂?可能是。前几张卡更快?绝对。
  • @Mooing Duck:当然可以,但是为什么要对不存在的需求进行编码呢? OP 谈到“每次都有新号码......”,没有明确的性能要求,也没有说明将提取多少号码。实际上,即使只是将每个 64 位无符号数与数组中的所有元素进行比较,对于大多数用途来说也可能足够快。
  • 我对他们想要的东西比他们所说的更多的解释进行编码,因为他们所说的永远不是他们想要的。有一次我有一个家庭作业,我们必须编写一个函数,如果字符串参数大于 5 个字符,则该函数返回 true。遵循规范的答案:bool IsGreatFive(const char*) {return true;}
【解决方案3】:

这是一个O(n)算法:

int64 generateNewValue(list_of_cards)
{
    return find_max(list_of_cards)+1;
}

注意:正如@a​​mit 在下面指出的,如果INT64_MAX 已经在列表中,这将失败。

据我所知,这是获得O(n)的唯一方法。如果你想处理那个(相当重要的)边缘情况,那么你将不得不进行某种适当的排序或搜索,这会将你带到O(n log n)

【讨论】:

  • O(n^2) 最坏的情况,确实很慢:|
  • 您还可以跟踪最大值/最小值然后您确定如果您选择说 max+1 它是新的,然后将其更新为最大值。第一次会很慢,但在 O(1) 之后,虽然它要求在范围的顶部和底部实际上有足够的空间。
  • @Oli:是的。但他确实提到这是面试问题,我想每个面试问题实际上都在隐含地说“可以多快....完成?[以及如何?]”
  • @Oli:我当然想到了这一点,但没有给出这个答案,因为一次又一次地循环超过 1000 个元素肯定不是一个好的答案。
  • 如果 max==MAX_INT_64 和 MIN_INT_64 也在列表中,此 [更新的解决方案] 将失败。
【解决方案4】:

@arne 快到了。你需要的是一个self-balancinginterval tree,它可以在 O(n lg n) 时间内构建。

然后取顶部节点,它会存储一些区间[i, j]。根据区间树的属性,i-1 和 j+1 都是新键的有效候选者,除非 i = @987654323 @ 或 j = UINT64_MAX。如果两者都是真的,那么你已经存储了 2^64 个元素并且你不可能生成一个新元素。存储新元素,这需要 O(lg n) 最坏情况时间。

即:初始化需要 O(n lg n),生成需要 O(lg n)。两者都是最坏情况的数字。这种方法的最大优点是顶部节点将保持“增长”(存储更大的间隔)并与其后继或前任合并,因此树实际上会在内存使用方面缩小,最终每次操作的时间衰减到 O(1)。您也不会浪费任何数字,因此您可以继续生成,直到获得 2^64 个。

【讨论】:

  • 不错!这应该比我的树快得多,尤其是当现有元素的数量增加时。 +1
  • 您应该能够从排序的 std::deque 范围而不是树中获得更好的性能。稍微复杂一点,但更好的局部性(更快),更少的内存(更快),没有平衡(更快),之后的生成是 O(1)。但是“范围”概念是一个很棒的想法。
  • 我刚刚意识到范围概念也使得返回 ANY 随机未使用的数字变得微不足道,而不是像其他人一样增加/减少。 (它们不会均匀分布,并且会大大增加内存使用量,直到您生成大量数据)
  • 忘记了 std::deque 的 O(n) 插入,所以没关系,除非你可以像其他人一样只在开头和结尾生成数字。
【解决方案5】:

这个算法有O(N lg N)初始化,O(1)查询和O(N)内存使用。我假设你有一些整数类型,我将其称为int64,它可以表示整数[0, int64_max]

  1. 对数字进行排序
  2. 创建一个包含区间[u, v]的链表
  3. 插入[1, first number - 1]
  4. 对于每个剩余的数字,插入[prev number + 1, current number - 1]
  5. 插入[last number + 1, int64_max]

您现在有一个表示未使用数字的列表。您可以简单地对其进行迭代以生成新数字。

【讨论】:

  • “对数字进行排序”意味着至少 O(lg n) 个临时空间(或 O(n²) 时间)。区间的链表占用 O(n) 空间。但实际上,生成操作需要 O(1) 时间。
  • @larsmans 不,因为 N 固定在 1000 ......但我确实说过 O(N lg N) 时间进行排序。也许我有双重标准……我会编辑它。
【解决方案6】:

我认为要走的路是使用某种散列。因此,您可以根据 MOD 操作将卡片存储在一些存储桶中。在您创建某种索引之前,您会被困在整个数组上的循环中。

如果你看过 java 中的 HashSet 实现,你可能会得到一个线索。

编辑:我假设你希望它们是随机数,如果你不介意下面的序列 MAX+1 是一个很好的解决方案:)

【讨论】:

  • OP 如何在哈希表中找到不存在的元素?
  • 您生成一个数字,从散列函数中获取结果并确定卡是否已经存在,您需要搜索单个存储桶而不是整个数组
  • 我发现很难估计此操作的预期时间,但查找的最坏情况已经是 O(n)(当所有整数碰巧映射到相同的哈希值时)。
  • 这就是哈希函数的工作原理,对吧 :) 在最坏的情况下,您最终会得到 O(n),但如果您的卡片是随机数,这将不常见。您可以争辩说,在最坏的情况下,您永远找不到新卡,因为您的随机数生成器将始终返回“5”,并且由于已经有“5”,因此您被卡住了。请注意,我假设(如我的回答中所述)他应该选择尚未在列表中的新随机卡 - 所以任务实际上是:“我有随机数。这已经在我的集合中了吗?”
  • 这种方法从预期的 O(1) 到预期的 O(n) 并随着表的增长而占用 Theta(n) 内存。我的从最坏情况 O(lg n) 到预期的 O(1),时间 内存。不过,在某些情况下,您的可能会更快,因此每个应用程序都需要权衡。
【解决方案7】:

您可以构建一个已经存在的元素的二叉树并遍历它,直到找到一个深度不是 64 且子节点少于两个的节点。然后,您可以构造一个“缺失”的子节点并拥有一个新元素。如果我没记错的话,应该相当快,大约 O(n)。

【讨论】:

  • 构建 n 个元素的二叉树需要 O(n lg n) 时间。此外,具有
  • 构建平衡二叉树不是 O(n)。
  • 只是想建议一下。仅仅因为他得到数字时没有排序,并不意味着他不能自己排序。
  • 如何构造一个新的子节点,同时保证它是唯一的?
  • 它是一棵二叉树,使用整数的位作为节点值。因此,如果我们找到一个深度为 63 且只有一个子节点(例如值为 0)的节点 a,我们可以通过将 1 附加到节点 a 的路径的位表示来创建新的子节点。如果我们已经遇到了以这种方式生成的数字,那么叶子就已经存在了。这与叶子不存在的初始假设相矛盾。
【解决方案8】:
bool seen[1001] = { false };
for each element of the original array
    if the element is in the range 0..1000
        seen[element] = true
find the index for the first false value in seen

【讨论】:

  • 是的 - 这比我的类似解决方案更快,但使用了整数数组。您可以在布尔数组“用完”时调整范围,以便添加更多值。重置您的布尔数组(memset)将比我使用递增值的循环重新填充更快。此外,如所列,我的设计有一个致命缺陷,因为我将列表中的第一个值用作“无效”标志,因此它不能包含在无效扫描中。尽管如此,我们都想到了从 0 开始并努力工作的想法..
【解决方案9】:

初始化: 不要对列表进行排序。 创建一个包含 0..999 的 1000 长的新数组。 迭代列表,如果任何数字在 0..999 范围内,则通过将新数组中的值替换为列表中第一项的值来使其在新数组中无效。

插入: 对新数组使用递增索引。如果新数组中此索引处的值不是列表中第一个元素的值,则将其添加到列表中,否则检查新数组中下一个位置的值。 当新数组用完时,使用 1000..1999 重新填充它并如上所述使现有值无效。是的,这是对列表的循环,但不必每次插入都这样做。

接近 O(1) 直到列表变得如此之大以至于偶尔迭代它以使“新”新数组失效变得很重要。也许你可以通过使用一个不断增长的新数组来缓解这种情况,也许总是列表的大小?

Rgds, 马丁

【讨论】:

    【解决方案10】:

    将它们全部放入大小> 1000的哈希表中,并找到空单元格(这就是停车问题)。为此生成一个密钥。这对于更大的桌子当然会更好。该表只需要 1 位条目。

    编辑:这是鸽巢原则。 这需要哈希函数的“模表大小”(或其他一些“半可逆”函数)。

    unsigned hashtab[1001] = {0,};
    unsigned long long long long numbers[1000] = { ... };
    
    void init (void)
    {
    unsigned idx;
    
    for (idx=0; idx < 1000; idx++) {
        hashtab [ numbers[idx] % 1001 ] += 1; }
    
    }
    
    unsigned long long long long generate(void)
    {
    unsigned idx;
    
    for (idx = 0; idx < 1001; idx++) {
        if ( !hashtab [ idx] ) break;  }
    
    return idx + rand() * 1001;
    }
    

    【讨论】:

    • 糟糕,我发现我刚刚重新发明了 Keith Thompson 的方法。
    【解决方案11】:

    基于这里的解决方案:question on array and number

    由于有 1000 个数字,如果我们考虑它们的余数为 1001,则至少会丢失一个余数。我们可以选择它作为我们缺失的号码。

    所以我们维护一个计数数组:C[1001],它将保持 C[r] 中余数为 r(除以 1001)的整数个数。

    我们还维护了一组 C[j] 为 0 的数字(比如使用链表)。

    当我们移动窗口时,我们减少第一个元素的计数(比如余数 i),即减少 C[i]。如果 C[i] 变为零,我们将 i 添加到数字集合中。我们用我们添加的新数字更新 C 数组。

    如果我们需要一个数字,我们只需从 j 的集合中选择一个 C[j] 为 0 的随机元素。

    对于新数字,这是 O(1),最初是 O(n)。

    这与其他解决方案类似,但不完全一样。

    【讨论】:

      【解决方案12】:

      像这样简单的东西怎么样:

      1) 将数组划分为小于等于 1000 及以上的数字

      2) 如果所有数字都适合下分区,则选择 1001(或任何大于 1000 的数字),我们就完成了。

      3) 否则,我们知道必须存在一个介于 1 和 1000 之间的数字,而该数字在较低的分区中是不存在的。

      4) 创建一个 1000 个元素的布尔数组,或一个 1000 个元素的长位域,或诸如此类的东西,并将该数组初始化为全 0

      5) 对于下分区中的每个整数,将其值作为数组/位域的索引,并将相应的布尔值设置为 true(即:进行基数排序)

      6) 遍历数组/位域并选择任何未设置值的索引作为解决方案

      这在 O(n) 时间内有效,或者因为我们已经将所有内容限定为 1000,技术上它是 O(1),但通常是 O(n) 时间和空间。对数据进行了 3 次遍历,这不一定是最优雅的方法,但复杂度仍然是 O(n)。

      【讨论】:

        【解决方案13】:

        您可以使用原始数组中没有的数字创建一个新数组,然后从这个新数组中选择一个。

        ¿O(1)?

        【讨论】:

        • 是的,O(2^64) = O(1),在微不足道的意义上。
        • 您只需要创建 1 次数组并根据需要使用尽可能多的次数,如果您每次选择一个都删除数字。而且只有 1000 张牌,大牌,是的,但只有 1000 张
        • 忘记了,我认为这 1000 张卡是预先确定的。对不起
        猜你喜欢
        • 2012-05-28
        • 2021-12-06
        • 2010-12-02
        • 1970-01-01
        • 1970-01-01
        • 2021-12-25
        • 2017-03-23
        • 2017-03-23
        • 1970-01-01
        相关资源
        最近更新 更多