Open classicemi opened 9 years ago
在underscore提供的众多工具方法中,有一个叫做shuffle()的方法,它的作用是将一个集合中的所有元素随机排列,作为数组返回。如果传入的是对象,返回的是包含对象所有随机排列后的value值的数组。
shuffle()
_.shuffle = function(obj) { var set = obj && obj.length === +obj.length ? obj : _.values(obj); var length = set.length; var shuffled = Array(length); for (var index = 0, rand; index < length; index++) { rand = _.random(0, index); if (rand !== index) shuffled[index] = shuffled[rand]; shuffled[rand] = set[index]; } return shuffled; };
这段代码的实现比较有技术含量,和underscore其他的方法相比更有意思一些。
首先对obj类型做判断,通过obj.length === +obj.length的真假判断obj是否是数组,underscore中判断数组基本上都是这么判断。set变量保存的值,如果obj是数组,则set = obj(原数组),如果obj是对象,则set = _.values(obj)(对象所有value值组成的数组)。(shuffle方法中调用的其他工具方法的作用请自行查阅文档)
obj
obj.length === +obj.length
set
set = obj
set = _.values(obj)
shuffle
然后根据set的长度初始化一个定长的空array作为最后的返回值,下面的一个for循环就是对这个数组的关键操作。为了方便描述,现在假设传入的obj是[1, 2, 3, 4, 5]这个数组,下面进入for循环,只用看前两个循环就可以:
[1, 2, 3, 4, 5]
// 第一次循环 index = 0 shuffled = [_, _, _, _, _] rand = 0到0的一个随机数(就是0) shuffled[0] = shuffled[0]; shuffled[0] = set[0]; --> [1, _, _, _, _] // 第二次循环 index = 1 shuffled = [_, _, _, _, _] rand = 0到1的一个随机数(可能是0也可能是1) 假设是0: shuffled[1] = shuffled[0]; --> [1, 1, _, _, _] shuffled[0] = set[1]; --> [2, 1, _, _, _] 假设是1: shuffled[1] = shuffled[1]; --> [1, _, _, _, _] shuffled[1] = set[1]; --> [1, 2, _, _, _]
第一次循环,结果是一定的,在shuffled数组中填入第一个元素。 第二次循环,rand是以0到当前索引值为闭区间的一个随机数,不论这个区间有多大,结果只有两类:
shuffled
rand
第一类:rand为当前索引值,shuffled[rand]位置上为空 这种情况下,新元素直接填充到shuffled数组的下一个空位上,其他元素没有任何变化。 第二类:rand为其他值,shuffled[rand]位置上有元素` 这种情况下,先将shuffled[rand]上的元素复制到shuffled[index]的位置上,再将要填入的新元素覆盖到shuffled[rand]上。
shuffled[rand]
shuffled[index]
以后的每一步都是这样的逻辑。
源码分析完了,不难理解,但问题的重点是,怎样证明通过这样的操作方法实现的数组是真正随机排列的数组。
此方法中用的算法名为Fisher–Yates shuffle,也是洗牌算法的一种。该算法的时间复杂度为O(n),性能上还可以。然后对于每一个元素(elemIndex)来说,它在插入shuffled数组时,对于[0, elemIndex]上的所有位置都是等可能的。在后面的循环中,对于[elemIndex, currentIndex]的所有位置也是等可能的。因此,对于任意一个元素来说,出现在任何位置的概率都是相等的。下面我们可以通过一个实验来大致感受一下: 实验对象选用一个1-10的数组。
var count = [[], [], [], [], [], [], [], [], [], []] var testArr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] var temp, i, j for (i = 0; i < 1000; i++) { temp = _.shuffle(testArr) for (j = 0; j < 10; j++) { count[j][temp[j]] ? count[j][temp[j]]++ : count[j][temp[j]] = 1 } } for (j = 1; j < 11; j++) { console.log(j + '出现在各位置的次数:', count[0][j], count[1][j], count[2][j], count[3][j], count[4][j], count[5][j], count[6][j], count[7][j], count[8][j], count[9][j]) }
结果如截图: 多执行几次可以观察到,每个数字在各位置出现的概率大致是相等的,对于前端实现随机打乱数组的需求已经基本满足要求了。
棒棒的
在underscore提供的众多工具方法中,有一个叫做
shuffle()
的方法,它的作用是将一个集合中的所有元素随机排列,作为数组返回。如果传入的是对象,返回的是包含对象所有随机排列后的value值的数组。这段代码的实现比较有技术含量,和underscore其他的方法相比更有意思一些。
首先对
obj
类型做判断,通过obj.length === +obj.length
的真假判断obj
是否是数组,underscore中判断数组基本上都是这么判断。set
变量保存的值,如果obj
是数组,则set = obj
(原数组),如果obj
是对象,则set = _.values(obj)
(对象所有value值组成的数组)。(shuffle
方法中调用的其他工具方法的作用请自行查阅文档)然后根据
set
的长度初始化一个定长的空array作为最后的返回值,下面的一个for循环就是对这个数组的关键操作。为了方便描述,现在假设传入的obj
是[1, 2, 3, 4, 5]
这个数组,下面进入for循环,只用看前两个循环就可以:第一次循环,结果是一定的,在
shuffled
数组中填入第一个元素。 第二次循环,rand
是以0到当前索引值为闭区间的一个随机数,不论这个区间有多大,结果只有两类:第一类:
rand
为当前索引值,shuffled[rand]
位置上为空 这种情况下,新元素直接填充到shuffled
数组的下一个空位上,其他元素没有任何变化。 第二类:rand
为其他值,shuffled[rand]
位置上有元素` 这种情况下,先将shuffled[rand]
上的元素复制到shuffled[index]
的位置上,再将要填入的新元素覆盖到shuffled[rand]
上。以后的每一步都是这样的逻辑。
源码分析完了,不难理解,但问题的重点是,怎样证明通过这样的操作方法实现的数组是真正随机排列的数组。
此方法中用的算法名为Fisher–Yates shuffle,也是洗牌算法的一种。该算法的时间复杂度为O(n),性能上还可以。然后对于每一个元素(elemIndex)来说,它在插入
shuffled
数组时,对于[0, elemIndex]上的所有位置都是等可能的。在后面的循环中,对于[elemIndex, currentIndex]的所有位置也是等可能的。因此,对于任意一个元素来说,出现在任何位置的概率都是相等的。下面我们可以通过一个实验来大致感受一下:实验对象选用一个1-10的数组。
结果如截图: 多执行几次可以观察到,每个数字在各位置出现的概率大致是相等的,对于前端实现随机打乱数组的需求已经基本满足要求了。