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

难度:中等 | 主题:数组、哈希表、设计

题目

设计一个数据结构,支持在平均 O(1) 时间内完成以下操作:

  • insert(val):当元素不存在时插入并返回 True;已存在返回 False

  • remove(val):当元素存在时删除并返回 True;不存在返回 False

  • getRandom():随机返回现有集合中的一个元素,每个元素被返回的概率必须相同

示例

 
输入:
 
["RandomizedSet","insert","remove","insert","getRandom","remove","insert","getRandom"]
 
[[],[1],[2],[2],[],[1],[2],[]]
 
输出:
 
[null,true,false,true,2,true,false,2]
 

思路

先讲个故事:电影院换座

电影院坐满了人,每个人都有自己的座位号。突然有个人要离开(删除),工作人员不想让整排人都挪位置(O(n)),于是想了个聪明办法:

让最后一排的人搬到空出来的座位上,然后最后一排座位空着,直接撤掉。

这样只有两个人挪了屁股,其他人动都没动。这就是 O(1) 删除的奥秘——交换 + pop


引导式推导:为什么需要两个数据结构?

需求拆解

操作需要的特性选什么数据结构
插入/查找O(1) 判断是否存在、O(1) 定位哈希表(值 → 下标)
随机获取等概率随机选一个数组(random.choice)
删除O(1) 删除任意元素都不能单独做到

关键洞察:哈希表和数组单独看都不完美,但组合起来就能补全彼此的短板。


graph LR

    subgraph 为什么两个结构

        HT["哈希表<br/>✓ O(1) 插入/查找/删除<br/>✗ 无法随机访问"] --> Combine["组合使用<br/>哈希表定位<br/>数组随机"]

        Arr["动态数组<br/>✓ O(1) 随机访问<br/>✗ O(n) 中间删除"] --> Combine

    end

删除的魔术:交换 + pop

直接删除数组中间元素 → 后面所有元素要前移 → O(n)。

解决方案

  1. 把待删除元素和数组最后一个元素交换(或覆盖)

  2. pop 掉最后一个元素(O(1))

  3. 更新最后一个元素在哈希表中的下标


graph LR

    subgraph 删除流程

        A["找到 val 在数组的下标 idx"] --> B["nums[idx] = 最后一个元素"]

        B --> C["更新最后一个元素<br/>val_to_index = idx"]

        C --> D["del val_to_index[val]"]

        D --> E["nums.pop() 删除末尾"]

    end


代码

 
import random
 
class RandomizedSet:
 
    def __init__(self):
 
        self.nums = []                    # 动态数组:支持 O(1) 随机访问
 
        self.val_to_index = {}            # 哈希表:值 → 数组下标
 
    def insert(self, val):
 
        if val in self.val_to_index:      # 已存在,不重复插入
 
            return False
 
        self.val_to_index[val] = len(self.nums)  # 记录下标
 
        self.nums.append(val)             # 数组末尾追加
 
        return True
 
    def remove(self, val):
 
        if val not in self.val_to_index:  # 不存在,无法删除
 
            return False
 
        idx = self.val_to_index[val]      # 待删除元素的下标
 
        last_val = self.nums[-1]          # 数组末尾元素
 
        self.nums[idx] = last_val         # 末尾元素覆盖到删除位置
 
        self.val_to_index[last_val] = idx # 更新末尾元素的新下标
 
        del self.val_to_index[val]        # 删除 val 的记录
 
        self.nums.pop()                   # 弹出末尾(已被复制到 idx)
 
        return True
 
    def getRandom(self):
 
        return random.choice(self.nums)   # 等概率随机返回
 

复杂度

指标解释
时间O(1) 平均插入/删除/随机获取均为常数时间
空间O(n)存储 n 个元素,哈希表和数组各一份

实战考量

频率分析

出现在:字节/美团/阿里 常考,约 35% 的设计题常会从这个角度切入。考察你组合基本数据结构解决复杂需求的能力。

延伸思考

Q:为什么不用单链表?

A:链表无法 O(1) 随机访问(getRandom 需要遍历),与题目要求冲突。

Q:删除时先 pop()del 哈希表行不行?

A:不行。如果删除的是末尾元素,pop()nums[-1] 已经变了,last_val 取错。所以必须先 delpop()

Q:如果允许重复元素呢?(381 题)

A:哈希表的值改成”下标集合”(Set[Int]),删除时从一个集合中移除一个下标,集合为空时删除键。

Q:getRandom 的概率真的均等吗?

A:random.choice 基于 randint(0, len-1),只要随机源均匀,每个元素被选中的概率就是 1/n。

Q:如果要求在 getRandom 后删除该元素?

A:先随机拿一个下标,然后用同样的”交换 + pop”逻辑删除。

易错点

  • 删除时必须更新被交换元素在哈希表中的下标,否则后续查找会出错

  • del self.val_to_index[val] 要在 pop() 之前执行

  • 插入时先检查 val in self.val_to_index,避免覆盖已有数据

  • 如果删除的是末尾元素,idx == len(nums)-1,交换逻辑仍然正确(自己覆盖自己,然后 pop)


生活类比

哈希表 + 数组 → 双数据结构组合 → O(1) 全部操作

就像图书馆的管理系统:**借阅索引卡(哈希表)**告诉你在哪个书架,但你不能随机从索引卡里抽一张(不能随机获取)。**书架上的书(数组)**可以随手抽一本,但你要在中间插一本或抽走一本时,所有书都要移位。

聪明管理员的做法:把索引卡和书架配合使用——用索引卡找到位置,用书架抽随机书。当有人还书(删除),让最后一排的书挪到空位,只需改一张索引卡。

两个不完美的结构,组合起来就是完美的。


相关题目

题目关系
146LRU缓存更复杂版本,加容量限制和淘汰策略(同属”双数据结构组合”题型)
380O(1)时间插入删除和获取随机元素本题

→ 返回题单:LeetCode学习路线图 > 一、数组与哈希(含双指针、矩阵)

速记卡(面试闪卡)

Q1:一句话讲清「380. O(1) 时间插入、删除和获取随机元素(Insert Delete GetRandom O(1))」到底是什么?

A:用数组存值、哈希表存值到下标,删除靠交换+pop,平均 O(1) 全操作。

Q2:一、题目:O(1) 三合一 —— 怎么理解?

A:像一个黑盒:插入、删除、等概率随机返回一个元素,三种操作都要平均 O(1)(Insert/Delete/GetRandom)。单个数据结构搞不定,得组合两个。

Q3:二、思路:两个不完美的拼成完美 —— 怎么理解?

A:像图书馆:索引卡(哈希表)能定位但不能随手抽,书架(数组)能随机抽但中间插拔要移位。把两者组合——索引卡找位置、书架随机抽,互补短板(Hybrid structure)。

Q4:三、删除的魔术:交换 + pop —— 怎么理解?

A:像电影院换座:让最后一排的人搬到空位,再撤掉最后一排,只有两人挪屁股(Swap + pop)。O(1) 删除的奥秘,记得更新被搬者的下标(Index update)。

Q5:四、复杂度与实战:组合即正义 —— 怎么理解?

A:时间 O(1) 平均、空间 O(n)。字节/美团/阿里常考,约 35% 设计题从这里切入,考你组合基础结构解复杂需求(Data structure composition)。注意先 del 再 pop。

Q6:核心速记主线有哪些?

  • 结构:动态数组(随机访问)+ 哈希表(值→下标)组合

  • 插入:O(1),记录下标再 append

  • 删除:交换末尾 + pop,更新被换者下标

  • 随机:random.choice 等概率

  • 易错:先 del 哈希再 pop;删末尾时 idx==len-1 仍正确

口诀

A:插入删除O(1)难,

数组哈希两相帮;

删除交换末位弹,

随机一抽正稳当。

相关链接