算法练习-第十三天【栈与队列】

栈与队列

150. 逆波兰表达式求值

参考:代码随想录

思路

根据提示,逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。那么可以使用栈来处理逆波兰表达式的计算。
当当前的字符是符号时,依次取出栈顶的2个元素。并根据符号进行运算,将计算后的结果插入回栈顶,继续向下运算。

func evalRPN(tokens []string) int {
	stack := []int{}
	for _, token := range tokens {
		num, err := strconv.Atoi(token)
		if 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)
			}
		} else {
			stack = append(stack, num)
		}
	}

	return stack[0]
}

总结

后缀表达式对于计算机来讲是友好的,因为利用栈的顺序来进行处理,不需要考虑回退等问题。

239. 滑动窗口最大值

参考:代码随想录

思路

根据题意求滑动窗口的最大值,第一想法是通过遍历数组,然后每次计算滑动窗口k范围内的最大值。时间复杂度是O(nk),很遗憾这种解法超时了。

func maxSlidingWindow(nums []int, k int) []int {
	rlt := []int{}
	rlt = append(rlt, maxValue(nums[:k]))
	for i := k + 1; i <= len(nums); i++ {
		rlt = append(rlt, maxValue(nums[i-k:i]))
	}

	return rlt
}

func maxValue(subnums []int) int {
	maxVal := math.MinInt32
	for _, num := range subnums {
		maxVal = max(maxVal, num)
	}

	return maxVal
}

func max(a, b int) int {
	if a > b {
		return a
	}

	return b
}

进阶

因为滑动窗口是一个元素进一个元素出这种方式,那么可以考虑使用大顶堆或者队列来完成。

  • 首先如果使用大顶堆时,堆顶是最大值,当窗口滑动时,每次只能移除最大值,无法移除其他的数值,那么就排除掉大顶堆来实现的可能。
  • 滑动窗口一个元素进一个元素出的模式很适合队列,要想获得最大值,就需要将最大值放到队列的出口处,每次调用queue.Front()就可以获取,整个队列如果维护滑动窗口内的所有数据并且从大到小排序的话,移除或添加元素会很复杂。其实并不需要关心滑动窗口内的所有元素,只需要关注可能成为最大值的元素即可。
  • 维护元素单调递减/递增的队列为单调队列
  • 此题采用单调递减队列时
    • queue.Push(int value)操作时,如果当前的元素值比队列尾部的大,则需要依次移除尾部小于此元素值的元素。
    • queue.Pop()操作时,如果当前元素等于队列头部元素则移除,否则不做任何操作。
func maxSlidingWindow(nums []int, k int) []int {
    // 单调队列。从大到小
    q := []int{}
    push := func(value int){
        // 新的值是否大于队尾值 则删除队尾值
        for len(q) >0 && value > q[len(q)-1]{
            q = q[:len(q)-1]
        }
        q = append(q, value)
    }
    // 一次只移动一个
    pop := func(value int){
        if len(q) > 0 && value == q[0]{
            q = q[1:]
        }
    }
    // 向队列添加k个元素
    for i := 0; i < k; i++{
        push(nums[i])
    }   
    ans := []int{}
    ans = append(ans, q[0])
    for i := k; i < len(nums); i++{
        pop(nums[i-k])
        push(nums[i])
        ans = append(ans, q[0])
    }
    return ans

}

自定义一个队列的实现方式

type MyQueue struct {
	queue []int
}

func (q *MyQueue) Front() int {
	return q.queue[0]
}

func (q *MyQueue) Back() int {
	return q.queue[len(q.queue)-1]
}

func (q *MyQueue) Push(x int) {
	for len(q.queue) > 0 && x > q.Back() {
		q.queue = q.queue[:len(q.queue)-1]
	}
	q.queue = append(q.queue, x)
}

func (q *MyQueue) Pop(x int) {
	if len(q.queue) > 0 && x == q.Front() {
		q.queue = q.queue[1:]
	}
}

// 单调递减队列
func NewMyQueue() *MyQueue {
	return &MyQueue{queue: []int{}}
}

func maxSlidingWindow(nums []int, k int) []int {
	rlt := []int{}
	q := NewMyQueue()
	for i := 0; i < k; i++ {
		q.Push(nums[i])
	}
	rlt = append(rlt, q.Front())
	for i := k; i < len(nums); i++ {
		q.Pop(nums[i-k])
		q.Push(nums[i])
		rlt = append(rlt, q.Front())
	}

	return rlt
}

总结

单调队列中的pushpop操作可以根据题意来进行处理并不是固定的。

347.前 K 个高频元素

参考:代码随想录

思路

统计前 K 个高频元素,需要进行如下三步:

  1. 使用map统计每个元素的频次
  2. 对map按照频次进行排序
  3. 取前 K 个元素

其中第二步对频次进行排序,因为快排时对整个数组元素进行排序,而本题只需要维护 K 个元素即可,因此使用到了优先队列。
优先队列是依靠堆来实现的,堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。

  • 大顶堆:父亲结点大于等于左右孩子
  • 小顶堆:父亲节点小于等于左右孩子

统计前 K 个高频元素如果使用大顶堆的话,堆顶是最大值,那么每次更新堆的时候移除的都是最大值,这不符合预期
使用小顶堆只需要维护 K 个频次元素,当有大于堆顶的频次进入时,移除堆顶元素。最后最小堆内的K个元素就是最高频的 K 个元素。

func topKFrequent(nums []int, k int) []int {
	m := map[int]int{}
	for _, num := range nums {
		m[num]++
	}
	h := &hp{}
	heap.Init(h)

	for value, cnt := range m {
		heap.Push(h, &pair{value, cnt})
		if h.Len() > k {
			heap.Pop(h)
		}
	}

	rlt := make([]int, k)
	for i := 0; i < k; i++ {
		rlt[k-i-1] = heap.Pop(h).(*pair).value
	}

	return rlt
}

type pair struct {
	value, cnt int
}

type hp []*pair

func (h hp) Len() int           { return len(h) }
func (h hp) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
func (h hp) Less(i, j int) bool { return h[i].cnt < h[j].cnt }

func (h *hp) Push(x interface{}) {
	*h = append(*h, x.(*pair))
}

func (h *hp) Pop() interface{} {
	a := *h
	x := a[len(a)-1]
	a = a[:len(a)-1]
	*h = a

	return x
}

总结

使用小顶堆只需要维护 K 个元素,时间复杂度是O(nlogk)。如果使用排序算法那么时间复杂度是O(logn)

posted @   neil_liu  阅读(22)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示