排序算法用于将一个序列变成有序的,而洗牌算法则用于将一个序列打“乱”,可以认为是排序算法相反操作。洗牌算法需要借助随机数实现来打“乱”序列。
什么才是“真的乱”
洗牌算法正确性的判断准则(“乱”的判断依据)有两个:
对于包含n个元素的序列,其全排列有n!种可能。故若序列打乱的结果有n!种且每种出现的概率一样,则是正确的洗牌算法。因打乱结果的种数肯定不大于n!,故反例有两种情况:
打乱结果的种数小于n!:显然此时全排列中的某些结果无法由洗牌算法产生,故此时的洗牌算法不对;
打乱结果的种数等于n!但每种的出现的概率不一样
另一个准则(与上述准则等价):排序算法使得每个元素出现在每个位置的概率一样(即1/n)。 这一准则也可由上述准则得到,任一元素x出现在任一位置i的排列种数为 (n-1)! ,故概率为 (n-1)!/n! = 1/n 。
怎样做到“真的乱”
洗牌算法代码通过随机获取元素并交换来产生随机结果。
比较著名、常用且实现很简洁的是 Knuth-Shuffle 算法。
原理:将数组分为已打乱和未打乱的前后两部分(初始时两者分别由0、n个元素),每次随机从未打乱部分中选择一个元素加入到已打乱部分中。易得种数为 n! 。
代码:四种写法本质上一样。时间复杂度O(n)
// 得到一个在闭区间 [min, max] 内的随机整数 int randInt(int min, int max); // 第一种写法 void shuffle(int[] arr) { int n = arr.length(); /******** 区别只有这两行 ********/ for (int i = 0 ; i < n; i++) { // 从 i 到最后随机选一个元素 int rand = randInt(i, n - 1); /*************************/ swap(arr[i], arr[rand]); } } // 第二种写法 for (int i = 0 ; i < n - 1; i++) int rand = randInt(i, n - 1); // 第三种写法 for (int i = n - 1 ; i >= 0; i--) int rand = randInt(0, i); // 第四种写法 for (int i = n - 1 ; i > 0; i--) int rand = randInt(0, i);