LeetCode体操-10 | 150. 逆波兰表达式求值、239. 滑动窗口最大值、347.前 K 个高频元素、栈与队列总结

80 阅读6分钟

150. 逆波兰表达式求值

题目

给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。 请你计算该表达式。返回一个表示表达式值的整数。

注意:

有效的算符为 '+'、'-'、'*' 和 '/' 。

每个操作数(运算对象)都可以是一个整数或者另一个表达式。

两个整数之间的除法总是 向零截断 。

表达式中不含除零运算。

输入是一个根据逆波兰表示法表示的算术表达式。

答案及所有中间计算结果可以用 32 位 整数表示。

示例 1:
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9

解题思路

  1. 创建一个空栈用于存储数字。
  2. 遍历tokens数组:
    • 如果当前token是数字,将其转换为整数并压入栈中。
    • 如果是运算符,则从栈中弹出两个数字,进行相应的运算,然后将结果压回栈中。
  3. 最后,栈顶的元素就是最终结果。

image.png

代码实现

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] <= 104
  • 1 <= k <= nums.length

解题思路

  1. 使用双端队列来存储可能成为窗口最大值的元素的索引。
  2. 队列中的元素保持递减顺序,队首元素是当前窗口的最大值的索引。
  3. 遍历数组,对于每个元素:
    • 如果队列不为空且队首元素已经不在当前窗口内,将其移除。
    • 从队尾移除所有小于当前元素的值的索引。
    • 将当前元素的索引加入队尾。
    • 如果已经形成了一个完整的窗口,将队首元素(即当前窗口最大值)加入结果数组。

image.png

代码实现

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 ≤ 数组中不相同的元素的个数。
  • 你的算法的时间复杂度必须优于 O(nlogn)O(n \log n) , n 是数组的大小。
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
  • 你可以按任意顺序返回答案。

解题思路

  1. 使用哈希表统计每个元素的频率。
  2. 使用小顶堆(最小堆)来维护 k 个频率最高的元素。
  3. 遍历哈希表,将元素加入堆中,如果堆的大小超过 k,则弹出堆顶元素(频率最小的元素)。
  4. 最后,堆中剩下的 k 个元素就是频率最高的 k 个元素。

image.png

代码实现

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;
            }
        }
    }
}

栈与队列总结

image.png

应用场景

  • 栈:

    1. 需要后进先出的操作顺序
    2. 需要匹配括号或标签
    3. 需要回溯或撤销操作
    4. 深度优先搜索(DFS)算法
  • 队列:

    1. 需要先进先出的操作顺序
    2. 需要按照特定顺序处理任务或数据
    3. 广度优先搜索(BFS)算法
    4. 需要维护一个滑动窗口

常见题目

    • 有效的括号(LeetCode 20)
    • 逆波兰表达式求值(LeetCode 150)
    • 字符串解码(LeetCode 394)
    • 删除字符串中的所有相邻重复项(LeetCode 1047)
  • 队列:

    • 滑动窗口最大值(LeetCode 239)
    • 实现栈使用队列(LeetCode 225)
    • 设计循环队列(LeetCode 622)
    • 实现队列使用栈(LeetCode 232)

解题步骤

    1. 创建一个栈(通常使用数组模拟)
    2. 遍历输入数据
    3. 根据问题要求,执行压栈(push)或出栈(pop)操作
    4. 在适当的时候检查栈顶元素(peek)
    5. 根据栈的最终状态或处理过程得出结果
  • 队列

    1. 创建一个队列(可以使用数组或链表模拟)
    2. 遍历输入数据
    3. 根据问题要求,执行入队(enqueue)或出队(dequeue)操作
    4. 在适当的时候检查队首元素(front)
    5. 根据队列的最终状态或处理过程得出结果