Fisher-Yates算法能确保每种排列等概率出现,需从后往前依次在当前位置及之前随机选索引交换;前提包括使用可修改List、均匀Random源、强种子;不同集合类型有对应优化策略。

它能真正让每个排列等概率出现,前提是用对了 List、Random 和调用方式。
核心是 Fisher-Yates 算法逻辑
不是随便交换两个位置,而是从后往前,每次在当前位置及之前的所有索引中随机选一个来交换。比如长度为 5 的列表,先处理索引 4(最后一个),随机选 [0,4] 中某位交换;再处理索引 3,随机选 [0,3] 中某位交换……依此类推。这样每个元素落到末尾位置的概率都是 1/5,倒数第二位是 1/4,层层递推,最终所有 5! = 120 种排列出现机会完全均等。
必须满足的三个前提条件
- 传入的是可修改的 List 实现,如 new ArrayList(original);Arrays.asList() 返回的固定大小列表、List.of() 或 Collections.unmodifiableList() 都会直接抛异常
- Random 源要均匀:默认的 new Random() 通常没问题,但若自己重写 nextInt() 或用老旧 JDK 版本,低位可能有周期性偏差
- 种子不能太弱:用 System.currentTimeMillis() 当 seed 在高并发下容易重复,导致多线程拿到一模一样的打乱结果;测试时建议用常量 seed,如 new Random(123L)
不同 List 类型的处理策略
JDK 内部做了适配:
- 对 ArrayList 这类支持 O(1) 随机访问的集合,直接原地交换,时间复杂度 O(n),空间 O(1)
- 对 LinkedList,因遍历慢,会先复制到数组、在数组上 shuffle、再逐个写回——避免 O(n²) 开销;但要注意:如果 shuffle 过程中被其他线程并发修改结构,可能触发 ConcurrentModificationException
- 元素少于阈值(通常是 5)时,无论什么 List 都走轻量路径,不转数组
非 List 集合怎么“随机”
Set、Map 本身无顺序概念,所谓“随机重组”其实是取其键或值转成 List 后再 shuffle:
- 随机取一个元素:new ArrayList(set).stream().skip(new Random().nextInt(set.size())).findFirst() 更高效;若需多次取样,先 shuffle 再 get(0) 或 subList(0,k)
- Map 随机键遍历:List<K> keys = new ArrayList<>(map.keySet()); Collections.shuffle(keys); 后续按 keys 顺序访问 map 即可
- 注意别误写 Arrays.asList(new int[]{...})——这返回的是 List<int[]>,不是你想要的 List<Integer>,shuffle 不起作用

















