Python-数据结构与算法(八、带有尾指针的链表队列及性能比较)

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

一、为什么要带尾指针来实现链表队列?

因为队列的操作实在两端进行的,而且凡是在链表进行的操作的时间复杂度一定是O(n)的!所以在尾部添加一个尾指针,时时刻刻都指向链表的最后一个元素,其下一个元素为None。此时添加元素的时间复杂度为O(1),但是入队的话还是要从前往后撸到这里,所以还是O(n)的,但是我们可以把链表尾当做是队尾呀。链表头当做队首,此时入队与出队的操作的时间复杂度就都是O(1)的了!所以说要带一个尾指针!
Python-数据结构与算法(八、带有尾指针的链表队列及性能比较)

声明:上图源自liuyubobobo老师上课的PPT,如有侵权请联系,立刻删除~

由于仅仅涉及到链表两端的操作,所以在这里我们就不用dummyhead啦,只需要维护两个成员变量:head和tail就好了!当然你也可以实现一个带dummyhead的队列,代码需要做出相应的调整。

二、带有尾指针的链表队列的实现

# -*- coding: utf-8 -*-
# Author:           Annihilation7
# Data:             2018-09-28
# Python version:   3.6

from linklist.linkedlist import Node   # 载入节点类

class LinkedListQueue:
    def __init__(self):
        """构造函数,注意无capacity的概念"""
        self._head = None   # 首节点,初始化为None
        self._tail = None   # 尾节点,初始化为None
        self._size = 0      # 有效元素的个数,初始化为0

    def getSize(self):
        """
        获取队列中有效元素的个数
        :return: 元素个数
        """
        return self._size

    def isEmpty(self):
        """
        判断队列是否为空
        :return: Bool值,空为True
        """
        return self._size == 0

    def enqueue(self, elem):
        """
        将元素elem入队,注意是在self._tail处进行入队操作
        时间复杂度:O(1)
        :param elem: 将要入队的元素,需要用一个Node来装载它
        """

        if self.isEmpty():      # 控队列需要特殊处理
            # 此时队列为空,则self._tail为None,此时self._head一定也是None
            # 因为绝对不存在self._tail为None,而self._head不是空的情况
            self._tail = Node(elem)     # 将self._tail设为携带elem,且下一个元素为None的一个节点
            self._head = self._tail     # 将self._head置为self._tail,即self._head和self._tail标签同时贴在了这一个Node上,这样就把self._head和self._tail关联起来了

        else:
            self._tail.next = Node(elem)    # 如果不为空正常在self._tail的后面添加Node
            self._tail = self._tail.next    # self._tail移动到队列的最后一个位置,即维护一下self._tail
        self._size += 1         # 不论如何,入队了肯定要对self._size进行维护

    def dequeue(self):
        """
        队首元素出队,注意是在self._head处进行出队操作
        时间复杂度:O(1)
        :return: 队首元素所携带的值
        """
        if self.isEmpty():      # 判空操作
            raise Exception('Dequeue failed, please check out the size.')
        retNode = self._head                # 记录一下待出队元素,便于返回其值
        self._head = self._head.next        # self._head移动到下一个元素
        retNode.next = None                 # 将retNode从队列中彻底断绝联系,使回收器能够将retNode回收

        if self.isEmpty():                  # 如果队列中只有一个元素,出队后空了,需要同时对self._tail做相应的处理
            self._tail = None               # 将self._tail设为None

        self._size -= 1                     # 维护self._size
        return retNode.elem                 # 返回出队元素的值

    def getFront(self):
        """
        看一下队首元素的值
        时间复杂度:O(1)
        :return: 队首元素的值
        """
        return self._head.elem

    def printLinkedListQueue(self):
        """打印队列"""
        print('Queue: Head---', end='')
        cur = self._head        # 从头开始
        while cur != None:      # 只要cur不是None,就打印并且往后撸
            cur.printNode()     # 调用Node的打印函数
            cur = cur.next
        print('---Tail')
        print('Size: %d' % (self.getSize()))

三、测试

from linklist.linkedlistQueue import LinkedListQueue
import numpy as np

np.random.seed(7)
test = LinkedListQueue()
print(test.getSize())
print(test.isEmpty())
for i in range(15): # 入队15次
    test.enqueue(np.random.randint(10))
test.printLinkedListQueue()
for i in range(12):
    test.dequeue()  # 出队12次,不用返回值了
test.printLinkedListQueue()

四、输出

0
True
Queue: Head---4  9  6  3  3  7  7  9  7  8  9  8  7  6  4  ---Tail
Size: 15
Queue: Head---7  6  4  ---Tail
Size: 3

五、性能比较

由于之前的代码可维护性还算可以,在这里我就把普通数组队列、循环队列和带有尾指针的链表队列的性能一起比较啦~

测试代码

from queue.general_queue import Queue       # 普通队列在这个Py文件中
from queue.loopqueue import LoopQueue           # 循环队列在这个Py文件中
from linklist.linkedlistQueue import LinkedListQueue    # 带尾指针的链表实现的队列
import numpy as np
import datetime
np.random.seed(7)

def count_time(func):
    def int_time(*args, **kwargs):
        start_time = datetime.datetime.now() # 程序开始时间
        func()
        over_time = datetime.datetime.now()  # 程序结束时间
        total_time = (over_time - start_time).total_seconds()
        print('共用时: %s 秒' % total_time)
    return int_time

generalQueue = Queue()						# 普通数组队列对象
loopQueue = LoopQueue()						# 循环队列对象
linkedlistQueue = LinkedListQueue()		# 带有尾指针的链表队列对象
nums = 30000		# 操作数为30000次

@count_time
def compute_generalQueue():
    global nums, generalQueue
    for i in range(nums):           # 入队30000次
        generalQueue.enqueue(np.random.randint(10))
    for i in range(nums):           # 出队30000次
        generalQueue.dequeue()

@count_time
def compute_loopQueue():
    global nums, loopQueue
    for i in range(nums):           # 同样的操作
        loopQueue.enqueue(np.random.randint(10))
    for i in range(nums):
        loopQueue.dequeue()

@count_time
def compute_linkedlistQueue():
    global nums, linkedlistQueue
    for i in range(nums):           # 同样的操作
        loopQueue.enqueue(np.random.randint(10))
    for i in range(nums):
        loopQueue.dequeue()

if __name__ == '__main__':
    print('普通队列:')
    compute_generalQueue()
    print('循环队列:')
    compute_loopQueue()
    print('带尾指针的链表队列:')
    compute_linkedlistQueue()

测试结果

普通队列:
共用时: 53.576464 秒
循环队列:
共用时: 0.10933 秒
带尾指针的链表队列:
共用时: 0.113004

总结

可以看出,普通数组队列还是那么奇慢无比,循环队列和带有尾指针的链表队列的性能是相当的呢~

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