栈与队列
栈与队列基础知识与Python实现
定义
栈是一种后进先出的数据结构,元素从顶端入栈,然后从顶端出栈。
队列是一种先进先出的数据结构,元素从后端入队,然后从前端出队。
python实现
- python中用数组/链表实现栈(stack)
对数组:添加要添在尾部,由于出栈是先拿数组尾部元素O(1),若添加在开头,出栈时间复杂度O(n)
对链表:添加要添在头部,若添加在尾部,出栈时间复杂度O(n)
常用数组操作:append() pop() - 内置模块deque、queue 实现栈
普通的queue.Queue或SimpleQueue没有类似于peek的功能,也无法用索引访问
deque可以用索引访问,可以实现和peek相似的功能
注:若用list实现队列(queue),pop时间复杂度O(n)
deque(double-end-queue双向队列)模块:
以近似O(1)的性能在deque的两端插入和删除元素
常用方法:
append():从右端添加元素(与list同)
appendleft():从左端添加元素
extend():从右端逐个添加可迭代对象(Python中的可迭代对象有:列表、元组、字典、字符串)(与list同)
extendleft():从左端逐个添加可迭代对象
pop():移除列表中的一个元素(默认最右端的一个元素),并且返回该元素的值(与list同),如果没有元素,将会报出IndexError
popleft():移除列表中的一个元素(默认最左端的一个元素),并且返回该元素的值,如果没有元素,将会报出IndexError
count():统计列表中指定元素出现的个数(与list同)
len():队列元素个数
insert(index,obj):在指定位置插入元素(与list同)
clear():将deque中的元素全部删除,最后长度为0
queue 模块即一个整数,用于设置队列的最大长度。一旦队列达到上限,插入数据将会被阻塞,直到有数据出队列之后才可以继续插入。如果 maxsize 设置为小于或等于零,则队列的长度没有限制。
2. queue.LifoQueue(maxsize=0)——后进先出(Last In First Out: LIFO),栈
3. queue.PriorityQueue(maxsize=0)
优先级队列,比较队列中每个数据的大小,值最小的数据拥有出队列的优先权(优先级的值越小,则表明优先级越高)(小顶堆)。数据一般以元组的形式插入,典型形式为**(priority_number, data)**。如果队列中的数据没有可比性,那么数据将被包装在一个类中,忽略数据值,仅仅比较优先级数字。
优先队列是堆的常用场合
import queue
q = queue.PriorityQueue()
q.put([1, 'HaiCoder']) # 1是级别 越小级别越高,优先出队
q.put([40, 1024])
q.put([3, 'Python'])
q.put([5, True])
if __name__ == '__main__':
while not q.empty(): # 不为空时候执行
print("get data =", q.get())
# put()/get()/empty()方法
输出:
get data = [1, 'HaiCoder'] # 1的优先级最高
get data = [3, 'Python']
get data = [5, True]
get data = [40, 1024]
使用heapq模块实现堆
堆是一种完全二叉树。只不过堆是二叉树顺序结构的实现,说白了就是将一个数组看作二叉树。也就是说,堆的逻辑结构是一棵二叉树,存储结构是数组。
a = [] #创建一个空堆
heappush(heap,item) # 添加新值,此时自动建立了小根堆,heap是堆的意思, item是要被压入到heap中的对象
item可以为元组。 这适用于将比较值(例如任务优先级)与跟踪的主记录进行赋值的场合:
import heapq
h = [] # 空堆
heapq.heappush(h, (5, 'write code')) # 5是优先级
heapq.heappush(h, (7, 'release product'))
heapq.heappush(h, (1, 'write spec'))
heapq.heappush(h, (3, 'create tests'))
heapq.heappop(h), heapq.heappop(h)
输出:
(1, 'write spec'),(3, 'create tests')
heapq.heappop()是从堆中弹出并返回最小的值
import heapq
a = [] #创建一个空堆
heapq.heappush(a,18)
heapq.heappush(a,1)
heapq.heappush(a,20)
heapq.heappush(a,10)
heapq.heappush(a,5)
heapq.heappush(a,200)
print(a)
输出:[1, 5, 20, 18, 10, 200]
heapq无法直接建立大顶堆,但可以采取如下方法:每次push时给元素加一个负号(即取相反数),此时最小值变最大值,反之亦然,那么实际上的最大值就可以处于堆顶了,返回时再取负即可。
a = []
for i in [1, 5, 20, 18, 10, 200]:
heapq.heappush(a,-i)
print(list(map(lambda x:-x,a)))
输出:[200, 18, 20, 1, 10, 5]
- queue.SimpleQueue
先进先出类型的简单队列,没有大小限制。相比于 Queue 队列会缺少一些高级功能
Queue、LifoQueue、PriorityQueue 和 SimpleQueue 对象的基本使用方法
以 Queue 队列为例进行介绍(其他三个对象也有相同的方法)。
- Queue.qsize()
返回队列中数据元素的个数。
import queue
q = queue.Queue()
q.put('python-100') # 在队列中插入元素 'python-100'
print(q.qsize()) # 输出队列中元素个数为1
- Queue.empty()
如果队列为空,返回 True,否则返回 False。
import queue
q = queue.Queue()
print(q.empty()) # 对列为空,返回 True
q.put('python-100') # 在队列中插入元素 'python-100'
print(q.empty()) # 对列不为空,返回 False
- Queue.full()
如果队列中元素个数达到上限,返回 True,否则返回 False。
import queue
q = queue.Queue(3) # 定义一个长度为3的队列
print(q.full()) # 元素个数未达到上限,返回 False
q.put('python') # 在队列中插入字符串 'python'
q.put('-') # 在队列中插入字符串 '-'
q.put('100') # 在队列中插入字符串 '100'
print(q.full()) # 元素个数达到上限,返回 True
- Queue.put(item, block=True, timeout=None)
item,放入队列中的数据元素。
block,当队列中元素个数达到上限继续往里放数据时:如果 block=False,直接引发 queue.Full 异常;如果 block=True,且 timeout=None,则一直等待直到有数据出队列后可以放入数据;如果 block=True,且 timeout=N,N 为某一正整数时,则等待 N 秒,如果队列中还没有位置放入数据就引发 queue.Full 异常。
timeout,设置超时时间。
import queue
try:
q = queue.Queue(2) # 设置队列上限为2
q.put('python') # 在队列中插入字符串 'python'
q.put('-') # 在队列中插入字符串 '-'
q.put('100', block = True, timeout = 5) # 队列已满,继续在队列中插入字符串 '100',等待5秒后会引发 queue.Full 异常
except queue.Full:
print('queue.Full')
- Queue.put_nowait(item)
相当于 Queue.put(item, block=False),当队列中元素个数达到上限继续往里放数据时直接引发 queue.Full 异常。
import queue
try:
q = queue.Queue(2) # 设置队列上限为2
q.put_nowait('python') # 在队列中插入字符串 'python'
q.put_nowait('-') # 在队列中插入字符串 '-'
q.put_nowait('100') # 队列已满,继续在队列中插入字符串 '100',直接引发 queue.Full 异常
except queue.Full:
print('queue.Full')
- Queue.get(block=True, timeout=None):从队列中取出数据并返回该数据内容。
block,当队列中没有数据元素继续取数据时:如果 block=False,直接引发 queue.Empty 异常;如果 block=True,且 timeout=None,则一直等待直到有数据入队列后可以取出数据;如果 block=True,且 timeout=N,N 为某一正整数时,则等待 N 秒,如果队列中还没有数据放入的话就引发 queue.Empty 异常。
timeout,设置超时时间。
import queue
try:
q = queue.Queue()
q.get(block = True, timeout = 5) # 队列为空,往队列中取数据时,等待5秒后会引发 queue.Empty 异常
except queue.Empty:
print('queue.Empty')
- Queue.get_nowait()
相当于 Queue.get(block=False)block,当队列中没有数据元素继续取数据时直接引发 queue.Empty 异常。
import queue
try:
q = queue.Queue()
q.get_nowait() # 队列为空,往队列中取数据时直接引发 queue.Empty 异常
except queue.Empty:
print('queue.Empty')
Queue、LifoQueue 和 PriorityQueue 对象的高级使用方法
SimpleQueue 是 Python 3.7 版本中新加入的特性,与 Queue、LifoQueue 和 PriorityQueue 三种队列相比缺少了 task_done 和 join 的高级使用方法,所以才会取名叫 Simple 了,下面介绍一下 task_done 和 join 的使用方法。
task_done,表示队列内的数据元素已经被取出,即每个 get 用于获取一个数据元素, 后续调用 task_done 告诉队列,该数据的处理已经完成。如果被调用的次数多于放入队列中的元素个数,将引发 ValueError 异常。
join,一直阻塞直到队列中的所有数据元素都被取出和执行,只要有元素添加到 queue 中就会增加。当未完成任务的计数等于0,join 就不会阻塞。
232 用栈实现队列
分析:
一个做输入栈,一个做输出栈,实现先入先出
注意:当输出栈中无元素时才能将输入栈中元素写入
代码:
class MyQueue:
def __init__(self):
self.stack1 = [] # python 中用数组实现栈
self.stack2 = []
def push(self, x: int) -> None:
self.stack1.append(x)
# 时O(1)
def pop(self) -> int:
if self.stack2 == []: # 当输出栈为空时,才能将输入栈中元素移入
while self.stack1 != []:
self.stack2.append(self.stack1.pop()) # 数组的append()和pop()操作
return self.stack2.pop()
def peek(self) -> int:
if self.stack2 == []:
while self.stack1 != []:
self.stack2.append(self.stack1.pop())
return self.stack2[-1]
def empty(self) -> bool:
if self.stack1 == [] and self.stack2 == []:
return True
return False
225 用队列实现栈
用两个栈:
class MyStack:
# 两个队列
def __init__(self):
self.queue_in = deque()
self.queue_out = deque() # 用于暂存
def push(self, x: int) -> None:
self.queue_in.append(x)
def pop(self) -> int:
if self.empty():
return None
for i in range(len(self.queue_in)-1):
self.queue_out.append(self.queue_in.popleft()) # popleft() 先进先出
self.queue_in,self.queue_out = self.queue_out,self.queue_in
return self.queue_out.popleft()
def top(self) -> int:
if self.empty():
return None
return self.queue_in[-1]
def empty(self) -> bool:
return len(self.queue_in)==0
只用一个栈:
class MyStack:
# 一个队列
def __init__(self):
self.queue = deque()
def push(self, x: int) -> None:
self.queue.append(x)
def pop(self) -> int:
if self.empty():
return None
else:
for _ in range(len(self.queue)-1):
self.queue.append(self.queue.popleft())
return self.queue.popleft()
def top(self) -> int:
if self.empty():
return None
else:
return self.queue[-1]
def empty(self) -> bool:
return len(self.queue)==0 # return not self.queue
debug:def empty中判断是否为空不能用self.queue==[] !! deque()而非数组
注:使用deque而不用list是因为deque可以以近似O(1)的性能在deque的两端插入和删除元素。由于队列时先入先出,这里pop使用popleft()
20 有效的括号
分析:先出现的括号最后配对——先入后出——栈
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 stack == [] or stack[-1] != item: # 两个条件先后很重要 否则会出现溢出
return False # 若当前item和栈顶不同 或 若还未遍历完但stack已空 均属于无效字符串
else:
stack.pop()
return stack == [] # 若遍历完但stack中还有 也无效
debug:
elif stack == [] or stack[-1] != item: # 两个条件先后很重要 否则会出现溢出
1047 删除字符串中的所有相邻重复项
法1 使用栈
分析:将字符串写入栈 后入先出
- 若栈非空且写入字符和栈顶相同,则指向下一个字符,栈顶元素pop,指针重复len(s)次;
- 其余情况(栈为空或写入字符和栈顶不相同):压入当前字符 跳到下一个字符
- 时:O(n) 空:O(n)
class Solution:
def removeDuplicates(self, s: str) -> str:
stack = []
for i in range(len(s)):
if stack and stack[-1] == s[i]:
stack.pop()
else:
stack.append(s[i])
return ''.join(stack)
法2 使用双指针——# 时O(n) 空O(n)
注意s[i] = s[j]
class Solution:
def removeDuplicates(self, s: str) -> str:
if len(s)==1:
return s
else:
i = j = 0 # i新列表索引 j原列表索引
s = list(s)
for j in range(len(s)):
s[i] = s[j] # !!!
if i>0 and s[i]==s[i-1]: # 和前一个值相等——消去
i -= 1 # 当前新列表左移一位
else:
i += 1
return ''.join(s[:i])
150 逆波兰表达式求值
分析:
遍历原字符串,将数据入栈,遇到符号则pop出两个数据运算,并将结果入栈
向零取整即向0方向取最接近精确值的整数,换言之就是舍去小数部分,因此又称截断取整。
python字符串与数字转化:
字符串str转数字:
float(str)
int(str)
数字num转字符串:
str(num)
python取整——向上取整、向下取整、四舍五入取整、向0取整
向上取整:math.ceil()
向下取整:math.floor()、整除"//"
四舍五入:round()
向0取整:int() ——就是取整数部分
class Solution:
def evalRPN(self, tokens: List[str]) -> int:
sym = {'+','-','*','/'} # 使用集合 查找时间复杂度O(1)
stack = []
for token in tokens:
if token in sym:
token1 = stack.pop()
token2 = stack.pop()
if token == '+':
stack.append(token2+token1) # stack.append(stack.pop()+stack.pop())
elif token == '-':
stack.append(token2-token1) # stack.append(-stack.pop()+stack.pop())
elif token == '*':
stack.append(token2*token1) # stack.append(stack.pop()*stack.pop())
else:
stack.append(int(token2/token1)) # 向0取整
else: # 数字直接压入栈
stack.append(int(token)) # 字符转整型数据
return stack[0]
239 滑动窗口最大值
分析:
# 设置双端队列(两边都可以pop,便于O(1)时间获取)存储可能的最大值(降序)
# 若新添加的元素小于最小值,则直接添加在末尾。
# 若大于或等于最小值,则将队尾元素pop(因为此元素已不可能是一个滑动窗中的最大值了,无需维护),继续与队尾比较,直至小于队尾或为空。
# 每次输出最大值前,要检查队首(即最大值索引)是否在滑动窗中,若不在,则pop,然后输出队首。
# 队列中保存的是降序排列的元素的索引,滑动窗左索引为l,右索引记为r,很明显r-l+1<=k(l>=r-k+1),r同时作为遍历数组的指针,若队首元素<=r-k,说明队首元素不在滑动窗中
# 时O(n)(nums 中的每个元素最多也就被 push和 pop 各一次,没有任何多余操作,所以整体的复杂度还是 O(n))
# 空O(k)(辅助队列)
小结:
设计单调队列的时候,pop,和push操作要保持如下规则:
pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
que = collections.deque()
res = []
for i in range(len(nums)): # i同时是滑动窗口的右端
while que and nums[i]>=nums[que[-1]]: # 若当前元素大于队尾 则将队尾出栈
que.pop()
que.append(i)
if que[0]<=i-k: # 判断队首元素是否还在滑动窗口中
que.popleft()
if i+1>=k: # +1 由于python从0开始 此处意为遍历的元素数目大于等于窗口大小 则开始输出最大值
res.append(nums[que[0]])
return res
# debug 判断队列deque()是否为空不能用None
347 前K个高频元素
优先级队列:一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。
而且优先级队列内部元素是自动依照元素的权值排列(有序)。缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。
堆:堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。
所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用priority_queue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。
总结:
优先级队列就是队首取元素,队尾添元素,内部排序的堆,升序就是小顶堆(队首小),降序就是大顶堆(队首大)。
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
# 统计次数
hashmap = {}
for i in range(len(nums)):
hashmap[nums[i]] = hashmap.get(nums[i],0)+1 # .get(key,初值)
# 用(小顶堆)
minheap = [] #heap的数据类型只能是一个列表
#用固定大小为k的小顶堆,扫描所有频率的数值
for key,val in hashmap.items(): #字典 items() 返回可遍历的(键,值)元组数组
heapq.heappush(minheap,(val,key))
if len(minheap)>k: #如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
heapq.heappop(minheap)
#找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
res = [0]*k
for i in range(k-1,-1,-1):
res[i]=heapq.heappop(minheap)[1]
return res