算法题 打乱数组

LeetCode 384. 打乱数组

问题描述

给你一个整数数组 nums,设计一个算法来打乱一个没有重复元素的数组。实现 Solution 类:

  • Solution(int[] nums) 使用整数数组 nums 初始化对象
  • int[] reset() 重置数组到它的初始状态并返回
  • int[] shuffle() 返回数组随机打乱后的结果

示例

// 以数组 [1,2,3] 为例
Solution solution = new Solution([1, 2, 3]);

// 重置数组到初始状态 [1, 2, 3] 并返回
solution.reset();

// 随机返回数组 [1, 2, 3] 打乱后的结果
// 例如,返回 [3, 1, 2]
solution.shuffle();

约束条件

  • 1 <= nums.length <= 50
  • -10⁶ <= nums[i] <= 10⁶
  • nums 中的所有元素都是唯一的
  • 最多调用 10⁴resetshuffle

算法思路

核心思想:Fisher-Yates 洗牌算法

Fisher-Yates 算法(也称为 Knuth 洗牌算法)是生成均匀随机排列的标准算法。

算法原理

  • 从数组的最后一个元素开始,向前遍历
  • 对于每个位置 i,随机选择一个索引 j,其中 0 <= j <= i
  • 交换 nums[i]nums[j]

为什么能保证均匀分布?

  • 对于长度为 n 的数组,总共有 n! 种排列
  • Fisher-Yates 算法恰好执行 n 次选择:
    • 第1次:n 种选择
    • 第2次:n-1 种选择
    • 第n次:1 种选择
  • 总共产生 n × (n-1) × … × 1 = n! 种结果,每种结果概率相等

步骤:

  1. 初始化:保存原始数组的副本
  2. 重置:返回原始数组的副本
  3. 洗牌
    • 复制当前数组(或原始数组)
    • 从后往前遍历数组
    • 对每个位置 i,生成随机索引 j ∈ [0, i]
    • 交换位置 i 和 j 的元素

代码实现

import java.util.*;

class Solution {
    /**
     * 原始数组的备份,用于reset操作
     */
    private int[] original;
    
    /**
     * 随机数生成器
     */
    private Random random;
    
    /**
     * 构造函数:初始化Solution对象
     * 
     * @param nums 输入的整数数组
     */
    public Solution(int[] nums) {
        // 保存原始数组的副本,避免外部修改影响
        this.original = nums.clone();
        this.random = new Random();
    }
    
    /**
     * 重置数组到初始状态
     * 
     * @return 原始数组的副本
     * 
     * 时间复杂度: O(n)
     * 空间复杂度: O(n)
     */
    public int[] reset() {
        // 返回原始数组的副本,确保外部无法修改内部状态
        return original.clone();
    }
    
    /**
     * 返回数组随机打乱后的结果(使用Fisher-Yates洗牌算法)
     * 
     * @return 打乱后的数组
     * 
     * 时间复杂度: O(n)
     * 空间复杂度: O(n)
     */
    public int[] shuffle() {
        // 创建当前数组的副本用于洗牌
        int[] shuffled = original.clone();
        int n = shuffled.length;
        
        // Fisher-Yates 洗牌算法
        // 从最后一个元素开始,向前遍历
        for (int i = n - 1; i > 0; i--) {
            // 生成随机索引 j,范围 [0, i](包含i)
            int j = random.nextInt(i + 1);
            
            // 交换 shuffled[i] 和 shuffled[j]
            swap(shuffled, i, j);
        }
        
        return shuffled;
    }
    
    /**
     * 交换数组中两个位置的元素
     * 
     * @param arr 数组
     * @param i 第一个位置
     * @param j 第二个位置
     */
    private void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

算法分析

  • 时间复杂度

    • reset(): O(n) - 需要克隆数组
    • shuffle(): O(n) - 需要遍历整个数组进行洗牌
  • 空间复杂度

    • 构造函数: O(n) - 存储原始数组副本
    • reset()shuffle(): O(n) - 返回新的数组副本
  • 随机性

    • Fisher-Yates 算法确保每种排列的概率都是 1/n!
    • 使用 Random.nextInt(i + 1) 生成均匀分布的随机数
  • 正确性

    • 不变性reset() 总是返回原始数组
    • 独立性:每次 shuffle() 调用都是独立的,不受之前调用影响
    • 完整性:所有元素都会被重新排列,不会丢失或重复

算法过程

nums = [1, 2, 3]

Fisher-Yates 洗牌过程:

初始: [1, 2, 3]

i = 2 (最后一个元素):
- 生成 j ∈ [0, 2],假设 j = 1
- 交换位置 2 和 1: [1, 3, 2]

i = 1 (倒数第二个元素):
- 生成 j ∈ [0, 1],假设 j = 0  
- 交换位置 1 和 0: [3, 1, 2]

i = 0 (第一个元素):
- 循环结束(i > 0 条件不满足)

最终结果: [3, 1, 2]

测试用例

public static void main(String[] args) {
    // 测试用例1:标准示例
    int[] nums1 = {1, 2, 3};
    Solution solution1 = new Solution(nums1);
    
    System.out.println("Original: " + Arrays.toString(nums1));
    System.out.println("Reset: " + Arrays.toString(solution1.reset()));
    
    // 多次洗牌,结果不同
    System.out.println("Shuffle 1: " + Arrays.toString(solution1.shuffle()));
    System.out.println("Shuffle 2: " + Arrays.toString(solution1.shuffle()));
    System.out.println("Shuffle 3: " + Arrays.toString(solution1.shuffle()));
    
    // 测试用例2:单元素数组
    int[] nums2 = {42};
    Solution solution2 = new Solution(nums2);
    System.out.println("Original: " + Arrays.toString(nums2));
    System.out.println("Shuffle: " + Arrays.toString(solution2.shuffle()));
    System.out.println("Reset: " + Arrays.toString(solution2.reset()));
    
    // 测试用例3:两个元素
    int[] nums3 = {10, 20};
    Solution solution3 = new Solution(nums3);
    System.out.println("Original: " + Arrays.toString(nums3));
    System.out.println("Shuffle 1: " + Arrays.toString(solution3.shuffle()));
    System.out.println("Shuffle 2: " + Arrays.toString(solution3.shuffle()));
    
    // 测试用例4:包含负数
    int[] nums4 = {-1, 0, 1, 2};
    Solution solution4 = new Solution(nums4);
    System.out.println("Original: " + Arrays.toString(nums4));
    System.out.println("Shuffle: " + Arrays.toString(solution4.shuffle()));
    
    // 测试用例5:reset
    int[] nums5 = {1, 2, 3, 4, 5};
    Solution solution5 = new Solution(nums5);
    int[] reset1 = solution5.reset();
    int[] shuffle1 = solution5.shuffle();
    int[] reset2 = solution5.reset();  
}

关键点

  1. Fisher-Yates 算法

    • 从后往前遍历(i = n-1i = 1
    • 随机索引范围是 [0, i],包含 i
    • 使用 random.nextInt(i + 1) 生成正确范围的随机数
  2. 数组克隆

    • 构造函数中克隆原始数组,防止外部修改影响
    • reset()shuffle() 返回新数组
  3. 随机数生成器

    • 使用 java.util.Random 而不是 Math.random()
    • Random 提供更好的随机性和性能
  4. 边界情况处理

    • 单元素数组:洗牌后仍然是自身
  5. 时间复杂度

    • 每次洗牌恰好执行 n-1 次交换

常见问题

  1. 为什么不能从前向后洗牌?

    • 从前向后会导致某些排列的概率更高
    • Fisher-Yates 必须从后向前才能保证均匀分布
  2. 随机索引为什么包含 i 本身?

    • 允许元素保持在原位置,这是合法的排列
    • 如果排除 i,会减少可能的排列数量
  3. 为什么不直接用 Collections.shuffle()?

    • Collections.shuffle() 内部也是使用 Fisher-Yates
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值