【练习】栈的经典习题


【练习】栈的经典习题

考察先进后出性质

对于栈这种数据结构的考察,主要考察先进后出特点的运用,比如表达式运算、括号合法性检测等问题,下面列出几个使用栈的经典场景。

71. 简化路径

71. 简化路径 | 力扣 | LeetCode |  🟠

给你一个字符串 path ,表示指向某一文件或目录的 Unix 风格 绝对路径 (以 '/' 开头),请你将其转化为更加简洁的规范路径。

在 Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (..) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。任意多个连续的斜杠(即,'//')都被视为单个斜杠 '/' 。 对于此问题,任何其他格式的点(例如,'...')均被视为文件/目录名称。

请注意,返回的 规范路径 必须遵循下述格式:

  • 始终以斜杠 '/' 开头。
  • 两个目录名之间必须只有一个斜杠 '/' 
  • 最后一个目录名(如果存在)不能  '/' 结尾。
  • 此外,路径仅包含从根目录到目标文件或目录的路径上的目录(即,不含 '.'  '..')。

返回简化后得到的 规范路径 

示例 1:

输入:path = "/home/"
输出:"/home"
解释:注意,最后一个目录名后面没有斜杠。 

示例 2:

输入:path = "/../"
输出:"/"
解释:从根目录向上一级是不可行的,因为根目录是你可以到达的最高级。

示例 3:

输入:path = "/home//foo/"
输出:"/home/foo"
解释:在规范路径中,多个连续斜杠需要用一个斜杠替换。

示例 4:

输入:path = "/a/./b/../../c/"
输出:"/c"

提示:

  • 1 <= path.length <= 3000
  • path 由英文字母,数字,'.''/'  '_' 组成。
  • path 是一个有效的 Unix 风格绝对路径。
题目来源:力扣 71. 简化路径

基本思路

这题比较简单,利用栈先进后出的特性处理上级目录 ..,最后组装化简后的路径即可。

详细题解

解法代码

class Solution:
    def simplifyPath(self, path: str) -> str:
        parts = path.split("/")
        stk = []
        # 借助栈计算最终的文件夹路径
        for part in parts:
            if part == "" or part == ".":
                continue
            if part == "..":
                if stk:
                    stk.pop()
                continue
            stk.append(part)
        # 栈中存储的文件夹组成路径
        res = ""
        while stk:
            res = "/" + stk.pop() + res
        return res if res else "/"

可视化

 算法可视化面板

143. 重排链表

143. 重排链表 | 力扣 | LeetCode |  🟠

给定一个单链表 L 的头节点 head ,单链表 L 表示为:

L0 → L1 → … → Ln - 1 → Ln

请将其重新排列后变为:

L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …

不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

示例 1:

输入:head = [1,2,3,4]
输出:[1,4,2,3]

示例 2:

输入:head = [1,2,3,4,5]
输出:[1,5,2,4,3]

提示:

  • 链表的长度范围为 [1, 5 * 104]
  • 1 <= node.val <= 1000
题目来源:力扣 143. 重排链表

基本思路

这题的难点在于:一个单链表只能从头部向尾部遍历节点,无法从尾部开始向头部遍历节点。

那么我们可以利用「栈」先进后出的结构特点,按从头到尾的顺序让链表节点入栈,那么出栈顺序就是反过来从尾到头了。

有了这个栈,算法的大致逻辑如下:

ListNode p = head;
while (p != null) {
    // 链表尾部的节点
    ListNode lastNode = stk.pop();
    // 按题目要求拼接
    ListNode next = p.next;
    p.next = lastNode;
    lastNode.next = next;
    p = next;
}

当然,处理单链表时细节问题比较多,注意操作指针时的顺序,避免操作失误形成环形链表,直接看我的代码注释吧。

详细题解

解法代码

class Solution:
    def reorderList(self, head: ListNode) -> None:
        stk = []
        # 先把所有节点装进栈里,得到倒序结果
        p = head
        while p is not None:
            stk.append(p)
            p = p.next

        p = head
        while p is not None:
            # 链表尾部的节点
            lastNode = stk.pop()
            next = p.next
            if lastNode == next or lastNode.next == next:
                # 结束条件,链表节点数为奇数或偶数时均适用
                lastNode.next = None
                break
            p.next = lastNode
            lastNode.next = next
            p = next

可视化

 算法可视化面板

类似题目

20. 有效的括号

20. 有效的括号 | 力扣 | LeetCode |  🟢

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

有效字符串需满足:

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

示例 1:

输入:s = "()"
输出:true

示例 2:

输入:s = "()[]{}"
输出:true

示例 3:

输入:s = "(]"
输出:false

提示:

  • 1 <= s.length <= 104
  • s 仅由括号 '()[]{}' 组成
题目来源:力扣 20. 有效的括号

基本思路

栈是一种先进后出的数据结构,处理括号问题的时候尤其有用。

括号的有效性判断在笔试中和现实中都很常见,比如说我们写的代码,编辑器会检查括号是否正确闭合。而且我们的代码可能会包含三种括号 [](){},判断起来有一点难度。

解决这个问题之前,我们先降低难度,思考一下,如果只有一种括号 (),应该如何判断字符串组成的括号是否有效呢?

假设字符串中只有圆括号,如果想让括号字符串有效,那么必须做到:

每个右括号 ) 的左边必须有一个左括号 ( 和它匹配

比如说字符串 ()))(( 中,中间的两个右括号左边就没有左括号匹配,所以这个括号组合是无效的。

那么根据这个思路,我们可以写出算法:

boolean isValid(String str) {
    // 待匹配的左括号数量
    int left = 0;
    for (int i = 0; i < str.length(); i++) {
        if (str.charAt(i) == '(') {
            left++;
        } else {
            // 遇到右括号
            left--;
        }

        // 右括号太多
        if (left == -1)
            return false;
    }
    // 是否所有的左括号都被匹配了
    return left == 0;
}

如果只有圆括号,这样就能正确判断有效性。对于三种括号的情况,我一开始想模仿这个思路,定义三个变量 left1left2left3 分别处理每种括号,虽然要多写不少 if else 分支,但是似乎可以解决问题。

但实际上直接照搬这种思路是不行的,比如说只有一个括号的情况下 (()) 是有效的,但是多种括号的情况下, [(]) 显然是无效的。

仅仅记录每种左括号出现的次数已经不能做出正确判断了,我们要加大存储的信息量,可以利用栈来模仿类似的思路。

我们这道题就用一个名为 left 的栈代替之前思路中的 left 变量,遇到左括号就入栈,遇到右括号就去栈中寻找最近的左括号,看是否匹配

详细题解

解法代码

class Solution:
    def isValid(self, str: str) -> bool:
        left = []
        for c in str:
            if c in '({[':
                # 字符 c 是左括号,入栈
                left.append(c)
            else:
                # 字符 c 是右括号
                if left and self.leftOf(c) == left[-1]:
                    left.pop()
                else:
                    # 和最近的左括号不匹配
                    return False
        # 是否所有的左括号都被匹配了
        return not left

    def leftOf(self, c: str) -> str:
        if c == '}':
            return '{'
        if c == ')':
            return '('
        return '['

可视化

 算法可视化面板

150. 逆波兰表达式求值

150. 逆波兰表达式求值 | 力扣 | LeetCode |  🟠

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

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

注意:

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

示例 1:

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

示例 2:

输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6

示例 3:

输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
输出:22
解释:该算式转化为常见的中缀算术表达式为:
  ((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22

提示:

  • 1 <= tokens.length <= 104
  • tokens[i] 是一个算符("+""-""*"  "/"),或是在范围 [-200, 200] 内的一个整数

逆波兰表达式:

逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。

  • 平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 
  • 该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 

逆波兰表达式主要有以下两个优点:

  • 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
  • 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中
题目来源:力扣 150. 逆波兰表达式求值

基本思路

逆波兰表达式发明出来就是为了方便计算机运用「栈」进行表达式运算的,其运算规则如下:

按顺序遍历逆波兰表达式中的字符,如果是数字,则放入栈;如果是运算符,则将栈顶的两个元素拿出来进行运算,再将结果放入栈。对于减法和除法,运算顺序别搞反了,栈顶第二个数是被除(减)数。

所以这题很简单,直接按照运算规则借助栈计算表达式结果即可。

详细题解

解法代码

class Solution:
    def evalRPN(self, tokens: List[str]) -> int:
        stk = []
        for token in tokens:
            if token in "+-*/":
                # 是个运算符,从栈顶拿出两个数字进行运算,运算结果入栈
                a = stk.pop()
                b = stk.pop()
                if token == "+":
                    stk.append(a + b)
                elif token == "*":
                    stk.append(a * b)
                # 对于减法和除法,顺序别搞反了,第二个数是被除(减)数
                elif token == "-":
                    stk.append(b - a)
                elif token == "/":
                    stk.append(int(b / a))  # Ensure the result is an integer
            else:
                # 是个数字,直接入栈即可
                stk.append(int(token))
        # 最后栈中剩下一个数字,即是计算结果
        return stk.pop()

可视化

 算法可视化面板

类似题目

225. 用队列实现栈

225. 用队列实现栈 | 力扣 | LeetCode |  🟢

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

实现 MyStack 类:

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

注意:

  • 你只能使用队列的标准操作 —— 也就是 push to backpeek/pop from frontsize 和 is empty 这些操作。
  • 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。

示例:

输入:
["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 2, 2, false]

解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False

提示:

  • 1 <= x <= 9
  • 最多调用100  pushpoptop  empty
  • 每次调用 pop  top 都保证栈不为空

进阶:你能否仅用一个队列来实现栈。

题目来源:力扣 225. 用队列实现栈

基本思路

底层用队列实现栈就比较简单粗暴了,只需要一个队列作为底层数据结构。

底层队列只能向队尾添加元素,所以栈的 pop API 相当于要从队尾取元素:

img

那么最简单的思路就是,把队尾元素前面的所有元素重新塞到队尾,让队尾元素排到队头,这样就可以取出了:

img

详细题解

解法代码

from collections import deque

class MyStack:
    def __init__(self):
        self.q = deque()
        self.top_elem = 0

    # 将元素 x 压入栈顶
    def push(self, x: int) -> None:
        # x 是队列的队尾,是栈的栈顶
        self.q.append(x)
        self.top_elem = x

    # 返回栈顶元素
    def top(self) -> int:
        return self.top_elem

    # 删除栈顶的元素并返回
    def pop(self) -> int:
        size = len(self.q)
        # 留下队尾 2 个元素
        while size > 2:
            self.q.append(self.q.popleft())
            size -= 1
        # 记录新的队尾元素
        self.top_elem = self.q[0]
        self.q.append(self.q.popleft())
        # 删除之前的队尾元素
        return self.q.popleft()

    # 判断栈是否为空
    def empty(self) -> bool:
        return not self.q

类似题目

388. 文件的最长绝对路径

388. 文件的最长绝对路径 | 力扣 | LeetCode |  🟠

假设有一个同时存储文件和目录的文件系统。下图展示了文件系统的一个示例:

这里将 dir 作为根目录中的唯一目录。dir 包含两个子目录 subdir1  subdir2 subdir1 包含文件 file1.ext 和子目录 subsubdir1subdir2 包含子目录 subsubdir2,该子目录下包含文件 file2.ext 

在文本格式中,如下所示(⟶表示制表符):

dir
⟶ subdir1
⟶ ⟶ file1.ext
⟶ ⟶ subsubdir1
⟶ subdir2
⟶ ⟶ subsubdir2
⟶ ⟶ ⟶ file2.ext

如果是代码表示,上面的文件系统可以写为 "dir\n\tsubdir1\n\t\tfile1.ext\n\t\tsubsubdir1\n\tsubdir2\n\t\tsubsubdir2\n\t\t\tfile2.ext" '\n'  '\t' 分别是换行符和制表符。

文件系统中的每个文件和文件夹都有一个唯一的 绝对路径 ,即必须打开才能到达文件/目录所在位置的目录顺序,所有路径用 '/' 连接。上面例子中,指向 file2.ext  绝对路径  "dir/subdir2/subsubdir2/file2.ext" 。每个目录名由字母、数字和/或空格组成,每个文件名遵循 name.extension 的格式,其中 name 和 extension由字母、数字和/或空格组成。

给定一个以上述格式表示文件系统的字符串 input ,返回文件系统中 指向 文件 的 最长绝对路径 的长度 。 如果系统中没有文件,返回 0

示例 1:

输入:input = "dir\n\tsubdir1\n\tsubdir2\n\t\tfile.ext"
输出:20
解释:只有一个文件,绝对路径为 "dir/subdir2/file.ext" ,路径长度 20

示例 2:

输入:input = "dir\n\tsubdir1\n\t\tfile1.ext\n\t\tsubsubdir1\n\tsubdir2\n\t\tsubsubdir2\n\t\t\tfile2.ext"
输出:32
解释:存在两个文件:
"dir/subdir1/file1.ext" ,路径长度 21
"dir/subdir2/subsubdir2/file2.ext" ,路径长度 32
返回 32 ,因为这是最长的路径

示例 3:

输入:input = "a"
输出:0
解释:不存在任何文件

示例 4:

输入:input = "file1.txt\nfile2.txt\nlongfile.txt"
输出:12
解释:根目录下有 3 个文件。
因为根目录中任何东西的绝对路径只是名称本身,所以答案是 "longfile.txt" ,路径长度为 12

提示:

  • 1 <= input.length <= 104
  • input 可能包含小写或大写的英文字母,一个换行符 '\n',一个制表符 '\t',一个点 '.',一个空格 ' ',和数字。
题目来源:力扣 388. 文件的最长绝对路径

基本思路

我觉得这道题还是比较实用的,因为在我做这道题之前,我就思考并解决过这个问题,可以在这里和大家分享下我的使用场景:

你可以看我的 GitHub 仓库中的文章目录,是通过缩进来表示层级的,很类似本题所说的场景。然而我需要把这些目录转化成 HTML 文档,按照文件目录的形式把这些 HTML 部署到 我的网站 上。你看,这是不是就涉及到本题生成文件的绝对路径的问题?

对于这个场景,我当时其实尝试很多可行的办法。但这里我还是写一个最简单直接容易理解的解法吧,那就是用栈来辅助,对于每一个路径,都去维护正确的父路径,从而计算最长路径的长度。具体看代码注释吧。

详细题解

解法代码

class Solution:
    def lengthLongestPath(self, input: str) -> int:
        # 这个栈存储之前的父路径。实际上这里只用存父路径的长度就够了,这个优化留给你吧
        stack = []
        max_len = 0
        for part in input.split("\n"):
            level = part.rfind("\t") + 1
            # 让栈中只保留当前目录的父路径
            while level < len(stack):
                stack.pop()
            stack.append(len(part) - level)
            # 如果是文件,就计算路径长度
            if "." in part:
                total_length = sum(stack) + len(stack) - 1
                # 加上父路径的分隔符
                max_len = max(max_len, total_length)
        return max_len

栈的设计题

除了上面几道题,还有一类常考题目是让你设计具备额外功能的栈结构。

155. 最小栈

155. 最小栈 | 力扣 | LeetCode |  🟠

设计一个支持 push pop top 操作,并能在常数时间内检索到最小元素的栈。

实现 MinStack 类:

  • MinStack() 初始化堆栈对象。
  • void push(int val) 将元素val推入堆栈。
  • void pop() 删除堆栈顶部的元素。
  • int top() 获取堆栈顶部的元素。
  • int getMin() 获取堆栈中的最小元素。

示例 1:

输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]

输出:
[null,null,null,null,-3,null,0,-2]

解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); –> 返回 -3.
minStack.pop();
minStack.top(); –> 返回 0.
minStack.getMin(); –> 返回 -2.

提示:

  • -231 <= val <= 231 - 1
  • poptop  getMin 操作总是在 非空栈 上调用
  • pushpoptop, and getMin最多被调用 3 * 104 次
题目来源:力扣 155. 最小栈

基本思路

根据我们之前亲自动手实现的栈,我们知道栈是一种操作受限的数据结构,只能从栈顶插入或弹出元素,所以对于标准的栈来说,如果想实现本题的 getMin 方法,只能老老实实把所有元素弹出来然后找最小值。想提高时间效率,那肯定要通过空间换时间的思路

不过在具体说解法之前,我想聊一下动态集合中维护最值的问题。这类问题看似简单,但实际上是个很棘手的问题。其实本题就是如下一个场景:

假设你有若干数字,你用一个 min 变量维护了其中的最小值,如果现在给这些数字中添加一个新数字,那么只要比较这个新数字和 min 的大小就可以得出最新的最小值。但如果现在从这些数字钟删除一个数字,你还能用 min 变量得到最小值吗?答案是不能,因为如果这个被删除的数字恰好是最小值,那么新的 min 变量应该更新为第二小的元素对吧,但是我没有记录第二小的元素是多少,所以只能把所有数字重新遍历一遍。

明确了难点再回到本题,就可以对症下药了。删除栈顶元素的时候,不确定新的最小值是多少,但楼下那哥们知道啊,他当时入栈时的最小值,就是现在的最小值呗。

所以这道题的关键就是,每个元素入栈时,还要记下来当前栈中的最小值。比方说,可以用一个额外的栈 minStk 来记录栈中每个元素入栈时的栈中的最小元素是多少,这样每次删除元素时就能快速得到剩余栈中的最小元素了。

img

当然,我们还可以做一些优化,减少 minStk 中存储的元素个数,我把原始解法和优化解法都写出来了,供参考。

PS:这道题并不难,但我还是很细致地分析了,希望你深刻理解其中的难点。下一步可以做一下 239. 滑动窗口最大值,请仔细观察和思考,队列结构是如何解决这个难点的。

详细题解

解法代码

# 原始思路
class MinStack1:
    def __init__(self):
        # 记录栈中的所有元素
        self.stk = []
        # 阶段性记录栈中的最小元素
        self.minStk = []

    def push(self, val: int) -> None:
        self.stk.append(val)
        # 维护 minStk 栈顶为全栈最小元素
        if not self.minStk or val <= self.minStk[-1]:
            # 新插入的这个元素就是全栈最小的
            self.minStk.append(val)
        else:
            # 插入的这个元素比较大
            self.minStk.append(self.minStk[-1])

    def pop(self) -> None:
        self.stk.pop()
        self.minStk.pop()

    def top(self) -> int:
        return self.stk[-1]

    def getMin(self) -> int:
        # minStk 栈顶为全栈最小元素
        return self.minStk[-1]

# 优化版
class MinStack:
    def __init__(self):
        # 记录栈中的所有元素
        self.stk = []
        # 阶段性记录栈中的最小元素
        self.minStk = []

    def push(self, val: int) -> None:
        self.stk.append(val)
        # 维护 minStk 栈顶为全栈最小元素
        if not self.minStk or val <= self.minStk[-1]:
            # 新插入的这个元素就是全栈最小的
            self.minStk.append(val)

    def pop(self) -> None:
        # 注意 Java 的语言特性,比较 Integer 相等要用 equals 方法
        if self.stk[-1] == self.minStk[-1]:
            # 弹出的元素是全栈最小的
            self.minStk.pop()
        self.stk.pop()

    def top(self) -> int:
        return self.stk[-1]

    def getMin(self) -> int:
        # minStk 栈顶为全栈最小元素
        return self.minStk[-1]

类似题目

895. 最大频率栈

895. 最大频率栈 | 力扣 | LeetCode |  🔴

设计一个类似堆栈的数据结构,将元素推入堆栈,并从堆栈中弹出出现频率最高的元素。

实现 FreqStack 类:

  • FreqStack() 构造一个空的堆栈。
  • void push(int val) 将一个整数 val 压入栈顶。
  • int pop() 删除并返回堆栈中出现频率最高的元素。
    • 如果出现频率最高的元素不只一个,则移除并返回最接近栈顶的元素。

示例 1:

输入:
["FreqStack","push","push","push","push","push","push","pop","pop","pop","pop"],
[[],[5],[7],[5],[7],[4],[5],[],[],[],[]]
输出:[null,null,null,null,null,null,null,5,7,5,4]
解释:
FreqStack = new 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 出现频率最高。堆栈变成 [5,7,5,7,4]。
freqStack.pop ();//返回 7 ,因为 5 和 7 出现频率最高,但7最接近顶部。堆栈变成 [5,7,5,4]。
freqStack.pop ();//返回 5 ,因为 5 出现频率最高。堆栈变成 [5,7,4]。
freqStack.pop ();//返回 4 ,因为 4, 5 和 7 出现频率最高,但 4 是最接近顶部的。堆栈变成 [5,7]。

提示:

  • 0 <= val <= 109
  • push 和 pop 的操作数不大于 2 * 104
  • 输入保证在调用 pop 之前堆栈中至少有一个元素。
题目来源:力扣 895. 最大频率栈

基本思路

这种设计数据结构的问题,主要是要搞清楚问题的难点在哪里,然后结合各种基本数据结构的特性,高效实现题目要求的 API

那么,我们仔细思考一下 pushpop 方法,难点如下:

1、每次 pop 时,必须要知道频率最高的元素是什么。

2、如果频率最高的元素有多个,还得知道哪个是最近 push 进来的元素是哪个。

为了实现上述难点,我们要做到以下几点:

1、肯定要有一个变量 maxFreq 记录当前栈中最高的频率是多少。

2、我们得知道一个频率 freq 对应的元素有哪些,且这些元素要有时间顺序。

3、随着 pop 的调用,每个 val 对应的频率会变化,所以还得维持一个映射记录每个 val 对应的 freq

综上,我们可以先实现 FreqStack 所需的数据结构:

class FreqStack {
    // 记录 FreqStack 中元素的最大频率
    int maxFreq = 0;
    // 记录 FreqStack 中每个 val 对应的出现频率,后文就称为 VF 表
    HashMap<Integer, Integer> valToFreq = new HashMap<>();
    // 记录频率 freq 对应的 val 列表,后文就称为 FV 表
    HashMap<Integer, Stack<Integer>> freqToVals = new HashMap<>();
}

其实这有点类似后文 手把手实现 LFU 算法,注意 freqToValsval 列表用一个栈实现,如果一个 freq 对应的元素有多个,根据栈的特点,可以首先取出最近添加的元素。

具体看解法代码吧,要记住在 pushpop 方法中同时修改 maxFreqVF 表、FV 表,否则容易出现 bug。

算法执行过程如下 GIF 所示:

img

详细题解

解法代码

class FreqStack:
    def __init__(self):
        # 记录 FreqStack 中元素的最大频率
        self.maxFreq = 0
        # 记录 FreqStack 中每个 val 对应的出现频率,后文就称为 VF 表
        self.valToFreq = {}
        # 记录频率 freq 对应的 val 列表,后文就称为 FV 表
        self.freqToVals = {}

    def push(self, val: int) -> None:
        # 修改 VF 表:val 对应的 freq 加一
        freq = self.valToFreq.get(val, 0) + 1
        self.valToFreq[val] = freq
        # 修改 FV 表:在 freq 对应的列表加上 val
        if freq not in self.freqToVals:
            self.freqToVals[freq] = []
        self.freqToVals[freq].append(val)
        # 更新 maxFreq
        self.maxFreq = max(self.maxFreq, freq)

    def pop(self) -> int:
        # 修改 FV 表:pop 出一个 maxFreq 对应的元素 v
        vals = self.freqToVals[self.maxFreq]
        v = vals.pop()
        # 修改 VF 表:v 对应的 freq 减一
        self.valToFreq[v] -= 1
        # 更新 maxFreq
        if not vals:
            # 如果 maxFreq 对应的元素空了
            self.maxFreq -= 1
        return v

文章作者: Mealsee
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Mealsee !
  目录