洗牌算法讲解——力扣384.打乱数组


【LeetCode 384】打乱数组(Java 详细题解 + Fisher--Yates 洗牌算法讲解)

一、题目描述

给定一个没有重复元素的整数数组 nums,设计一个算法实现数组的随机打乱,使得所有排列出现的概率相等。

你需要实现一个类 Solution,包含以下三个方法:

java 复制代码
class Solution {
    public Solution(int[] nums) { } // 初始化对象
    public int[] reset() { }        // 重置数组并返回
    public int[] shuffle() { }      // 随机打乱数组并返回
}

二、示例

输入:

复制代码
["Solution", "shuffle", "reset", "shuffle"]
[[[1,2,3]], [], [], []]

输出:

复制代码
[null, [3,1,2], [1,2,3], [1,3,2]]

解释:

java 复制代码
Solution solution = new Solution([1, 2, 3]);
solution.shuffle(); // 返回任意 [1,2,3] 的随机排列
solution.reset();   // 返回初始状态 [1, 2, 3]
solution.shuffle(); // 再次打乱

三、题目要求分析

我们需要支持三种操作:

  1. 初始化(Solution)

    保存原始数组,以便后续可以重置。

  2. reset()

    返回数组的初始状态。

  3. shuffle()

    返回数组的随机排列,并保证所有排列的概率相同。


四、核心思路:Fisher--Yates 洗牌算法

Fisher--Yates 是一种经典的等概率随机打乱算法

它保证每个元素在任意位置的概率完全相等。

算法过程:

设数组长度为 n

  1. 从后向前遍历数组;
  2. 对于每个位置 i,在 [0, i] 范围内随机选择一个索引 j
  3. 交换 nums[i]nums[j]

伪代码如下:

复制代码
for i from n-1 to 1:
    j = random(0, i)
    swap(nums[i], nums[j])

这能确保:

  • 每个元素有 1/n 的概率出现在每个位置;
  • 所有 n! 种排列出现概率相等。

五、代码实现(Java)

java 复制代码
import java.util.Random;

class Solution {
    private int[] original;  // 保存初始数组
    private int[] array;     // 当前数组
    private Random rand;     // 随机数生成器

    // 构造函数:初始化数组
    public Solution(int[] nums) {
        original = nums.clone();
        array = nums.clone();
        rand = new Random();
    }

    // 重置数组到原始状态
    public int[] reset() {
        array = original.clone();
        return array;
    }

    // 打乱数组
    public int[] shuffle() {
        for (int i = array.length - 1; i > 0; i--) {
            int j = rand.nextInt(i + 1);  // 生成 [0, i] 范围内的随机索引
            swap(array, i, j);
        }
        return array;
    }

    // 交换两个元素
    private void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

六、执行流程示例

nums = [1, 2, 3] 为例:

  1. 初始化:

    复制代码
    original = [1, 2, 3]
    array = [1, 2, 3]
  2. 打乱过程:

    • i = 2, j = random(0, 2) → 假设 j = 0 → [3, 2, 1]
    • i = 1, j = random(0, 1) → 假设 j = 1 → [3, 2, 1]
      → 打乱结果可能为 [3,2,1] 或其他排列。
  3. reset:

    复制代码
    返回 [1, 2, 3]

七、复杂度分析

操作 时间复杂度 空间复杂度
reset() O(n) O(n)
shuffle() O(n) O(1)(原地打乱)
构造函数 O(n) O(n)

八、为什么要使用 Fisher--Yates 算法?

许多初学者会尝试使用以下错误做法:

java 复制代码
Collections.shuffle(Arrays.asList(nums));

这虽然能打乱数组,但并不适用于原生 int[],而且在算法面试中,你需要展示算法设计能力,而不仅是调用现成 API。

Fisher--Yates 洗牌算法是:

  • 数学上证明等概率
  • 时间复杂度 O(n)
  • 空间复杂度 O(1)
  • 面试官非常喜欢考察的随机算法经典题。

九、进阶思考

  1. 如果数组中存在重复元素,如何保证"等概率"?

    • Fisher--Yates 仍然适用,只是最终排列会有重复结果。
  2. 如果想要每次 shuffle 结果都不重复,该如何实现?

    • 可以用一个集合记录已出现过的排列,但复杂度会非常高(O(n!)),通常不可行。
  3. 若需要部分打乱前 k 个元素 ,可以在洗牌时只迭代到 i = k - 1


十、总结

要点 内容
核心算法 Fisher--Yates 洗牌
实现要点 从后向前遍历,每次随机交换
保证等概率 每个元素独立均匀地分布在任意位置
时间复杂度 O(n)
空间复杂度 O(1)

十一、参考链接


相关推荐
shandianchengzi2 分钟前
【小白向】错位排列|图文解释公考常见题目错位排列的递推式Dn=(n-1)(Dn-2+Dn-1)推导方式
笔记·算法·公考·递推·排列·考公
I_LPL3 分钟前
day26 代码随想录算法训练营 回溯专题5
算法·回溯·hot100·求职面试·n皇后·解数独
Yeats_Liao4 分钟前
评估体系构建:基于自动化指标与人工打分的双重验证
运维·人工智能·深度学习·算法·机器学习·自动化
cpp_25018 分钟前
P9586 「MXOI Round 2」游戏
数据结构·c++·算法·题解·洛谷
浅念-12 分钟前
C语言编译与链接全流程:从源码到可执行程序的幕后之旅
c语言·开发语言·数据结构·经验分享·笔记·学习·算法
有时间要学习27 分钟前
面试150——第五周
算法·深度优先
晚霞的不甘1 小时前
Flutter for OpenHarmony 可视化教学:A* 寻路算法的交互式演示
人工智能·算法·flutter·架构·开源·音视频
望舒5131 小时前
代码随想录day25,回溯算法part4
java·数据结构·算法·leetcode
C++ 老炮儿的技术栈2 小时前
Qt 编写 TcpClient 程序 详细步骤
c语言·开发语言·数据库·c++·qt·算法
KYGALYX2 小时前
逻辑回归详解
算法·机器学习·逻辑回归