【问题标题】:Randomly selecting k different numbers in a range在一个范围内随机选择 k 个不同的数字
【发布时间】:2015-05-31 14:59:09
【问题描述】:

我需要在0 to n-1 范围内随机选择k 元素。 n 最高可达 10^9。而k 的范围可以是1 to n-1。我可以在 O(n) 时间内完成此操作,只需对包含值 0 to n-1 的数组进行改组并从中选择第一个 k 元素即可。但是当k 很小时,这种方法在时间和内存上都是低效的。这个问题有O(k)的解决方案吗?

注意:选定的k 数字必须不同。

我正在考虑一个解决方案。我可以想到两种方法。设R为要返回的集合。

  1. 在范围内选择一个随机值并将其添加到R。继续这样做直到|R| = k。这个过程需要sum(n/i) for n+1-k <= i <= ntime 和 O(k) 空间。
  2. 在数组中插入 0 到 n-1,对其进行随机播放,从中获取第一个 k 元素。这个过程需要 O(n+k) 的时间和空间。

所以对于给定的k,我可以在 O(k) 时间内选择更可取的方法。

【问题讨论】:

标签: java algorithm sorting random random-sample


【解决方案1】:

修正的 Fisher-Yates 算法

可以改进随机播放解决方案,因为您只需随机播放数组的前 k 个元素。但这仍然是 O(n),因为天真的 shuffle 实现需要一个大小为 n 的数组,需要将其初始化为来自的 n 值0 到 n-1.

Initialize value[n] to {0..n-1}
For i from 0 to k-1:
  swap(value[i], value[random_in_range(i, n)])
Result is value[0..k-1]

为了改进这一点,我们可以使用一种虚拟数组,由两部分组成:

  1. value:前 k 个元素的数组,将作为结果选择。这被初始化为 {0..k-1}

  2. rest:具有 k 个条目容量的稀疏数据结构(例如哈希表),包含数组的所有剩余条目,这些条目不简单他们自己的索引。最初是空的。

现在我们可以定义从 value 数组中交换元素 i 和 j 的函数(注意:ik,由上述算法保证):

# To swap elements i and j
If j < k:
  # Both elements to be swapped are in the selection
  tmp = value[i]; value[i] = value[j]; value[j] = tmp
Else If j in rest:
  # Element j has been swapped before
  tmp = value[i]; value[i] = rest[j]; rest[j] = tmp
Else:
  # The value at j is still j, we now add it to the virtual array
  rest[j] = value[i]; value[i] = j

对于 k≤n 的任何值,使用 O(k) 空间和时间。

三种算法策略

使用 O(k) 内存的更简单的解决方案是只保留所有选定值的哈希表,并生成值直到哈希表包含 k 值,拒绝重复.

对于小的k,随机选择的元素重复的概率是微不足道的,朴素的哈希表当然是最简单的解决方案。另一方面,如果 k 是 n 的重要部分,则哈希表主要是浪费空间,因为大小为 n的简单位向量> 足以记录已看到的值。对于非常大的k,当样本填满时,拒绝算法将花费太多时间,并且洗牌所需的完整向量并不比用于保存样本。

因此,实用的解决方案可能是使用三个解决方案中的任何一个,使用更少的空间和时间:对于 k 的值足够大,n 位位向量小于具有 k 个条目的哈希表,但没有大到拒绝的概率很大(例如,n/64≤k ≤n/4),使用位向量。对于较小的 k 值,使用简单的哈希表算法,对于接近 n 的 k 值,使用 Fisher-Yates shuffle完整的 n 元素向量(但仅限于 k 步)。

因为我们只在 k>cn 的情况下选择 O(n) 策略常数 c,复合算法仍然是 O(k) 时间和空间,因为在该约束下,n 在 O(k )。

【讨论】:

  • 修改后的 Fisher-Yates 算法似乎很完美。谢谢。
  • @crysoberil:还有另一种方法使用著名的(但略有争议)未初始化数组技巧,即 O(k) 时间和 O(n) 空间,但涉及访问 n 的未初始化值-元素数组。不幸的是,这在 Java 或符合标准的 C++ 中都不起作用。在第一种情况下,因为不可能无法初始化数组,而在第二种情况下,因为标准坚持未初始化的值是不稳定的(!),因此读取它们是 UB。所以我没有将它添加到列表中。
  • 这样更好:repository.cmu.edu/cgi/…(萨克斯算法)
  • @DaveGalvin:这是一个出色的算法,可能值得作为答案,但我并不是 100% 清楚这就是所要求的。 Saxe 算法生成排序序列。要产生排序序列的随机排列,您需要事后洗牌,并且对于 Saxe 算法的所有简单性,对于接近 n 的 k,洗牌 iota(n) 更容易。 (但如果要寻找的是与顺序无关的样本,那么 Saxe 可能是要走的路。)
  • 如果有人正在寻找上述 cmets 中提到的论文(其链接现在无用),我相信它是 Jon Louis Bentley 的 Generating Random Numbers in Sorted Order和詹姆斯 B. 萨克森。当前有效的链接是kilthub.cmu.edu/articles/…。四十年前最初发表这篇文章的 ACM 将以高昂的价格卖给你,我认为这是完全不合情理的。但那是我们正在经历的时代。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-07-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多