一、队列基础知识
简介:
队列(Queue):简称为队,一种线性表数据结构,是一种只允许在表的一端进行插入操作,而在表的另一端进行删除操作的线性表。
我们把队列中允许插入的一端称为 队尾(rear)
;把允许删除的另一端称为 队头(front)
。当表中没有任何数据元素时,称之为 空队
。
队列有两种基本操作:插入操作
和 删除操作
。
- 队列的插入操作又称为
入队
。 - 队列的删除操作又称为
出队
。
队列是一种FIFO结构。
1.1 队列的顺序储存实现
为了算法设计上的方便以及算法本身的简单,我们约定:队头指针 self.front 指向队头元素所在位置的前一个位置,而队尾指针 self.rear 指向队尾元素所在位置。
- 初始化时:创建一个空队列 self.queue,定义队列大小 self.size。令队头指针 self.front 和队尾指针 self.rear 都指向 -1。即 self.front = self.rear = -1。
- 判断队列为空:根据 self.front 和 self.rear 的指向位置关系进行判断。如果对头指针 self.front 和队尾指针 self.rear 相等,则说明队列为空。
- 判断队列为满:如果 self.rear 指向队列最后一个位置,即 self.rear == self.size - 1,则说明队列已满。
- 获取队头元素:先判断队列是否为空,为空直接抛出异常。如果不为空,因为 self.front 指向队头元素所在位置的前一个位置,所以队头元素在 self.front 后面一个位置上,返回 self.queue[self.front + 1]。
- 获取队尾元素:先判断队列是否为空,为空直接抛出异常。如果不为空,因为 self.rear 指向队尾元素所在位置,所以直接返回 self.queue[self.rear]。
- 入队操作:先判断队列是否已满,已满直接抛出异常。如果不满,则将队尾指针 self.rear 向右移动一位,并进行赋值操作。此时 self.rear 指向队尾元素。
- 出队操作:先判断队列是否为空,为空直接抛出异常。如果不为空,则将队头指针 self.front 指向元素赋值为 None,并将 self.front 向右移动一位。
实现代码:
class Queue:
# 初始化空队列
def __init__(self, size=100):
self.size = size
self.queue = [None for _ in range(size)]
self.front = -1
self.rear = -1
# 判断队列是否为空
def is_empty(self):
return self.front == self.rear
# 判断队列是否已满
def is_full(self):
return self.rear + 1 == self.size
# 入队操作
def enqueue(self, value):
if self.is_full():
raise Exception('Queue is full')
else:
self.rear += 1
self.queue[self.rear] = value
# 出队操作
def dequeue(self):
if self.is_empty():
raise Exception('Queue is empty')
else:
self.front += 1
return self.queue[self.front]
# 获取队头元素
def front_value(self):
if self.is_empty():
raise Exception('Queue is empty')
else:
return self.queue[self.front + 1]
# 获取队尾元素
def rear_value(self):
if self.is_empty():
raise Exception('Queue is empty')
else:
return self.queue[self.rear]
1.2循环队列的顺序存储实现
在队列的顺序存储实现中,当队列中第 0 ~ size - 1 位置均被队列元素占用时,有 self.rear == self.size - 1,队列已满,再进行入队操作就会抛出队列已满的异常。
此外,由于出队操作总是删除当前的队头元素,将 self.front 进行右移,而插入操作又总是在队尾进行。经过不断的出队、入队操作,队列的变化就像是使队列整体向右移动。当队尾指针 self.rear == self.size - 1 时,此时再进行入队操作就又抛出队列已满的异常。而之前因为出队操作而产生空余位置也没有利用上,这就造成了假溢出
问题。
为了解决假溢出
问题,有两种做法:
第一种:每一次删除队头元素之后,就将整个队列往前移动 1 个位置。其代码如下所示:
# 出队操作
def dequeue(self):
if self.is_empty():
raise Exception('Queue is empty')
else:
value = self.queue[0]
for i in range(self.rear):
self.queue[i] = self.queue[i + 1]
return value
这种情况下,队头指针似乎用不到了。因为队头指针总是在队列的第 0 个位置。但是因为删除操作涉及到整个队列元素的移动,所以每次删除操作的时间复杂度就从 O ( 1 ) O(1) O(1)变为了 O ( N ) O(N) O(N) 。这种方式不太可取。
第二种:将队列想象成为头尾相连的循环表,利用数学中的求模运算,使得空间得以重复利用,这样就解决了问题。
这样在进行插入操作时,如果队列的第 self.size - 1 个位置被占用之后,只要队列前面还有可用空间,新的元素加入队列时就可以从第 0 个位置开始继续插入。
我们约定:self.size 为循环队列的最大元素个数。队头指针 self.front 指向队头元素所在位置的前一个位置,而队尾指针 self.rear 指向队尾元素所在位置。则:
- 入队时,队尾指针循环前进 1 个位置,即 self.rear = (self.rear + 1) % self.size。
- 出队时,队头指针循环前进 1 个位置,即 self.front = (self.front + 1) % self.size。
注意:循环队列在一开始初始化,队列为空时,self.front 等于 self.rear。而当充满队列后,self.front 还是等于 self.rear。这种情况下就无法判断「队列为空」还是「队列为满」了。
为了区分循环队列中「队列为空」还是「队列已满」的情况,有多种处理方式:
- 方式 1:增加表示队列中元素个数的变量 self.count,用来以区分队列已满还是队列为空。在入队、出队过程中不断更新元素个数 self.count 的值。
队列已满条件为:队列中元素个数等于队列整体容量,即 self.count == self.size,
队空为空条件为:队列中元素个数等于 0,即 self.count == 0。 - 方式 2:增加标记变量 self.tag,用来以区分队列已满还是队列为空。
队列已满条件为:self.tag == 1 的情况下,因插入导致 self.front == self.rear。
队列为空条件为:在 self.tag == 0 的情况下,因删除导致 self.front == self.rear。 - 方式 3:特意空出来一个位置用于区分队列已满还是队列为空。入队时少用一个队列单元,即约定以「队头指针在队尾指针的下一位置」作为队满的标志。
队列已满条件为:队头指针在队尾指针的下一位置,即 (self.rear + 1) % self.size == self.front。
队列为空条件为:队头指针等于队尾指针,即 self.front == self.rear。
循环队列的代码实现:
class Queue:
# 初始化空队列
def __init__(self, size=100):
self.size = size + 1
self.queue = [None for _ in range(size + 1)]
self.front = 0
self.rear = 0
# 判断队列是否为空
def is_empty(self):
return self.front == self.rear
# 判断队列是否已满
def is_full(self):
return (self.rear + 1) % self.size == self.front
# 入队操作
def enqueue(self, value):
if self.is_full():
raise Exception('Queue is full')
else:
self.rear = (self.rear + 1) % self.size
self.queue[self.rear] = value
# 出队操作
def dequeue(self):
if self.is_empty():
raise Exception('Queue is empty')
else:
self.queue[self.front] = None
self.front = (self.front + 1) % self.size
return self.queue[self.front]
# 获取队头元素
def front_value(self):
if self.is_empty():
raise Exception('Queue is empty')
else:
value = self.queue[(self.front + 1) % self.size]
return value
# 获取队尾元素
def rear_value(self):
if self.is_empty():
raise Exception('Queue is empty')
else:
value = self.queue[self.rear]
return value
与普通队列的主要区别就是在self.pointer+1后要对self.size进行取模mod。
二、0622.设计循环队列
题目描述:
设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
你的实现应该支持如下操作:
MyCircularQueue(k): 构造器,设置队列长度为 k 。
Front: 从队首获取元素。如果队列为空,返回 -1 。
Rear: 获取队尾元素。如果队列为空,返回 -1 。
enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
isEmpty(): 检查循环队列是否为空。
isFull(): 检查循环队列是否已满。\
示例:
MyCircularQueue circularQueue = new MyCircularQueue(3); // 设置长度为 3
circularQueue.enQueue(1); // 返回 true
circularQueue.enQueue(2); // 返回 true
circularQueue.enQueue(3); // 返回 true
circularQueue.enQueue(4); // 返回 false,队列已满
circularQueue.Rear(); // 返回 3
circularQueue.isFull(); // 返回 true
circularQueue.deQueue(); // 返回 true
circularQueue.enQueue(4); // 返回 true
circularQueue.Rear(); // 返回 4
代码:
class MyCircularQueue:
def __init__(self, k: int):
self.size = k + 1
self.queue = [None for _ in range(self.size)]
self.front = 0
self.rear = 0
def enQueue(self, value: int) -> bool:
if self.isFull():
return False
else:
self.rear = (self.rear + 1) % self.size
self.queue[self.rear] = value
return True
def deQueue(self) -> bool:
if self.isEmpty():
return False
else:
self.queue[self.front] = None
self.front = (self.front + 1) % self.size
return True
def Front(self) -> int:
if self.isEmpty():
return -1
else:
return self.queue[(self.front+1) % self.size]
def Rear(self) -> int:
if self.isEmpty():
return -1
else:
return self.queue[self.rear]
def isEmpty(self) -> bool:
return self.front == self.rear
def isFull(self) -> bool:
return (self.rear + 1) % self.size == self.front
运行结果:
三、队列和广度优先搜索
3.1 广度优先搜索
广度优先搜索算法(Breadth First Search):简称为 BFS,又译作宽度优先搜索 / 横向优先搜索。是一种用于遍历或搜索树或图的算法。该算法从根节点开始,沿着树的宽度遍历树或图的节点。如果所有节点均被访问,则算法中止。
广度优先遍历类似于树的层次遍历过程。呈现出一层一层向外扩张的特点。先看到的节点先访问,后看到的节点后访问。遍历到的节点顺序符合先进先出
的特点,所以广度优先搜索可以通过队列
来实现。
基于队列实现的广度优先搜索实现步骤:
- graph 为存储无向图的字典变量,start 为开始节点。
- 然后定义 visited 为标记访问节点的 set 集合变量。定义 q 为存放节点的队列。
- 首先将起始节点放入队列 q中,即 q.put(start)。并将其标记为访问,即 visited.add(start)。
- 从队列中取出第一个节点 node_u。访问节点 node_u,并对节点进行相关操作(看具体题目要求)。
- 遍历与节点 node_u 相连并构成边的节点 node_v。
- 如果 node_v 没有被访问过,则将 node_v 节点放入队列中,并标记访问,即 q.append(node_v),visited.add(node_v)。
- 重复步骤 4 ~ 5,直到 q 为空。
基于队列实现的广度优先搜索实现代码:
import collections
def bfs(graph, start):
visited = set(start)
q = collections.deque([start])
while q:
node_u = q.popleft()
print(node_u)
for node_v in graph[node_u]:
if node_v not in visited:
visited.add(node_v)
q.append(node_v)
四、463.岛屿的周长
题目描述:
给定一个 row x col 的二维网格地图 grid ,其中:grid[i][j] = 1 表示陆地, grid[i][j] = 0 表示水域。
网格中的格子 水平和垂直 方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。
岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。
示例:
输入:grid = [[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]]
输出:16
解释:它的周长是上面图片中的 16 个黄色的边
分析:
最简单的方法可以用双循环遍历每一个节点,判断该陆地节点的四条边,边相邻的方块为水域便加一,该方法的时间复杂度为
O
(
M
N
)
O(MN)
O(MN)。
代码:
class Solution:
def islandPerimeter(self, grid: List[List[int]]) -> int:
m = len(grid)
n = len(grid[0])
dx = [0,1,0,-1]
dy = [1,0,-1,0]
perimeter = 0
for i in range(m):
for j in range(n):
if grid[i][j]:
ans = 0
for k in range(4):
i_temp = i + dx[k]
j_temp = j + dy[k]
if i_temp < 0 or i_temp >= m or j_temp < 0 or j_temp >= n or grid[i_temp][j_temp] == 0:
ans += 1
perimeter += ans
return perimeter
运行结果:
使用深度优先搜索:
代码:
class Solution:
def islandPerimeter(self, grid: List[List[int]]) -> int:
m = len(grid)
n = len(grid[0])
result = 0
for i in range(m):
for j in range(n):
if grid[i][j]:
result += self.dfs(i,j,grid,m,n)
return result
def dfs(self,i,j,grid,m,n):
if i < 0 or j < 0 or i >= m or j >= n or grid[i][j] == 0:
return 1
if grid[i][j] == 2:
return 0
grid[i][j] = 2
ans = 0
dx = [0,1,0,-1]
dy = [1,0,-1,0]
for k in range(4):
i_temp = i + dx[k]
j_temp = j + dy[k]
ans += self.dfs(i_temp,j_temp,grid,m,n)
return ans
由于使用深度搜索,相当于使用了递归去解决,耗费了更多的时间,并且耗费了更多的内存。
五、542.01矩阵
题目描述:
给定一个由 0 和 1 组成的矩阵 mat ,请输出一个大小相同的矩阵,其中每一个格子是 mat 中对应位置元素到最近的 0 的距离。
两个相邻元素间的距离为 1 。
示例:
输入:mat = [[0,0,0],[0,1,0],[0,0,0]]
输出:[[0,0,0],[0,1,0],[0,0,0]]
分析:
求所有元素1到0的曼哈顿距离,也可以转化成求元素0到元素1的曼哈顿距离,这样每次都能累计距离,然后在访问未访问过的节点的同时进行更新。
代码:
本题参考了作者的答案,下面的代码来自代码
import collections
class Solution:
def updateMatrix(self, mat: List[List[int]]) -> List[List[int]]:
row_count = len(mat)
col_count = len(mat[0])
dist_map = [[0 for _ in range(col_count)] for _ in range(row_count)]
zeroes_pos = []
for i in range(row_count):
for j in range(col_count):
if mat[i][j] == 0:
zeroes_pos.append((i, j))
directions = {(1, 0), (-1, 0), (0, 1), (0, -1)}
queue = collections.deque(zeroes_pos)
visited = set(zeroes_pos)
while queue:
i, j = queue.popleft()
for direction in directions:
new_i = i + direction[0]
new_j = j + direction[1]
if 0 <= new_i < row_count and 0 <= new_j < col_count and (new_i, new_j) not in visited:
dist_map[new_i][new_j] = dist_map[i][j] + 1
queue.append((new_i, new_j))
visited.add((new_i, new_j))
return dist_map
运行结果: