# 栈与队列算法(3)——总结

14 min read
Table of Contents

20. 有效的括号

20. 有效的括号

简单

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

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。

  2. 左括号必须以正确的顺序闭合。

  3. 每个右括号都有一个对应的相同类型的左括号。

示例 1:

**输入:**s = ”()”

**输出:**true

示例 2:

**输入:**s = ”()[]{}”

**输出:**true

示例 3:

**输入:**s = ”(]”

**输出:**false

示例 4:

**输入:**s = ”([])”

**输出:**true

示例 5:

**输入:**s = ”([)]”

**输出:**false

提示:

  • 1 <= s.length <= 104
  • s 仅由括号 '()[]{}' 组成

image-20251009115610957

用 map 来存括号的对应关系。

遍历到的左括号存入栈中,当遇到右括号时,与栈顶元素对应的右括号做匹配,若相等则说明括号有效,同时弹出栈顶元素,不等则括号无效。

另外需要注意,如果 len(s) 为奇数则一定不匹配,如果遇到右括号时栈中无元素,则说明多了右括号,不匹配返回false

如果匹配完毕,栈中无元素,说明所有括号都有效匹配。

func isValid(s string) bool {
if len(s) % 2 != 0 {
return false
}
// 用切片模拟栈
stack := make([]rune, 0)
// 哈希表 m 记录右括号对应的左括号
m := make(map[rune]rune)
m[')'] = '('
m['}'] = '{'
m[']'] = '['
for _, v := range s {
if v == '(' || v == '{' || v == '[' {
stack = append(stack, v)
} else {
// 如果是右括号,先判断栈内是否有元素
if len(stack) == 0 {
return false
}
peek := stack[len(stack)-1]
if peek != m[v] {
return false
} else {
// 弹出栈顶元素
stack = stack[:len(stack)-1]
}
}
}
// 如果栈中不包含元素,则代表完全匹配
return len(stack) == 0
}

1047. 删除字符串中的所有相邻重复项

1047. 删除字符串中的所有相邻重复项

简单

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

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

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

示例:

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

提示:

  1. 1 <= s.length <= 105
  2. s 仅由小写英文字母组成。
func removeDuplicates(s string) string {
var stack []byte
// 用字符串模拟栈,字符串尾部是栈入口,字符串头部是栈底部
for i := 0; i < len(s); i++ {
// 栈不空且与栈顶元素相等,弹出栈顶元素
if len(stack) != 0 && s[i] == stack[len(stack)-1] {
stack = stack[:len(stack)-1]
} else {
// 把元素放入栈
stack = append(stack, s[i])
}
}
return string(stack)
}

150. 逆波兰表达式求值

150. 逆波兰表达式求值

中等

给你一个字符串数组 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 + * 也可以依据次序计算出正确结果。
  • 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中

遍历 tokens 读到数字放入栈中,读到运算符就弹出栈顶的2个元素做运算,需要注意相减和相除时的元素顺序

在判断元素是否是运算符,第一想到的是用哈希表。然而在用哈希表时遇到很多类型方面的问题。

首先是要判断元素是否在map中,最省空间的是用空struct,初始化时用 "+": {},,对map中元素赋值时用 set["+"] = struct{}{}

遍历的 token 是字符串,遇到例如 token“12” 时,不是单个字符 '1''2',而是整体的 "12"字符串,长度为2。因此不能用 map[byte]struct{},它只适合处理单字符场景。

把数字放入栈中,需要提前将 string 转为 int

下面的代码几乎没有用到库函数。

func evalRPN(tokens []string) int {
stack := []int{}
set := map[string]struct{} {
"+": {},
"-": {},
"*": {},
"/": {},
}
for i := 0; i < len(tokens); i++ {
if _, ok := set[tokens[i]]; ok {
// 从栈中取值做运算
b := stack[len(stack)-1]
a := stack[len(stack)-2]
c := tokens[i]
stack = stack[:len(stack)-2]
switch {
case c == "+":
stack = append(stack, a+b)
case c == "-":
stack = append(stack, a-b)
case c == "*":
stack = append(stack, a*b)
case c == "/":
stack = append(stack, a/b)
}
} else {
// 把数字放入栈中
num, _ := strconv.Atoi(tokens[i])
stack = append(stack, num)
}
}
return stack[len(stack)-1]
}

使用库函数,判断元素是否是数字

func evalRPN(tokens []string) int {
stack := []int{}
for _, token := range tokens {
val, err := strconv.Atoi(token)
if err == nil {
stack = append(stack, val)
} else { // 如果err不为nil说明不是数字
num1, num2 := stack[len(stack)-2], stack[(len(stack))-1]
stack = stack[:len(stack)-2]
switch token {
case "+":
stack = append(stack, num1+num2)
case "-":
stack = append(stack, num1-num2)
case "*":
stack = append(stack, num1*num2)
case "/":
stack = append(stack, num1/num2)
}
}
}
return stack[0]
}

239.滑动窗口最大值

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

单调队列(仅适用于本题)

队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。

设计单调队列的时候,pop,和push操作要保持如下规则:

  1. pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作

    func (m *MyQueue) Pop(val int) {
    if !m.Empty() && val == m.Front() {
    m.queue = m.queue[1:]
    }
    }
  2. push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止

    func (m *MyQueue) Push(val int) {
    for !m.Empty() && val > m.Back() {
    m.queue = m.queue[:len(m.queue)-1]
    }
    m.queue = append(m.queue, val)
    }

动画演示:

239.滑动窗口最大值-2
// 封装单调队列的方式解题
type MyQueue struct {
queue []int
}
func NewMyQueue() *MyQueue {
return &MyQueue{
queue: make([]int, 0),
}
}
func (m *MyQueue) Front() int {
return m.queue[0]
}
func (m *MyQueue) Back() int {
return m.queue[len(m.queue)-1]
}
func (m *MyQueue) Empty() bool {
return len(m.queue) == 0
}
func (m *MyQueue) Push(val int) {
for !m.Empty() && val > m.Back() {
m.queue = m.queue[:len(m.queue)-1]
}
m.queue = append(m.queue, val)
}
func (m *MyQueue) Pop(val int) {
if !m.Empty() && val == m.Front() {
m.queue = m.queue[1:]
}
}
func maxSlidingWindow(nums []int, k int) []int {
queue := NewMyQueue()
length := len(nums)
res := make([]int, 0)
// 先将前k个元素放入队列
for i := 0; i < k; i++ {
queue.Push(nums[i])
}
// 记录前k个元素的最大值
res = append(res, queue.Front())
for i := k; i < length; i++ {
// 滑动窗口移除最前面的元素
queue.Pop(nums[i-k])
// 滑动窗口添加最后面的元素
queue.Push(nums[i])
// 记录最大值
res = append(res, queue.Front())
}
return res
}

347. 前 K 个高频元素

347. 前 K 个高频元素

中等

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

示例 1:

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

输出:[1,2]

示例 2:

**输入:**nums = [1], k = 1

输出:[1]

示例 3:

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

输出:[1,2]

提示:

  • 1 <= nums.length <= 105

  • k 的取值范围是 [1, 数组中不相同的元素的个数]

  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

**进阶:**你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。

map+排序

先用 map 统计元素出现次数,再根据出现次数排序。

使用了 sort.Slice() 库函数

func topKFrequent(nums []int, k int) []int {
ans := []int{}
map_num := map[int]int{}
// 统计元素出现次数
for _, v := range nums {
map_num[v]++
}
// 统计出现过的元素(去重)
for k, _ := range map_num {
ans = append(ans, k)
}
// 根据出现频率排序元素
sort.Slice(ans, func(i, j int) bool {
return map_num[ans[i]] > map_num[ans[j]]
})
return ans[:k]
}

xmind 总结

🔗xmind 源文件

My avatar

Thanks for reading my blog post! Feel free to check out my other posts or contact me via the social links in the footer.


More Posts

Comments