代码随想录刷题记录(五)——栈与队列

栈与队列

1. 理论基础

在这里插入图片描述

1.1 栈(stack)

基本概念:栈是一种后进先出(LIFO,Last In First Out)的数据结构,意味着最后添加到栈中的元素将是第一个被移除的元素。
基本操作

  • push:向栈顶添加一个元素。
  • pop:移除栈顶的元素,并返回它。
  • peek 或 top:查看栈顶元素,但不移除它。
  • isEmpty:检查栈是否为空。

1.2 队列(queue)

基本概念:队列是一种先进先出(FIFO,First In First Out)的数据结构,意味着最先添加到队列中的元素将是第一个被移除的元素。
基本操作

  • enqueue:在队列的末尾添加一个元素。
  • dequeue:移除队列的前端元素,并返回它。
  • front:查看队列前端的元素,但不移除它。
  • isEmpty:检查队列是否为空。

1.3 相关概念的比较

栈的首尾:

  • 栈顶(Top):栈的顶部,新元素被添加的地方,也是元素被移除的地方。在栈中,最后被添加的元素会最先被移除,这被称为后进先出(LIFO,Last In First Out)的特性。
  • 栈底(Bottom):通常不用于操作,可以理解为栈的起始位置,但在实际使用中,栈底并不用于元素的添加或移除。

队列的首尾:

  • 队首(Front):队列的前端,元素被移除的地方。在队列中,最先被添加的元素会最先被移除,这被称为先进先出(FIFO,First In First Out)的特性。
  • 队尾(Rear or Tail):队列的后端,新元素被添加的地方。

2. 相关题目

232.用栈实现队列

题目描述: 使用栈实现队列的下列操作:

  • push(x) – 将一个元素放入队列的尾部。
  • pop() – 从队列首部移除元素。
  • peek() – 返回队列首部的元素。
  • empty() – 返回队列是否为空。
    你 只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

思路: 用栈模拟队列的行为,需要两个栈,一个输入栈,一个输出栈。在python中,采用list模拟栈,list方法中与队列中操作的对应关系如下:

  • push(x) -> 列表的.append(x):将元素 x 添加到栈的顶部
  • pop() -> 列表的.pop():弹出栈的顶部元素
  • empty() -> not list:检查栈是否为空
    在这里插入图片描述
class MyQueue:

    def __init__(self):
        # 创建输入栈和输出栈,in主要负责push,out主要负责pop
        self.stack_in = []
        self.stack_out = []

    def push(self, x: int) -> None:
        # 新元素进来,就往in里面append
        self.stack_in.append(x)
        
    def pop(self) -> int:
        # 弹出元素需要从out里面弹出
        # 如果out里面没有元素,先要将所有元素从in里面转移到out里
        if self.empty(): # 先判断是否为空
            return None 
        
        if self.stack_out:
            return self.stack_out.pop()
        else:
            for i in range(len(self.stack_in)):# 将in中的元素全部转移到out里
                self.stack_out.append(self.stack_in.pop())
            return self.stack_out.pop()
            
    def peek(self) -> int: # 查看栈顶元素的操作,但不移除它
        ans = self.pop()
        self.stack_out.append(ans) # 将弹出的队首元素重新压入栈内(相当于队首)
        return ans

    def empty(self) -> bool: # 只要in或者out有元素,说明队列不为空
        return not (self.stack_in or self.stack_out)

# Your MyQueue object will be instantiated and called as such:
# obj = MyQueue()
# obj.push(x)
# param_2 = obj.pop()
# param_3 = obj.peek()
# param_4 = obj.empty()

225. 用队列实现栈

题目描述: 使用队列实现栈的下列操作:

  • push(x) – 元素 x 入栈
  • pop() – 移除栈顶元素
  • top() – 获取栈顶元素
  • empty() – 返回栈是否为空
    思路: 队列模拟栈,用一个队列就够了。队列是先进先出的规则,把一个队列中的数据导入另一个队列中,数据的顺序并没有变,并没有变成先进后出的顺序。在python中,使用双向队列deque()来模拟队列,采用其popleft()和append()方法,且deque可以用索引访问。
from collections import deque
class MyStack:

    def __init__(self):
        self.que = deque()

    def push(self, x: int) -> None: # 直接append
        self.que.append(x) # 直接在队尾进行添加

    def pop(self) -> int:
        if self.empty(): # 首先确定不空
            return None
        for i in range(len(self.que)-1): # 将元素从队首取出,在添加到队尾,循环len(self.que)-1次后弹出下一个元素
            self.que.append(self.que.popleft())
        return self.que.popleft()

    def top(self) -> int: # 和pop不同的是,top不会弹出这个元素,而是只显示
        if self.empty():
            return None
        for i in range(len(self.que)-1):
            self.que.append(self.que.popleft())
        temp = self.que.popleft()
        self.que.append(temp)
        return temp
        
    def empty(self) -> bool:
        return not self.que
    


# Your MyStack object will be instantiated and called as such:
# obj = MyStack()
# obj.push(x)
# param_2 = obj.pop()
# param_3 = obj.top()
# param_4 = obj.empty()

20. 有效的括号

题目描述: 给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串,判断字符串是否有效。有效字符串需满足:

  • 左括号必须用相同类型的右括号闭合。
  • 左括号必须以正确的顺序闭合。
  • 注意空字符串可被认为是有效字符串。

思路: 实际上就是判断对于给定的某个左括号,是否有与之相配的右括号。这里关于正确匹配有两点要注意的:(1)匹配性:每个左括号(包括 (, {, [)必须与相同类型的右括号(包括 ), }, ])闭合。(2)顺序性:左括号必须按照正确的顺序闭合,即不能出现一个右括号在它的匹配左括号之前。
而对于不匹配的情况,包含以下几种情况:
(1)括号数目不对:左括号数 != 右括号数
(2)括号匹配错误:不是按照正确的括号进行匹配,如 【[}】。那么我们如何判断左右括号是否匹配呢?可以借用栈,每次遇到左括号【 [{(】,就将相应的右括号压入栈【 ]})】,直到遇到第一个右括号【)】,将右括号与栈顶的元素进行比较【)】,如果匹配,则将栈内的右括号弹出(pop),如果不匹配,则返回false。如果栈已经空了,但仍然碰到了新的括号,说明左右括号数目不匹配,也返回false。

class Solution:
    def isValid(self, s: str) -> bool:
        stack = []

        for item in s:
            if item == '(':
                stack.append(')')
            elif item == '{':
                stack.append('}')
            elif item == '[':
                stack.append(']')
            elif not stack or stack[-1] != item:  # 这里采用列表的[-1]检查栈顶元素
                return False
            else:
                stack.pop()
        return True if not stack else False
            

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

题目描述: 给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。在 S 上反复执行重复项删除操作,直到无法继续删除。在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
思路: 和上一题“有小括号很类似”,这里的不同的字母等效于上述的括号类型。

方法一:使用栈

class Solution:
    def removeDuplicates(self, s: str) -> str:
        res = list() #创建一个列表,用来模拟栈
        
        for item in s: #遍历s中的字符
            if res and res[-1] == item: # 如果该列表不为空,则弹出栈顶元素
                res.pop()
            else:
                res.append(item)
        
        return ''.join(res)

方法二:采用快慢指针

class Solution:
    def removeDuplicates(self, s: str) -> str:
        fast = slow = 0
        res = list(s)
        while fast < len(res):
            # 赋值操作不能放在后面,因为fast+1后可能=len(res),会溢出
            res[slow] = res[fast]
            # 一定不要漏掉边界值slow > 0 
            if slow > 0 and res[slow] == res[slow-1]:
                slow -= 1
            else:
                slow += 1
            fast += 1
        return ''.join(res[0:slow])

150. 逆波兰表达式求值

题目描述: 根据 逆波兰表示法,求表达式的值。有效的运算符包括 + , - , * , / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:整数除法只保留整数部分。 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
思路: 逆波兰表达式是一种后缀表达式,所谓后缀就是指运算符写在后面。平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。

  • 逆波兰表达式有以下两个优点:
    • 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
    • 适合用栈操作运算:遇到数字则入栈;遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中。
  • 代码随想录中引入了operator库【from operator import add, sub, mul】,我在不采用额外库函数的情况下实现代码。还需要注意一点,在弹出两个栈顶的元素时,是遵从先进后出的规则,比如对于栈中[1,2]两个元素进行相减的操作,需要用后出栈的元素减去先出栈的元素。
  • 关于除法有点要注意的地方,按照提议,逆波兰表达式中的除法取两者商的整数部分,这其实是一种向零取整的做法。而python中的//是一种向-∞取整的做法,所以需要区分复数除法和非负数除法。
class Solution:
    def evalRPN(self, tokens):
        stack = []
        for token in tokens:
            if token in {'+', '-', '*', '/'}:
                op2 = stack.pop()
                op1 = stack.pop()
                if token == '+':  # 弹出两个栈顶的元素进行加法运算
                    stack.append(op1 + op2)
                elif token == '-':  # 应该用后弹出的减去先弹出的
                    stack.append(op1 - op2)
                elif token == '*':
                    stack.append(op1 * op2)
                elif token == '/':  # 注意处理除数为0的情况
                    if op2 == 0:
                        raise ValueError('division by zero')
                    else:
                        if op1 * op2 >= 0:
                            stack.append( op1 // op2)  # 使用整数除法
                        else:
                            stack.append( - abs(op1 //abs(op2)))
            else:  # 如果是数字,直接压栈
                stack.append(int(token))  # 记得转换为int在压入栈。
        return stack.pop()


# 创建 Solution 类的实例
solution = Solution()

# 准备测试用例的 tokens
tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]

# 调用 evalRPN 方法并传入 tokens
result = solution.evalRPN(tokens)

# 打印结果
print("The result of the RPN expression is:", result)

239. 滑动窗口最大值

题目描述: 给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。

  • 进阶:你能在线性时间复杂度内解决此题吗?

思路: 这里采用一种单调队列的方法,它是一种特殊类型的队列数据结构,用于维护一个序列的滑动窗口的特定性质,最常见的是单调性。具有以下特点:
(1)动态维护:单调队列可以在窗口滑动时动态维护其单调性。
(2)快速访问:在单调递减队列中,最大元素始终位于队列的前端;在单调递增队列中,最小元素始终位于队列的前端。
(3)高效操作:入队、出队和访问极值的操作都可以在 O(1) 时间内完成。

  • 实现:单调队列通常使用双端队列(deque)实现,因为双端队列允许在两端快速地添加和删除元素。
  • 根据题意,我们要求出滑动窗口中的最大值,实际上就是要维护单调队列中的最大值。但不意味着碰到比最大值小的值都丢弃不用,而是确保它的单调性。单调队列在这里实际上用作了一个“缓冲区”,它确保了在任何时候,窗口的最大值都可以快速访问,并且随着窗口的滑动,最大值可以被快速更新。
class MyQueue: # 单调队列
    def __init__(self):
        self.queue = deque()  # 采用双向队列实现单调队列,直接使用list会超时?也不太方便
        
    # 每次弹出时,要比较当前要弹出的值是否等于队列出口的元素,如果相等,则弹出;
    # 否则说明要弹出的数并非此前滑动窗口中的最大值
    # 同时pop之前要判断队列当前是否为空
    def pop(self, value):
        if self.queue and value == self.queue[0]:
            self.queue.popleft()  # list.pop()时间复杂度为O(n),而deque中的popleft()复杂度为O(n)
    
    # 如果push的值大于队列末尾的数值,则将队列后端的值弹出,直到该value小于等于当前队列末尾的数值
    # 这样确保队列里的数值是单调从大到小的
    def push(self,value):
        while self.queue and value > self.queue[-1]:
            self.queue.pop() # 弹出右端的元素
        self.queue.append(value)

    # 查询当前队列中的最大值,直接返回队列前段也就是fromt即可
    def front(self):
        return self.queue[0]

class Solution:
    def maxSlidingWindow(self,nums,k):
        que = MyQueue() # 创建一个队列
        result = [] # 记录每个滑动窗口的最大值
        # 先将初始滑动窗口的前k个元素放进队列
        for i in range(k):
            que.push(nums[i])
        result.append(que.front())

        for i in range(k, len(nums)):
            que.pop(nums[i-k]) # 移除滑动窗口最前端的元素,调用pop函数改变单调队列
            que.push(nums[i]) # 加入新的的元素,调用push函数改变单调队列
            result.append(que.front()) # 记录最大值
        return result

            
class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        # 创建队列
        que = MyQueue()
        result = []
        # 将前k个元素放进队列
        for i in range(k):
            que.push(nums[i])
        result.append(que.front())
        for i in range(k, len(nums)):
            que.pop(nums[i-k]) # 移除窗口最前面元素
            que.push(nums[i]) # 加入新元素
            result.append(que.front())
        return result

347.前 K 个高频元素

题目描述: 给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
思路: 我们可以用哈希表来统计每个数字出现的频率,如何统计频率前k高的元素,可以采用优先级队列的思想。
优先级队列: 一种按照元素的优先级(权值)来排序的特殊队列。在很多编程语言中,优先级队列通常是基于堆数据结构实现的。是一种特殊的完全二叉树,其中每个节点的值都必须大于或等于其子节点的值(大顶堆),或者小于或等于其子节点的值(小顶堆)。

  • 使用大顶堆还是小顶堆?
    • 如果使用大顶堆,堆顶始终是当前最大元素,这会导致我们无法保留前 K 高频元素。使用小顶堆,我们可以逐步移除频率最小的元素,最终保留下来的是频率最高的 K 个元素。
  • 为什么不采用快速排序?
    • 快速排序(快排)需要将整个数组排序,这将导致时间复杂度为 O(n log n)。
    • 使用优先级队列,特别是小顶堆,可以在 O(n log k) 的时间复杂度内解决问题,这在 k 远小于 n 时更优。

算法逻辑:

  • 首先,遍历数组并使用哈希表统计每个元素的出现次数。
  • 将哈希表中的元素和它们的频率转换为一个 vector,并根据频率对它们进行排序。
  • 使用小顶堆,将 vector 中的元素依次推入堆中,如果堆的大小超过 K,就弹出堆中频率最小的元素(即堆顶元素)。
  • 最后,堆中的元素即为出现频率前 K 高的元素。
import heapq # 导入python的堆排序模块
class Solution:
    def topKFrequent(self, nums, k):
        map1 = {} # 创建一个字典,存储每个数字出现的次数
        for i in range(len(nums)):
            # 如果字典 map_ 中已经存在键 nums[i],则取出其对应的值,加一,然后将结果重新赋值给 map_[nums[i]]。
            # 如果字典 map_ 中不存在键 nums[i],则使用默认值 0,加一,然后将结果 1 赋值给 map_[nums[i]]。
            map1[nums[i]] = map1.get(nums[i], 0 ) + 1

        # 对频率排序,定义一个小顶堆,大小为k
        pri_que = [] # 小顶堆

        # 用固定大小为k的小顶堆,扫描所有频率的数值
        for key,freq in map1.items(): # item是字典对象的一个方法,它返回一个包含所有(键,值)对的视图对象
            heapq.heappush(pri_que, (freq, key))# 将频率和元素组成的元组推入堆中,保持堆的性质

            if len(pri_que) > k:
                heapq.heappop(pri_que)

        # 找出前k个高频元素,因为小顶堆最先弹出的是最小的,所以倒序输出到数组
        result = [0]*k
        for i in range (k-1 , -1, -1): # 逆序遍历;或者可以正序遍历,再采用result[::-1]
            result[i] = heapq.heappop(pri_que)[1] # 注意弹出的是(freq,key)元组,我们要取这个元组的第二个值,故用索引[1]
        return result


nums = list(map(int, input().split()))
k = int(input())
sol = Solution()
result = sol.topKFrequent(nums,k)
print(result)

总结

  • 栈是一种数据结构,它的操作受限于两个基本动作:压栈(push)和弹栈(pop)。压栈是将元素添加到栈顶,而弹栈是从栈顶移除元素。这遵循了后进先出的原则。
  • 栈和递归是可以互换的。原因在于他们都遵循后进先出(LIFO,Last In First Out)的原则。递归是一种编程技术,其中函数调用自身。在递归调用中,最后一个被调用的函数会是第一个完成执行的,这同样遵循了后进先出的原则。在递归中,每次函数调用都会创建一个新的执行上下文,包括参数、局部变量等。这些上下文可以想象成被“压入”到一个隐式的调用栈中。当递归到达基本情况(base case)时,函数开始返回,每个函数调用的上下文被逐个“弹出”,直到最初的调用完成。
  • 20
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值