【问题标题】:Extract first N unique integers from an Array从数组中提取前 N 个唯一整数
【发布时间】:2010-10-14 13:41:41
【问题描述】:

我有一个很大的整数列表(数千个),我想从中提取前 N 个(大约 10-20 个)唯一元素。列表中的每个整数大约出现 3 次。

为此编写一个算法是微不足道的,但我想知道什么是速度和内存效率最高的方法。

在我的案例中还有一些额外的限制和信息:

  • 在我的用例中,我在数组上多次提取我的唯一值,每次都从头开始跳过一些元素。我跳过的元素数量在唯一提取期间是未知的。我什至没有上限。因此排序效率不高(我必须保留数组的顺序)。

  • 整数到处都是,所以位数组作为查找解决方案是不可行的。

  • 我想不惜一切代价避免在搜索期间进行临时分配。

我目前的解决方案大致如下:

  int num_uniques = 0;
  int uniques[16];
  int startpos = 0;

  while ((num_uniques != N) && (start_pos < array_length))
  {
    // a temporary used later.
    int insert_position;

    // Get next element.
    int element = array[startpos++];

    // check if the element exist. If the element is not found
    // return the position where it could be inserted while keeping
    // the array sorted.

    if (!binary_search (uniques, element, num_uniques, &insert_position))
    {

      // insert the new unique element while preserving 
      // the order of the array.

      insert_into_array (uniques, element, insert_position);

      uniques++;
    }
  }

binary_search / insert into array 算法完成了工作,但性能不是很好。 insert_into_array 调用会大量移动元素,这会减慢一切。

有什么想法吗?


编辑

很好的答案,伙计们!每个人都应该得到一个公认的答案,但我只能给出一个。我将实现你的一些想法,并使用一些典型数据进行性能测试。具有导致最快实施的想法的人得到了公认的答案。

我将在现代 PC 和嵌入式 CortexA8-CPU 上运行代码,并以某种方式对结果进行加权。也将发布结果。


编辑:点球大战的结果

Core-Duo 上的时序,在 160kb 测试数据集上进行 100 次迭代。

Bruteforce (Pete):            203 ticks
Hash and Bruteforce (Antti):  219 ticks
Inplace Binary Tree (Steven): 390 ticks
Binary-Search (Nils):         438 ticks

http://torus.untergrund.net/code/unique_search_shootout.zip(C 源和测试数据)

补充说明:

  • 就地二叉树绝对适合真正的随机分布(我的测试数据有上升的趋势)。

  • 二分搜索在我的测试数据上运行良好,超过 32 个唯一值。它的执行几乎是线性的。

【问题讨论】:

  • 是否必须将找到的元素保持在原来的顺序?
  • 您是否认为需要处理超出起始位置的唯一值不足并且您需要再次换行到源开头的情况? (例如,如果您是随机选择起点,或者来源可能有大量重复(您没有指定))
  • 找到的元素的顺序不是问题。
  • 我不想回绕,以防我找不到足够的唯一性。在这种情况下,我对少于 N 个唯一身份感到满意。
  • 所以你让我们都悬而未决! :)

标签: algorithm sorting search


【解决方案1】:

给定一个名为 L 的大小为 N 的整数列表

迭代L一次,找出数组中的最大值和最小值。

分配(1 次分配)一个名为 A 的大小为(小 .. 大)的整数数组。 将此数组初始化为零

迭代L,使用L(i)下标A,增加那里找到的整数。

然后进行处理。在 L 中选择您的起点,然后在列表中向前走,查看 A(i)。选择你想要的 A(i) > 2 的集合。

完成后,丢弃 A。

如果你真的空间不足,请使用 2 位而不是整数,解释如下

00 count = 0
01 count = 1
10 count = 2
11 count > 2

【讨论】:

  • +0。如果被扫描的整数空间不比 N 大很多,这是一个好主意,但提问者明确表示整数“无处不在,因此作为查找解决方案的位数组是不可行的。”
  • 好的。为什么不可行?假设 N 是 2^32,那么这个位数很容易适合虚拟内存。
【解决方案2】:

我会尝试对不平衡二叉树中的唯一性进行排序。这将为您节省重新排列唯一列表的成本,并且如果源列表足够随机,则插入到树中的内容不会严重失衡。 (并且您可以使用二叉树一次性完成搜索并插入所有内容。)如果它确实变得不平衡,那么最坏的情况将与迭代 16 元素列表而不是进行二分查找。

您知道二叉树的最大大小,因此您可以提前预分配所有必要的内存,这应该不是问题。您甚至可以使用“我的节点内存不足”条件来让您知道何时完成。

(编辑:显然人们认为我在这里提倡使用异常。我不是。我可能提倡实际常见的 lisp 样式条件,但不是大多数语言中发现的转义继续样式异常。此外,它看起来就像他想为此做C一样。)

【讨论】:

  • 抱歉,使用异常进行流控制不是一个好主意。否则,很好的答案。
  • 如果我对 Jay 的理解正确,那也不例外,因为内存是预先分配的。它只会让树检测到它没有更多的空槽。您当然可以将其实现为抛出异常,但正如您所说,为此目的是不明智的。
  • 我认为他会在 C 中执行此操作,它没有例外。 if(nextindex>memoryslots) { return(ALL_SIXTEEN_FOUND); } ...或其他。
  • 是的,C 或 C++,应该是这样。自己做比较;不要使用期望。您的回答听起来就是您的建议;如果您明确表示您不建议使用异常进行流控制,我会支持您。
【解决方案3】:

如果您有数千个整数并且每个整数大约出现 3 次,您的算法应该很快找到 N 个唯一整数的集合,对于小 e 大致在 N(1+e) 步内(假设整数的顺序相对随机)。

这意味着您的算法会将 N 次随机整数插入到 uniques 数组中。插入数字 K 将平均移动数组中的 K/2 个元素,产生 (N^2)/4 个移动操作。您的二分搜索大约需要 N * (log(N)-1) 步。这将为您的算法产生 (N^2)/4 + N(log(N)-1) + N(1+e) 的总复杂度。

我认为你可以做得更好,例如通过以下方式:

int num_uniques = 0, startpos = 0, k, element;
int uniques[16];

/* Allocate and clear a bit table of 32 * 32 = 1024 bits. */
uint32 bit_table[32], hash;
memzero((void *)(&bit_table), sizeof(bit_table));

while (num_uniques < N && startpos < array_length) {
  element = array[startpos++];

  /* Hash the element quickly to a number from 0..1023 */
  hash = element ^ (element >> 16);
  hash *= 0x19191919;
  hash >>= 22;
  hash &= 1023;

  /* Map the hash value to a bit in the bit table.
     Use the low 5 bits of 'hash' to index bit_table
     and the other 5 bits to get the actual bit. */
  uint32 slot=hash & 31;
  uint32 bit=(1u << (hash >> 5));

  /* If the bit is NOT set, this is element is guaranteed unique. */
  if (!(bit_table[slot] & bit)) {
    bit_table[slot] |= bit;
    uniques[num_uniques++] = element;
  } else { /* Otherwise it can be still unique with probability
              num_uniques / 1024. */
    for (k=0; k<num_uniques; k++) { if (uniques[k] == element) break }
    if (k==num_uniques) uniques[num_uniques++] = element;
  }
}

该算法将在 N + N^2 / 128 的预期时间内运行,因为运行内部循环(索引变量 k)的概率很低。

【讨论】:

  • e代表什么数量?
  • j_random:这只是一个小数字,对应于在找到 N 个唯一值之前列表中有重复数字的概率。
【解决方案4】:

使用二叉树的数组表示。数组的大小可以是 3N。基本上

arr[i] = 值

arr[i+1] = 左子数组索引

arr[i+2] = 右子数组索引

遍历每个 k 插入的“树”,如果未找到 k,则更新其父级的 [i+1] 或 [i+2] 并将其添加到下一个空索引。当数组中的空间用完时,您就有了答案。

例如

找到 42243123 的前 3 个唯一值:数组大小=3 * 3 = 9。

在下表中,“v”是值,“l”是左子索引,“r”是右子索引。

 v  l  r  v  l  r  v  l  r
 _________________________
-1 -1 -1 -1 -1 -1 -1 -1 -1
 4 -1 -1 -1 -1 -1 -1 -1 -1
 4  3 -1  2 -1 -1 -1 -1 -1
 4  3 -1  2 -1 -1 -1 -1 -1
 4  3 -1  2 -1 -1 -1 -1 -1
 4  3 -1  2 -1  6  3 -1 -1

你的空间不够了。

数组索引 0 mod 3 是你的答案。

您可以使用 4 人一组来保持顺序:

数组[i] = 值

array[i+1] = 原始数组中的位置

数组[i+2] = 左子索引

array[i+3] = 右子索引

【讨论】:

  • 隐式二叉树是个好主意!
【解决方案5】:

在您施加的限制下,您将实现的最快时间复杂度是O(n) 使用带有O(1) 查找的字典,而不是使用二叉树来查找唯一整数。当您可以在固定时间内查找它们时,为什么还要费心搜索它们?

由于您只处理“数千条记录”,因此其他任何事情都是微不足道的。

【讨论】:

  • 这可能是最好的主意。预先制作一个长度为 p (素数)的数组,每次看到一个新的 int 时,如果该位置尚未被占用,请将其粘贴到 array[int%p] 中。成功添加 16 后,您就完成了。 O(1) 对于您处理的每个 int 。如果发生碰撞,您只会浪费一点时间。
  • @John Rasch:这是一个很好的方法,但请注意,字典/哈希表的 O(1) 时间查找是半真半假的;这取决于您的哈希表是否“足够大”以及输入数据中没有模式会导致病态的 O(n) 缓慢。
  • @j_random_hacker - 好点,特别是考虑到要查找的少量数据。除非使用 Jay 的建议,否则单独的哈希函数可能会浪费足够的时间来提高搜索效率
【解决方案6】:

对于这么小的数组(如果您想要前 20 个元素,平均有 10 个要检查相等性),线性扫描通常会执行二进制搜索,即使您不必插入元素。

【讨论】:

  • 是的 - 我也想过这个问题,但我不愿添加 O(n*n) 算法。要求可能会在一天内发生变化。不过好主意。保持简单。
  • 向数组中插入 N 个元素已经是 O(N*N);您正在消除具有许多分支的二进制搜索。
  • @Nils 由于您的唯一数组很小,它是一个 O(n) 算法,其中 n 是大列表的大小。
【解决方案7】:

不要将唯一的整数存储到数组中,而是使用实际的二叉树。这样可以避免重复移动数组元素。

【讨论】:

    【解决方案8】:

    为什么不直接将数组元素插入 std::set 并在集合有 N 个元素时停止?集合保证没有重复。它们也保证被排序,所以如果你从 begin() 到 end() 遍历一个集合,你会按照 operator<.>

    【讨论】:

    • 该集合在我插入元素时进行分配,并且运行时开销很高。
    • @Nils:运行时开销完全取决于节点的分配——否则, set 应该更快,因为它使用 O(log n) 查找,然后使用 O(1) 插入 (您的插入是 O(n))。您可以编写自己的分配器来避免动态内存分配,尽管这有点工作。
    • 我不确定您的意思是“插入时是否分配”...您只是为 10-20 个元素提取和复制分配。而且我不确定与红黑树相比你期望的速度有多快。
    • @Nils “运行时开销高”:有多慢?它需要多快?
    • 难道问题不需要元素按原始顺序排序,而不是升序吗?
    猜你喜欢
    • 2022-10-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多