LeetCode Task02 队列与广度优先搜索 622.设计循环队列 463.岛屿的周长 542.01矩阵


一、队列基础知识

队列基础知识

简介:
队列(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. 入队时,队尾指针循环前进 1 个位置,即 self.rear = (self.rear + 1) % self.size。
  2. 出队时,队头指针循环前进 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

运行结果:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值