代码随想录之栈与队列篇

115 阅读9分钟

代码随想录之栈与队列篇

T232-使用栈实现队列

见力扣第232题[使用栈实现队列]

题目描述

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(pushpoppeekempty):

实现 MyQueue 类:

  • void push(int x) 将元素 x 推到队列的末尾
  • int pop() 从队列的开头移除并返回元素
  • int peek() 返回队列开头的元素
  • boolean empty() 如果队列为空,返回 true ;否则,返回 false

我的思路

  • 使用两个栈来模拟队列
  • 执行push(int x)操作的时候,将元素压入到pushStack
  • 执行pop()的时候,判断popStack中是否有元素,如果没有,将pushStack中的元素倒入popStack
class MyQueue {

    private Stack<Integer> pushStack;
    private Stack<Integer> popStack;
    
    
    public MyQueue() {
        this.pushStack = new Stack<>();
        this.popStack = new Stack<>();
    }

    public void push(int x) {
        pushStack.push(x);
    }

    public int pop() {
        // 先检查 popStack 中是否有元素
        if (popStack.isEmpty()) {
            transfer(pushStack, popStack);
        }
        // 从 popStack 中弹出元素
        return popStack.pop();
    }

    public int peek() {
        if (popStack.isEmpty()) {
            transfer(pushStack, popStack);
        }
        return popStack.peek();
    }

    public boolean empty() {
        return popStack.isEmpty() && pushStack.isEmpty();
    }
    
    private void transfer(Stack<Integer> pushStack, Stack<Integer> popStack) {
        while (!pushStack.isEmpty()) {
            popStack.push(pushStack.pop());
        }
    }
}
  • 时间复杂度:push()empty()方法的时间复杂度为O(1)O(1)pop()peek()的平均时间复杂度为O(1)O(1)
  • 空间复杂度:O(N)O(N)。假设队列有nnpush()的操作,队列中会有n个元素。

T225-使用队列实现栈

见力扣第225题[使用队列实现栈]

题目描述

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(pushtoppopempty)。

实现 MyStack 类:

  • void push(int x) 将元素 x 压入栈顶。
  • int pop() 移除并返回栈顶元素。
  • int top() 返回栈顶元素。
  • boolean empty() 如果栈是空的,返回 true ;否则,返回 false

我的思路

  • 添加元素和删除元素都在队尾
class MyStack {

    private final ArrayList<Integer> list;

    private int sz;

    public MyStack() {
        list = new ArrayList<>();
        sz = 0;
    }

    public void push(int x) {
        list.add(x);
        sz++;
    }

    public int pop() {
        int val = list.get(sz - 1);
        list.remove(sz - 1);
        sz--;
        return val;
    }

    public int top() {
        return list.get(sz - 1);
    }

    public boolean empty() {
        return sz == 0;
    }
}
  • 时间复杂度:O(1)O(1),添加、删除以及对末尾元素的访问都是常数级的复杂度
  • 空间复杂度:O(N)O(N),需要使用ArrayList存储元素

T20-有效的括号

见力扣第20题[有效的括号]

题目描述

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

示例 1:

输入:s = "()"

输出:true

我的思路

  • 使用栈来存储s中的每个字符
  • 如果s.charAt(i)恰好和stack.peek()是一对
    • 将栈顶元素弹出:stack.pop()
    • s.charAt(i)压入栈中
  • 判断stack是否为空
public boolean isValid(String s) {
    if (s.length() % 2 != 0) return false;
    Stack<Character> stack = new Stack<>();
    for (char c : s.toCharArray()) {
        if (!stack.isEmpty() && (c - stack.peek()) <= 2 && (c - stack.peek()) > 0) {
            stack.pop();
        } else {
            stack.push(c);
        }
    }
    return stack.isEmpty();
}
  • 时间复杂度:O(N)O(N),遍历整个字符串
  • 空间复杂度:O(N)O(N),需要栈存储未匹配的括号

T1047-删除字符串中的所有相邻重复项

见力扣第1047题[删除字符串中的所有相邻重复项]

题目描述

给出由小写字母组成的字符串 s重复项删除操作会选择两个相邻且相同的字母,并删除它们。

s 上反复执行重复项删除操作,直到无法继续删除。

在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。

示例:

输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"

我的思路

  • 这一题和括号匹配问题解法相似,都是使用栈
  • 如果匹配,则弹出栈顶元素,否则压入栈中
public String removeDuplicates(String s) {
    if (s.length() <= 1) return s;
    ArrayDeque<Character> q = new ArrayDeque<>();

    for (char c : s.toCharArray()) {
        if (!q.isEmpty() && c == q.peekLast()) {
            q.removeLast();
        } else {
            q.addLast(c);
        }
    }
    StringBuilder sb = new StringBuilder();
    for (char c : q) {
        sb.append(c);
    }
    return sb.toString();
}
  • 时间复杂度:O(N)O(N),遍历整个字符串
  • 空间复杂度:O(N)O(N),使用一个栈存储未被匹配的元素

T150-逆波兰表达式求值

见力扣第150题[逆波兰表达式求值]

题目描述

给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。

请你计算该表达式。返回一个表示表达式值的整数。

注意:

  • 有效的算符为 '+''-''*''/'
  • 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
  • 两个整数之间的除法总是 向零截断
  • 表达式中不含除零运算。
  • 输入是一个根据逆波兰表示法表示的算术表达式。
  • 答案及所有中间计算结果可以用 32 位 整数表示。

我的思路

  • 使用一个栈来存储操作数
  • 遍历字符串的字符
    • 如果字符是运算符,则弹出两个操作数,将结果入栈
    • 如果字符是操作数,则压入栈中
  • 返回操作数栈中的运算结果
public int evalRPN(String[] tokens) {
    if (tokens.length <= 2) return Integer.parseInt(tokens[tokens.length - 1]);
    int[] dataStack = new int[tokens.length / 2 + 1]; // 最多 n / 2 + 1 个操作数
    int index = 0; // 模拟栈帧

    for (String token : tokens) {
        switch (token) {
            case "+": {
                // 加法,取出两位操作数相加,计算结果赋值到第一个操作数的位置
                dataStack[index - 2] = dataStack[index - 2] + dataStack[index - 1];
                // 栈帧指向有效操作数的后一位
                index--;
                break;
            }
            case "-": {
                dataStack[index - 2] = dataStack[index - 2] - dataStack[index - 1];
                index--;
                break;
            }
            case "*": {
                dataStack[index - 2] = dataStack[index - 2] * dataStack[index - 1];
                index--;
                break;
            }
            case "/": {
                dataStack[index - 2] = dataStack[index - 2] / dataStack[index - 1];
                index--;
                break;
            }
            default: {
                // 操作数,直接入栈
                dataStack[index++] = Integer.parseInt(token);
            }
        }
    }
    return dataStack[0];
}

时间复杂度:O(N)O(N),需要遍历一次tokens数组

空间复杂度:O(N)O(N),需要长度为(n/2)+1(n / 2) + 1的数组模拟栈

T239-滑动窗口最大值

见力扣第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

我的思路

  • 维护一个优先队列,存储窗口中的元素
  • 窗口滑动的时候,从队列中增删指定的元素
  • 然后将堆顶的元素添加到结果数组中

超时

思路

  • 使用单调队列,队列中存储元素的索引,并且索引对应的元素时严格单调递减的
  • 每次窗口滑动的时候
    • 将即将入队的元素和队尾元素作比较,如果队尾元素小于即将入队的元素,则直接弹出队尾元素
    • 如果队首的索引小于窗口的左边界,则弹出此索引,直到该队首所在的索引在窗口的范围之内
public int[] maxSlidingWindowI(int[] nums, int k) {
    int n = nums.length;
    Deque<Integer> dq = new ArrayDeque<>();
    int[] res = new int[n - k + 1];

    int l = 0;
    int r = 0;
    while (r < k) {
        while (!dq.isEmpty() && nums[r] >= nums[dq.peekLast()]) {
            dq.pollLast();
        }
        dq.offerLast(r++);
    }

    res[l] = nums[dq.peekFirst()];

    while (r < n) {
        // 如果队尾索引指向的元素小于或者等于准备的入队的元素,直接弹出
        while (!dq.isEmpty() && nums[r] >= nums[dq.peekLast()]) {
            dq.pollLast();
        }
        dq.offerLast(r++);
        l++;
        // 如果队首索引不在窗口中
        while (dq.peekFirst() < l) {
            dq.pollFirst();
        }
        res[l] = nums[dq.peekFirst()];
    }
    return res;
}
  • 时间复杂度:O(N)O(N),每个下标恰好被放入到队列中一次,并且最多被队列弹出一次
  • 空间复杂度:O(K)O(K),队列中最多不会存储超过窗口长度即KK个元素,因此队列使用的空间为O(K)O(K)

T347-前K个高频元素

见力扣第347题[前K个高频元素]

题目描述

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

示例 1:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

我的思路

  • 使用map记录每个数的频率,
  • 遍历map,将每个数的频率和数记录到List<int[]>
  • 调用集合的Collections.sort()方法,重写Comparator<>接口
  • 遍历前k个频率比较高的元素,赋值给结果数组
public int[] topKFrequent(int[] nums, int k) {
    Map<Integer, Integer> map = new HashMap<>();
    for (int num : nums) {
        if (!map.containsKey(num)) {
            map.put(num, 0);
        }
        map.put(num, map.get(num) + 1);
    }

    List<int[]> frequency = new ArrayList<>();
    for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
        int[] pairs = new int[2];
        pairs[0] = entry.getKey();
        pairs[1] = entry.getValue();
        frequency.add(pairs);
    }
    Collections.sort(frequency, (o1, o2) -> o2[1] - o1[1]);
    int[] res = new int[k];
    for (int i = 0; i < k; i++) {
        res[i] = frequency.get(i)[0];
    }
    return res;
}

优化方案

  • 桶排序,记录每个数字出现的频率
public int[] topKFrequentI(int[] nums, int k) {
    int max = Integer.MIN_VALUE;
    int min = Integer.MAX_VALUE;
    int[] ans = new int[k];

    // 找到数组中的最大值和最小值
    for (int i : nums) {
        max = Math.max(i, max); // 更新最大值
        min = Math.min(i, min); // 更新最小值
    }

    // 创建一个桶数组,用于记录每个数字出现的频率
    int[] buckets = new int[max - min + 1];
    for (int num : nums) {
        buckets[num - min]++; // 将数字映射到桶数组中,并增加频率
    }

    // 找到桶数组中的最大频率
    int maxF = 0;
    for (int bucket : buckets) {
        maxF = Math.max(bucket, maxF); // 更新最大频率
    }

    int index = 0; // 结果数组的索引
    // 找到出现频率最高的 k 个数字
    while (index < k) {
        for (int i = 0; i < buckets.length; i++) {
            if (maxF == buckets[i]) {
                ans[index] = i + min; // 将数字从桶数组映射回原数组的值
                index++;              // 更新结果数组的索引
            }
        }
        maxF--; // 减少最大频率,继续查找下一个频率最高的数字
    }

    return ans; // 返回结果数组
}

总结

  • 用栈实现队列,用队列实现栈来掌握的栈与队列的基本操作。

  • 接着,通过括号匹配问题、字符串去重问题、逆波兰表达式问题来系统讲解了栈在系统中的应用,以及使用技巧。

  • 通过求滑动窗口最大值,以及前K个高频元素介绍了两种队列:单调队列和优先级队列,这是特殊场景解决问题的利器,是一定要掌握的。