洗牌
以下就是常见的完全错误的随机排列算法:
function shuffle(arr){ return arr.sort(function(){ return Math.random() - 0.5; });}以上代码看似巧妙利用了 Array.prototype.sort 实现随机,但是,却有非常严重的问题,甚至是完全错误。
var arr = [0,1,2,3,4,5,6,7,8,9];var res = [0,0,0,0,0,0,0,0,0,0];var t = 10000;for(var i = 0; i < t; i++){ var sorted = shuffle(arr.slice(0)); sorted.forEach(function(o,i){ res[i] += o; });}res = res.map(function(o){ return o / t;});console.log(res);将上面的 shuffle 方法用这段测试代码在 chrome 浏览器中测试一下,可以得出结果,发现结果并不随机分布,各个位置的平均值越往后越大,这意味着这种随机算法越大的数字出现在越后面的概率越大。
function bubbleSort(arr, compare){ var len = arr.length; for(var i = 0; i < len - 1; i++){ for(var j = 0; j < len - 1 - i; j++){ var k = j + 1; if(compare(arr[j], arr[k]) > 0){ var tmp = arr[j]; arr[j] = arr[k]; arr[k] = tmp; } } } return arr;}function shuffle(arr){ return bubbleSort(arr, function(){ return Math.random() - 0.5; });}var arr = [0,1,2,3,4,5,6,7,8,9];var res = [0,0,0,0,0,0,0,0,0,0];var t = 10000;for(var i = 0; i < t; i++){ var sorted = shuffle(arr.slice(0)); sorted.forEach(function(o,i){ res[i] += o; });}res = res.map(function(o){ return o / t;});console.log(res);上面的代码的随机结果也是不均匀的,测试平均值的结果越往后的越大。(笔者之前没有复制原数组所以错误得出均匀的结论,已更正于 2016-05-10)
function insertionSort(arr, compare){ var len = arr.length; for(var i = 0; i < len; i++){ for(var j = i + 1; j < len; j++){ if(compare(arr[i], arr[j]) > 0){ var tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } } } return arr;}function shuffle(arr){ return insertionSort(arr, function(){ return Math.random() - 0.5; });}var arr = [0,1,2,3,4,5,6,7,8,9];var res = [0,0,0,0,0,0,0,0,0,0];var t = 10000;for(var i = 0; i < t; i++){ var sorted = shuffle(arr.slice(0)); sorted.forEach(function(o,i){ res[i] += o; });}res = res.map(function(o){ return o / t;});console.log(res);由于插入排序找后面的大数与前面的数进行交换,这一次的结果和冒泡排序相反,测试平均值的结果自然就是越往后越小。原因也和上面类似,对于插入排序,越往后的数字越容易随机交换到前面。
function quickSort(arr, compare){ arr = arr.slice(0); if(arr.length <= 1) return arr; var mid = arr[0], rest = arr.slice(1); var left = [], right = []; for(var i = 0; i < rest.length; i++){ if(compare(rest[i], mid) > 0){ right.push(rest[i]); }else{ left.push(rest[i]); } } return quickSort(left, compare).concat([mid]) .concat(quickSort(right, compare));}function shuffle(arr){ return quickSort(arr, function(){ return Math.random() - 0.5; });}var arr = [0,1,2,3,4,5,6,7,8,9];var res = [0,0,0,0,0,0,0,0,0,0];var t = 10000;for(var i = 0; i < t; i++){ var sorted = shuffle(arr.slice(0)); sorted.forEach(function(o,i){ res[i] += o; });}res = res.map(function(o){ return o / t;});console.log(res);快速排序并没有两两元素进行比较,它的概率分布也不随机。
function shuffle(arr){ var len = arr.length; for(var i = 0; i < len - 1; i++){ var idx = Math.floor(Math.random() * (len - i)); var temp = arr[idx]; arr[idx] = arr[len - i - 1]; arr[len - i -1] = temp; } return arr;}在上面的算法里,我们每一次循环从前 len - i 个元素里随机一个位置,将这个元素和第 len - i 个元素进行交换,迭代直到 i = len - 1 为止。
我们同样可以检验一下这个算法的随机性:
function shuffle(arr){ var len = arr.length; for(var i = 0; i < len - 1; i++){ var idx = Math.floor(Math.random() * (len - i)); var temp = arr[idx]; arr[idx] = arr[len - i - 1]; arr[len - i -1] = temp; } return arr;}var arr = [0,1,2,3,4,5,6,7,8,9];var res = [0,0,0,0,0,0,0,0,0,0];var t = 10000;for(var i = 0; i < t; i++){ var sorted = shuffle(arr.slice(0)); sorted.forEach(function(o,i){ res[i] += o; });}res = res.map(function(o){ return o / t;});console.log(res);从结果可以看出这个算法的随机结果应该是均匀的。不过我们的测试方法其实有个小小的问题,我们只测试了平均值,实际上平均值接近只是均匀分布的必要而非充分条件,平均值接近不一定就是均匀分布。不过别担心,事实上我们可以简单从数学上证明这个算法的随机性。