基于集合的方法:
如果阈值很低(比如低于 40%),建议的方法是:
- 有一组最后
N*T
生成的值的队列。
- 生成值时,请不断重新生成它,直到它不包含在集合中。
- 推送到队列时,弹出最旧的值并将其从集合中删除。
伪代码:
generateNextValue:
// once we're generated more than N*T elements,
// we need to start removing old elements
if queue.size >= N*T
element = queue.pop
set.remove(element)
// keep trying to generate random values until it's not contained in the set
do
value = getRandomValue()
while set.contains(value)
set.add(value)
queue.push(value)
return value
如果阈值很高,您可以将上面的内容反过来:
- 让集合代表所有不在最后
N*T
生成的值中的值。
- 反转所有集合操作(将所有集合添加替换为移除,反之亦然,并将集合替换为
contains
)!contains
。
伪代码:
generateNextValue:
if queue.size >= N*T
element = queue.pop
set.add(element)
// we can now just get a random value from the set, as it contains all candidates,
// rather than generating random values until we find one that works
value = getRandomValueFromSet()
//do
// value = getRandomValue()
//while !set.contains(value)
set.remove(value)
queue.push(value)
return value
基于洗牌的方法:(比上面的稍微复杂一些)
如果阈值很高,则上述操作可能需要很长时间,因为它可能会继续生成已经存在的值。
在这种情况下,一些基于 shuffle 的方法可能是一个更好的主意。
- 随机播放数据。
- 重复处理第一个元素。
- 这样做时,将其移除并将其插入到 range 中的随机位置
[N*T, N]
。
例子:
假设 N*T = 5 并且所有可能的值为[1,2,3,4,5,6,7,8,9,10]
.
然后我们首先洗牌,给我们,比方说,[4,3,8,9,2,6,7,1,10,5]
。
然后我们将其删除4
并重新插入到范围内的某个索引中[5,10]
(比如索引 5)。
然后我们有[3,8,9,2,4,6,7,1,10,5]
.
并根据需要继续删除下一个元素并将其重新插入。
执行:
如果我们不关心效率,那么数组就可以了——获取一个元素会花费O(n)
时间。
为了提高效率,我们需要使用支持有效随机位置插入和第一个位置删除的有序数据结构。首先想到的是(自平衡)二叉搜索树,按索引排序。
我们不会存储实际的索引,索引将由树的结构隐式定义。
在每个节点上,我们将有一个子节点的计数(自身 + 1)(需要在插入/删除时更新)。
插入可以如下完成:(暂时忽略自平衡部分)
// calling function
insert(node, value)
insert(node, N*T, value)
insert(node, offset, value)
// node.left / node.right can be defined as 0 if the child doesn't exist
leftCount = node.left.count - offset
rightCount = node.right.count
// Since we're here, it means we're inserting in this subtree,
// thus update the count
node.count++
// Nodes to the left are within N*T, so simply go right
// leftCount is the difference between N*T and the number of nodes on the left,
// so this needs to be the new offset (and +1 for the current node)
if leftCount < 0
insert(node.right, -leftCount+1, value)
else
// generate a random number,
// on [0, leftCount), insert to the left
// on [leftCount, leftCount], insert at the current node
// on (leftCount, leftCount + rightCount], insert to the right
sum = leftCount + rightCount + 1
random = getRandomNumberInRange(0, sum)
if random < leftCount
insert(node.left, offset, value)
else if random == leftCount
// we don't actually want to update the count here
node.count--
newNode = new Node(value)
newNode.count = node.count + 1
// TODO: swap node and newNode's data so that node's parent will now point to newNode
newNode.right = node
newNode.left = null
else
insert(node.right, -leftCount+1, value)
在当前节点可视化插入:
如果我们有类似的东西:
4
/
1
/ \
2 3
我们想插入5
现在的位置1
,它会这样做:
4
/
5
\
1
/ \
2 3
请注意,例如,当红黑树执行操作以保持自身平衡时,这些操作都不涉及比较,因此它不需要知道任何已插入元素的顺序(即索引)。但它必须适当地更新计数。
整体效率将是O(log n)
获得一个元素。