1. 逆波兰表达式求值解析与实现
1.1 逆波兰表达式原理
逆波兰表达式(Reverse Polish Notation,RPN)是一种数学表达式的书写方式,其特点是运算符位于操作数之后。这种表示法最大的优势是不需要括号来标识运算的优先级,所有运算顺序完全由操作符的位置决定。
传统中缀表达式:(2 + 3) * 4
逆波兰表达式:2 3 + 4 *
这种表达式的计算过程天然适合用栈结构来实现。当遇到数字时压入栈中,遇到运算符时弹出栈顶的两个元素进行计算,然后将结果重新压入栈中。整个过程只需要一次从左到右的扫描即可完成计算。
1.2 代码实现详解
cpp复制class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
for (auto& x : tokens){
char c = x[0];
// 处理数字(包括负数)
if (x.size() > 1 || isdigit(c)) {
st.push(stoi(x));
continue;
}
// 弹出第二个操作数
int s = st.top();
st.pop();
// 根据运算符进行计算
if(c == '+'){
st.top() += s;
}else if (c == '-'){
st.top() -= s;
}else if (c == '*'){
st.top() *= s;
}else{
st.top() /= s;
}
}
return st.top();
}
};
关键点解析:
- 数字判断逻辑:
x.size() > 1 || isdigit(c)这个条件可以正确处理负数情况,因为负数第一个字符是'-'但长度大于1 - 运算顺序注意:先弹出的元素是第二个操作数,这在减法和除法中尤为重要
- 栈操作:每次运算后结果直接存入栈顶,减少了中间变量的使用
1.3 边界条件与测试用例
常见边界情况:
- 单个数字的表达式:
["42"] - 连续运算:
["4","13","5","/","+"] - 负数处理:
["3","-4","+"] - 大数运算:注意整型溢出问题
提示:在实际面试中,建议先询问面试官关于除法的处理方式(向零取整还是向下取整),不同语言可能有不同实现。
2. 滑动窗口最大值问题解析
2.1 问题分析与暴力解法
滑动窗口最大值问题要求我们在一个数组上,对于每个大小为k的窗口,找出其中的最大值。最直观的解法是对于每个窗口,遍历其中的所有元素找出最大值,这样的时间复杂度是O(n*k)。
对于n=1e5量级的数据,这样的解法显然会超时。我们需要一种能在O(1)时间内获取当前窗口最大值的数据结构。
2.2 单调队列解法
单调队列是一种能在O(1)时间内获取队列中最大/最小值的特殊数据结构。其核心思想是维护一个队列,其中元素按照从大到小的顺序排列,且保证队列中的元素都在当前窗口内。
cpp复制vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> dq;
vector<int> res;
for(int i = 0; i < nums.size(); i++) {
// 移除超出窗口范围的元素
if(!dq.empty() && dq.front() == i - k) {
dq.pop_front();
}
// 维护单调递减队列
while(!dq.empty() && nums[dq.back()] < nums[i]) {
dq.pop_back();
}
dq.push_back(i);
// 当窗口形成时记录结果
if(i >= k - 1) {
res.push_back(nums[dq.front()]);
}
}
return res;
}
关键点解析:
- 队列中存储的是元素下标而非值,便于判断是否在窗口内
- 每次新元素加入时,从队尾移除所有小于它的元素,保持队列单调递减
- 队首元素即为当前窗口最大值,但当它超出窗口范围时需要移除
2.3 复杂度分析与优化
- 时间复杂度:O(n),每个元素最多入队出队一次
- 空间复杂度:O(k),队列最大长度为k
注意事项:在实际实现中,容易犯的错误包括:
- 忘记处理初始窗口未形成的情况
- 队列中存储值而非下标导致无法判断元素是否在窗口内
- 没有正确处理相等元素的情况
3. 前K个高频元素问题解析
3.1 问题分析与基本解法
这个问题要求我们找出数组中出现频率最高的k个元素。最直接的思路是:
- 统计每个元素的出现频率(哈希表)
- 对频率进行排序
- 取前k个元素
这种方法的时间复杂度主要在排序步骤,为O(n log n)。我们可以使用堆或快速选择算法来优化。
3.2 小根堆解法
维护一个大小为k的小根堆,当堆中元素超过k时,弹出堆顶(当前最小的频率),这样最终堆中剩下的就是频率最大的k个元素。
cpp复制vector<int> topKFrequent(vector<int>& nums, int k) {
// 统计频率
unordered_map<int, int> freq;
for(int num : nums) freq[num]++;
// 定义小根堆比较函数
auto cmp = [](pair<int,int>& a, pair<int,int>& b) {
return a.second > b.second;
};
priority_queue<pair<int,int>, vector<pair<int,int>>, decltype(cmp)> pq(cmp);
// 维护大小为k的堆
for(auto& [num, count] : freq) {
pq.push({num, count});
if(pq.size() > k) pq.pop();
}
// 提取结果
vector<int> res;
while(!pq.empty()) {
res.push_back(pq.top().first);
pq.pop();
}
return res;
}
复杂度分析:
- 时间复杂度:O(n log k),每个元素插入堆的操作是O(log k)
- 空间复杂度:O(n)用于存储频率统计
3.3 快速选择算法解法
快速选择算法是快速排序的变种,可以在平均O(n)时间内找到第k大的元素。我们可以基于频率进行快速选择。
cpp复制class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> freq;
for(int num : nums) freq[num]++;
vector<pair<int, int>> elements(freq.begin(), freq.end());
vector<int> res;
// 快速选择
int left = 0, right = elements.size() - 1;
while(left <= right) {
int pivot = partition(elements, left, right);
if(pivot == k - 1) {
for(int i = 0; i <= pivot; i++) {
res.push_back(elements[i].first);
}
break;
} else if(pivot < k - 1) {
left = pivot + 1;
} else {
right = pivot - 1;
}
}
return res;
}
int partition(vector<pair<int, int>>& nums, int left, int right) {
int pivot = nums[right].second;
int i = left;
for(int j = left; j < right; j++) {
if(nums[j].second >= pivot) {
swap(nums[i++], nums[j]);
}
}
swap(nums[i], nums[right]);
return i;
}
};
关键点:
- 分区函数将频率高的元素放在左侧
- 根据分区结果决定继续处理左半部分还是右半部分
- 当分区点正好是第k-1个位置时,左侧就是前k高频元素
3.4 两种解法的比较
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 小根堆 | O(n log k) | O(n) | k远小于n时更高效 |
| 快速选择 | 平均O(n) | O(n) | 需要原地修改时更优 |
实际应用建议:在面试中,通常期望先提出小根堆解法,因为它更直观且易于实现。如果面试官要求优化,再讨论快速选择算法。
4. 算法实战经验分享
4.1 栈与队列的应用场景
栈和队列是算法中最基础但最强大的数据结构之一。在实际应用中:
-
栈适合处理具有"最近相关性"的问题,如:
- 括号匹配
- 函数调用栈
- 浏览器的前进后退
- 逆波兰表达式
-
队列(特别是双端队列)适合处理需要维护顺序的问题:
- 滑动窗口问题
- 广度优先搜索
- 任务调度
4.2 单调数据结构的使用技巧
单调栈/队列是解决许多复杂问题的利器,其核心在于维护数据的有序性。使用时需要注意:
- 明确维护的是递增还是递减序列
- 确定存储的是值还是索引(滑动窗口问题通常需要存储索引)
- 处理相等元素时的逻辑(通常取决于具体问题要求)
4.3 优先级队列的实用技巧
优先级队列(堆)在解决Top K问题时非常高效。实际使用中:
- C++中
priority_queue默认是大根堆,要创建小根堆需要自定义比较器 - 当需要根据元素的多个属性排序时,可以存储pair或自定义结构体
- 对于动态数据流中的Top K问题,堆是最佳选择
4.4 算法选择的心得
在实际解决问题时,选择哪种算法需要考虑:
- 数据规模:小数据量时简单算法可能更优
- 数据特征:部分有序的数据可能使某些算法更高效
- 实现复杂度:在时间有限的情况下选择更易实现的算法
- 语言特性:不同语言对某些数据结构的支持程度不同
例如在Python中,由于heapq模块只支持小根堆,要实现大根堆需要存储负值;而在C++中可以直接指定比较器。