数据结构 4. 队列

本文介绍了队列的基本概念,包括队列的定义、性质和应用场景。队列是一种先进先出(FIFO)的数据结构,允许在队尾进行插入操作,在队头进行删除操作。队列分为顺序存储和链式存储两种方式,分别用数组和链表实现。文中还详细讲解了循环队列的概念和实现,并提到了两个练习题目:设计一个双端队列(Design Circular Deque)和滑动窗口最大值(Sliding Window Maximum)。
摘要由CSDN通过智能技术生成

一、队列

1. 队列的定义

队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。
队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表。

队列分为顺序存储和链式存储两种方式。

顺序存储就是用数组实现,比如有一个n个元素的队列,数组下标0的一端是队头,入队操作就是通过数组下标一个个顺序追加,不需要移动元素,但是如果删除队头元素,后面的元素就要往前移动,对应的时间复杂度就是O(n),性能自然不高。

为了提高出队的性能,就有了循环队列,什么是循环队列呢?就是有两个指针,front指向队头,rear指向对尾元素的下一个位置,元素出队时front往后移动,如果到了对尾则转到头部,同理入队时rear后移,如果到了对尾则转到头部,这样通过下标front出队时,就不需要移动元素了。

在这里插入图片描述
同时规定,当队列为空时,front和rear相等,那么队列什么时候判断为满呢?按照循环操作rear依次后移,然后再从头开始,也是出现rear和front相等时,队列满。这样跟队列空的情况就相同了,为了区分这种情况,规定数组还有一个空闲单元时,就表示队列已满,因为rear 可能在front后面,也可能循环到front前面,所以队列满的条件就变成了(rear+1)%maxsize = front ,同时队列元素个数的计算就是(rear -front+maxsize)%maxsize。

循环队列要事先申请好空间,整个过程都不能释放,而且要有固定的长度,如果长度事先无法估计,这种方式显然不够灵活;所以就引入了链式存储队列,其实就是线性表的单链表,只是它只能对尾进,队头出。并且规定队头指针指向链队列的头结点,对尾指针指向终端节点,当队列为空时,front和rear都指向头结点。

入队操作,就是在链表尾部插入结点;出队操作就是头结点的后继结点出队,然后将头结点的后继后移。如果最后除了头结点外,只剩一个元素了,就把rear也指向头结点。

在这里插入图片描述

2. 队列的实现

2.1 用数组实现一个顺序队列

class QueueUnderflow(ValueError):
    pass

class SQueue():
    def __init__(self, init_len = 8):
        self._len = init_len          # 存储区长度
        self._elems = [0]*init_len    # 元素存储
        self._head = 0                # 表头元素下标
        self._num = 0                 # 元素个数
        
    def is_empty(self):               # 判断队列是否为空
        return self._num == 0
    
    def peek(self):
        if self._num ==0:
            raise QueueUnderflow
        return self._elems[self._head]
    
    def dequeue(self):                # 删除队首元素
        if self._num == 0:
            raise QueueUnderflow
        e = self._elems[self._head]
        self._head = (self._head+1) % self._len
        self._num -= 1
        return e
    
    def enqueue(self, e):             # 在队尾插入元素
        if self._num == self._len:
            self.__extend()
        self._elems[(self._head+self._num) % self._len] = e
        self._num += 1
        
    def __extend(self):               # 扩展队列
        old_len = self._len 
        self._len *=2
        new_elems = [0]*self._len
        for i in range(old_len):
            new_elems[i] = self._elems[(self._head+1)%old_len]
        self._elems, self._head = new_elems, 0

2.2 用链表实现一个链式队列

class Head(object):
    def __init__(self):
        self.left = None
        self.right = None

class Node(object):
    def __init__(self, value):
        self.value = value
        self.next = None

class Queue(object):
    def __init__(self):
        #初始化节点
        self.head = Head()

    def enqueue(self, value):
        #插入一个元素
        newnode = Node(value)
        p = self.head
        if p.right:
            #如果head节点的右边不为None
            #说明队列中已经有元素了
            #就执行下列的操作
            temp = p.right
            p.right = newnode
            temp.next = newnode
        else:
            #这说明队列为空,插入第一个元素
            p.right = newnode
            p.left = newnode

    def dequeue(self):
        #取出一个元素
        p = self.head
        if p.left and (p.left == p.right):
            #说明队列中已经有元素
            #但是这是最后一个元素
            temp = p.left
            p.left = p.right = None
            return temp.value
        elif p.left and (p.left != p.right):
            #说明队列中有元素,而且不止一个
            temp = p.left
            p.left = temp.next
            return temp.value

        else:
            #说明队列为空
            #抛出查询错误
            raise LookupError('queue is empty!')

    def is_empty(self):
        if self.head.left:
            return False
        else:
            return True

    def top(self):
        #查询目前队列中最早入队的元素
        if self.head.left:
            return self.head.left.value
        else:
            raise LookupError('queue is empty!')

2.3 实现一个循环队列

class LoopQueue:
    def __init__(self, capacity=10):
        """
        构造函数
        :param capacity: 循环队列的初始容量,默认为10。
        """
        self._capacity = capacity + 1   # 对于用户来说,其容量为capacity。而对于内部实现来说,需要满足判空与判满的奇异性,
        # 当self._tail + 1 = self._front是,此时判定为满,此时还剩余一个空间,所以真实容量是用户指定容量加一!
        self._front = 0     # 队首的索引(闭区间)
        self._tail = 0      # 队尾的索引(开区间,就像C++的.end()迭代器那样)self._front=self._tail=0,即初始化为空
        self._data = [float('nan')] * self._capacity  # 初始化为nan * self._capacity这么多容量
      
    def isEmpty(self):
        """
        判断循环队列是否为空
        :return: bool值,空为True
        """
        return self._front == self._tail    # self._tail和self._tail相等时表示空。

    def getCapacity(self):
        """
        获取循环队列当前的容量
        :return: 循环队列的容量
        """
        return self._capacity - 1   # 对于用户来说得到的容量需要减一哦

    def getSize(self):
        """
        获得循环队列内有效元素的个数
        :return: 有效元素的个数
        """
        retSize = None      # 要返回的size
        if self._tail >= self._front:            # self._tail在self._front后面(包括等于),就和普通队列一样
            retSize = self._tail - self._front
        else:           # 此时self._front > self._tail
            retSize = self._capacity - (self._front - self._tail)  # 讲过啦,很简单
        return retSize

    def enqueue(self, elem):
        """
        将元素elem入队
        时间复杂度:O(1)
        :param elem: 要入队的元素
        """
        if (self._tail + 1) % self._capacity == self._front:    # 满了
            self._resize(self.getCapacity() * 2)        # 扩大为getCapacity()的两倍
            # 解释一下这里为什么不是self._capacity * 2。首先self._capacity和self.getCapacity()之间差一个1,
            # 其次此时真实可容纳元素的空间是self.getCapacity(),扩大为它的二倍,也就是此时真实可容纳元素的空间
            # 变为原先的两倍,self._capacity也容易维护,只需加一即可。
        self._data[self._tail] = elem   # 将self._tail的位置的元素置为elem
        self._tail = (self._tail + 1) % self._capacity # 维护self._tail,注意是循环队列哦,要对全体空间取余的!

    def dequeue(self):
        """
        循环队列的出队操作
        时间复杂度:O(1)
        :return: 出队元素的值
        """
        if self.isEmpty():      # 队列此时没有元素
            raise Exception('Error.The loop queue is empty, can not make dequeue operation.')   # 抛出异常
        ret_val = self._data[self._front]   # 记录一下队首的元素,方便返回
        self._data[self._front] = None  # 手动回收self._front处的元素
        self._front = (self._front + 1) % self._capacity    # 维护self._front,直接加一就好,注意循环队列的性质,要对
        # 全体空间取余

        if self.getSize() and self.getCapacity() // self.getSize() == 4:    # 队列不为空且有效元素个数为可容纳元素的四分之一时,缩容
            self._resize(self.getCapacity() // 2)   # 缩容为原先的二分之一
        return ret_val  # 返回队首元素

    def getFront(self):
        """
        获取队首的元素的值(队列一般只关心队首)
        :return: 队首元素的值
        """
        if self.isEmpty():      # 空队列抛异常就完事了
            raise Exception('Error. The loop queue is empty, can not get any mumber.')
        return self._data[self._front]  # 获得self._front索引处的元素

    def printLoopQueue(self):
        """对循环队列内的有效元素进行打印操作"""
        print('LoopQueue: Front--- ', end='')       # 队首
        index = self._front     # 从队首开始
        while index != self._tail:  # 没到达队尾就一直打印
            if index + 1 != self._tail:     # 没到最后一次的打印。为了对称,强迫症。。
                print(self._data[index], end='  ')      # 打印当前元素
                index = (index + 1) % self._capacity    # index向后推进,注意是循环队列,到self._data的尾部就要返回到0索引处哦,
                # 所以要对真实的存储空间取余,而不是对self.getCapacity()取余!这么做就错了!
            else:
                print(self._data[index], end=' ')
                break       # 最后一次打印操作,完事直接退出循环就好
        print('---Tail')    # 队尾
        print('Size: %d, Capacity: %d' % (self.getSize(), self.getCapacity()))  # 有效元素个数以及当前容量的打印


    # private
    def _resize(self, capacity):
        """
        扩/缩容操作,将容量扩/缩至capacity(这里的capacity是面向用户,所以真正的self._capaciry应该是capacity+1,才能容纳capacity这么多元素呀)
        :param capacity: 新的容量(基于用户的角度)
        """
        # 此时千万不能先做self._capacity = capacity + 1 。因为一会儿要将当前队列中的元素全部取出来,一旦
        # self._tail在self._front的前面,而self._capacity已经改变,就不能全部取出来了!好好想一下~以前就进过坑
        tmp_list = [float('nan')] * (capacity + 1)  # 建立一个新的list,真实容量为capacity+1,原因你懂得
        index = self._front     # 准备开始遍历原先的self._data,转移元素!从self._front开始
        while index != self._tail:  # 若index一直没到self._tail,就继续往后撸
            tmp_list[index - self._front] = self._data[index]  # 注意在这里我把原先的元素都放到新list的以索引零开始的地方顺序的放置元素
            index = (index + 1) % self._capacity    # index往后撸,注意循环队列的性质。
        self._data = tmp_list           # 更新self._data为新的那个数据,tmp_list会被自动垃圾回收的,不用担心它。


        self._tail = self.getSize()     # 维护self._tail。就是 0 + self.getSize()。因为转移元素并不改变size呀。
        self._front = 0                 # 维护self._front,因为我是从零开始放的,所以置零。
        # 上面这两句话的顺序一定不能变!变了对扩容没有影响,但是缩容就会出错!虽然我们心里知道是从零开始放的,但是应该先安排self._tail。因为
        # 一旦先把self._front置零,geiSize()方法瞬间出错!随后调用self.getSize()就出现问题了!导致self._tail出现在缩容后数组
        # 索引的overflow位置,打印的话就会无线循环打印!因为self._front始终不能等于self._tail呀!所以一定要先安排self._tail!

        self._capacity = capacity + 1   # 最后再维护self._capacity!

3. 练习

641. Design Circular Deque(设计一个双端队列)

[https://leetcode-cn.com/problems/design-circular-deque/]
题目描述
设计实现双端队列。
你的实现需要支持以下操作:

MyCircularDeque(k):构造函数,双端队列的大小为k。
insertFront():将一个元素添加到双端队列头部。 如果操作成功返回 true。
insertLast():将一个元素添加到双端队列尾部。如果操作成功返回 true。
deleteFront():从双端队列头部删除一个元素。 如果操作成功返回 true。
deleteLast():从双端队列尾部删除一个元素。如果操作成功返回 true。
getFront():从双端队列头部获得一个元素。如果双端队列为空,返回 -1。
getRear():获得双端队列的最后一个元素。 如果双端队列为空,返回 -1。
isEmpty():检查双端队列是否为空。
isFull():检查双端队列是否满了。
示例:

MyCircularDeque circularDeque = new MycircularDeque(3); // 设置容量大小为3
circularDeque.insertLast(1); // 返回 true
circularDeque.insertLast(2); // 返回 true
circularDeque.insertFront(3); // 返回 true
circularDeque.insertFront(4); // 已经满了,返回 false
circularDeque.getRear(); // 返回 2
circularDeque.isFull(); // 返回 true
circularDeque.deleteLast(); // 返回 true
circularDeque.insertFront(4); // 返回 true
circularDeque.getFront(); // 返回 4

提示:

所有值的范围为 [1, 1000]
操作次数的范围为 [1, 1000]
请不要使用内置的双端队列库。

代码实现

class MyCircularDeque:

    def __init__(self, k: int):
        """
        Initialize your data structure here. Set the size of the deque to be k.
        """
        self.head, self.tail = -1, -1
        self.vec = [None for _ in range(k)]

        
    def insertFront(self, value: int) -> bool:
        """
        Adds an item at the front of Deque. Return true if the operation is successful.
        """
        if self.isFull(): return False
        self.head -= 1
        if self.head < 0:
            self.head = len(self.vec)-1
        self.vec[self.head] = value
        return True

    def insertLast(self, value: int) -> bool:
        """
        Adds an item at the rear of Deque. Return true if the operation is successful.
        """
        if self.isFull():
            return False
        self.tail += 1
        if self.tail == len(self.vec):
            self.tail = 0
        self.vec[self.tail] = value
        return True

    def deleteFront(self) -> bool:
        """
        Deletes an item from the front of Deque. Return true if the operation is successful.
        """
        if self.isEmpty():
            return False
        self.vec[self.head] = None
        self.head += 1
        return True

    def deleteLast(self) -> bool:
        """
        Deletes an item from the rear of Deque. Return true if the operation is successful.
        """
        if self.isEmpty(): return False
        self.vec[self.tail] = None
        self.tail -= 1
        return True
        

    def getFront(self) -> int:
        """
        Get the front item from the deque.
        """
        print(self.head, len(self.vec))
        return self.vec[self.head]

    def getRear(self) -> int:
        """
        Get the last item from the deque.
        """
        return self.vec[self.tail]

    def isEmpty(self) -> bool:
        """
        Checks whether the circular deque is empty or not.
        """
        for x in self.vec:
            if x != None:
                return False
        return True

    def isFull(self) -> bool:
        """
        Checks whether the circular deque is full or not.
        """
        for x in self.vec:
            if x == None:
                return False
        return True


# Your MyCircularDeque object will be instantiated and called as such:
# obj = MyCircularDeque(k)
# param_1 = obj.insertFront(value)
# param_2 = obj.insertLast(value)
# param_3 = obj.deleteFront()
# param_4 = obj.deleteLast()
# param_5 = obj.getFront()
# param_6 = obj.getRear()
# param_7 = obj.isEmpty()
# param_8 = obj.isFull()

239. 滑动窗口最大值

[https://leetcode-cn.com/problems/sliding-window-maximum/]
题目描述
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口 k 内的数字。滑动窗口每次只向右移动一位。

返回滑动窗口最大值。

示例:

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:

滑动窗口的位置 最大值


[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
注意:

你可以假设 k 总是有效的,1 ≤ k ≤ 输入数组的大小,且输入数组不为空。
线性时间复杂度

代码实现

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        # solution1 每次用大顶堆取最大,注意heapq没有实现大顶堆,要用取反的方式来完成大顶堆功能
        # 代码参考为gymer的评论
        # 同时要注意,不能使用remove来去除滑出元素(remove为O(n)),使用一个字典记住返回的最大数字的次数来规避这个问题
        # 滑动窗口初始化时为k,每次将滑动窗口加大1,对窗口内的数字做堆排序,获取(不是取出)其中最大元素,如果字典中该元素计数值大于0,表示这个元素应该是一个不属于当前k滑动窗口的数字且之后加入堆的数字都小于该数字(因为没有减少滑动窗口),此时应将改数字从堆删除并再取出一个新的堆顶,重复此操作直到取出的数字在字典中对应出现次数为0,表示为当前k滑动窗口真实最大值,取出真实最大值之后要把应滑出k范围窗口的数值在计数器中+1,以便后期取到该值时进行抑制。
        # 这个方法的时间复杂度会比其他语言直接使用大顶堆稍慢,因为大顶堆排序时元素个数大于k,最坏情况可达n,所以复杂度为O(nlogn),其他语言直接用大顶堆为O(nlogk),使用remove的话复杂度为O(n^2)
        # 空间复杂度也比其他语言大,为O(n),其他语言为O(1)
        # import heapq
        # from collections import defaultdict
        # if not nums:
        #     return []
        # nums = [-1 * x for x in nums]
        # counter = defaultdict(int)
        # windows = nums[:k]
        # res = []
        # heapq.heapify(windows)
        # res.append(-1 * windows[0])
        # counter[nums[0]] += 1
        # for i in range(len(nums) - k):
        #     heapq.heappush(windows, nums[i + k])
        #     min_num = windows[0]
        #     while counter[min_num] > 0:
        #         counter[min_num] -= 1
        #         heapq.heappop(windows)
        #         min_num = windows[0]
        #     res.append(-1 * min_num)
        #     counter[nums[i + 1]] += 1
        # return res
        
        # solution2 使用双端队列,双端队列的作用是过滤那些,存在于一个滑动窗口中,被夹在两个大于它的数中间的数字,如3,1,2中的1,双端队列的维护规则是,在保证对头与队尾长度差不超过一个滑动窗口k的情况下,降序排列队列中的元素,对小于新加元素的对中元素进行pop操作,即将不可能进结果的数字pop掉
        # 时间复杂度为O(n*1),其中1的原因是因为每个数字仅会在双端队列中存在一次
        total  = len(nums)
        re = []
        if total==0:
            return re
        dequeue = []
        for i,x in enumerate(nums):
            if i>=k and i-dequeue[0]>=k:#表示超出k滑动窗口,需要从前pop一个元素
                dequeue.pop(0)
            while dequeue and x>=nums[dequeue[-1]]:#将该元素加到队尾,并保证整个双端队列降序,如存在比该元素小的数字存在于队列中,将其删除
                dequeue.pop()
            dequeue.append(i)
            if i>=k-1:
                re.append(nums[dequeue[0]])
        return re


# 超过时间复杂度O(n)

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        if not nums:
            return []
        res = []
        for i in range(len(nums)-k+1):
            window = nums[i:i+k]
            m_num = max(window)
            res.append(m_num)
        return res
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值