物理操作转化为逻辑操作的代码示范

问题

题目简述:设计一个数据结构,支持以 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)

💡 记账思维拆解

  1. 为什么不用纯数组/纯哈希表?

    • 纯数组:删除需要移动后续所有元素(O(N) 物理搬运)。
    • 纯哈希表:无法等概率随机访问(键无序)。
    • 结合后:数组负责“随机访问的物理载体”,哈希表负责“快速定位的逻辑账本”,两者互补。
  2. 删除操作的“记账”本质

    • 物理直觉:删除 nums[idx] 后,要把 idx+1 到末尾的元素全部前移 → O(N)。
    • 记账思维:不移动中间元素,而是把末尾元素“挪”到空缺位置。因为末尾元素被挪走后,原末尾位置自然空出,直接 pop() 即可。整个过程只修改了 2 个数组位置和 2 条账本记录,完全避免了大规模物理搬运。
  3. 边界处理

    • 当待删除元素本身就是末尾元素时,idx == len(nums)-1,此时 last_val == val,覆盖操作等价于“自己覆盖自己”,后续 pop()del 依然正确,无需特殊判断。

⚠️ 复杂度验证

  • insert:数组 append + 哈希表写入 → O(1)
  • remove:哈希表查找 + 数组赋值 + 哈希表更新 + 数组 pop → O(1)
  • getRandomrandom.choice 基于数组索引 → O(1)

这道题刷透后,你会深刻理解:当物理操作代价高时,优先思考“能否通过维护一个轻量级账本,把物理操作转化为逻辑操作”——这正是后续 LRU/LFU 缓存设计的底层逻辑。

posted @ 2026-08-30 21:38  立体风  阅读(9)  评论(0)    收藏  举报