1. 最大频率栈问题解析
第一次看到LeetCode 895这道题时,我正喝着咖啡准备开始一天的刷题。题目描述很简单:设计一个类似栈的数据结构,但pop操作需要返回出现频率最高的元素。如果多个元素频率相同,就返回最靠近栈顶的那个。我的第一反应是——这不就是给普通栈加了个"统计员"吗?
1.1 问题核心需求
这个数据结构需要支持两个操作:
- push(val):将整数val压入栈
- pop():移除并返回栈中出现频率最高的元素。如果频率相同,返回最近被压入的那个
举个例子:
freqStack = FreqStack() freqStack.push(5) # 栈 [5] freqStack.push(7) # 栈 [5,7] freqStack.push(5) # 栈 [5,7,5] freqStack.push(7) # 栈 [5,7,5,7] freqStack.push(4) # 栈 [5,7,5,7,4] freqStack.push(5) # 栈 [5,7,5,7,4,5] freqStack.pop() # 返回5,因为5出现3次(其他最多2次)→栈[5,7,5,7,4] freqStack.pop() # 返回7,因为5和7都出现2次,但7更靠近栈顶→栈[5,7,5,4] freqStack.pop() # 返回5→栈[5,7,4] freqStack.pop() # 返回4→栈[5,7]1.2 为什么这是个困难题
这道题被标记为"困难"有几个原因:
- 需要同时维护频率信息和入栈顺序
- 常规栈结构无法直接满足需求
- 需要设计高效的数据结构组合(时间复杂度要求O(1))
我在白板上画了半小时才理清思路——原来需要三个核心组件:
- 频率计数器(哈希表):记录每个值的出现次数
- 频率到元素栈的映射:相同频率的元素按入栈顺序保存
- 当前最大频率值:快速定位要弹出的元素
2. 数据结构设计与实现
2.1 核心数据结构选择
经过多次尝试,我发现最优解需要以下数据结构组合:
unordered_map<int, int> freq:记录每个值的出现频率unordered_map<int, stack<int>> group:按频率分组保存元素int maxFreq:记录当前最大频率
关键点:当元素被push时,它的频率会增加,并被放入对应频率的栈中。pop时直接从maxFreq对应的栈顶取元素。
2.2 C++实现详解
class FreqStack { private: unordered_map<int, int> freq; unordered_map<int, stack<int>> group; int maxFreq = 0; public: void push(int val) { int f = ++freq[val]; // 更新频率 maxFreq = max(maxFreq, f); // 更新最大频率 group[f].push(val); // 放入对应频率栈 } int pop() { int val = group[maxFreq].top(); // 获取当前最高频率元素 group[maxFreq].pop(); // 移除该元素 freq[val]--; // 更新频率 // 如果当前频率栈为空,降低maxFreq if (group[maxFreq].empty()) { maxFreq--; } return val; } };2.3 时间复杂度分析
- push操作:O(1)
- 哈希表查找和更新:O(1)
- 栈操作:O(1)
- pop操作:O(1)
- 栈顶访问和弹出:O(1)
- 哈希表更新:O(1)
- maxFreq调整:O(1)
3. 关键问题与解决方案
3.1 为什么需要group结构
这是我最初设计时踩过的坑。如果只用频率计数器,当多个元素频率相同时,无法快速确定哪个更靠近栈顶。group结构完美解决了这个问题——每个频率对应一个栈,自然保持了入栈顺序。
3.2 边界条件处理
几个容易出错的场景:
- 连续pop导致maxFreq递减的情况
- 相同元素多次push后的频率更新
- 当所有元素频率都为1时的行为(退化为普通栈)
我的测试用例:
# 测试用例1:相同元素多次push stack.push(1) stack.push(1) stack.push(2) assert stack.pop() == 1 # 频率2 > 1 assert stack.pop() == 2 # 频率都为1,2更靠近栈顶 # 测试用例2:maxFreq递减 stack.push(3) stack.push(3) stack.push(2) stack.push(2) stack.pop() # 3 (freq=2) stack.pop() # 2 (freq=2) stack.pop() # 3 (freq=1)4. 不同语言实现对比
4.1 Java版本特点
class FreqStack { private Map<Integer, Integer> freq; private Map<Integer, Deque<Integer>> group; private int maxFreq; public FreqStack() { freq = new HashMap<>(); group = new HashMap<>(); maxFreq = 0; } public void push(int val) { int f = freq.getOrDefault(val, 0) + 1; freq.put(val, f); maxFreq = Math.max(maxFreq, f); group.computeIfAbsent(f, z->new ArrayDeque()).push(val); } public int pop() { int val = group.get(maxFreq).pop(); freq.put(val, freq.get(val)-1); if (group.get(maxFreq).isEmpty()) { maxFreq--; } return val; } }Java注意点:
- 使用Deque替代Stack(性能更好)
- computeIfAbsent简化group初始化
- 需要处理null的情况(getOrDefault)
4.2 Python实现技巧
from collections import defaultdict class FreqStack: def __init__(self): self.freq = defaultdict(int) self.group = defaultdict(list) self.max_freq = 0 def push(self, val: int) -> None: self.freq[val] += 1 self.max_freq = max(self.max_freq, self.freq[val]) self.group[self.freq[val]].append(val) # 用list模拟栈 def pop(self) -> int: val = self.group[self.max_freq].pop() self.freq[val] -= 1 if not self.group[self.max_freq]: self.max_freq -= 1 return valPython特色:
- 使用defaultdict避免键检查
- 用list的append/pop模拟栈(Python中栈通常用list实现)
- 不需要显式初始化内部数据结构
5. 实际应用场景
这种数据结构在以下场景很有价值:
- 缓存系统:优先淘汰高频访问项
- 推荐系统:展示高频交互内容
- 负载均衡:路由高频请求到不同服务器
- 数据分析:实时统计流数据中的高频元素
我在工作中曾用类似结构实现过一个实时热点新闻系统——每当用户点击新闻时调用push,展示热点时调用pop,效果非常好。
6. 常见面试问题
根据我的面试经验,面试官常会追问:
如果要求pop返回频率最低的元素怎么修改?
- 解决方案:维护minFreq并同步更新
如果数据量非常大(内存放不下)怎么办?
- 解决方案:使用外部存储+缓存,或近似算法
如何扩展支持多线程?
- 解决方案:对共享变量加锁(但会影响性能)
如果pop需要返回最早入栈的元素怎么处理?
- 解决方案:额外维护一个时间戳队列
7. 优化与变种
7.1 空间优化方案
当元素频率降低时,group中的低频率栈可能永远不会再被访问。可以添加清理逻辑:
void cleanup(int freq) { if (group[freq].empty()) { group.erase(freq); } }7.2 支持频次阈值
实际应用中可能需要只关注高频元素:
void push(int val, int threshold) { if (++freq[val] >= threshold) { group[freq[val]].push(val); maxFreq = max(maxFreq, freq[val]); } }7.3 多维度频率统计
扩展支持时间维度频率:
struct TimestampedVal { int val; time_t timestamp; }; unordered_map<int, stack<TimestampedVal>> timeGroup;8. 刷题建议
对于这类设计题,我的刷题方法是:
- 先理解常规数据结构(栈/哈希表)的特性
- 在白板上画出操作流程
- 编写测试用例验证边界条件
- 思考时间/空间复杂度
- 尝试不同语言的实现
这道题在LeetCode周赛中出现过多次变种,建议把相关题目都练习一遍:
- LFU Cache
- All O`one Data Structure
- Insert Delete GetRandom O(1)