有效的方法从一个非常大的集合生成一个看似随机的排列而不重复?

时间:2015-09-02 15:52:51

标签: algorithm random permutation

我有一个非常大的集合(数十亿或更多,它预计会以指数级增长到某个级别),我想从中生成看似随机的元素而不重复。我知道我可以选择一个随机数并重复并记录我生成的元素,但是在生成数字时会占用越来越多的内存,并且在数百万个元素输出后不会实用。

我的意思是,我可以说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 ......"的字符,我想要一个算法逐个生成它们而不重复,但使它看起来随机的。

4 个答案:

答案 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字节映射到您的编号(例如使用哈希函数)。