欢迎光临
我们一直在努力

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

在算法题中,“O(1) 时间复杂度”是高频要求,尤其涉及插入、删除、随机访问三类操作时,单一数据结构往往难以满足所有需求。本文将拆解 LeetCode 380 题,讲解如何通过“数组+哈希表”的组合结构,实现一个高效的 RandomizedSet 类,并解析核心代码的设计逻辑与优化方向。

一、题目分析

题目要求

实现 RandomizedSet 类,支持以下三个操作,且每个操作的平均时间复杂度必须为 O(1):

  • insert(int val):元素 val 不存在时插入,返回 true;存在则返回 false。

  • remove(int val):元素 val 存在时删除,返回 true;不存在则返回 false。

  • getRandom():随机返回集合中的一个元素,每个元素被返回的概率相同(测试用例保证调用时集合非空)。

核心矛盾

单一数据结构的局限性:

  • 数组:随机访问(arr[index])是 O(1),但插入/删除中间元素需移动后续元素,时间复杂度 O(n)。

  • 哈希表(对象/Map):插入/删除/查找是 O(1),但无法通过索引随机访问元素,无法满足 getRandom() 需求。

  • 解决方案:组合两者优势——用数组保证随机访问,用哈希表存储“元素→索引”映射,快速定位元素位置,规避数组插入/删除的 O(n) 操作。

    二、代码实现与逐行解析

    以下是基于 TypeScript 的实现代码,核心逻辑为“数组存元素,对象存索引映射”,完全满足 O(1) 平均时间复杂度要求。

    class RandomizedSet {
    // 数组:存储元素,保证getRandom()的O(1)随机访问
    private arr: Array<number>;
    // 对象:存储“元素→索引”映射,实现O(1)查找元素位置
    private comObj: Record<number, number>;

    constructor() {
    this.arr = [];
    this.comObj = {};
    }

    insert(val: number): boolean {
    // 先判断元素是否存在(O(1):对象属性查找)
    if (val in this.comObj) {
    return false;
    }
    // 存储元素索引:数组长度即为新元素的索引(push前长度不变)
    this.comObj[val] = this.arr.length;
    // 数组尾部插入元素(O(1):无需移动其他元素)
    this.arr.push(val);
    return true;
    }

    remove(val: number): boolean {
    // 元素不存在则返回false(O(1)查找)
    if (!(val in this.comObj)) {
    return false;
    }
    // 获取待删除元素的索引
    const deleteIndex = this.comObj[val];
    // 获取数组最后一个元素(用于交换)
    const lastVal = this.arr[this.arr.length 1];

    // 关键操作:将待删除元素与最后一个元素交换
    this.arr[deleteIndex] = lastVal;
    // 更新最后一个元素的索引映射(O(1)赋值)
    this.comObj[lastVal] = deleteIndex;

    // 数组尾部删除元素(O(1):无需移动其他元素)
    this.arr.pop();
    // 从对象中删除待删除元素的映射(O(1)删除)
    delete this.comObj[val];
    return true;
    }

    getRandom(): number {
    // 生成[0, arr.length)的随机整数(O(1)数学运算)
    const randomIndex = Math.floor(Math.random() * this.arr.length);
    // 数组索引访问元素(O(1))
    return this.arr[randomIndex];
    }
    }

    核心逻辑拆解

    1. insert 方法(O(1))

    步骤:先通过 val in this.comObj 判断元素是否存在(对象属性查找为 O(1));若不存在,先记录元素的索引(数组当前长度即为新元素插入后的索引),再将元素插入数组尾部(push 操作 O(1)),最后更新对象映射。

    注意:必须先记录索引再 push,因为 push 后数组长度会增加,索引会对应变化。

    2. remove 方法(O(1))

    这是最关键的优化点,核心是“交换+尾部删除”规避 O(n) 遍历:

  • 获取待删除元素的索引 deleteIndex 和数组最后一个元素 lastVal。

  • 将 lastVal 赋值到 deleteIndex 位置,覆盖待删除元素(O(1) 赋值)。

  • 更新 lastVal 在对象中的索引为 deleteIndex(保持映射一致性)。

  • 删除数组最后一个元素(pop 操作 O(1)),再删除对象中待删除元素的映射。

  • 通过这种方式,无需遍历数组删除元素,将时间复杂度从 O(n) 降至 O(1)。

    3. getRandom 方法(O(1))

    利用数组索引访问的优势:生成一个 [0, 数组长度) 的随机整数作为索引,直接通过 arr[randomIndex] 获取元素,全程无遍历,时间复杂度 O(1)。

    三、关键注意事项与避坑点

    1. 元素存在性判断:用 in 而非直接判断值

    若写成 if (this.comObj[val]) 会出现误判:当元素索引为 0 时,this.comObj[val] = 0,转为布尔值为 false,会误判元素不存在。

    val in this.comObj 直接判断“键是否存在”,与值无关,是最严谨的方式。

    2. 索引一致性维护

    删除元素时,必须同步更新最后一个元素的索引映射,否则后续查找该元素会得到错误索引,导致功能异常。

    3. 空数组边界处理

    题目虽保证 getRandom() 调用时集合非空,但实际开发中可添加边界判断,避免数组为空时返回 undefined:

    getRandom(): number {
    if (this.arr.length === 0) {
    throw new Error("集合为空,无法获取随机元素");
    }
    const randomIndex = Math.floor(Math.random() * this.arr.length);
    return this.arr[randomIndex];
    }

    四、优化方向:用 Map 替代 Object

    当前代码用 Record<number, number> 作为哈希表,存在一个隐式问题:JavaScript 对象的键本质是字符串/符号,数字键会被自动转为字符串(如 comObj[5] 实际存储为 comObj["5"])。虽不影响功能,但类型不够精准。

    优化方案:用Map<number, number> 替代对象,原生支持数字键,语义更清晰,且 has()、set()、delete() 方法更贴合键值对操作:

    class RandomizedSet {
    private arr: number[];
    private comMap: Map<number, number>;

    constructor() {
    this.arr = [];
    this.comMap = new Map();
    }

    insert(val: number): boolean {
    if (this.comMap.has(val)) {
    return false;
    }
    this.comMap.set(val, this.arr.length);
    this.arr.push(val);
    return true;
    }

    remove(val: number): boolean {
    if (!this.comMap.has(val)) {
    return false;
    }
    const deleteIndex = this.comMap.get(val)!; // 非空断言,已判断存在
    const lastVal = this.arr[this.arr.length 1];
    if (deleteIndex !== this.arr.length 1) { // 优化:若为最后一个元素,跳过交换
    this.arr[deleteIndex] = lastVal;
    this.comMap.set(lastVal, deleteIndex);
    }
    this.arr.pop();
    this.comMap.delete(val);
    return true;
    }

    getRandom(): number {
    const randomIndex = Math.floor(Math.random() * this.arr.length);
    return this.arr[randomIndex];
    }
    }

    额外优化:当待删除元素就是数组最后一个元素时,可跳过交换逻辑,减少无效操作,小幅提升性能。

    五、复杂度证明

    • insert:对象/Map 查找(O(1))+ 数组 push(O(1))+ 映射更新(O(1)),整体 O(1)。

    • remove:对象/Map 查找(O(1))+ 元素交换(O(1))+ 数组 pop(O(1))+ 映射删除(O(1)),整体 O(1)。

    • getRandom:随机数生成(O(1))+ 数组索引访问(O(1)),整体 O(1)。

    所有操作均为常数级,满足题目“平均 O(1) 时间复杂度”的要求。

    六、总结

    本题的核心思路是“组合数据结构弥补单一结构的不足”:数组保证随机访问,哈希表(对象/Map)保证高效的插入/删除/查找。其中,删除操作的“交换+尾部删除”是实现 O(1) 的关键技巧,也是算法题中高频的优化思路。

    无论是用对象还是 Map 作为哈希表,核心逻辑一致,可根据场景选择:对象更简洁,Map 类型更精准、性能更稳定(适合大量键值对操作)。掌握这种“1+1>2”的数据结构组合思路,能解决更多复杂的算法优化问题。

    赞(0)
    未经允许不得转载:171主机测评 » LeetCode 380. O(1) 时间插入、删除和获取随机元素 题解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址