我正在尝试在java上实现Fisher-Yates shuffle算法。它有效,但当我的ArrayList的大小> gt; 100000,它变得非常慢。我将向您展示我的代码,您是否看到了优化代码的方法?我对ArrayList中的.get和.set的复杂性进行了一些研究,它对我来说是有意义的O(1)。
更新1:我注意到我的实施是错误的。这是适当的Fisher-Yates算法。我还包括了我的next()
函数,所以你们可以看到它。我测试了java.Random以查看我的next()
函数是否是问题,但它给出了相同的结果。我认为问题在于使用我的数据结构。
更新2:我做了一个测试,ArrayList是RandomAccess的一个实例。所以问题就不存在了。
private long next(){ // MurmurHash3
seed ^= seed >> 33;
seed *= 0xff51afd7ed558ccdL;
seed ^= seed >> 33;
seed *= 0xc4ceb9fe1a85ec53L;
seed ^= seed >> 33;
return seed;
}
public int next(int range){
return (int) Math.abs((next() % range));
}
public ArrayList<Integer> shuffle(ArrayList<Integer> pList){
Integer temp;
int index;
int size = pList.size();
for (int i = size - 1; i > 0; i--){
index = next(i + 1);
temp = pList.get(index);
pList.set(index, pList.get(i));
pList.set(i, temp);
}
return pList;
}
答案 0 :(得分:2)
编辑:在您正确实施Fisher-Yates算法后添加了一些评论。
Fisher-Yates算法依赖于均匀分布的随机整数来产生无偏置置换。使用散列函数(MurmurHash3)生成随机数并引入abs和modulo操作以强制数字在固定范围内使得实现不那么健壮。
此实现使用java.util.Random PRNG,应该可以满足您的需求:
public <T> List<T> shuffle(List<T> list) {
// trust the default constructor which sets the seed to a value very likely
// to be distinct from any other invocation of this constructor
final Random random = new Random();
final int size = list.size();
for (int i = size - 1; i > 0; i--) {
// pick a random number between one and the number
// of unstruck numbers remaining (inclusive)
int index = random.nextInt(i + 1);
list.set(index, list.set(i, list.get(index)));
}
return list;
}
我无法在代码中看到任何重大的性能瓶颈。但是,这里有一个火,忘记了上面的实现与Collections#shuffle方法的比较:
public void testShuffle() {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
list.add(i);
}
System.out.println("size: " + list.size());
System.out.println("Fisher-Yates shuffle");
for (int i = 0; i < 10; i++) {
long start = System.currentTimeMillis();
shuffle(list);
long stop = System.currentTimeMillis();
System.out.println("#" + i + " " + (stop - start) + "ms");
}
System.out.println("Java shuffle");
for (int i = 0; i < 10; i++) {
long start = System.currentTimeMillis();
Collections.shuffle(list);
long stop = System.currentTimeMillis();
System.out.println("#" + i + " " + (stop - start) + "ms");
}
}
给了我以下结果:
size: 1000000
Fisher-Yates shuffle
#0 84ms
#1 60ms
#2 42ms
#3 45ms
#4 47ms
#5 46ms
#6 52ms
#7 49ms
#8 47ms
#9 53ms
Java shuffle
#0 60ms
#1 46ms
#2 44ms
#3 48ms
#4 50ms
#5 46ms
#6 46ms
#7 49ms
#8 50ms
#9 47ms
答案 1 :(得分:1)
(更适合代码审查论坛。)
我改变了我的想法:
Random random = new Random(42);
for (ListIterator<Integer>.iter = pList.listIterator(); iter.hasNext(); ) {
Integer value = iter.next();
int index = random.nextInt(size);
iter.set(pList.get(index));
pList.set(index, value);
}
由于ArrayList是大型数组的列表,因此可以在ArrayList构造函数中设置initialCapacity。 trimToSize()
也可以做点什么。使用ListIterator意味着已经存在于当前部分数组中,这可能有所帮助。
Random构造函数的可选参数(此处为42)允许选择固定的随机序列(=可重复),允许在开发时间和跟踪相同序列期间。
答案 2 :(得分:0)
尝试此代码并将执行时间与fisher yates方法进行比较。 这可能是“下一步”慢慢的方法
function fisherYates(array) {
for (var i = array.length - 1; i > 0; i--) {
var index = Math.floor(Math.random() * i);
//swap
var tmp = array[index];
array[index] = array[i];
array[i] = tmp;
}
答案 3 :(得分:0)
结合分散在评论和其他答案中的一些片段:
next
方法中,您没有说什么。它应该替换为nextInt
java.util.Random
方法
以下是它的外观示例。 (请注意,speedTest
方法甚至不是远程用作&#34;基准测试&#34;,而应仅表示即使对于大型列表,执行时间也可忽略不计。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
class FisherYatesShuffle {
public static void main(String[] args) {
basicTest();
speedTest();
}
private static void basicTest() {
List<Integer> list = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5));
shuffle(list, new Random(0));;
System.out.println(list);
}
private static void speedTest() {
List<Integer> list = new ArrayList<Integer>();
int n = 1000000;
for (int i=0; i<n; i++) {
list.add(i);
}
long before = System.nanoTime();
shuffle(list, new Random(0));;
long after = System.nanoTime();
System.out.println("Duration "+(after-before)/1e6+"ms");
System.out.println(list.get(0));
}
public static <T> void shuffle(List<T> list, Random random) {
for (int i = list.size() - 1; i > 0; i--) {
int index = random.nextInt(i + 1);
T t = list.get(index);
list.set(index, list.get(i));
list.set(i, t);
}
}
}
旁白:你给了一个列表作为参数,并返回相同的列表。在某些情况下,这个可能是合适的,但在这里没有任何意义。这种方法的签名和行为有几种选择。但最有可能的是,它应该收到List
,然后就地清理这个列表。实际上,明确检查列表是否实现java.util.RandomAccess接口也是有意义的。对于未实现List
接口的RandomAccess
,此算法会降级为二次性能。在这种情况下,最好将给定列表复制到实现RandomAccess
的列表中,随机复制该副本,然后将结果复制回原始列表中。