算法训练营day11 打卡-栈和队列part2

Q1题目链接https://leetcode.com/problems/evaluate-reverse-polish-notation/

leetcode 150 medium 逆波兰表达式求值

讲解链接:

代码随想录

看到题目的第一思路:

不太理解什么是逆波兰表达式,其实就是把符号放在数字后面的表达式

代码随想录之后的想法和总结:

1逆波兰表达式 可以看作是 二叉树的后序遍历,计算机也是同样的思考方式(当看到数字和运算符号时)

2可以利用之前做过的 消除相邻字符串的题目思路- 用栈的思路来解决此题

3 如何运用栈:遇到数字就加入到栈里,遇到操作符就取出最近的两个数字进行计算,然后再把计算结果加入到栈里,以此往复直到所有数字/操作符都遍历完毕,最后储存在栈里的那一个数字就是最终计算结果

遇到的困难:

1 在计算符号的问题中,“-”和“/”要单独分情况处理

减法是用(-a+b)来代替(b-a)避免出现运算上的问题

除法是将两个元素分别储存,然后再用除号按顺序操作,避免出现运算顺序问题

2 怎么在算法中写出“遇到数字就加入到栈里,遇到操作符就取出最近的两个数字进行计算,然后再把计算结果加入到栈里”的逻辑实现?

用stack.push(stack.pop() + stack.pop());来实现,用运算符号把两个pop出的数字进行计算操作之后,再把他们的计算结果push进栈里,进行下一步的计算

举例:

Suppose the stack contains [3, 4, 5] and you encounter a "+" token. The stack operations would proceed as follows:

  1. stack.pop() -> Returns 5, the stack now is [3, 4].
  2. stack.pop() -> Returns 4, the stack now is [3].
  3. 5 + 4 -> The result is 9.
  4. stack.push(9) -> The stack now is [3, 9].

In essence, this line is a concise way of popping the two most recent operands from the stack, performing the addition, and then pushing the result back onto the stack.

可以记录备用的固定代码方法模版:


class Solution {
    public int evalRPN(String[] tokens) {
        // Create a stack to hold integer values
        Deque<Integer> stack = new LinkedList<>();
        
        // Iterate over each token in the input array
        for (String s : tokens) {
            // If the token is "+", pop the top two elements, add them, and push the result back
            if ("+".equals(s)) {
                stack.push(stack.pop() + stack.pop());
            }
            // If the token is "-", pop the top two elements, subtract the first popped element from the second, and push the result back
            else if ("-".equals(s)) {
                stack.push(-stack.pop() + stack.pop());
            }
            // If the token is "*", pop the top two elements, multiply them, and push the result back
            else if ("*".equals(s)) {
                stack.push(stack.pop() * stack.pop());
            }
            // If the token is "/", pop the top two elements, divide the second popped element by the first, and push the result back
            else if ("/".equals(s)) {
                int temp1 = stack.pop();  // First popped element (divisor)
                int temp2 = stack.pop();  // Second popped element (dividend)
                stack.push(temp2 / temp1); // Push the result of the division back to the stack
            }
            // If the token is a number, convert it to an integer and push it to the stack
            else {
                stack.push(Integer.valueOf(s));
            }
        }
        // The final result is the remaining element in the stack
        return stack.pop();
    }
}

时间复杂度以及空间复杂度:

time:

The method processes each token in the input array exactly once. The main operations within the loop are:

  • Checking the token type (operator or number), which takes constant time, O(1).
  • Pushing and popping from the stack, which also takes constant time, O(1)O

Since each token is processed once and each operation within the loop is O(1) the overall time complexity is: O(n)where nnn is the number of tokens in the input array.

space:

The space complexity is determined by the stack used to store the operands. In the worst case, the stack could store all the tokens (if they are all numbers), which means the space complexity is: O(n)where nnn is the number of tokens in the input array.


Q2题目链接https://leetcode.com/problems/sliding-window-maximum/

leetoce 239 hard滑动窗口最大值

“怎么维护单调队列的单调一致性?-只关注队首元素作为最大值

讲解链接:

代码随想录

看到题目的第一思路:

很难 没有思路

代码随想录之后的想法和总结:

1 这道题目使用了deque的数据结构-双端队列(因为队列的前端和后端都可以进行插入和删除,因此不遵循fifo原则)

2移动滑动窗口的过程模拟:遍历数组的时候,每移动一个位置,抛弃前面的元素就pop, 新加入的元素就push, getmaxvalue来返回当前队列里的最大值

最核心的要点就是只维护有可能成为最大值的元素,而不是把所有滑动窗口里的元素都放进来,比如1->3,那么要push3的时候可以直接把队列里的1弹出去,因为维护1没有任何意义,在往后移动的时候,1也不需要做真正的pop操作,因为之前已经被弹出去了,所以这样维护队首最大值的意义就是在移动过程中,get max value就直接返回队首第一个元素,它一定是队列的最大值

一些其他的评论区总结回答:。

  • 抓住一个逻辑:哪些数对结果没有影响,把这些数踢出即可。1.后进数比前面都大,前面的数全踢出。两点原因第一是生命周期,第二是他最大。2.后进的数比中间的数大,把中间的数踢出。原因一致。
  • 这道题的关键是如何维持队列的递减性,每次pop元素,移除的都是队列最左侧的值(确认最大值还在不在),每次push元素,从右侧开始循环移除比自己小的值(保证队列是递减的,而且每一个新的值都可以加入队列),之后循环输出队列最左侧的最大值就可以了。至于5 3 4的问题,当4进入队列时,3已经被移除(因为他比新来的4小,没必要维护他)所以5还在的时候输出5,5不在的时候输出4,根本没有3的事了
  • 确保这两个条件成立,1. 队首的元素一定在窗口里 2. 单调队列(单调递减)。 (tips: 队列可以用来存储 nums 下标) 具体做法如下: 1.窗口滑动后,队首不在窗口里,就从首部弹出。 2.队尾 ≤ 添加进来的元素,就从尾部弹出,直到队列为空或队尾 > 添加进来的元素, 把元素添加进去。(比较的是 nums【i】, 存储的是 i)

遇到的困难:

Q:为什么在pop function 中要比较被弹出元素和当前队列首元素是否相同/为什么要比较两者大小?

A: 我们确保只有在滑出元素是当前队列的首元素时,才从队列中删除它。这样,我们可以确保队列中的首元素始终是当前窗口的最大值,并且在滑动窗口过程中正确地维护队列的单调性。

  • 当窗口滑动时,最左边的元素(滑出元素)可能是当前队列的首元素(即当前窗口的最大值)。
  • 如果滑出元素与队列的首元素相等,表示该元素已经在队列中,是当前窗口的最大值,需要移除以便更新新的窗口最大值。
  • 如果滑出元素不是队列的首元素,说明该元素已经被移除,不再是当前窗口的最大值,因此无需再次移除。

Q:push function如何在队列前端入口处维护最大值? 算法思想:每次向单调队列中插入元素时,比较当前元素与队列尾部元素大小,若待插入元素的值大于队列尾部元素,队列尾部元素出队,循环此操作,直到队列元素位空或遇到队列元素值大于或等于当前待插入元素,此时将待插入元素入队。

为什么每次插入元素需要和队列尾部元素比较,这种比较是否会漏掉元素呢? 回答: 1.每次待插入元素与队列尾部元素比较,是为了将最大值元素维护在队列的入口前端; 2.这样不会漏掉元素,因为待插入的元素与队列尾部元素比较时,被弹出的队列元素一定是在当前元素前插入的,换句话说被“抛弃”的元素一定是先于当前插入元素弹出,所以对集合/数组的最大值不会产生影响。

可以记录备用的固定代码方法模版:

from collections import deque


class MyQueue: #单调队列(从大到小
    def __init__(self):
        self.queue = deque() #这里需要使用deque实现单调队列,直接使用list会超时
    
    #每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
    #同时pop之前判断队列当前是否为空。
    def pop(self, value):
        if self.queue and value == self.queue[0]:
            self.queue.popleft()#list.pop()时间复杂度为O(n),这里需要使用collections.deque()
            
    #如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
    #这样就保持了队列里的数值是单调从大到小的了。
    def push(self, value):
        while self.queue and value > self.queue[-1]:
            self.queue.pop()
        self.queue.append(value)
        
    #查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
    def front(self):
        return self.queue[0]
    
class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        que = MyQueue()
        result = []
        for i in range(k): #先将前k的元素放进队列
            que.push(nums[i])
        result.append(que.front()) #result 记录前k的元素的最大值
        for i in range(k, len(nums)):
            que.pop(nums[i - k]) #滑动窗口移除最前面元素
            que.push(nums[i]) #滑动窗口前加入最后面的元素
            result.append(que.front()) #记录对应的最大值
        return result

时间复杂度以及空间复杂度:

MyQueue Class
  1. pop(value):

    • popleft operation is O(1)
    • Time Complexity: O(1)
  2. push(value):

    • Each element is added and removed at most once.
    • Amortized Time Complexity: O(1)per operation.
  3. front():

    • Accessing the front element is O(1)
    • Time Complexity: O(1=
Solution Class
  1. Initialization:

    • Push first k elements: O(k)=
  2. Sliding Window:

    • For each of the remaining n−kn - kn−k elements, call pop and push: O(1) each.
    • Total Time Complexity: O(n−k)
  3. Result Compilation:

    • Call front n−k+1n - k + 1n−k+1 times: O(1) each.
    • Total Time Complexity: O(n−k+1).

Overall Time Complexity: O(n)

Space Complexity

  1. MyQueue Class:

    • Space for the deque: O(k)
  2. Solution Class:

    • Space for the result list: O(n−k+1)

Overall Space Complexity: O(n)


Q3题目链接https://leetcode.com/problems/top-k-frequent-elements/

leetoce 347 medium 前k个高频元素

讲解链接:

代码随想录

看到题目的第一思路:

给所有元素排序,然后找出前k个最高频率的元素-这样时间复杂度太高O log(n)

代码随想录之后的想法和总结:

1 遇到top k问题-都可以用堆来解决-而不是用map存储所有元素,再从大到小排序找出第k个元素,因为我们只需要前k个元素顺序,而不需要所有元素顺序

2 为什么用小顶堆而不是大顶堆?因为大顶堆会把最高频/最大的元素弹出,而我们需要的是剔除最小/最低频的数字,这不是我们需要的结果

遇到的困难:

最重要的是:我们所建的小顶堆 它不会储存所有数组中的元素,只是储存k个元素,比如k为2,那么小顶堆只储存出现频率最高的前两个数,然后频率相对低的放在堆顶,那么如果后来遍历遇到的数字频率更高,就会把堆顶的数字挤掉,那么在堆中,留下的永远都是数组中出现频率最高的元素

重新整理并且复习了关于堆的概念,元素入堆,元素出堆,以及堆化和建堆的区别

并且了解了为什么堆这个数据结构能实现我们在top k问题的需求,用堆来实现了优先级队列

(优先级队列其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列,大顶堆相当于元素从大到小的顺序出队的优先级队列)

可以记录备用的固定代码方法模版:


#时间复杂度:O(nlogk)
#空间复杂度:O(n)
import heapq
class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        #要统计元素出现频率
        map_ = {} #nums[i]:对应出现的次数
        for i in range(len(nums)):
            map_[nums[i]] = map_.get(nums[i], 0) + 1
        
        #对频率排序
        #定义一个小顶堆,大小为k
        pri_que = [] #小顶堆
        
        #用固定大小为k的小顶堆,扫描所有频率的数值
        for key, freq in map_.items():
            heapq.heappush(pri_que, (freq, key))
            if len(pri_que) > k: #如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
                heapq.heappop(pri_que)
        
        #找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
        result = [0] * k
        for i in range(k-1, -1, -1):
            result[i] = heapq.heappop(pri_que)[1]
        return result

时间复杂度以及空间复杂度:

T: 首先遍历所有数组中的元素-O(n)

然后在堆中每加入一个元素并且维持堆-O log(k),其中 k是堆的大小,堆中元素的个数


今日收获,学习时长:

栈和队列总结:

part1 关于栈与队列

首先学习了栈和队列这种数据结构的基本概念,然后练习了如何用栈实现队列,(两个栈来实现一个队列的功能)如何用队列实现栈(一个队列就够了),

然后分别用栈解决了括号匹配问题,字符串去重问题,逆波兰表达式问题,用队列解决了滑动窗口最大值问题(单调队列- 主要思想是队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列)

然后通过求前 K 个高频元素,引出另一种队列就是优先级队列(优先级队列缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,所以在这里又复习了堆的概念,

  • 堆是一棵完全二叉树,根据成立条件可分为大顶堆和小顶堆。大(小)顶堆的堆顶元素是最大(小)的。
  • 优先队列的定义是具有出队优先级的队列,通常使用堆来实现。
  • 堆的常用操作及其对应的时间复杂度包括:元素入堆 O(log⁡n)、堆顶元素出堆 O(log⁡n) 和访问堆顶元素 O(1) 等。
  • 完全二叉树非常适合用数组表示,因此我们通常使用数组来存储堆。
  • 堆化操作用于维护堆的性质,在入堆和出堆操作中都会用到。
  • 输入 n 个元素并建堆的时间复杂度可以优化至 O(n) ,非常高效。
  • Top-k 是一个经典算法问题,可以使用堆数据结构高效解决,时间复杂度为 O(n logk)

Q:栈里面的元素在内存中是连续分布的么?

这个问题有两个陷阱:

  • 陷阱1:栈是容器适配器,底层容器使用不同的容器,导致栈内数据在内存中不一定是连续分布的。
  • 陷阱2:缺省情况下,默认底层容器是deque,那么deque在内存中的数据分布是什么样的呢? 答案是:不连续的,下文也会提到deque。
  • 25
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值