算法题 O(1) 时间插入、删除和获取随机元素

LeetCode 380. O(1) 时间插入、删除和获取随机元素

问题描述

实现 RandomizedSet 类:

  • RandomizedSet() 初始化 RandomizedSet 对象
  • bool insert(int val) 当元素 val 不存在时,向集合中插入该项,并返回 true;否则,返回 false
  • bool remove(int val) 当元素 val 存在时,从集合中移除该项,并返回 true;否则,返回 false
  • int getRandom() 随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一项)

要求:每个函数都必须在 平均时间复杂度为 O(1) 的情况下完成。

示例

// 初始化一个空的集合
RandomizedSet randomSet = new RandomizedSet();

// 向集合中插入 1 ,返回 true 表示插入成功
randomSet.insert(1);

// 向集合中插入 2 ,返回 true 表示插入成功  
randomSet.insert(2);

// getRandom 应随机返回 1 或 2
randomSet.getRandom();

// 从集合中移除 1 ,返回 true 表示删除成功
randomSet.remove(1);

// 2 已在集合中,所以返回 false 表示插入失败
randomSet.insert(2);

// getRandom 应该返回 2
randomSet.getRandom();

算法思路

核心思想:哈希表 + 动态数组

  1. 哈希表(HashMap)

    • 提供 O(1) 的查找、插入、删除操作
    • 存储 值 -> 索引 的映射关系
  2. 动态数组(ArrayList)

    • 支持 O(1) 的随机访问(通过索引)
    • 可以通过 Math.random() 生成随机索引

关键:数组的删除操作通常是 O(n) 的,需要移动后面的元素。

  • 交换删除
    • 当需要删除某个元素时,将其与数组最后一个元素交换
    • 然后删除最后一个元素(O(1) 操作)
    • 同时更新哈希表中的索引映射

步骤:

插入操作

  1. 检查元素是否已存在(通过哈希表)
  2. 如果不存在,将元素添加到数组末尾
  3. 在哈希表中记录 元素 -> 数组索引 的映射

删除操作

  1. 检查元素是否存在
  2. 如果存在:
    • 获取要删除元素的索引 idx
    • 获取数组最后一个元素 lastElement
    • lastElement 移动到 idx 位置
    • 更新哈希表中 lastElement 的索引为 idx
    • 从数组末尾删除元素
    • 从哈希表中删除 val 的映射

随机获取

  1. 生成 [0, size) 范围内的随机索引
  2. 返回数组中对应位置的元素

代码实现

import java.util.*;

class RandomizedSet {
    /**
     * 动态数组:存储所有元素,支持O(1)随机访问
     */
    private List<Integer> nums;
    
    /**
     * 哈希表:存储元素到数组索引的映射,支持O(1)查找
     */
    private Map<Integer, Integer> valToIndex;
    
    /**
     * 随机数生成器:用于生成随机索引
     */
    private Random random;
    
    /**
     * 构造函数:初始化数据结构
     */
    public RandomizedSet() {
        nums = new ArrayList<>();
        valToIndex = new HashMap<>();
        random = new Random();
    }
    
    /**
     * 插入元素
     * 
     * @param val 要插入的元素
     * @return 如果插入成功返回true,如果元素已存在返回false
     * 
     * 时间复杂度: O(1)
     * 空间复杂度: O(1)
     */
    public boolean insert(int val) {
        // 如果元素已存在,返回false
        if (valToIndex.containsKey(val)) {
            return false;
        }
        
        // 将元素添加到数组末尾
        nums.add(val);
        
        // 在哈希表中记录元素到索引的映射
        // 新元素的索引就是数组的最后一个位置(size-1)
        valToIndex.put(val, nums.size() - 1);
        
        return true;
    }
    
    /**
     * 删除元素
     * 
     * @param val 要删除的元素
     * @return 如果删除成功返回true,如果元素不存在返回false
     * 
     * 时间复杂度: O(1)
     * 空间复杂度: O(1)
     */
    public boolean remove(int val) {
        // 如果元素不存在,返回false
        if (!valToIndex.containsKey(val)) {
            return false;
        }
        
        // 获取要删除元素的索引
        int idx = valToIndex.get(val);
        
        // 获取数组最后一个元素
        int lastElement = nums.get(nums.size() - 1);
        
        // 将最后一个元素移动到要删除元素的位置
        nums.set(idx, lastElement);
        
        // 更新哈希表中最后一个元素的索引
        valToIndex.put(lastElement, idx);
        
        // 从数组末尾删除最后一个元素
        nums.remove(nums.size() - 1);
        
        // 从哈希表中删除要删除元素的映射
        valToIndex.remove(val);
        
        return true;
    }
    
    /**
     * 随机获取一个元素
     * 
     * @return 集合中的随机元素
     * 
     * 时间复杂度: O(1)
     * 空间复杂度: O(1)
     */
    public int getRandom() {
        // 生成 [0, size) 范围内的随机索引
        int randomIndex = random.nextInt(nums.size());
        
        // 返回对应位置的元素
        return nums.get(randomIndex);
    }
}

算法分析

  • 时间复杂度O(1)(平均)

    • insert:哈希表查找 O(1) + 数组添加 O(1)
    • remove:哈希表查找 O(1) + 数组交换 O(1) + 数组删除末尾 O(1)
    • getRandom:随机数生成 O(1) + 数组随机访问 O(1)
  • 空间复杂度O(n)

    • 数组存储 n 个元素
    • 哈希表存储 n 个键值对
  • 关键

    • 结合了哈希表的快速查找和数组的随机访问
    • 通过交换删除将删除操作优化到 O(1)

算法过程

插入操作:

初始状态: nums = [], valToIndex = {}

insert(1):
- 1 不在哈希表中
- nums = [1]
- valToIndex = {1: 0}
- 返回 true

insert(2):
- 2 不在哈希表中  
- nums = [1, 2]
- valToIndex = {1: 0, 2: 1}
- 返回 true

删除操作:

当前状态: nums = [1, 2, 3, 4], valToIndex = {1:0, 2:1, 3:2, 4:3}

remove(2):
- 2 存在,索引 idx = 1
- lastElement = 4
- 将 4 移动到索引 1: nums = [1, 4, 3, 4]
- 更新 valToIndex: {1:0, 4:1, 3:2, 4:3}
- 删除末尾元素: nums = [1, 4, 3]
- 删除 2 的映射: valToIndex = {1:0, 4:1, 3:2}
- 返回 true

随机获取:

当前状态: nums = [1, 4, 3]

getRandom():
- 生成随机索引: 0, 1, 或 2
- 返回 nums[randomIndex]: 1, 4, 或 3
- 每个元素被选中的概率都是 1/3

测试用例

public static void main(String[] args) {
    // 测试用例1:标准示例
    RandomizedSet randomSet = new RandomizedSet();
    System.out.println("Test 1 - insert(1): " + randomSet.insert(1)); // true
    System.out.println("Test 2 - insert(2): " + randomSet.insert(2)); // true
    System.out.println("Test 3 - getRandom(): " + randomSet.getRandom()); // 1 or 2
    System.out.println("Test 4 - remove(1): " + randomSet.remove(1)); // true
    System.out.println("Test 5 - insert(2): " + randomSet.insert(2)); // false
    System.out.println("Test 6 - getRandom(): " + randomSet.getRandom()); // 2
    
    // 测试用例2:边界情况 - 单元素
    RandomizedSet set2 = new RandomizedSet();
    System.out.println("Test 7 - insert(5): " + set2.insert(5)); // true
    System.out.println("Test 8 - remove(5): " + set2.remove(5)); // true
    System.out.println("Test 9 - insert(5): " + set2.insert(5)); // true
    System.out.println("Test 10 - getRandom(): " + set2.getRandom()); // 5
    
    // 测试用例3:重复插入和删除
    RandomizedSet set3 = new RandomizedSet();
    System.out.println("Test 11 - insert(10): " + set3.insert(10)); // true
    System.out.println("Test 12 - insert(20): " + set3.insert(20)); // true
    System.out.println("Test 13 - insert(30): " + set3.insert(30)); // true
    System.out.println("Test 14 - remove(20): " + set3.remove(20)); // true
    System.out.println("Test 15 - remove(20): " + set3.remove(20)); // false
    System.out.println("Test 16 - insert(20): " + set3.insert(20)); // true
    
    // 测试用例4:负数
    RandomizedSet set4 = new RandomizedSet();
    System.out.println("Test 17 - insert(-1): " + set4.insert(-1)); // true
    System.out.println("Test 18 - insert(-2): " + set4.insert(-2)); // true
    System.out.println("Test 19 - getRandom(): " + set4.getRandom()); // -1 or -2
    System.out.println("Test 20 - remove(-1): " + set4.remove(-1)); // true
    
    // 测试用例5:大量操作验证随机性
    RandomizedSet set5 = new RandomizedSet();
    for (int i = 1; i <= 100; i++) {
        set5.insert(i);
    }
    // 随机获取10次,验证不会抛异常
    for (int i = 0; i < 10; i++) {
        int randomVal = set5.getRandom();
        System.out.println("Test 21 - getRandom(): " + randomVal);
        assert randomVal >= 1 && randomVal <= 100;
    }
    
    // 测试用例6:删除后再插入
    RandomizedSet set6 = new RandomizedSet();
    set6.insert(100);
    set6.remove(100);
    set6.insert(100);
    System.out.println("Test 22 - getRandom(): " + set6.getRandom()); // 100
}

关键点

  1. 数据结构

    • ArrayList:支持 O(1) 随机访问和末尾操作
    • HashMap:支持 O(1) 查找、插入、删除
  2. 交换删除

    • 将要删除的元素与最后一个元素交换
    • 删除末尾元素(O(1))
    • 更新哈希表中的索引映射
  3. 索引维护

    • 插入时:新元素索引 = size - 1
    • 删除时:更新被移动元素的索引
  4. 随机性

    • 使用 Random.nextInt(size) 生成均匀分布的随机索引
    • 每个元素被选中的概率相等
  5. 边界情况处理

    • 空集合的插入
    • 重复插入/删除
    • 单元素集合的操作

常见问题

  1. 为什么不能直接用 HashSet?

    • HashSet 支持 O(1) 插入和删除,但不支持 O(1) 随机获取
    • 需要遍历才能随机获取,时间复杂度 O(n)
  2. 交换删除会影响随机性吗?

    • 只是改变了元素在数组中的位置
    • 随机获取时仍然均匀选择所有索引
  3. 如果删除最后一个元素?

    • lastElement 就是要删除的元素
    • 交换后删除末尾,逻辑一致
  4. 空间复杂度能优化?

    • 当前实现已经是最优的 O(n) 空间复杂度
    • 需要同时存储元素和索引映射,无法进一步优化
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值