费歇尔-耶茨洗牌是反向的。
The Fisher-Yates shuffle is backward

原始链接: https://possiblywrong.wordpress.com/2020/12/10/the-fisher-yates-shuffle-is-backward/

费歇尔-耶茨洗牌算法是随机排列列表的标准算法,确保每个排列的可能性相同。传统上,它*反向*遍历列表,将每个元素与它前面的随机选择的元素交换。 然而,一个看似更简单的变体——*正向*遍历——实现了相同的均匀分布。这种“前向洗牌”通过将每个元素与当前位置或之前位置的随机元素交换来工作。 作者质疑为什么前向方法不更常用,指出它在数学上等同于传统的费歇尔-耶茨算法(以相反的顺序应用相同的随机交换)。它也与通常与外部数据源一起使用的“内向外”费歇尔-耶茨变体相关。作者认为这种前向版本更简单,并想知道通常包含的避免不必要交换的优化是否会影响实际效率。最终,两种方法都能正确地以相同概率洗牌列表中的每个排列。

一个 Hacker News 的讨论围绕着 Fisher-Yates 洗牌算法,用于随机排列列表。发帖者指出,该算法的一个“反向”版本出乎意料地常见。 评论者指出,反向版本可以被认为是一种“在线”洗牌,适用于随机化未知或增长长度的列表,或用于抽样。一位用户建议使用 `enumerate` 的标准实现不是必需的,并提供了一个使用 `random.randint` 的替代方案,以更清晰地表达意图。 另一位评论者承认总是使用“正向”版本,强调了独立发现如何导致不同的但有效的实现。这场讨论强调了 Fisher-Yates 洗牌存在多种有效的方法,而偏好通常取决于可读性和特定用例需求。
相关文章

原文

Given a list of n elements, such as cards in a deck, what is the right way to shuffle the list? That is, what is the appropriate algorithm to (pseudo)randomly permute the elements in the list, so that each of the n! possible permutations is equally likely?

This is an interesting problem, in part because it is easy to get wrong. The standard, all-the-cool-kids-know-it response is the Fisher-Yates shuffle, consisting of a sequence of n-1 carefully specified random transpositions, with the following basic implementation in Python:

def fisher_yates_shuffle(a):
    """Shuffle list a[0..n-1] of n elements."""
    for i in range(len(a) - 1, 0, -1): # i from n-1 downto 1
        j = random.randint(0, i) # inclusive
        a[i], a[j] = a[j], a[i]

Note that the loop index i decreases from n-1 down to 1. Everywhere I have looked, this is how the algorithm is always presented. The motivation for this post is to wonder aloud why the following variant– which seems simpler, at least to me– is not the “standard” approach, with the only difference being that the loop runs “forward” instead of backward:

def forward_shuffle(a):
    "Shuffle list a[0..n-1] of n elements."""
    for i in range(1, len(a)): # i from 1 to n-1
        j = random.randint(0, i) # inclusive
        a[i], a[j] = a[j], a[i]

It’s worth emphasizing that this is different from what seems to be the usual “forward” version of the algorithm (e.g., this “equivalent version”), that seems to consistently insist on also “mirroring” the ranges of the random draws, so that they are decreasing with each loop iteration instead of the loop index:

def mirror_shuffle(a):
    "Shuffle list a[0..n-1] of n elements."""
    for i in range(0, len(a) - 1): # i from 0 to n-2
        j = random.randint(i, len(a) - 1) # inclusive
        a[i], a[j] = a[j], a[i]

There are a couple of ways to see and/or prove that forward_shuffle does indeed yield a uniform distribution on all possible permutations. One is by induction– the rather nice loop invariant is that, after each iteration i, the sublist a[0..i] is a uniformly random permutation of the original sublist a[0..i]. (Contrast this with the normal Fisher-Yates shuffle, where after each iteration indexed by i, the “suffix” sublist a[i..n-1] is essentially a uniformly permuted reservoir-sampled subset of the entire original list.)

Another way to see that forward_shuffle works as desired is to relate its behavior to that of the original fisher_yates_shuffle, which has already been proved correct. Consider the set of independent discrete random variables \{X_1, X_2, \ldots, X_{n-1}\}, with each X_i distributed uniformly between 0 and i, inclusive. These X_i are the random draws returned from random.randint(0, i).

Imagine generating the entire set of those n-1 independent random draws up front, then applying the sequence of corresponding transpositions (i, X_i). The original Fisher-Yates shuffle applies those transpositions in order of decreasing i, while forward_shuffle applies the same set of random transpositions, but in reverse order. Thus, the permutations resulting from fisher_yates_shuffle and forward_shuffle are inverses of each other… and if a random permutation is uniformly distributed, then so is its inverse.

There is nothing special here– indeed, this forward_shuffle is really just a less dressed-up implementation of what is usually referred to as the “inside-out” version of Fisher-Yates, that for some reason seems to be presented as only appropriate when shuffling a list generated from an external source (possibly of unknown length):

def forward_shuffle(source):
    "Return shuffled list from external source."""
    a = []
    for i, x in enumerate(source):
        a.append(x)
        j = random.randint(0, i) # inclusive
        a[i], a[j] = a[j], a[i]
    return a

I say “less dressed-up” because I’ve skipped what seems to be the usual special case j==i comparison that would eliminate the swapping. The above seems simpler to me, and I would be curious to know if these (branchless) swaps are really less efficient in practice.

联系我们 contact @ memedata.com