算法基础之队列的理论和应用


1.队列的基础定义

官方定义:队列是只允许在一端进行插入操作,而另一端进行删除操作的线性表。
解释:队列也是一种线性结构,相比数组,队列对应的操作是数组的子集,只能从一端(队尾)添加元素,只能从另一端(队首)取出元素。
队列是一种先进先出First in First out,FIFO)的数据结构。
在这里插入图片描述

2.队列的实现

基本需求:
在这里插入图片描述

class Queue(object):
    """队列"""
    def __init__(self):
        self.items = []

    def is_empty(self):
        return self.items == []

    def enqueue(self, item):
        """进队列"""
        self.items.insert(0,item)#进的放在队头

    def dequeue(self):
        """出队列"""
        return self.items.pop()

    def size(self):
        """返回大小"""
        return len(self.items)

检验一下,

if __name__ == "__main__":
    q = Queue()
    q.enqueue("hello")
    q.enqueue("my")
    q.enqueue("friend")
    print(q.size())
    print(q.items)
    print(q.dequeue())
    print(q.items)
    print(q.dequeue())
    print(q.items)
    print(q.dequeue())
    print(q.items)
3
['friend', 'my', 'hello']
hello
['friend', 'my']
my
['friend']
friend
[]

应该ok了。

3.数组队列的问题

我们来看如果删除队首元素的话,队列是怎么实现的。假设下列例子中,队首是左侧,队尾是右侧。
在这里插入图片描述
当我们删除队首元素时,a即会出队。然后bcde依次向前挪一个位置,时间复杂度是O(n)。
在这里插入图片描述
但是如果a出队后,剩余元素不去挪动位置,毕竟bcde还是保持我们队列中的样子(b是队首,e是队尾)。
在这里插入图片描述
所以,我们可以在索引为1的位置记录为队首(front),队尾(tail)其实就是原来的size,记录下一次元素入队的位置。这样的好处是,下一次删除队首元素的操作,只需要维护front即可,不需要所有元素都向前移动一个单位(详细见下个部分‘循环队列’)。
在这里插入图片描述

4.循环队列

当我们队列为空时(即没有元素在队列里),front和tail指的都是索引为0的位置,照理说front指的是第一个元素,而tail指的是最后一个元素的后一个位置。所以,当front == tail时,意味着队列为空。
在这里插入图片描述
当有一个元素a入队了,我们只需要维护tail即可,把tail挪到索引为1的位置,指向下一次元素入队的位置。
在这里插入图片描述
依此类推,当我们入队5个元素后,tail需要维护,即把它挪到索引为5的位置。
在这里插入图片描述
这时我们如果要把队首元素a出队,只需要维护front即可,把front向后移动一个单位。这样的时间复杂度是O(1)。
在这里插入图片描述
到现在来看,好像跟循环队列没什么关系啊?
我们来看这个情况,此时加了新元素h到索引为7的位置,tail无法维护了,而且我们注意到前面空出来的位置是没办法被后面元素利用到的。因此,前面还有利用的空间,所以我们可以把队列看成一个‘环状’首尾相接的形状。
在这里插入图片描述

那么我们怎么实现位置7之后是位置0的呢?可以想一下,7+1=8,8/8取余是0,则我们把tail维护到索引为0的位置。
此时又进来一个新元素i,则tail再+1即可。
在这里插入图片描述
这时,我们看到还有一个空位置在索引1上。如果我们此时再引入一个新元素,tail接下来就要挪到索引2上,跟front是同一个位置了,可此时我们队列不为空(recall:front == tail意味着队列为空)。所以,我们会定义tail + 1 == front为队列列满了(我们是有意识地浪费掉一个空间)。严谨来说,我们是**(tail + 1)% c == front为队列列满**了(c是队列的长度),因为当front指的是索引0,而tail指的是索引7的时候,也是满的情况。
在这里插入图片描述
循环队列的实现如下:

class LoopQueue(object):
    def __init__(self, n=2): #先默认capacity为2,方便后面测试
        self.arr = [None] * (n+1)  # 由于特意浪费了一个空间,所以arr的实际大小应该是用户传入的容量+1
        self.front = 0
        self.tail = 0
        self.size = 0

    def __str__(self):
        return str(self.arr)

    def __len__(self):
        return len(self.arr)

    def __iter__(self):
        return iter(self.arr)

    def get_size(self):
        # 获取队列元素个数
        return self.size

    def get_capaticty(self):
        # 获取队列容积(实际可存储元素个数)
        return self.__len__() - 1

    def is_full(self):
        # 判断队列是否为满
        return (self.tail+1) % len(self.arr) == self.front

    def is_empty(self):
        # 判断队列是否为空
        return self.size == 0

    def get_front(self):
        # 获取队首
        return self.arr[self.front]

    def enqueue(self, e):
        # 入队
        if self.is_full():
            self.resize(self.get_capaticty() * 2)  # 如果队列满,以当前队列容积的2倍进行扩容
        self.arr[self.tail] = e
        self.tail = (self.tail+1) % len(self.arr)
        self.size += 1

    def dequeue(self):
        # 出队
        if self.is_empty():
            raise Exception("Cannot dequeue from en empty queue")

        result = self.arr[self.front]
        self.arr[self.front] = None
        self.front = (self.front+1) % len(self.arr)
        self.size -= 1

        # 如果元素个数少于容积的1/4并且元素个数
        if self.size < self.get_capaticty() // 4 and self.get_capaticty() > 1:
            self.resize(self.get_capaticty() // 2)
        return result

    def resize(self, new_capacity):
        new_arr = [None] * (new_capacity+1)
        for i in range(self.size):
            new_arr[i] = self.arr[(i+self.front) % len(self.arr)]

        self.arr = new_arr
        self.front = 0
        self.tail = self.size

注意此时enqueue和dequeue的均摊复杂度都是O(1)。
检验一下,

if __name__ == '__main__':
    loop_queue = LoopQueue()
    loop_queue.enqueue("hello")
    loop_queue.enqueue("my")
    print(loop_queue.arr)
    loop_queue.enqueue("friend")
    print(loop_queue.get_size())
    print(loop_queue.arr)
    print(loop_queue.dequeue())
    print(loop_queue.arr)
    print(loop_queue.dequeue())
    print(loop_queue.arr)
    loop_queue.enqueue("hello")
    print(loop_queue.arr)
    loop_queue.enqueue("my")
    print(loop_queue.arr)
    loop_queue.enqueue("love")
    print(loop_queue.arr)
    print(loop_queue.dequeue())
    print(loop_queue.dequeue())
    print(loop_queue.dequeue())
    print(loop_queue.arr)
    print(loop_queue.dequeue())
    print(loop_queue.arr)
['hello', 'my', None]
3
['hello', 'my', 'friend', None, None]
hello
[None, 'my', 'friend', None, None]
my
[None, None, 'friend', None, None]
[None, None, 'friend', 'hello', None]
[None, None, 'friend', 'hello', 'my']
['love', None, 'friend', 'hello', 'my']
friend
hello
my
['love', None, None, None, None]
love
[None, None, None]

我们在延申一下,双端队列double-ended queue, deque)是一种具有队列和栈的性质的数据结构。双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行。双端队列可以在队列任意一端入队和出队。
在这里插入图片描述
我们看看实现双端队列的需求:
在这里插入图片描述
具体实现:

class Deque(object):
    """双端队列"""
    def __init__(self):
        self.items = []

    def is_empty(self):
        """判断队列是否为空"""
        return self.items == []

    def add_front(self, item):
        """在队头添加元素"""
        self.items.insert(0,item)

    def add_rear(self, item):
        """在队尾添加元素"""
        self.items.append(item)

    def remove_front(self):
        """从队头删除元素"""
        return self.items.pop(0)

    def remove_rear(self):
        """从队尾删除元素"""
        return self.items.pop()

    def size(self):
        """返回队列大小"""
        return len(self.items)

检验的例子如下:

if __name__ == "__main__":
    deque = Deque()
    deque.add_front(1)
    deque.add_front(2)
    print(deque.items)
    deque.add_rear(3)
    deque.add_rear(4)
    print(deque.items)
    print(deque.size())
    print(deque.remove_front())
    print(deque.remove_front())
    print(deque.remove_rear())
    print(deque.remove_rear())
[2, 1]
[2, 1, 3, 4]
4
2
1
4
3

5.队列的应用

5.1回文词检测

在这里插入图片描述

有两种思路,一种思路是因为是对称的,我们可以判断正序和逆序遍历的结果是否一样,当然这种方法只需要两边循环一下就可以了;另一种思路是从两段同时读取,判断是否一样,一样的话就继续读取,直到遍历完了或还剩最中间那个元素。后者采用的思想就是前面双端队列的原理。

def palchecker(aString):

    deque = Deque()
	# 回文词入队
    for i in aString:
        deque.add_front(i)

    state = True
    # 双向读取并比较
    while deque.size() > 1 and state:
        df = deque.remove_front()
        dr = deque.remove_rear()
        # print(df,dr)
        if df != dr:
            state = False

    return state

检验一下呗:

palchecker('abaa')
>>False

5.2用栈实现队列(232)

Implement a first in first out (FIFO) queue using only two stacks. The implemented queue should support all the functions of a normal queue (push, peek, pop, and empty).

Implement the MyQueue class:

  • void push(int x) Pushes element x to the back of the queue.
  • int pop() Removes the element from the front of the queue and returns
    it.
  • int peek() Returns the element at the front of the queue.
  • boolean empty() Returns true if the queue is empty, false otherwise.

Notes:

  • You must use only standard operations of a stack, which means only push to top, peek/pop from top, size, and is empty operations are valid.
  • Depending on your language, the stack may not be supported natively. You may simulate a stack using a list or deque (double-ended queue) as long as you use only a stack’s standard operations.
    在这里插入图片描述
    在这里插入图片描述
    先思考一下,队列是先入先出的,而栈是后入先出的,如果要用栈实现队列的效果,需要两个栈来实现。
    第一种思想:在一个栈中维持所有元素的出队顺序。具体来说,所有的元素在入队操作完成后只会保存在一个栈中,且其出栈的顺序和出队的顺序是一致的
    在这里插入图片描述
#leetcode 232
class MyQueue:
    def __init__(self):
        """
        Initialize your data structure here.
        """
        self._s1, self._s2 = [], []
        
    def push(self, x):
        """
        Push element x to the back of queue.
        :type x: int
        :rtype: void
        """
        while self._s1:
            self._s2.append(self._s1.pop())  
        self._s1.append(x)
        while self._s2:
            self._s1.append(self._s2.pop())

    def pop(self):
        """
        Removes the element from in front of queue and returns that element.
        :rtype: int
        """
        return self._s1.pop()

    def peek(self):
        """
        Get the front element.
        :rtype: int
        """
        return self._s1[-1]

    def empty(self):
        """
        Returns whether the queue is empty.
        :rtype: bool
        """
        return not self._s1

第二种思想:将两个栈一个用于入队,一个用于出队。假设栈inStack 用于实现入队操作,栈 outStack 用于实现出队操作。
在这里插入图片描述

#leetcode 232 方法2
class MyQueue:
    def __init__(self):
        """
        Initialize your data structure here.
        """
        self._in_stack, self._out_stack, self._front = [], [], None
        
    def push(self, x):
        """
        Push element x to the back of queue.
        :type x: int
        :rtype: void
        """
        if not self._in_stack:
            self._front = x
        self._in_stack.append(x)

    def pop(self):
        """
        Removes the element from in front of queue and returns that element.
        :rtype: int
        """
        if self.empty():
            rasie Exception("[ERROR] The queue is empty!")
            
        if not self._out_stack:
            while self._in_stack:
                self._out_stack.append(self._in_stack.pop())
        return self._out_stack.pop()

    def peek(self):
        """
        Get the front element.
        :rtype: int
        """
        if self.empty():
            rasie.Exception("[ERROR] The queue is empty!")
            
        if not self._out_stack:
            return self_front
        else:
            return self._out_stack[-1]

    def empty(self):
        """
        Returns whether the queue is empty.
        :rtype: bool
        """
        return not self._in_stack and not self._out_stack

5.3生产窗口最大值数组(程序员面试指南)

在这里插入图片描述
这题似曾相识,发现跟之前数组Leetcode总结的固定滑动窗口差不多。但我们这里用队列的思想来思考(主要难点是怎么用双向队列实现最大值的更新)。
现有一个双向队列名为qmax。
当我们遍历到数组的第i个元素的时候,有如下规则:
如果qmax为空,直接插入i。
如果qmax不为空,则获取qmax队尾的坐标,记为j。
如果a[j]>a[i],说明队尾的值比他数组大,则直接插入队列。
如果a[j]<=a[i],需要把j从qmax弹出,然后会到第二步,再决定插入的策略。

#生成窗口最大数组
def getMaxWindow(arr, w):
    if arr == None or w < 1 or len(arr) < w:
        return
    qmax = []
    res = []
    for i in range(len(arr)):
        while qmax and arr[qmax[-1]] <= arr[i]:
            qmax.pop()
        qmax.append(i)
        if qmax[0] <= i - w:
            qmax.pop(0)
        if i-w+1 >= 0:
            res.append(arr[qmax[0]])
    return res
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值