一、为什么考设计题?
Mermaid · 渲染中(下方为源码)
graph LR A[设计题] --> B[选基础结构] B --> C[多结构组合] C --> D[权衡复杂度]
面试中的"设计数据结构"题考查:
- 组合多种基础结构实现复杂功能
- 权衡时间复杂度(空间换时间)
- API 设计能力(接口清晰、边界处理)
核心思路:没有万能结构,只有组合。哈希表 + 链表、哈希表 + 堆、数组 + 哈希...
二、经典设计题
2.1 LRU 缓存(哈希表 + 双向链表)
已在 LRU 缓存 中详解。核心:
class LRUCache {
private int capacity;
private Map<Integer, Node> map;
private Node head, tail; // 虚拟头尾
public int get(int key) { /* 查找 + 移到头部 */ }
public void put(int key, int value) { /* 插入/更新 + 淘汰尾部 */ }
}- get/put 均 O(1)
2.2 LFU 缓存(双哈希 + 双向链表)
问题:淘汰使用频率最低的,频率相同淘汰最久未使用的。
class LFUCache {
private int capacity, minFreq;
private Map<Integer, Node> keyMap; // key → 节点
private Map<Integer, LinkedHashSet<Node>> freqMap; // freq → 节点集合
public int get(int key) {
if (!keyMap.containsKey(key)) return -1;
Node node = keyMap.get(key);
increaseFreq(node);
return node.val;
}
public void put(int key, int value) {
if (capacity == 0) return;
if (keyMap.containsKey(key)) {
Node node = keyMap.get(key);
node.val = value;
increaseFreq(node);
return;
}
if (keyMap.size() >= capacity) {
// 淘汰 minFreq 中最旧的
LinkedHashSet<Node> set = freqMap.get(minFreq);
Node evict = set.iterator().next();
set.remove(evict);
keyMap.remove(evict.key);
}
Node newNode = new Node(key, value, 1);
keyMap.put(key, newNode);
freqMap.computeIfAbsent(1, k -> new LinkedHashSet<>()).add(newNode);
minFreq = 1;
}
private void increaseFreq(Node node) {
int oldFreq = node.freq;
freqMap.get(oldFreq).remove(node);
if (freqMap.get(oldFreq).isEmpty() && oldFreq == minFreq) minFreq++;
node.freq++;
freqMap.computeIfAbsent(node.freq, k -> new LinkedHashSet<>()).add(node);
}
}2.3 最小栈(辅助栈)
class MinStack {
private Deque<Integer> stack = new ArrayDeque<>();
private Deque<Integer> minStack = new ArrayDeque<>();
public void push(int val) {
stack.push(val);
minStack.push(minStack.isEmpty() ? val : Math.min(val, minStack.peek()));
}
public void pop() { stack.pop(); minStack.pop(); }
public int top() { return stack.peek(); }
public int getMin() { return minStack.peek(); }
}2.4 用栈实现队列
class MyQueue {
private Deque<Integer> in = new ArrayDeque<>();
private Deque<Integer> out = new ArrayDeque<>();
public void push(int x) { in.push(x); }
public int pop() {
if (out.isEmpty()) transfer();
return out.pop();
}
public int peek() {
if (out.isEmpty()) transfer();
return out.peek();
}
private void transfer() {
while (!in.isEmpty()) out.push(in.pop());
}
}- 均摊 O(1):每个元素最多被转移一次
2.5 数据流的中位数(双堆)
class MedianFinder {
private PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
private PriorityQueue<Integer> minHeap = new PriorityQueue<>();
public void addNum(int num) {
maxHeap.offer(num);
minHeap.offer(maxHeap.poll());
if (minHeap.size() > maxHeap.size()) {
maxHeap.offer(minHeap.poll());
}
}
public double findMedian() {
if (maxHeap.size() > minHeap.size()) return maxHeap.peek();
return (maxHeap.peek() + minHeap.peek()) / 2.0;
}
}- addNum:O(log n),findMedian:O(1)
2.6 前缀树(Trie)
class Trie {
private Trie[] children = new Trie[26];
private boolean isEnd;
public void insert(String word) {
Trie node = this;
for (char c : word.toCharArray()) {
int idx = c - 'a';
if (node.children[idx] == null) node.children[idx] = new Trie();
node = node.children[idx];
}
node.isEnd = true;
}
public boolean search(String word) {
Trie node = searchPrefix(word);
return node != null && node.isEnd;
}
public boolean startsWith(String prefix) {
return searchPrefix(prefix) != null;
}
private Trie searchPrefix(String s) {
Trie node = this;
for (char c : s.toCharArray()) {
int idx = c - 'a';
if (node.children[idx] == null) return null;
node = node.children[idx];
}
return node;
}
}三、设计题的思路框架
| 步骤 | 内容 |
|---|---|
| 1. 明确操作 | 列出所有 API 及其期望复杂度 |
| 2. 选基础结构 | 哈希表(O(1)查找)+ 有序结构(链表/堆) |
| 3. 设计关联 | 结构之间如何互相引用 |
| 4. 处理边界 | 容量满、key 不存在、空结构 |
常见组合模式
| 需求 | 组合 |
|---|---|
| O(1) 查找 + O(1) 删除 | 哈希表 + 双向链表 |
| O(1) 查找 + 有序 | 哈希表 + 跳表/平衡树 |
| 动态最值 | 堆(优先队列) |
| 前缀匹配 | Trie |
| 区间查询 | 线段树 / 树状数组 |
四、面试常见题
- 🟢 最小栈、用栈实现队列、用队列实现栈
- 🟡 LRU 缓存、数据流中位数、前缀树
- 🟠 LFU 缓存、全 O(1) 数据结构、随机化集合
- 🔴 最大频率栈、范围模块、序列化二叉树
五、调试技巧
- 画图:双向链表操作必须画图,标注指针变化顺序。
- 虚拟节点:链表加 dummy head/tail 避免空判断。
- 一致性:多个结构必须同步更新(如 map 和链表)。
- 容量检查:put 之前先判断是否需要淘汰。