物理操作转化为逻辑操作的代码示范
问题
题目简述:设计一个数据结构,支持以 O(1) 的时间复杂度插入、删除元素,并且能以 O(1) 的时间等概率随机返回一个元素。
为什么契合你的思维:
物理直觉:数组随机访问是 O(1),但删除不是;哈希表插入删除是 O(1),但无法随机访问。
记账思维(状态压缩与映射):将两者结合。用数组存真实数据,用哈希表记录“值 -> 数组索引”的映射。删除时,把数组末尾的元素“搬”到被删除元素的位置,然后更新哈希表。这里没有真正去“整理”数组,而是通过修改索引的“账本”(哈希表)实现了逻辑上的 O(1) 删除。
代码示范及解读
这道题是“记账思维”中状态压缩与映射的经典体现。核心在于:用数组保证随机访问,用哈希表记录“值→索引”的账本,删除时通过“末尾元素填补空缺 + 更新账本”实现逻辑上的 O(1) 删除,避免物理搬运。
下面是完整 Python 实现,关键位置标注了“记账”操作:
import random
class RandomizedSet:
def __init__(self):
# 数组:存储真实数据(保证 O(1) 随机访问)
self.nums = []
# 账本:记录 "值 -> 数组索引" 的映射(保证 O(1) 查找/删除定位)
self.val_to_idx = {}
def insert(self, val: int) -> bool:
# 如果值已存在,直接返回 False(不修改账本)
if val in self.val_to_idx:
return False
# 【记账】将新值追加到数组末尾,并在账本中记录其索引
self.val_to_idx[val] = len(self.nums)
self.nums.append(val)
return True
def remove(self, val: int) -> bool:
# 如果值不存在,直接返回 False(无需改账本)
if val not in self.val_to_idx:
return False
# 【核心记账操作】删除时不做物理搬运,而是"替换+更新账本"
idx = self.val_to_idx[val] # 1. 从账本查到待删除元素的索引
last_val = self.nums[-1] # 2. 拿到数组末尾元素
# 3. 把末尾元素搬到待删除位置(覆盖),并更新账本中末尾元素的索引
self.nums[idx] = last_val
self.val_to_idx[last_val] = idx
# 4. 删除数组末尾元素,并从账本中移除待删除值的记录
self.nums.pop()
del self.val_to_idx[val]
return True
def getRandom(self) -> int:
# 数组天然支持 O(1) 随机访问,直接从账本对应的数组中随机取
return random.choice(self.nums)
💡 记账思维拆解
-
为什么不用纯数组/纯哈希表?
- 纯数组:删除需要移动后续所有元素(O(N) 物理搬运)。
- 纯哈希表:无法等概率随机访问(键无序)。
- 结合后:数组负责“随机访问的物理载体”,哈希表负责“快速定位的逻辑账本”,两者互补。
-
删除操作的“记账”本质
- 物理直觉:删除
nums[idx]后,要把idx+1到末尾的元素全部前移 → O(N)。 - 记账思维:不移动中间元素,而是把末尾元素“挪”到空缺位置。因为末尾元素被挪走后,原末尾位置自然空出,直接
pop()即可。整个过程只修改了 2 个数组位置和 2 条账本记录,完全避免了大规模物理搬运。
- 物理直觉:删除
-
边界处理
- 当待删除元素本身就是末尾元素时,
idx == len(nums)-1,此时last_val == val,覆盖操作等价于“自己覆盖自己”,后续pop()和del依然正确,无需特殊判断。
- 当待删除元素本身就是末尾元素时,
⚠️ 复杂度验证
insert:数组append+ 哈希表写入 → O(1)remove:哈希表查找 + 数组赋值 + 哈希表更新 + 数组pop→ O(1)getRandom:random.choice基于数组索引 → O(1)
这道题刷透后,你会深刻理解:当物理操作代价高时,优先思考“能否通过维护一个轻量级账本,把物理操作转化为逻辑操作”——这正是后续 LRU/LFU 缓存设计的底层逻辑。

浙公网安备 33010602011771号