150. 逆波兰表达式求值
题目
给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。 请你计算该表达式。返回一个表示表达式值的整数。
注意:
有效的算符为 '+'、'-'、'*' 和 '/' 。
每个操作数(运算对象)都可以是一个整数或者另一个表达式。
两个整数之间的除法总是 向零截断 。
表达式中不含除零运算。
输入是一个根据逆波兰表示法表示的算术表达式。
答案及所有中间计算结果可以用 32 位 整数表示。
示例 1:
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
解题思路
- 创建一个空栈用于存储数字。
- 遍历tokens数组:
- 如果当前token是数字,将其转换为整数并压入栈中。
- 如果是运算符,则从栈中弹出两个数字,进行相应的运算,然后将结果压回栈中。
- 最后,栈顶的元素就是最终结果。
代码实现
function evalRPN(tokens: string[]): number {
const stack: number[] = [];
for (const token of tokens) {
if (token === '+' || token === '-' || token === '*' || token === '/') {
const b = stack.pop();
const a = stack.pop();
switch (token) {
case '+':
stack.push(a + b);
break;
case '-':
stack.push(a - b);
break;
case '*':
stack.push(a * b);
break;
case '/':
stack.push(Math.trunc(a / b)); // 使用Math.trunc进行向零截断
break;
}
} else {
stack.push(parseInt(token));
}
}
return stack[0];
}
239. 滑动窗口最大值
题目
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
进阶: 你能在线性时间复杂度内解决此题吗?
示例 1:
输入: nums = [1,3,-1,-3,5,3,6,7], k = 3
输出: [3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
示例 2:
输入: nums = [1], k = 1
输出: [1]
提示:
1 <= nums.length <= 105-104 <= nums[i] <= 1041 <= k <= nums.length
解题思路
- 使用双端队列来存储可能成为窗口最大值的元素的索引。
- 队列中的元素保持递减顺序,队首元素是当前窗口的最大值的索引。
- 遍历数组,对于每个元素:
- 如果队列不为空且队首元素已经不在当前窗口内,将其移除。
- 从队尾移除所有小于当前元素的值的索引。
- 将当前元素的索引加入队尾。
- 如果已经形成了一个完整的窗口,将队首元素(即当前窗口最大值)加入结果数组。
代码实现
function maxSlidingWindow(nums: number[], k: number): number[] {
const result: number[] = [];
const deque: number[] = []; // 存储索引
for (let i = 0; i < nums.length; i++) {
// 移除不在当前窗口的元素
if (deque.length > 0 && deque[0] <= i - k) {
deque.shift();
}
// 移除所有小于当前元素的值
while (deque.length > 0 && nums[deque[deque.length - 1]] < nums[i]) {
deque.pop();
}
// 添加当前元素索引
deque.push(i);
// 如果已经形成了一个完整的窗口,添加最大值到结果
if (i >= k - 1) {
result.push(nums[deque[0]]);
}
}
return result;
}
347.前 K 个高频元素
题目
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:
- 输入: nums = [1,1,1,2,2,3], k = 2
- 输出: [1,2]
示例 2:
- 输入: nums = [1], k = 1
- 输出: [1]
提示:
- 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
- 你的算法的时间复杂度必须优于 , n 是数组的大小。
- 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
- 你可以按任意顺序返回答案。
解题思路
- 使用哈希表统计每个元素的频率。
- 使用小顶堆(最小堆)来维护 k 个频率最高的元素。
- 遍历哈希表,将元素加入堆中,如果堆的大小超过 k,则弹出堆顶元素(频率最小的元素)。
- 最后,堆中剩下的 k 个元素就是频率最高的 k 个元素。
代码实现
function topKFrequent(nums: number[], k: number): number[] {
// 使用 Map 统计频率
const frequencyMap = new Map<number, number>();
for (const num of nums) {
frequencyMap.set(num, (frequencyMap.get(num) || 0) + 1);
}
// 创建最小堆
const minHeap = new MinHeap<[number, number]>((a, b) => a[1] - b[1]);
// 遍历频率 Map,维护大小为 k 的最小堆
for (const [num, freq] of frequencyMap.entries()) {
if (minHeap.size() < k) {
minHeap.push([num, freq]);
} else if (freq > minHeap.peek()[1]) {
minHeap.pop();
minHeap.push([num, freq]);
}
}
// 返回堆中的元素
return minHeap.toArray().map(item => item[0]);
}
// 最小堆的实现
class MinHeap<T> {
private heap: T[] = [];
private compare: (a: T, b: T) => number;
constructor(compareFunction: (a: T, b: T) => number) {
this.compare = compareFunction;
}
push(val: T) {
this.heap.push(val);
this.bubbleUp(this.heap.length - 1);
}
pop(): T | undefined {
if (this.heap.length === 0) return undefined;
if (this.heap.length === 1) return this.heap.pop();
const result = this.heap[0];
this.heap[0] = this.heap.pop()!;
this.bubbleDown(0);
return result;
}
peek(): T {
return this.heap[0];
}
size(): number {
return this.heap.length;
}
toArray(): T[] {
return [...this.heap];
}
private bubbleUp(index: number) {
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2);
if (this.compare(this.heap[index], this.heap[parentIndex]) < 0) {
[this.heap[index], this.heap[parentIndex]] = [this.heap[parentIndex], this.heap[index]];
index = parentIndex;
} else {
break;
}
}
}
private bubbleDown(index: number) {
const lastIndex = this.heap.length - 1;
while (true) {
let smallestIndex = index;
const leftIndex = 2 * index + 1;
const rightIndex = 2 * index + 2;
if (leftIndex <= lastIndex && this.compare(this.heap[leftIndex], this.heap[smallestIndex]) < 0) {
smallestIndex = leftIndex;
}
if (rightIndex <= lastIndex && this.compare(this.heap[rightIndex], this.heap[smallestIndex]) < 0) {
smallestIndex = rightIndex;
}
if (smallestIndex !== index) {
[this.heap[index], this.heap[smallestIndex]] = [this.heap[smallestIndex], this.heap[index]];
index = smallestIndex;
} else {
break;
}
}
}
}
栈与队列总结
应用场景
-
栈:
- 需要后进先出的操作顺序
- 需要匹配括号或标签
- 需要回溯或撤销操作
- 深度优先搜索(DFS)算法
-
队列:
- 需要先进先出的操作顺序
- 需要按照特定顺序处理任务或数据
- 广度优先搜索(BFS)算法
- 需要维护一个滑动窗口
常见题目
-
栈
- 有效的括号(LeetCode 20)
- 逆波兰表达式求值(LeetCode 150)
- 字符串解码(LeetCode 394)
- 删除字符串中的所有相邻重复项(LeetCode 1047)
-
队列:
- 滑动窗口最大值(LeetCode 239)
- 实现栈使用队列(LeetCode 225)
- 设计循环队列(LeetCode 622)
- 实现队列使用栈(LeetCode 232)
解题步骤
-
栈
- 创建一个栈(通常使用数组模拟)
- 遍历输入数据
- 根据问题要求,执行压栈(push)或出栈(pop)操作
- 在适当的时候检查栈顶元素(peek)
- 根据栈的最终状态或处理过程得出结果
-
队列
- 创建一个队列(可以使用数组或链表模拟)
- 遍历输入数据
- 根据问题要求,执行入队(enqueue)或出队(dequeue)操作
- 在适当的时候检查队首元素(front)
- 根据队列的最终状态或处理过程得出结果