对于混洗ArrayList,算法太慢了

时间:2014-10-08 12:45:07

标签: java algorithm arraylist shuffle

我正在尝试在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;

}

4 个答案:

答案 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)

结合分散在评论和其他答案中的一些片段:

  • 原始代码不是Fisher-Yates-Shuffle的实现。它只是交换随机元素。这意味着某些排列比其他排列更可能,结果并非真正随机
  • 如果存在瓶颈,它可能(基于提供的代码)仅在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的列表中,随机复制该副本,然后将结果复制回原始列表中。

相关问题