用栈实现队列
232. 用栈实现队列 - 力扣(LeetCode)
用两个栈实现先入先出队列
在Go中用切片模拟栈,一个输入栈,一个输出栈
- 入队:存入输入栈
- 出队:弹出输出栈顶,若输出栈为空,将输入栈所有数据存入输出栈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
type MyQueue struct {
stackIn []int // 输入栈
stackOut []int // 输出栈
}
func Constructor() MyQueue {
return MyQueue{
stackIn: make([]int, 0),
stackOut: make([]int, 0),
}
}
// 入队
func (this *MyQueue) Push(x int) {
this.stackIn = append(this.stackIn, x) // 往输入栈追加
}
// 出队
func (this *MyQueue) Pop() int {
if len(this.stackOut) == 0 { // 输出栈为空
if len(this.stackIn) == 0 { // 输入栈也为空
return -1
}
for i := len(this.stackIn) - 1; i >= 0; i-- { // 从输入栈顶逐个取元素存入输出栈
this.stackOut = append(this.stackOut, this.stackIn[i]) // 输入栈元素存入输出栈
}
this.stackIn = []int{} // 清空输入栈
}
res := this.stackOut[len(this.stackOut)-1] // 取出出栈元素
this.stackOut = this.stackOut[:len(this.stackOut)-1] // 更新输出栈
return res
}
// 获取队头值
func (this *MyQueue) Peek() int {
res := this.Pop() // 出队
if res == -1 { // 队空
return -1
}
this.stackOut = append(this.stackOut, res) // 放回输出栈
return res
}
// 判队空
func (this *MyQueue) Empty() bool {
return len(this.stackOut) == 0 && len(this.stackIn) == 0
}
|
- 时间复杂度: push和empty为O(1),pop和peek为O(n)
- 空间复杂度: O(n)
用队列实现栈
225. 用队列实现栈 - 力扣(LeetCode)
用两个队列实现一个后入先出(LIFO)的栈
其实这道题目就是用一个队列就够了,一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时再去弹出对头元素就是栈的顺序了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
type MyStack struct {
queue []int
}
func Constructor() MyStack {
return MyStack{
queue: make([]int, 0),
}
}
// 进栈
func (this *MyStack) Push(x int) {
this.queue = append(this.queue, x) // 追加到队列尾
}
// 出栈
func (this *MyStack) Pop() int {
// count:队首到倒数第二个元素的个数
for count := len(this.queue) - 1; count != 0; count-- { // 除了最后一个元素其余元素重新加到队尾
val := this.queue[0] // 取一个队首元素
this.queue = this.queue[1:] // 队首后面的元素前移
this.queue = append(this.queue, val) // 重新加到队尾
}
res := this.queue[0] // 取队首元素
this.queue = this.queue[1:len(this.queue)] // 更新队列
return res
}
// 获取栈顶值
func (this *MyStack) Top() int {
res := this.Pop() // 栈顶出栈
this.queue = append(this.queue, res) // 重新加到队尾
return res
}
// 判栈空
func (this *MyStack) Empty() bool {
return len(this.queue) == 0
}
|
- 时间复杂度: pop为O(n),其他为O(1)
- 空间复杂度: O(n)
有效的括号
20. 有效的括号 - 力扣(LeetCode)
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
遍历字符串,遇到左括号加入栈,遇到右括号就弹出栈顶元素(左括号),若不匹配,则返回false,遍历结束后,若栈内还剩左括号,返回false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
func isValid(s string) bool {
stack := make([]rune, 0)
for _, v := range s {
if v == '(' || v == '[' || v == '{' { // 遍历到左括号
stack = append(stack, v) // 入栈
} else { // 遍历到右括号
if len(stack) <= 0 { // 没有左括号
return false
}
if v == ')' && stack[len(stack)-1] != '(' { // 左括号不匹配
return false
} else if v == ']' && stack[len(stack)-1] != '[' { // 左括号不匹配
return false
} else if v == '}' && stack[len(stack)-1] != '{' { // 左括号不匹配
return false
}
stack = stack[:len(stack)-1] // 出栈
}
}
if len(stack) > 0 { // 还剩左括号未匹配
return false
}
return true
}
|
优化代码:哈希表,返回判断结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
func isValid(s string) bool {
hash := map[rune]rune{')': '(', ']': '[', '}': '{'}
stack := make([]rune, 0)
for _, v := range s {
if v == '(' || v == '[' || v == '{' { // 遍历到左括号
stack = append(stack, v) // 入栈
} else if len(stack) > 0 && stack[len(stack)-1] == hash[v] { // 遍历到右括号栈非空且匹配成功
stack = stack[:len(stack)-1] // 出栈
} else { // 遍历到右括号栈空或匹配不成功
return false
}
}
return len(stack) == 0
}
|
删除字符串中的所有相邻重复项
1047. 删除字符串中的所有相邻重复项 - 力扣(LeetCode)
给出由小写字母组成的字符串 S
,重复项删除操作会选择两个相邻且相同的字母,并删除它们。在 S 上反复执行重复项删除操作,直到无法继续删除。
遍历字符串,若栈为空或与栈顶元素不同,则入栈,若与栈顶元素不同,则栈顶元素出栈,遍历结束后,将栈中元素拼接为字符串返回
1
2
3
4
5
6
7
8
9
10
11
|
func removeDuplicates(s string) string {
stack := make([]rune, 0)
for _, v := range s {
if len(stack) == 0 || stack[len(stack)-1] != v { // 栈为空或与栈顶元素不同
stack = append(stack, v) // 入栈
} else { // 栈非空且与栈顶元素相同
stack = stack[:len(stack)-1] // 出栈
}
}
return string(stack)
}
|
逆波兰表达式求值
150. 逆波兰表达式求值 - 力扣(LeetCode)
给你一个字符串数组 tokens
,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。
注意:
- 有效的算符为
'+'
、'-'
、'*'
和 '/'
。
- 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
- 两个整数之间的除法总是 向零截断 。
- 表达式中不含除零运算。
- 输入是一个根据逆波兰表示法表示的算术表达式。
- 答案及所有中间计算结果可以用 32 位 整数表示。
遍历给定字符串,遇到运算符就将栈顶两个元素取出用该运算符处理,结果再入栈,遍历结束返回栈中的运算结果
注意:除法中栈顶元素是除数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
func evalRPN(tokens []string) int {
stack := make([]int, 0)
for _, v := range tokens {
if v == "+" { // 遍历到运算符
val := stack[len(stack)-2] + stack[len(stack)-1] // 获取运算结果
stack = stack[:len(stack)-1] // 更新栈
stack[len(stack)-1] = val // 结果入栈
} else if v == "-" { // 遍历到运算符
val := stack[len(stack)-2] - stack[len(stack)-1] // 获取运算结果
stack = stack[:len(stack)-1] // 更新栈
stack[len(stack)-1] = val // 结果入栈
} else if v == "*" { // 遍历到运算符
val := stack[len(stack)-2] * stack[len(stack)-1] // 获取运算结果
stack = stack[:len(stack)-1] // 更新栈
stack[len(stack)-1] = val // 结果入栈
} else if v == "/" { // 遍历到运算符
val := stack[len(stack)-2] / stack[len(stack)-1] // 获取运算结果
stack = stack[:len(stack)-1] // 更新栈
stack[len(stack)-1] = val // 结果入栈
} else { // 遇到数字
val, _ := strconv.Atoi(v)
stack = append(stack, val) // 入栈
}
}
return stack[0]
}
|
压缩代码:swich
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func evalRPN(tokens []string) int {
stack := make([]int, 0)
for _, v := range tokens {
val, err := strconv.Atoi(v) // 转为数字
if err == nil { // 说明是数字
stack = append(stack, val) // 数字入栈
} else { // 说明是运算符
switch v {
case "+": stack[len(stack)-2] = stack[len(stack)-2] + stack[len(stack)-1] // 运算结果入栈
case "-": stack[len(stack)-2] = stack[len(stack)-2] - stack[len(stack)-1] // 运算结果入栈
case "*": stack[len(stack)-2] = stack[len(stack)-2] * stack[len(stack)-1] // 运算结果入栈
case "/": stack[len(stack)-2] = stack[len(stack)-2] / stack[len(stack)-1] // 运算结果入栈
}
stack = stack[:len(stack)-1] // 更新栈
}
}
return stack[0]
}
|
滑动窗口最大值
239. 滑动窗口最大值 - 力扣(LeetCode)
给定一个数组和整数k,用k作为滑动窗口大小,窗口从最左侧移动到最右侧,每次移动一位,返回每次窗口移动后的最大值
单调队列:队首出队,队尾入队,队内元素单调;
- pop(value):若窗口移除的元素等于单调队列的队首元素,则队列弹出元素,否则不用任何操作
- push(value):若push的元素大于队尾元素,则弹出队尾元素,直到push的元素小于等于队尾元素或队空
保持如上规则,每次窗口移动的时候,只要取队首即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
type MyQueue struct {
queue []int
}
func Constructor() *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 := Constructor()
res := make([]int, 0)
for i := 0; i < k; i++ { // 前k个元素入队
queue.Push(nums[i]) // 入队
}
res = append(res, queue.Front()) // 记录前k个元素的最大值
for i := k; i < len(nums); i++ { // 遍历数组
queue.Pop(nums[i-k]) // 滑动窗口移除最前面的元素
queue.Push(nums[i]) // 滑动窗口添加最后面的元素
res = append(res, queue.Front()) // 记录最大值
}
return res
}
|
前 K 个高频元素
347. 前 K 个高频元素 - 力扣(LeetCode)
给定一个数组和一个整数 k
,返回数组中出现频率前 k
高的元素
先统计频率,再对频率排序;统计元素出现的频率可以用map;对频率进行排序,可以用快排和堆
快排
用map统计完频率后,收集不重复的元素,利用字典值对不重复的元素排序,最后返回前k个不重复的元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
func topKFrequent(nums []int, k int) []int {
res := make([]int, 0)
mp := make(map[int]int)
for _, v := range nums { // 遍历数组
mp[v]++ // 字典值(频率)加一
}
for key := range mp { // 遍历字典key
res = append(res, key) // 记录不重复的元素
}
sort.Slice(res, func(a, b int) bool {
return mp[res[a]] > mp[res[b]] // 降序
})
return res[:k]
}
|
- 时间复杂度: O(nlogn)
- 空间复杂度: O(n)
用map统计完频率后,构建小顶堆
堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果是父结点大于等于左右孩子那就是大顶堆,小于等于左右孩子就是小顶堆。大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),用小顶堆每次将最小的元素弹出,最后小顶堆里积累的就是前k个最大元素
更深理解
- 之所以将交换、比较等操作分离出来,是因为增删切片元素需要传递切片地址,操作切片指针时,无法用索引下标访问数组元素
- Go语言中函数参数的传递方式都是值传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
type Heap [][2]int // 二维表示元素及其频率
// 交换两个节点
func (h Heap) swap(i, j int) {
h[i], h[j] = h[j], h[i]
}
// 比较两个节点
func (h Heap) less(i, j int) bool {
return h[i][1] < h[j][1] // 比较频率 如果实现的是大顶堆,修改这里的判断即可
}
// 节点向上比较交换
func (h Heap) up(i int) {
for {
f := (i - 1) / 2 // 父节点在数组中的位置
if i == f || h.less(f, i) { // 当前节点为根节点或父节点小于当前节点
break
}
h.swap(f, i) // 交换当前节点和父节点的值
i = f // 更新当前节点索引继续向上比较
}
}
// 注意go中所有参数都是值传递 所以要让h的变化在函数外也起作用 此处要传指针
// 添加节点
func (h *Heap) Push(x [2]int) {
*h = append(*h, x) // 加入末尾
h.up(len(*h) - 1) // 向上比较交换
}
// 节点向下比较交换
func (h Heap) down(i int) {
for {
l := 2*i + 1 // 左孩子
r := 2*i + 2 // 右孩子
if l >= len(h) { // 当前节点已经是叶子节点
break
}
minChild := l // 默认左孩子值小于右孩子值
if r < len(h) && h.less(r, l) { // 右孩子存在且小于左孩子
minChild = r // 更新当前节点的最小子节点
}
if h.less(i, minChild) { // 当前节点小于最小子节点
break
}
h.swap(i, minChild) // 交换当前节点和最小子节点位置
i = minChild // 更新当前节点索引继续向下比较
}
}
// 删除堆中位置为i的节点 返回被删节点的值
func (h *Heap) Remove(i int) ([2]int, bool) {
if i < 0 || i > len(*h)-1 { // 索引越界
return [2]int{0, 0}, false
}
n := len(*h) - 1 // 最后一个节点的索引
h.swap(i, n) // 交换要被删除节点和最后的节点
x := (*h)[n] // 保存最后节点值
*h = (*h)[0:n] // 删除最后节点
h.down(i) // 被交换的当前节点向下比较交换
return x, true
}
// 弹出堆顶的元素,并返回其值
func (h *Heap) Pop() [2]int {
x, _ := h.Remove(0)
return x
}
// 根据传入的切片建堆
func BuildHeap(arr [][2]int) Heap {
h := Heap(arr) // 获取给定切片
for i := len(h)/2 - 1; i >= 0; i-- { // 从堆中末尾节点的父节点到根节点
h.down(i) // 向下比较交换
}
return h
}
func topKFrequent(nums []int, k int) []int {
mp := map[int]int{}
res := make([]int, 0)
for _, v := range nums { // 遍历数组
mp[v]++ // 字典值(频率)加一
}
h := BuildHeap([][2]int{}) // 初始化堆
for key, value := range mp { // 遍历字典
h.Push([2]int{key, value}) // 元素入堆
if len(h) > k { // 堆的节点数超过k个
h.Pop() // 删除顶点(min)
}
}
for i := 0; i < k; i++ { // 遍历堆
res = append(res, h.Pop()[0])
}
return res
}
|
- 时间复杂度: O(nlogk)
- 空间复杂度: O(n)
使用go中标准库提供的heap
,需要先实现两个接口定义的五个方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
type Heap [][2]int // 二维表示元素及其频率
func (h Heap) Len() int {
return len(h)
}
// 比较两个节点
func (h Heap) Less(i, j int) bool {
return h[i][1] < h[j][1] // 按照频率排序节点
}
// 交换两个节点
func (h Heap) Swap(i, j int) {
h[i], h[j] = h[j], h[i]
}
// go中所有参数都是值传递 所以要让h的变化在函数外也起作用 此处要传指针
// 添加节点
func (h *Heap) Push(x interface{}) {
*h = append(*h, x.([2]int)) // 向末尾添加节点
}
// 删除节点
func (h *Heap) Pop() interface{} {
old := *h // 保存当前根
n := len(old) // 节点数
x := old[n-1] // 保存最后一个节点
*h = old[0 : n-1] // 删除最后一个节点
return x // 返回删除的节点
}
func topKFrequent(nums []int, k int) []int {
mp := map[int]int{}
res := make([]int, k)
for _, v := range nums { // 遍历数组
mp[v]++ // 字典值(频率)加一
}
h := &Heap{} // 初始化堆
heap.Init(h) // 用go标准库提供的heap
for key, value := range mp { // 遍历字典
heap.Push(h, [2]int{key, value}) // 元素入堆
if h.Len() > k { // 堆的节点数超过k个
heap.Pop(h) // 删除顶点(min)
}
}
for i := 0; i < k; i++ { // 遍历堆
res[k-i-1] = heap.Pop(h).([2]int)[0] // 将堆中排序后的元素加入结果集
}
return res
}
|
自己实现小顶堆
- 借助 哈希表 来建立数字和其出现次数的映射,遍历一遍数组统计元素的频率
- 维护一个元素数目为 k 的最小堆
- 每次都将新的元素与堆顶元素(堆中频率最小的元素)进行比较
- 如果新的元素的频率比堆顶端的元素大,则弹出堆顶端的元素,将新的元素添加进堆中
- 最终,堆中的 k 个元素即为前 k 个高频元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
func topKFrequent(nums []int, k int) []int {
res := make([]int, 0)
// 统计各元素频率
mp := make(map[int]int)
for _, v := range nums {
mp[v]++
}
// 维护一个大小为k的最小堆
// 每个节点表示为元素及其频率
heap := make([][2]int, 0)
// 遍历字典元素入堆
for key, v := range mp {
// 判当前堆大小是否大于k
if len(heap) >= k {
// 判当前元素是否小于堆顶元素
if v < heap[0][1] {
// 当前元素无需入堆
continue
} else {
// 修改堆顶元素为当前元素
heap[0] = [2]int{key, v}
}
} else {
// 当前元素入堆
heap = append(heap, [2]int{key, v})
}
// 堆化,从尾节点遍历到根节点,维护小顶堆性质
for i := len(heap)/2 - 1; i >= 0; i-- {
heapify(heap, len(heap), i)
}
}
// 小顶堆排序为降序
for i := len(heap) - 1; i >= 0; i-- {
// 交换堆顶元素和未排定的末尾元素
heap[0], heap[i] = heap[i], heap[0]
// 将未排定元素从堆顶向下重新堆化
heapify(heap, i, 0)
}
// 将堆中元素全部放入结果集
for _, v := range heap {
res = append(res, v[0])
}
return res
}
func heapify(heap [][2]int, len, curIdx int) {
// 初始化最小元素索引为当前元素索引
minIdx := curIdx
// 求得当前元素左右孩子索引
l := 2*curIdx + 1
r := 2*curIdx + 2
// 判左右孩子是否小于当前最小元素
if l < len && heap[l][1] < heap[minIdx][1] {
minIdx = l
}
if r < len && heap[r][1] < heap[minIdx][1] {
minIdx = r
}
if minIdx != curIdx {
// 交换当前元素与最小元素
heap[curIdx], heap[minIdx] = heap[minIdx], heap[curIdx]
// 向下递归
heapify(heap, len, minIdx)
}
}
|
桶排序
依旧使用哈希表统计频率,统计完成后,创建一个数组,将频率作为数组下标,对于出现频率不同的数字集合,存入对应的数组下标即可
由于k
不会大于数组长度且最大频率可能为len(nums)
,所以定义len(nums)+1
个桶,每个桶中存相同频率的元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
func topKFrequent(nums []int, k int) []int {
res := make([]int, 0)
// 统计各元素频率
mp := make(map[int]int)
for _, v := range nums {
mp[v]++
}
// 初始化len(nums)+1个桶
// 每个桶中存相同频率的元素
buckets := make([][]int, len(nums)+1)
// 遍历字典元素入对应桶
for key, v := range mp {
// 判该频率的桶是否为空
if len(buckets[v]) == 0 {
// 初始化该频率的桶
buckets[v] = make([]int, 0)
}
// 按频率将元素放入对应桶
buckets[v] = append(buckets[v], key)
}
// 倒序遍历桶取k个元素
for i := len(buckets) - 1; i >= 0; i-- {
// 判该桶是否非空
if len(buckets[i]) != 0 {
// 遍历该桶
for _, v := range buckets[i] {
// 桶中当前元素加入结果集
res = append(res, v)
// 更新k
k--
// 判是否取够k个元素
if k == 0 {
return res
}
}
}
}
return res
}
|
附加
最小栈
155. 最小栈 - 力扣(LeetCode)
设计一个支持 push
,pop
,top
操作,并能在常数时间内检索到最小元素的栈
思路一:维护一个整型量保存最小值,在push和pop时更新最小值属性,若pop了最小值,则要遍历栈更新最小值(不推荐)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
type MinStack struct {
minVal int
stack []int
}
func Constructor() MinStack {
return MinStack{
minVal: math.MaxInt,
stack: make([]int, 0)}
}
func (this *MinStack) Push(val int) {
this.stack = append(this.stack, val)
if val < this.minVal {
this.minVal = val
}
}
func (this *MinStack) Pop() {
if this.Top() == this.minVal {
this.minVal = math.MaxInt
for i := 0; i < len(this.stack)-1; i++ {
if this.stack[i] < this.minVal {
this.minVal = this.stack[i]
}
}
}
this.stack = this.stack[:len(this.stack)-1]
}
func (this *MinStack) Top() int {
return this.stack[len(this.stack)-1]
}
func (this *MinStack) GetMin() int {
return this.minVal
}
|
思路二:维护一个最小栈,保证栈顶一定是最小值,若push元素大于最小值,则向最小栈push最小值,pop正常。保证每次pushpop后最小栈顶一定最小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
type MinStack struct {
min []int
stack []int
}
func Constructor() MinStack {
return MinStack{
min: make([]int, 0),
stack: make([]int, 0)}
}
func (this *MinStack) Push(val int) {
minVal := this.GetMin()
if val < minVal {
this.min = append(this.min, val)
} else {
this.min = append(this.min, minVal)
}
this.stack = append(this.stack, val)
}
func (this *MinStack) Pop() {
this.min = this.min[:len(this.min)-1]
this.stack = this.stack[:len(this.stack)-1]
}
func (this *MinStack) Top() int {
return this.stack[len(this.stack)-1]
}
func (this *MinStack) GetMin() int {
if len(this.min) != 0 {
return this.min[len(this.min)-1]
}
return 1 << 31 // 最小整型值
}
|
字符串解码
394. 字符串解码 - 力扣(LeetCode)
给定一个字符串,将括号里的字符重复括号前数字的次数,最终返回一个字符串
思路:遍历字符串入栈,遇到右括号时,不断弹栈,保存括号里的字符和括号前的数字,将括号里字符重复入栈给定次数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
func decodeString(s string) string {
stack := make([]rune, 0)
for _, v := range s {
// 解码
if v == ']' {
temp := make([]rune, 0)
// 逆序取出括号中的字符串
for stack[len(stack)-1] != '[' {
temp = append(temp, stack[len(stack)-1])
stack = stack[:len(stack)-1]
}
// 弹出'['
stack = stack[:len(stack)-1]
// 保存重复次数(rune -> int)
countSlice := make([]rune, 0)
// 逆序取出数字
for len(stack) != 0 && stack[len(stack)-1] <= '9' && stack[len(stack)-1] >= '0' {
countSlice = append(countSlice, stack[len(stack)-1])
stack = stack[:len(stack)-1]
}
// 翻转
for i, j := 0, len(countSlice)-1; i < j; i, j = i+1, j-1 {
countSlice[i], countSlice[j] = countSlice[j], countSlice[i]
}
count, _ := strconv.Atoi(string(countSlice))
// 重复字符入栈
for range count {
// 逆序遍历括号中字符
for i := len(temp) - 1; i >= 0; i-- {
stack = append(stack, temp[i])
}
}
} else {
stack = append(stack, v)
}
}
return string(stack)
}
// 写法二:用slices.Reverse翻转 直接追加打散的切片元素
func decodeString(s string) string {
stack := make([]byte, 0)
for i := 0; i < len(s); i++ {
if s[i] == ']' {
// 取出逆序字符串
inter := make([]byte, 0)
for len(stack) != 0 && stack[len(stack)-1] != '[' {
inter = append(inter, stack[len(stack)-1])
stack = stack[:len(stack)-1]
}
// 弹出]
stack = stack[:len(stack)-1]
// 取出逆序数字
countByte := make([]byte, 0)
for len(stack) != 0 && stack[len(stack)-1] >= '0' && stack[len(stack)-1] <= '9' {
countByte = append(countByte, stack[len(stack)-1])
stack = stack[:len(stack)-1]
}
// 翻转逆序
slices.Reverse(countByte)
slices.Reverse(inter)
count, _ := strconv.Atoi(string(countByte))
// 解码字符串入栈
for j := 0; j < count; j++ {
stack = append(stack, inter...)
}
} else {
stack = append(stack, s[i])
}
}
return string(stack)
}
|
克隆图
133. 克隆图 - 力扣(LeetCode)
给定无向连通图中一个节点的引用,返回该图的深拷贝(克隆)
思路一:非递归,用栈实现(不推荐)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
func cloneGraph(node *Node) *Node {
if node == nil {
return node
}
stack := make([]*Node, 0)
newStack := make([]*Node, 0)
mp := make(map[int]*Node)
temp := make(map[int]bool)
// 给定节点入栈
stack = append(stack, node)
temp[node.Val] = true
// 遍历所有旧节点创建对应新节点
for len(stack) != 0 {
// 取出一个节点作为当前节点
cur := stack[len(stack)-1]
stack = stack[:len(stack)-1]
// 将当前节点的邻居节点入栈
for _, v := range cur.Neighbors {
// 未遍历过该节点
if !temp[v.Val] {
stack = append(stack, v)
temp[v.Val] = true
}
}
// 构造当前节点的拷贝
newNode := &Node{
Val: cur.Val,
Neighbors: cur.Neighbors,
}
// 将新节点存入字典
mp[newNode.Val] = newNode
// 将新节点存入新栈
newStack = append(newStack, newNode)
}
for len(newStack) != 0 {
// 取出一个节点作为当前节点
cur := newStack[len(newStack)-1]
newStack = newStack[:len(newStack)-1]
// 保存当前节点的新邻居节点
temp := make([]*Node, 0)
// 遍历当前节点的邻居节点
for i := 0; i < len(cur.Neighbors); i++ {
// 在字典中找到对应的新节点
if v, ok := mp[cur.Neighbors[i].Val]; ok {
// 保存当前节点的新邻居节点
temp = append(temp, v)
}
}
// 修改所有邻居节点为新节点
cur.Neighbors = nil
cur.Neighbors = append(cur.Neighbors, temp...)
}
// 字典中找给定节点对应的新节点
if v, ok := mp[node.Val]; ok {
return v
}
return nil
}
|
思路二:递归
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
func cloneGraph(node *Node) *Node {
// 记录旧->新节点的对应
mp := make(map[*Node]*Node)
return clone(node, mp)
}
func clone(cur *Node, mp map[*Node]*Node) *Node {
if cur == nil {
return cur
}
// 字典中存在该节点对应的新节点
if v, ok := mp[cur]; ok {
return v
}
// 构建新节点
newNode := &Node{
Val: cur.Val,
Neighbors: make([]*Node, len(cur.Neighbors)),
}
// 更新字典:旧节点->新节点
mp[cur] = newNode
// 遍历当前节点的邻居节点
for i := 0; i < len(cur.Neighbors); i++ {
newNode.Neighbors[i] = clone(cur.Neighbors[i], mp)
}
return newNode
}
|
01 矩阵
542. 01 矩阵 - 力扣(LeetCode)
思路一:对每个元素BFS(超时)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
func updateMatrix(mat [][]int) [][]int {
// 遍历给定矩阵
for i := 0; i < len(mat); i++ {
for j := 0; j < len(mat[0]); j++ {
// 遇到元素1
if mat[i][j] == 1 {
queue := make([][2]int, 0)
// 当前元素入队
queue = append(queue, [2]int{i, j})
// 记录距离
dist := 0
// 标记是否找到最近元素0
isFind := false
// 标记该坐标是否被遍历过
mp := make(map[[2]int]bool)
// BFS
for len(queue) != 0 && !isFind {
// 记录本层元素数
count := len(queue)
// 遍历本层所有元素
for k := 0; k < count && !isFind; k++ {
// 记录队首元素后出队
curI, curJ := queue[0][0], queue[0][1]
queue = queue[1:]
// 标记当前元素已遍历过
mp[[2]int{curI, curJ}] = true
// 找到元素0
if mat[curI][curJ] == 0 {
// 修改矩阵对应元素为距离
mat[i][j] = dist
// 标记找到最近元素0
isFind = true
}
// 上方元素入队
if curI != 0 && !mp[[2]int{curI - 1, curJ}] {
queue = append(queue, [2]int{curI - 1, curJ})
}
// 下方元素入队
if curI != len(mat)-1 && !mp[[2]int{curI + 1, curJ}] {
queue = append(queue, [2]int{curI + 1, curJ})
}
// 左方元素入队
if curJ != 0 && !mp[[2]int{curI, curJ - 1}] {
queue = append(queue, [2]int{curI, curJ - 1})
}
// 右方元素入队
if curJ != len(mat[0])-1 && !mp[[2]int{curI, curJ + 1}] {
queue = append(queue, [2]int{curI, curJ + 1})
}
}
// 距离+1
dist++
}
}
}
}
return mat
}
|
思路二BFS:更新每个元素0四周需要更新的元素,再更新每个更新过的元素四周需要更新的元素,每次都更新完一级四周需要更新的元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
func updateMatrix(mat [][]int) [][]int {
queue := make([][2]int, 0)
// 遍历给定矩阵
for i := 0; i < len(mat); i++ {
for j := 0; j < len(mat[0]); j++ {
// 遇到元素0
if mat[i][j] == 0 {
// 坐标入队
queue = append(queue, [2]int{i, j})
} else {
// 标记当前元素需要更新
mat[i][j] = -1
}
}
}
// 设置四周偏移量
ds := [4][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}
// 遍历队列
for len(queue) != 0 {
// 保存队首后出队
x, y := queue[0][0], queue[0][1]
queue = queue[1:]
// 遍历当前元素的四周
for _, offset := range ds {
// 给当前元素下标设置偏移量
curX := x + offset[0]
curY := y + offset[1]
// 坐标有效且对应元素需要更新
if curX >= 0 && curY >= 0 && curX < len(mat) && curY < len(mat[0]) && mat[curX][curY] == -1 {
// 更新元素值
mat[curX][curY] = mat[x][y] + 1
// 更新后元素的下标入队
queue = append(queue, [2]int{curX, curY})
}
}
}
return mat
}
|
数据流的中位数
295. 数据流的中位数 - 力扣(LeetCode)
使用两个优先队列(堆)来维护整个数据流数据,为了可以在 O(1) 的复杂度内取得当前中位数,令 l
为大根堆,r
为小根堆,并人为固定 l
和 r
之前存在如下关系:
- 当数据流元素数量为偶数:
l
和 r
大小相同,此时中位数为两者堆顶元素的平均值;
- 当数据流元素数量为奇数:
l
比 r
多一,此时中位数为 l
的堆顶原数
当添加一个数 num 时需要分情况讨论:
-
$num≤max{l}$
此时 num 小于等于中位数,需要将该数添加到 l 中。新的中位数将小于等于原来的中位数,因此可能需要将 l 中最大的数移动到 r 中。
-
$num>max{r}$
此时 num 大于中位数,需要将该数添加到 r 中。新的中位数将大于等于原来的中位数,因此可能需要将 r 中最小的数移动到 l 中。
具体是否移动取决与俩堆的大小,目标是维持俩堆大小相等或左比右大1
特别地,当 l 为空时,将 num 添加到 l 中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
|
// 左为大顶堆 右为小顶堆
type MedianFinder struct {
heapL []int
heapR []int
}
func Constructor() MedianFinder {
return MedianFinder{
heapL: make([]int, 0),
heapR: make([]int, 0),
}
}
func (this *MedianFinder) AddNum(num int) {
// 判加入值是否小于等于左的最大值
if len(this.heapL) == 0 || num <= this.heapL[0] {
// 加入左尾
push(&this.heapL, num, 1)
// 判左是否比右多一个以上
if len(this.heapL) > len(this.heapR)+1 {
// 左最大加入右尾
push(&this.heapR, this.heapL[0], 0)
// 更新左
remove(&this.heapL, 0, 1)
}
} else {
// 加入右尾
push(&this.heapR, num, 0)
// 判右是否比左多
if len(this.heapR) > len(this.heapL) {
// 右最小加入左尾
push(&this.heapL, this.heapR[0], 1)
// 更新右
remove(&this.heapR, 0, 0)
}
}
}
func (this *MedianFinder) FindMedian() float64 {
// 判左长度是否大于右
if len(this.heapL) > len(this.heapR) {
return float64(this.heapL[0])
} else {
return float64(this.heapL[0]+this.heapR[0]) / 2.0
}
}
func push(heap *[]int, val, sign int) {
// 加入堆尾
*heap = append(*heap, val)
// 从尾向上维护堆
up(*heap, len(*heap)-1, sign)
}
func remove(heap *[]int, delIdx, sign int) {
// 交换要删除元素和尾元素
swap(*heap, delIdx, len(*heap)-1)
// 删除尾元素
*heap = (*heap)[:len(*heap)-1]
// 从删除元素索引开始向下维护堆
down(*heap, delIdx, sign)
}
func down(heap []int, curIdx, sign int) {
l := 2*curIdx + 1
r := 2*curIdx + 2
// 小顶堆
if sign == 0 {
minIdx := curIdx
if l < len(heap) && heap[l] < heap[minIdx] {
minIdx = l
}
if r < len(heap) && heap[r] < heap[minIdx] {
minIdx = r
}
if minIdx != curIdx {
heap[curIdx], heap[minIdx] = heap[minIdx], heap[curIdx]
down(heap, minIdx, sign)
}
} else { // 大顶堆
maxIdx := curIdx
if l < len(heap) && heap[l] > heap[maxIdx] {
maxIdx = l
}
if r < len(heap) && heap[r] > heap[maxIdx] {
maxIdx = r
}
if maxIdx != curIdx {
heap[curIdx], heap[maxIdx] = heap[maxIdx], heap[curIdx]
down(heap, maxIdx, sign)
}
}
}
func up(heap []int, curIdx, sign int) {
// 父节点索引
f := (curIdx - 1) / 2
// 判父节点索引是否越界
if f >= 0 {
if (sign == 0 && heap[curIdx] < heap[f]) || (sign == 1 && heap[curIdx] > heap[f]) {
heap[f], heap[curIdx] = heap[curIdx], heap[f]
up(heap, f, sign)
}
}
}
func swap(heap []int, i, j int) {
heap[i], heap[j] = heap[j], heap[i]
}
|
- 时间复杂度:
- addNum: O(logn),其中 n 为累计添加的数的数量
- findMedian: O(1)。
- 空间复杂度:O(n),主要为堆的开销
简化路径
71. 简化路径 - 力扣(LeetCode)
给定一个字符串 path
,表示指向某一文件或目录的 Unix 风格 绝对路径 (以 '/'
开头),要求将其转化为 更加简洁的规范路径。
在 Unix 风格的文件系统中规则如下:
- 一个点
'.'
表示当前目录本身。
- 此外,两个点
'..'
表示将目录切换到上一级(指向父目录)。
- 任意多个连续的斜杠(即,
'//'
或 '///'
)都被视为单个斜杠 '/'
。
- 任何其他格式的点(例如,
'...'
或 '....'
)均被视为有效的文件/目录名称。
返回的 简化路径 必须遵循下述格式:
- 始终以斜杠
'/'
开头。
- 两个目录名之间必须只有一个斜杠
'/'
。
- 最后一个目录名(如果存在)不能 以
'/'
结尾。
- 此外,路径仅包含从根目录到目标文件或目录的路径上的目录(即,不含
'.'
或 '..'
)。
返回简化后得到的 规范路径
思路:栈内只存有效文件/目录名,最后对栈内各元素加分隔符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
// 写法一:手动分割
func simplifyPath(path string) string {
// 栈内存各个有效文件/目录名称
stack := make([]string, 0)
i := 0
for i < len(path) {
// 判是否遇到.
if path[i] == '.' {
// 遍历该连续.
cnt := 0
for i < len(path) && path[i] == '.' {
i++
cnt++
} // 此时i指向不为.
// 判是否为.开头的文件/目录名称
if i < len(path) && path[i] != '/' {
start := i
// 遍历该有效字符
for i < len(path) && path[i] != '/' {
i++
} // 此时i指向/
stack = append(stack, path[start-cnt:i])
} else if cnt == 1 { // 处理.
continue
} else if len(stack) != 0 && cnt == 2 {
stack = stack[:len(stack)-1]
} else if cnt >= 3 {
stack = append(stack, path[i-cnt:i])
}
} else if path[i] == '/' {
for i < len(path) && path[i] == '/' {
i++
} // 此时i指向不为/
} else { // 此时i指向有效字符且非.
start := i
// 遍历该有效字符
for i < len(path) && path[i] != '/' {
i++
} // 此时i指向/
stack = append(stack, path[start:i])
}
}
// 将各有效文件/目录用分隔符'/'连接
res := strings.Join(stack, "/")
// 路径首加/
res = "/" + res
return res
}
// 写法二:调库函数分割
func simplifyPath(path string) string {
// 栈内存储有效文件/目录名称
stack := []string{}
// 遍历按分隔符'/'切割后的字符串切片
for _, v := range strings.Split(path, "/") {
if v == ".." {
if len(stack) != 0 {
stack = stack[:len(stack)-1]
}
} else if v != "." && v != "" {
stack = append(stack, v)
}
}
res := "/" + strings.Join(stack, "/")
return res
}
|
基本计算器
224. 基本计算器 - 力扣(LeetCode)
给定一个字符串表达式 s
,实现一个基本计算器来计算并返回它的值。
思路:使用两个栈 nums 和 ops
-
nums : 存放所有的数字
-
ops :存放加、减、左括号
从前往后对遍历到的字符分情况处理:
- 空格 : 跳过
(
: 直接加入 ops
中,等待与之匹配的 )
- 数字 : 从当前位置开始继续往后取,将整一个连续数字整体取出,加入
nums
)
: 使用现有的 nums
和 ops
进行计算,直到ops
为空或遇到左括号,计算结果放到 nums
,ops
中该左括号出栈
+/-
: 使用现有的 nums
和 ops
进行计算,直到ops
为空或遇到左括号,计算结果放到 nums
,最后+/-
入栈,;若为(-
,则加0
后再入栈
注意:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
func calculate(s string) int {
// 清除字符串s中所有空格
s = strings.ReplaceAll(s, " ", "")
// nums中预加0防止一开始为-
nums, ops := []int{0}, []byte{}
// 利用nums和ops计算
cal := func() {
op := ops[len(ops)-1]
ops = ops[:len(ops)-1]
if op == '+' {
nums[len(nums)-2] += nums[len(nums)-1]
} else {
nums[len(nums)-2] -= nums[len(nums)-1]
}
nums = nums[:len(nums)-1]
}
for i := 0; i < len(s); i++ {
if s[i] == ' ' {
continue
} else if s[i] == '(' {
ops = append(ops, '(')
} else if s[i] <= '9' && s[i] >= '0' {
// 将整一个连续数字整体取出
start := i
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
i++
}
num, _ := strconv.Atoi(s[start:i])
nums = append(nums, num)
i--
} else { // + - )
// 处理ops中+/-
for len(ops) != 0 && ops[len(ops)-1] != '(' {
cal()
}
if s[i] == ')' {
// (出栈
ops = ops[:len(ops)-1]
} else { // +/-
// 判字符串中是否为(-或(+
if i != 0 && s[i-1] == '(' {
// 加0变(0-或(0+
nums = append(nums, 0)
}
ops = append(ops, s[i])
}
}
}
if len(ops) != 0 {
cal()
}
return nums[len(nums)-1]
}
|