嗨!
其实我最早使用洗牌算法是在一次需要做打乱一个数组顺序时接触到的,那时想了好久没有想到什么好的办法,自己写了一个比较笨的方法,就是每次去获取随机数,在0到数组长度-1之间,如果这个数获取过了,则重新获取。
这样做有个很大的问题就是,最后一次的时候可能要产生很多次才会产生你需要的那个值,当时还自做聪明的做了个判断,就是如果最后只有一个数时就不产生随机数了,直接获取,但这样依然计算周期很长,特别是当数组很大时。
后来发现java提供有这样的一个算法:
java.util.Collections.shuffle(List<?> list);
此处需要传一个list,那如果是数组呢?
既然洗牌,就是将一个数组顺序打乱,也就是找一个快速将数组打乱的方法。
那么随机数是肯定要的(当然也可以使用当前时间的毫秒数去模一个数,但程序运行速度很快时时间可能就不是一个好的选择了,还有个原因就是难以做到比较真实的随机)。
方法1 :可以使用2个数组实现,一个保存结果,一个保存已经取的位置下标
public static int[] shuffle( int[] array) {
int length = array.length;
// 结果
int[] resultArray = new int[length];
//临时数组,用来保存已经选择的下标
int[] tempArray = new int[length];
for (int i = 0; i < tempArray.length; i++) {
tempArray[i] = -1;
}
Random random = new Random();
// 随机数
int rnd;
// 临时位置
int temp;
//表示前进或者后退
int step;
//此条件成立表示洗牌完成
for (int i = 0; i < length; i++) {
//
rnd = random.nextInt(length - 1);
temp = rnd;
step = 1;
while (isInArray(temp, tempArray)) {
//代表此位置已经获取过了,取下一位
temp += step;
if (temp > length - 1) {
temp = rnd - 1;
//向后查询
step = -1;
}
}
//已经找到数据
tempArray[i] = temp;
resultArray[i] = array[temp];
System.out.print(array[temp] + ",");
}
return resultArray;
}
/**
* 判断一个数是否在数组中.
*
* @param num
* @param arr
* @return
*/
private static boolean isInArray(int num, int[] arr) {
for (int i = 0; i < arr.length; i++) {
if (num == arr[i]) {
return true;
}
}
return false;
}
这种还算是经过一定优化的,可能随机性并不是很强,因为在一定范围内取值其实是同一个(比如:第 4 , 5 , 6 三个位置都取过了,下次无论随机到 4,5,6其中的一个,结果都是7)
那么终极版本是什么样的呢?
就是每次取到的数从末位往前放,然后下次从0 到 数组长度 - 已经取到的位数 之间取值。具体看代码:
public static void shuffle(int[] array) {
Random random = new Random();
int length = array.length;
for (int i = length; i > 1; i--) {
// 把随机位置交换到当前位置,既然是随机,就应该保证位置可能不变的情况,因此 random.nextInt 取 i
// 谢谢 [virtualspider](https://www.jianshu.com/u/cfb9abcf0c97) 的提醒
swap(array, i-1, random.nextInt(i));
}
}
private static void swap(int[] array, int i, int j) {
if (i != j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
这段代码看起来很简洁,理解上面可能有一定难度,但是大家可以画一个图形帮助理解。
流程就是这样的,嘿嘿