我有一个非常大的集合(数十亿或更多,它预计会以指数级增长到某个级别),我想从中生成看似随机的元素而不重复。我知道我可以选择一个随机数并重复并记录我生成的元素,但是在生成数字时会占用越来越多的内存,并且在数百万个元素输出后不会实用。
我的意思是,我可以说1,2,3到数十亿,每个都是恒定的时间而不记得所有以前的,或者我可以说1,3,5,7,9和然后2,4,6,8,10,但有没有更复杂的方法来做到这一点,最终得到一组看似随机的排列?
更新
1,该集合在生成过程中不会改变大小。我的意思是当用户的输入线性增加时,该组的大小呈指数级增长。
2,简而言之,该集就像1到10亿或更多的每个整数的集合。
3,总之,它高达100亿,因为每个元素都带有许多独立选择的信息,例如。想象一下RPG角色有10个属性,每个属性可以从1到100(对于我的问题,不同的选择可以有不同的范围),因此有10 ^ 20个可能的角色,数字" 10873456879326587345"将对应于具有" 11,88,35 ......"的字符,我想要一个算法逐个生成它们而不重复,但使它看起来随机的。
答案 0 :(得分:2)
我会使用随机数并将其与集合开头的元素交换。
这是一些伪代码
set = [1, 2, 3, 4, 5, 6]
picked = 0
Function PickNext(set, picked)
If picked > Len(set) - 1 Then
Return Nothing
End If
// random number between picked (inclusive) and length (exclusive)
r = RandomInt(picked, Len(set))
// swap the picked element to the beginning of the set
result = set[r]
set[r] = set[picked]
set[picked] = result
// update picked
picked++
// return your next random element
Return temp
End Function
每次选择一个元素时,都有一个交换,唯一使用的额外内存是拾取的变量。如果元素在数据库中或在内存中,则可能发生交换。
编辑以下是工作实施的一个方面http://jsfiddle.net/sun8rw4d/
的JavaScript
var set = [];
set.picked = 0;
function pickNext(set) {
if(set.picked > set.length - 1) { return null; }
var r = set.picked + Math.floor(Math.random() * (set.length - set.picked));
var result = set[r];
set[r] = set[set.picked];
set[set.picked] = result;
set.picked++;
return result;
}
// testing
for(var i=0; i<100; i++) {
set.push(i);
}
while(pickNext(set) !== null) { }
document.body.innerHTML += set.toString();
编辑2 最后,该集的随机二进制步行。这可以通过O(Log2(N))堆栈空间(内存)来实现,该堆栈空间(内存)仅为10亿。这里没有任何混乱或交换。使用trinary而不是二进制可能会产生更好的伪随机结果。
// on the fly set generator
var count = 0;
var maxValue = 64;
function nextElement() {
// restart the generation
if(count == maxValue) {
count = 0;
}
return count++;
}
// code to pseudo randomly select elements
var current = 0;
var stack = [0, maxValue - 1];
function randomBinaryWalk() {
if(stack.length == 0) { return null; }
var high = stack.pop();
var low = stack.pop();
var mid = ((high + low) / 2) | 0;
// pseudo randomly choose the next path
if(Math.random() > 0.5) {
if(low <= mid - 1) {
stack.push(low);
stack.push(mid - 1);
}
if(mid + 1 <= high) {
stack.push(mid + 1);
stack.push(high);
}
} else {
if(mid + 1 <= high) {
stack.push(mid + 1);
stack.push(high);
}
if(low <= mid - 1) {
stack.push(low);
stack.push(mid - 1);
}
}
// how many elements to skip
var toMid = (current < mid ? mid - current : (maxValue - current) + mid);
// skip elements
for(var i = 0; i < toMid - 1; i++) {
nextElement();
}
current = mid;
// get result
return nextElement();
}
// test
var result;
var list = [];
do {
result = randomBinaryWalk();
list.push(result);
} while(result !== null);
document.body.innerHTML += '<br/>' + list.toString();
以下是使用一小组64个元素进行的几次运行的结果。 JSFiddle http://jsfiddle.net/yooLjtgu/
30,46,38,34,36,35,37,32,33,31,42,40,41,39,44,45,43,54,50,52,53,51,48,47 ,49,58,60,59,61,62,56,57,55,14,22,18,20,19,21,16,15,17,26,28,29,27,24,25,23 ,6,2,4,5,3,0,1,63,10,8,7,9,12,11,13
30,14,22,18,16,15,17,20,19,21,26,28,29,27,24,23,25,6,10,8,7,9,12,13 ,11,2,0,63,1,4,5,3,46,38,42,44,45,43,40,41,39,34,36,35,37,32,31,33,54 ,58,56,55,57,60,59,61,62,50,48,49,47,52,51,53
正如我在评论中提到的,除非你有一个有效的方法跳过到你的“动态”一代中的特定点,否则效率不会很高。
答案 1 :(得分:2)
感谢有趣的问题。您可以使用模幂运算创建一个带有几个字节的“伪随机”*(循环)置换。假设我们有n个元素。搜索大于n + 1的素数p。然后找到一个原始根g modulo p。基本上通过原始根的定义,动作x - > (g * x)%p是{1,...,p-1}的循环置换。所以x - &gt; ((g *(x + 1))%p)-1是{0,...,p-2}的循环置换。如果它给出一个更大(或相等)n的值,我们可以通过重复先前的排列来得到{0,...,n-1}的循环置换。
我将这个想法实现为Go包。 https://github.com/bwesterb/powercycle
package main
import (
"fmt"
"github.com/bwesterb/powercycle"
)
func main() {
var x uint64
cycle := powercycle.New(10)
for i := 0; i < 10; i++ {
fmt.Println(x)
x = cycle.Apply(x)
}
}
这会输出类似
的内容0
6
4
1
2
9
3
5
8
7
但根据所选的发电机,这可能会有所不同。
速度快但不超快:在我五岁的i7上,计算100亿个元素的一个循环应用程序需要不到210ns。更多细节:
BenchmarkNew10-8 1000000 1328 ns/op
BenchmarkNew1000-8 500000 2566 ns/op
BenchmarkNew1000000-8 50000 25893 ns/op
BenchmarkNew1000000000-8 200000 7589 ns/op
BenchmarkNew1000000000000-8 2000 648785 ns/op
BenchmarkApply10-8 10000000 170 ns/op
BenchmarkApply1000-8 10000000 173 ns/op
BenchmarkApply1000000-8 10000000 172 ns/op
BenchmarkApply1000000000-8 10000000 169 ns/op
BenchmarkApply1000000000000-8 10000000 201 ns/op
BenchmarkApply1000000000000000-8 10000000 204 ns/op
为什么我说“伪随机”?好吧,我们总是在创建一种非常特殊的循环:即使用模幂运算的循环。它看起来很伪随机。
答案 2 :(得分:1)
如果它是可枚举的,那么使用一个伪随机整数生成器,调整到0 ... 2 ^ n - 1的时间,其中上限刚好大于你的集合的大小,并生成伪随机整数,丢弃那些超过你的集合的大小。使用这些整数来索引集合中的项目。
答案 3 :(得分:0)
预先计算一系列索引(例如在文件中),它具有您需要的属性,然后随机选择枚举的起始索引并以循环方式使用该系列。
预先计算的系列的长度应为>集的最大大小。
如果你将这个(取决于你的编程语言等)与文件映射相结合,你的最终nextIndex(INOUT state)
函数(几乎)就像return mappedIndices[state++ % PERIOD];
一样简单,如果你有一个固定大小的每个条目(例如8个字节 - &gt; uint64_t)。
当然,返回的值可能是&gt;你当前的尺寸。只需绘制索引,直到得到一个&lt; =你设置的当前大小。
更新(回复问题更新):
如果要在RPG中创建10亿个唯一字符,还有另一种方法可以实现目标:生成GUID并为自己编写一个从GUID计算数字的函数。 man uuid
如果您使用的是unix系统。否则谷歌吧。 uuid的某些部分不是随机的,而是包含元信息,某些部分是系统的(例如您的网卡MAC地址)或随机的,取决于生成器算法。但它们非常非常独特。因此,每当您需要一个新的唯一编号时,生成一个uuid并通过一些算法将其转换为您的编号,该算法基本上以非平凡的方式将uuid字节映射到您的编号(例如使用哈希函数)。