【问题标题】:Algorithm too slow for shuffling ArrayList洗牌 ArrayList 的算法太慢了
【发布时间】:2014-10-08 12:45:06
【问题描述】:

我正在尝试在 java 上实现 Fisher-Yates 洗牌算法。它可以工作,但是当我的 ArrayList 的大小> 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;

}

【问题讨论】:

  • 所以下次只需使用“编辑”;D 请向我们展示 next() 方法,因为它也可能是瓶颈。
  • 显示 next() 方法的代码......这可能需要这么长时间。
  • 水晶球 : 在next 中是否创建了Random 实例?
  • 你为什么使用List而不是int[]
  • 每次循环迭代时,您都在计算 pList.size(),请改用 size 变量,除此之外,如果在其中实例化随机类,瓶颈可能在下一个函数中函数将其拉出迭代循环,并将实例作为引用变量传递给下一个

标签: java algorithm arraylist shuffle


【解决方案1】:

编辑:在正确实现Fisher-Yates 算法后添加了一些cmets。

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

【讨论】:

    【解决方案2】:

    (更适合代码审查论坛。)

    我改变了我可以改变的:

    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)允许选择一个固定的随机序列(= 可重复),允许在开发期间计时并跟踪相同的序列。

    【讨论】:

    • 顺便说一句:这也不是 Fisher-Yates-Shuffle。但至少显示正确使用java.uti.Random...
    【解决方案3】:

    结合一些已经散布在cmet中的片段和其他答案:

    • 原始代码不是Fisher-Yates-Shuffle 的实现。它只是交换随机元素。这意味着某些排列比其他排列更有可能,结果并不是真正随机的
    • 如果存在瓶颈,它可能(根据提供的代码)仅存在于next 方法中,而您没有提及。它应该替换为java.util.Random 实例的nextInt 方法

    以下是它的外观示例。 (请注意,speedTest 方法甚至不打算作为“基准”,而只是表明即使对于大型列表,执行时间也可以忽略不计)。

    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 接口也是有意义的。对于未实现RandomAccess 接口的List,此算法将降级为二次性能。在这种情况下,最好将给定的列表复制到实现RandomAccess 的列表中,将这个副本打乱,然后将结果复制回原始列表中。

    【讨论】:

      【解决方案4】:

      试试这个代码并将执行时间与您的 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;
      }
      

      【讨论】:

        猜你喜欢
        • 2015-12-10
        • 2023-04-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-07-15
        相关资源
        最近更新 更多