Python-数据结构与算法(四、循环队列)

保证一周更两篇吧,以此来督促自己好好的学习!代码的很多地方我都给予了详细的解释,帮助理解。好了,干就完了~加油!
声明:本python数据结构与算法是imooc上liuyubobobo老师java数据结构的python改写,并添加了一些自己的理解和新的东西,liuyubobobo老师真的是一位很棒的老师!超级喜欢他~
如有错误,还请小伙伴们不吝指出,一起学习~
No fears, No distractions.

一、何为循环队列?干啥的?

上文说过普通队列的出队操作的时间复杂度为O(n)(因为要整体移动元素),而循环队列就可以完美解决这个问题,使得出队与入队操作均为O(1)的时间复杂度,怎么样,很厉害吧~让我们一起来探究一下循环队列究竟为何物。
图片来源:imoocliuyubobobo
上图借用了一下liuyubobobo老师上课用的一张图片,如有侵权请联系,立刻删除!我只讲一下核心部分,关于循环队列的具体细节还请小伙伴们百度。

两个关键索引:

front和tail。顾名思义,一个为队首的索引,一个为队尾下一个元素的索引(类似于C++容器的.end()迭代器)。出队维护front,入队维护tail,很简单。此时不需要对元素进行整体的移动,只移动并维护这两个标记就好了。

两个关键状态:

空队列——front == tail。为什么呢?因为front是闭区间,而tail是开区间,即[front, tail)。当front和tail相等时,区间内不包含任何元素,所以认定为空队列是非常合理的。至于front和tail是否一定等于零并没有这方面的要求,只要front和tail相等,无论任何位置,队列就是空队列,也很好理解。但是一般初始化一个空循环队列的时候它俩是等于零的。。。
满队列——(tail+1)%capacity == front。想象一下,一直在入队操作,以至于tail和front相等了。不错,这个时候确实可以代表满员了,因为tail是开区间嘛,没问题~但是前面空队列的判断条件也是front和tail相等呀,此时出现了二义性,因此在这里我们牺牲一个空间,只能让tail到达front前面的一个位置,就表示满了。还有一个小坑就是要对capacity取余,想一下tail在数组的最后一个位置,索引为capacity-1,此时入队必然overflow,因此加一后对capacity取余回到索引0位置,完成循环的特性~这种小操作以后会用到很多的。

一个可以省略的成员变量:

一个关于循环队列类的size。如果队列内维护一个size(有效元素的个数)成员变量,循环队列的实现与维护将变得很轻松,会少很多坑!但是这个size对于循环队列来说是多余的,因为我们仅用front和tail就可以表达size了,只不过有一点点绕弯,为了更好的理解循环队列的思想,在这里我省略了size成员变量,老师课上是有这个变量的,并且老师也鼓励我们完成不带size的循环队列!Let’s go!代码都尽量给出了详细注释,帮助理解吧。
在这里插入图片描述

二、实现

# -*- coding: utf-8 -*-
# Author:           Annihilation7
# Data:             2018-09-25		凌晨一点半
# Python version:   3.6

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这么多容量
        # tip:其实完全可以加入self._size来更好的阐述循环队列,有了这个成员变量循环队列的操作也变得简单了许多,而且定义
        # 也更加清晰,但是为了阐述循环队列底层的工作原理,应用self._front和self._tail完全可以表述self._size。这样
        # 我认为会对循环队列有着更深的认识。如果没有self._size这一成员变量,主要有两个坑:
        # 1. self._size的求法。
        # 2. self._resize的时候有大坑!!!真的对程序debug的能力有了提升--

    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._dat为新的那个数据,self._data会被自动垃圾回收的,不用担心它。


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

        self._capacity = capacity + 1   # 最后再维护self._capacity!讲过了,前面的很多操作依赖于原先的最大容量,等他们都完事了,最后维护这个

三、测试

import loopqueue    # LoopQueue类写在这个py文件里面

testLoopQueue = loopqueue.LoopQueue()

print('入队15次:')
for i in range(15):
    testLoopQueue.enqueue(i)
print('front:', testLoopQueue._front)
print('tail:', testLoopQueue._tail)
print('队列元素打印:', end=' ')
testLoopQueue.printLoopQueue()

print('出队12次:')
docker = []
for i in range(12):
    docker.append(testLoopQueue.dequeue())
    print('steps: %d, front: %d, tail: %d, Size: %d, Capacity: %d' % (i + 1, testLoopQueue._front, testLoopQueue._tail, testLoopQueue.getSize(), testLoopQueue.getCapacity()))
print('出队docker:', docker)
print('队列元素打印:', end=' ')
testLoopQueue.printLoopQueue()
print('队首元素:', testLoopQueue.getFront())

四、输出

入队15次:
front: 0
tail: 15
队列元素打印: LoopQueue: Front--- 0  1  2  3  4  5  6  7  8  9  10  11  12  13  14 ---Tail
Size: 15, Capacity: 20
出队12次:
steps: 1, front: 1, tail: 15, Size: 14, Capacity: 20
steps: 2, front: 2, tail: 15, Size: 13, Capacity: 20
steps: 3, front: 3, tail: 15, Size: 12, Capacity: 20
steps: 4, front: 4, tail: 15, Size: 11, Capacity: 20
steps: 5, front: 5, tail: 15, Size: 10, Capacity: 20
steps: 6, front: 6, tail: 15, Size: 9, Capacity: 20
steps: 7, front: 7, tail: 15, Size: 8, Capacity: 20
steps: 8, front: 8, tail: 15, Size: 7, Capacity: 20
steps: 9, front: 9, tail: 15, Size: 6, Capacity: 20
steps: 10, front: 0, tail: 5, Size: 5, Capacity: 10
steps: 11, front: 1, tail: 5, Size: 4, Capacity: 10
steps: 12, front: 2, tail: 5, Size: 3, Capacity: 10
出队docker: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
队列元素打印: LoopQueue: Front--- 12  13  14 ---Tail
Size: 3, Capacity: 10
队首元素: 12

五、总结

不带size成员变量的循环队列确实有一些坑,不过一旦迈过了这些坑心情还是很舒畅的O(∩_∩)O。

若有还可以改进、优化的地方,还请小伙伴们批评指正!

猜你喜欢

转载自blog.csdn.net/Annihilation7/article/details/82847774
今日推荐