洗牌算法 - Fisher-Yates shuffle

算法解释1

Fisher–Yates随机置乱算法也被称做高纳德置乱算法,通俗说就是生成一个有限集合的随机排列。Fisher-Yates随机置乱算法是无偏的,所以每个排列都是等可能的,当前使用的Fisher-Yates随机置乱算法是相当有效的,需要的时间正比于要随机置乱的数,不需要额为的存储空间开销。

一、算法流程:
需要随机置乱的n个元素的数组a:
for i 从n-1到1

j <—随机整数(0 =< j <= i)

交换a[i]和a[j]

         end

二、实例

各列含义:范围、当前数组随机交换的位置、剩余没有被选择的数、已经随机排列的数


第一轮:从1到8中随机选择一个数,得到6,则交换当前数组中第8和第6个数


第二论:从1到7中随机选择一个数,得到2,则交换当前数组中第7和第2个数


下一个随机数从1到6中摇出,刚好是6,这意味着只需把当前线性表中的第6个数留在原位置,接着进行下一步;以此类推,直到整个排列完成。

洗牌算法 - Fisher-Yates shuffle_第1张图片

截至目前,所有需要的置乱已经完成,所以最终的结果是:7 5 4 3 1 8 2 6

三、Java源代码

[plain]  view plain  copy
  1. package simpleGa;  
  2.   
  3. import java.util.Arrays;  
  4. import java.util.Random;  
  5.   
  6. public class Test {  
  7.   
  8.     public static void main(String[] args) {  
  9.         int[] arr = new int[10];  
  10.         int i;  
  11.         //初始的有序数组  
  12.         System.out.println("初始有序数组:");  
  13.         for (i = 0; i < 10; i++) {  
  14.             arr[i] = i + 1;  
  15.             System.out.print(" " + arr[i]);  
  16.         }  
  17.         //费雪耶兹置乱算法  
  18.         System.out.println("\n" + "每次生成的随机交换位置:");  
  19.         for (i = arr.length - 1; i > 0; i--) {  
  20.             //随机数生成器,范围[0, i]  
  21.             int rand = (new Random()).nextInt(i+1);  
  22.             System.out.print(" " + rand);  
  23.             int temp = arr[i];  
  24.             arr[i] = arr[rand];  
  25.             arr[rand] = temp;  
  26.         }  
  27.         //置换之后的数组  
  28.         System.out.println("\n" + "置换后的数组:");  
  29.         for (int k: arr)  
  30.             System.out.print(" " + k);  
  31.     }  
  32. }  

洗牌算法 - Fisher-Yates shuffle_第2张图片

分析:从运行结果可以看到随着算法的进行,可供选择的随机数范围在减小,与此同时此时数组里的元素更加趋于无序。

转载自: http://blog.csdn.net/lhkaikai/article/details/25627161

证明概率相等:

洗牌算法 - Fisher-Yates shuffle_第3张图片

算法解释2

原文给出了三个循序渐近的例子,下面来看。转载过来时原文的动画贴不过来,可以移步原文去看http://www.cnblogs.com/Wayou/p/fisher_yates_shuffle.html

一般化方法

原文引入的现实情境是这样的,假如你要洗牌,那么最随机的做法无疑是从牌堆里随便抽一张出来,然后放在一边,之后从剩下的牌里重复之前的操作,直到所有牌都被抽出来放到了另一堆中。抽象到代码世界,按相同的做法,就是随机从数组里取出一个元素,保存到另一个数组,然后重复之,直到原数组中所有元素都处理掉。

原文给出的演示我觉得不够直观,下方是我自己写的动画用以演示此算法过程(也可在这里查看:http://sandbox.runjs.cn/show/1hylhpck ):

下面是按这个思路的一个实现:

function shuffle(array) {
    var copy = [],
        n = array.length,
        i;
    // 如果还剩有元素则继续。。。
    while (n) {
        // 随机抽取一个元素
        i = Math.floor(Math.random() * array.length);
        // 如果这个元素之前没有被选中过。。
        if (i in array) {
            copy.push(array[i]);
            delete array[i];
            n--;
        }
    }
    return copy;
}

我们创建了一个copy数组,然后遍历目标数组,将其元素复制到copy数组里,同时将该元素从目标数组中删除,这样下次遍历的时候就可以跳过这个序号。而这一实现的问题正在于此,即使一个序号上的元素已经被处理过了,由于随机函数产生的数是随机的,所有这个被处理过的元素序号可能在之后的循环中不断出现,一是效率问题,另一个就是逻辑问题了,存在一种可能是永远运行不完!

Note:
Math.random()产生[0,1)的小数
delete 操作只将数组元素的值删除,但不影响数组长度,删除后原来位置的值变为undefined

改进的做法

上面的分析已经看出问题的所在了,所以改进的做法就是处理完一个元素后,我们用Array的splice()方法将其从目标数组中移除同时也更新了目标数组的长度,如此一来下次遍历的时候是从新的长度开始,不会重复处理的情况了。

动画演示(http://sandbox.runjs.cn/show/v6a7gq0f)

function shuffle(array) {
    var copy = [],
        n = array.length,
        i;
    // 如果还剩有元素。。
    while (n) {
        // 随机选取一个元素
        i = Math.floor(Math.random() * n--);
        // 移动到新数组中
        copy.push(array.splice(i, 1)[0]);
    }
    return copy;
}

再次优化的最终版本

上面的做法已经可以了,但上面的改进依然还有提升空间。因为调用splice来删除数组元素会导致删除位置之后的所有元素要做shift操作来向前补充,从而达到将数组长度减小的目的,当然这是在后台自动完成的,但这无疑增加了算法的复杂度。

注意到我们要做的仅仅是将数组元素重新排序,已经取出来的元素和剩下的元素之和一定是等于数组原来的总元素个数的。所以可以考虑不创建新的数组来保存已经抽取的元素,可以这样,随机从数组中抽出一个元素,然后与最后个元素交换,相当于把这个随机抽取的元素放到了数组最后面去,表示它已经是被随机过了,同时被换走的那个元素跑到前面去了,会在后续的重复操作中被随机掉。一轮操作过后,下一轮我们只在剩下的n-1个元素也就是数组的前n-1个元素中进行相同的操作,直到进行到第一个。

动画演示(http://sandbox.runjs.cn/show/jabgttzr):

function shuffle(array) {
    var m = array.length,
        t, i;
    // 如果还剩有元素…
    while (m) {
        // 随机选取一个元素…
        i = Math.floor(Math.random() * m--);
        // 与当前元素进行交换
        t = array[m];
        array[m] = array[i];
        array[i] = t;
    }
    return array;
}

更加简洁的版本

上面介绍的便是在各语言中都广为实现的Fisher-Yates乱序算法。但具体到JavaScript,我们其实可以结合数组自带的sort()方法编写出更简洁的代码来达到目的。中间变量以及值交换什么的都省了,虽然后台实现肯定还是会进行值交换的,但我们不关心,一切交给sort()让它自己处理。但这种方法也只是简洁而以,效果是不如上面介绍的算法的,因为随着数组元素越多,其随机性会变差。

function shuffle(array) {
    return array.sort(function() {
        return Math.random() - 0.5
    });
}

 

REFERENCE

  • Fisher–Yates Shuffle: http://bost.ocks.org/mike/shuffle/
  • Why the Fisher-Yates Shuffle is the best algorithm from Quora: http://www.quora.com/Algorithms/Are-there-any-better-shuffling-algorithms-than-Fisher%E2%80%93Yates-shuffle
  • 45 Useful JavaScript Tips, Tricks and Best Practices http://flippinawesome.org/2013/12/23/45-useful-javascript-tips-tricks-and-best-practices/

另外,可以查阅维基百科上关于洗牌算法的介绍: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle


你可能感兴趣的:(算法)