🏗️ 数据结构设计 — 手工打造高效容器

LRU缓存、最小栈、LFU缓存——面试中设计题是区分度最高的题型

📖 数据结构设计的核心思路

设计题考察的不是算法难度,而是数据结构的组合与权衡。每种数据结构都有优劣,设计题要求你根据操作需求,选择合适的组合,达到所有操作的高效。

常见设计题模式: 需求 数据结构组合 ────────────────────────────────────────────── O(1)查找 + O(1)删除 + 顺序 哈希表 + 双向链表 (LRU) O(1)获取最小值 主栈 + 辅助栈 (MinStack) O(1)查找 + 频率优先 哈希表 + 频率桶 (LFU) O(log n)插入 + 中位数 两个堆 (MedianFinder) O(1)随机访问 + O(1)删除 数组 + 哈希表 (RandomSet) 设计心法: 1. 列出所有操作的时间要求 2. 分析每个操作需要什么数据结构 3. 组合数据结构,确保所有操作达标

1. 设计题解题框架

第一步:列出操作要求。把题目要求的所有操作及其期望时间复杂度列出来。例如 LRU:get O(1)、put O(1)。
第二步:分析瓶颈。哪个操作最难高效实现?这个操作决定了你需要什么数据结构。例如 O(1) 删除指定元素 → 需要哈希表定位位置。
第三步:组合结构。单一数据结构往往不够,需要组合。关键是保持两个结构的信息同步(增删改时同步更新)。

2. Python 设计题技巧

# OrderedDict 模拟 LRU(Python 3.7+)
from collections import OrderedDict

class LRUCache:
    def __init__(self, capacity):
        self.cache = OrderedDict()
        self.capacity = capacity
    
    def get(self, key):
        if key not in self.cache:
            return -1
        self.cache.move_to_end(key)  # 移到末尾=最近使用
        return self.cache[key]
    
    def put(self, key, value):
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.capacity:
            self.cache.popitem(last=False)  # 删除最久未用
面试中可以用 OrderedDict 快速实现 LRU,但要能从头手写哈希表+双向链表的版本。面试官可能会要求不使用内置 OrderedDict。

🎯 题目一:LRU 缓存 (LC 146)

题目描述:设计一个满足 LRU(最近最少使用)缓存约束的数据结构,实现 getput 操作,均需 O(1) 时间。

LRU 缓存工作原理 (capacity=2): put(1,1): cache = {1:1} order: [1] put(2,2): cache = {1:1, 2:2} order: [1, 2] get(1): → 1 order: [2, 1] (1被访问,移到最近) put(3,3): cache = {1:1, 3:3} order: [1, 3] (2是最久未用,被淘汰) get(2): → -1 (已淘汰) 核心: 哈希表(key→节点) + 双向链表(维护顺序) 链表头部 = 最久未用 链表尾部 = 最近使用

思路分析

哈希表 + 双向链表:
- 哈希表:O(1) 查找 key 对应的链表节点
- 双向链表:O(1) 删除节点、移动到头部/尾部
- get 命中:将节点移到链表尾部(最近使用)
- put 满了:删除链表头部节点(最久未用),同步删除哈希表
时间复杂度:get O(1),put O(1)

代码实现

class LRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.cache = {}        # key → value
        self.order = []        # 简化版: 用列表维护顺序
    
    def get(self, key: int) -> int:
        if key in self.cache:
            self.order.remove(key)      # O(n) 简化
            self.order.append(key)
            return self.cache[key]
        return -1
    
    def put(self, key: int, value: int) -> None:
        if key in self.cache:
            self.order.remove(key)
        elif len(self.cache) >= self.capacity:
            lru = self.order.pop(0)     # O(n) 简化
            del self.cache[lru]
        self.cache[key] = value
        self.order.append(key)
上面代码用 list 模拟,get/put 的 remove 操作是 O(n)。面试中如果要严格 O(1),需要手写双向链表。LC 提交时 Python 用 OrderedDict 或 list 版本(测试数据规模小时能通过),正式面试需解释 O(1) 版本的实现。

O(1) 版本核心代码

# O(1) 版本: 手写双向链表 + 哨兵节点
class Node:
    def __init__(self, key=0, val=0):
        self.key = key
        self.val = val
        self.prev = None
        self.next = None

class LRUCache:
    def __init__(self, capacity):
        self.cap = capacity
        self.cache = {}  # key → Node
        # 哨兵节点: head(最久) ↔ tail(最近)
        self.head = Node()
        self.tail = Node()
        self.head.next = self.tail
        self.tail.prev = self.head
    
    def _remove(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev
    
    def _add_to_tail(self, node):
        node.prev = self.tail.prev
        node.next = self.tail
        self.tail.prev.next = node
        self.tail.prev = node
    
    def get(self, key):
        if key not in self.cache:
            return -1
        node = self.cache[key]
        self._remove(node)
        self._add_to_tail(node)
        return node.val
    
    def put(self, key, value):
        if key in self.cache:
            self._remove(self.cache[key])
        node = Node(key, value)
        self.cache[key] = node
        self._add_to_tail(node)
        if len(self.cache) > self.cap:
            lru = self.head.next
            self._remove(lru)
            del self.cache[lru.key]

🎯 题目二:最小栈 (LC 155)

题目描述:设计一个栈,支持 push、pop、top 和 getMin 操作,且都是 O(1) 时间。

最小栈工作原理: 操作 主栈 辅助栈(最小值) push(-2) [-2] [-2] push(0) [-2, 0] [-2, -2] push(-3) [-2, 0, -3] [-2, -2, -3] getMin() → -3 pop() [-2, 0] [-2, -2] top() → 0 getMin() → -2 核心: 辅助栈与主栈同步增长 每次push时,辅助栈记录当前最小值

思路分析

双栈法:主栈存数据,辅助栈存每个位置对应的最小值。push 时辅助栈压入 min(当前值, 辅助栈顶);pop 时两个栈同步弹出。辅助栈顶永远是当前最小值。

代码实现

class MinStack:
    def __init__(self):
        self.stack = []
        self.min_stack = []
    
    def push(self, val: int) -> None:
        self.stack.append(val)
        self.min_stack.append(
            min(val, self.min_stack[-1] if self.min_stack else val)
        )
    
    def pop(self) -> None:
        self.stack.pop()
        self.min_stack.pop()
    
    def top(self) -> int:
        return self.stack[-1]
    
    def getMin(self) -> int:
        return self.min_stack[-1]

复杂度分析

所有操作:O(1)

空间复杂度:O(n)

MinStack 的辅助栈优化:可以只在最小值变化时压入辅助栈(不等时压入),pop 时判断是否需要弹出辅助栈。空间更优但代码稍复杂。面试用同步版本即可。

🎯 题目三:LFU 缓存 (LC 460)

题目描述:设计 LFU(最不经常使用)缓存,实现 get 和 put。当缓存满时,淘汰使用频率最低的 key;如果有多个频率相同的,淘汰最久未使用的。

LFU 缓存工作原理 (capacity=2): put(1,1): freq={1:1} buckets={1:[1]} put(2,2): freq={1:1, 2:1} buckets={1:[1,2]} get(1): → 1 freq[1]=2 buckets={1:[2], 2:[1]} put(3,3): 淘汰freq=1中最久未用→ 淘汰2 freq={1:3, 1:2} buckets={1:[3], 2:[1]} get(2): → -1 (已淘汰) get(3): → 3 freq[3]=2 buckets={2:[1,3]} get(1): → 1 freq[1]=3 buckets={2:[3], 3:[1]} 核心: key→(val,freq) + freq→有序key列表

思路分析

三层结构:
- key_to_val_freq:哈希表,key → (value, frequency),O(1) 查找
- freq_to_keys:哈希表,freq → OrderedDict of keys,同一频率的 key 按使用时间排序
- min_freq:当前最小频率,用于淘汰时快速定位
get 时频率+1,put 满时从 min_freq 桶中淘汰最前面的 key。

代码实现

import collections

class LFUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.min_freq = 0
        self.key_to_val_freq = {}  # key → (val, freq)
        self.freq_to_keys = collections.defaultdict(
            collections.OrderedDict
        )  # freq → OrderedDict of keys
    
    def get(self, key: int) -> int:
        if key not in self.key_to_val_freq:
            return -1
        val, freq = self.key_to_val_freq[key]
        # 从旧频率桶中移除
        del self.freq_to_keys[freq][key]
        if not self.freq_to_keys[freq]:
            del self.freq_to_keys[freq]
            if self.min_freq == freq:
                self.min_freq += 1
        # 加入新频率桶
        self.freq_to_keys[freq + 1][key] = None
        self.key_to_val_freq[key] = (val, freq + 1)
        return val
    
    def put(self, key: int, value: int) -> None:
        if self.capacity <= 0:
            return
        if key in self.key_to_val_freq:
            _, freq = self.key_to_val_freq[key]
            self.key_to_val_freq[key] = (value, freq)
            self.get(key)  # 借用get更新频率
            return
        # 新key,需要淘汰
        if len(self.key_to_val_freq) >= self.capacity:
            # 淘汰min_freq桶中最久未用的
            evict_key, _ = self.freq_to_keys[self.min_freq].popitem(last=False)
            del self.key_to_val_freq[evict_key]
        self.key_to_val_freq[key] = (value, 1)
        self.freq_to_keys[1][key] = None
        self.min_freq = 1

复杂度分析

get:O(1),put:O(1)

空间复杂度:O(capacity)

LFU 是 LRU 的进阶版。LRU 按时间淘汰(最久未用),LFU 按频率淘汰(最不常用),频率相同时按时间淘汰。理解 LRU 是理解 LFU 的前提。

🔬 设计题面试策略

数据结构设计题是面试中区分度最高的题型之一,以下是应对策略:

1. 先写接口,再写实现

# 先定义好类的方法签名
class MyDataStructure:
    def __init__(self, ...):
        pass
    def operation1(self, ...):
        pass
    def operation2(self, ...):
        pass

# 然后逐个实现,边写边解释复杂度

2. 常见组合模式

# O(1) 随机访问 + O(1) 删除: 数组 + 哈希表
# 数组存元素,哈希表存元素→索引
# 删除时: 用末尾元素覆盖,O(1)

# O(1) 查找 + 有序: 哈希表 + 平衡树/跳表
# Java: HashMap + TreeMap
# Python: dict + SortedList (from sortedcontainers)

# O(1) 最值 + 栈操作: 主栈 + 辅助栈
# O(1) 中位数: 最大堆 + 最小堆

3. 答题步骤

(1) 确认所有操作及其复杂度要求
(2) 分析每个操作需要什么数据结构支持
(3) 设计组合方案,画图说明数据流转
(4) 先写简化版(可能不是最优),再优化
(5) 处理边界情况:空容器、容量为0等

设计题的评分标准:(1) 功能正确;(2) 时间复杂度达标;(3) 代码清晰;(4) 能解释为什么选择这种组合。面试官看重的是设计思路,不是一次写对。
成就解锁:架构设计师 — 掌握LRU缓存、最小栈、LFU缓存三大经典数据结构设计
LeetCode AC验证:LC 146 LRU Cache ✅ | LC 155 Min Stack ✅ | LC 460 LFU Cache ✅

📝 课后练习

  1. LC 380 O(1)时间插入、删除和获取随机元素(数组+哈希)
  2. LC 295 数据流的中位数(双堆)
  3. LC 432 全 O(1) 的数据结构(双向链表+哈希)
  4. LC 355 设计推特(多路归并+哈希)
  5. LC 341 扁平化嵌套列表迭代器(栈)

🔑 本课要点回顾

📚 扩展阅读

思考题:如何设计一个支持 O(1) 的 insert、delete、getRandom 的数据结构?提示:数组支持 O(1) 随机访问但删除 O(n),哈希表支持 O(1) 查找删除但无法随机访问。组合两者:数组存元素,哈希表存索引,删除时用末尾元素覆盖目标位置。