Python:栈和队列的 python 列表实现

本文详细介绍了栈和队列这两种基本的数据结构,包括它们的概念、基本操作、Python实现及时间复杂度分析。栈遵循后进先出原则,而队列遵循先进先出原则,文章还提供了具体的Python代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 栈

1.1 栈的基本概念

栈是一种简单的数据结构,是由一系列对象组成的集合,这些对象的插入和删除操作遵循“后进先出”的原则。可以在任何时候向栈中插入一个对象,但是只能取得或者删除最后插入的对象(即所谓的“栈顶”)。

1.2 栈的基本操作:

假设栈为 S,栈的基本操作:

  1. S.push(e):将一个元素 e 添加到栈 S 的栈顶;
  2. S.pop():从栈 S 中返回栈顶的元素,如果栈为空,这个操作将提示错误。
  3. S.top():不移除栈顶元素的情况下,返回栈顶元素;
  4. S.is_empty():判断栈是否为空
  5. len(S):返回栈中元素的数量

1.3 栈的 Python List 实现:

class Error(Exception):
    pass


class ArrayStack:
    def __init__(self):
        self.__data = []

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

    def is_empty(self):
        return len(self.__data) == 0

    def push(self, e):
        self.__data.append(e)

    def top(self):
        if self.is_empty():
            raise Error('Stack is Empty!')
        else:
            return self.__data[-1]

    def pop(self):
        if self.is_empty():
            raise Error('Stack is Empty!')
        else:
            return self.__data.pop()


if __name__ == '__main__':
    S = ArrayStack()
    S.push(5)
    S.push(3)
    print(len(S))
    print(S.pop())
    print(S.is_empty())
    print(S.pop())
    print(S.is_empty())
    S.push(7)
    S.push(9)
    print(S.top())
    S.push(4)
    print(len(S))
    print(S.pop())
    S.push(6)

结果如下:
在这里插入图片描述

1.4 时间复杂度分析:

基于上一篇博客Python 序列:列表 (list),元组(tuple),字符串(str)深入分析(包括扩容和摊销)。关于 append 摊销的分析,这里的时间复杂度分析就很简单了:

操作时间复杂度
S.push(e)O(1)
S.pop()O(1)
S.top()O(1)
S.is_empty()O(1)
len(S)O(1)

2. 队列

2.1 队列的基本概念

队列是一种简单的数据结构,是由一些列对象组成的集合,这些对象的插入和删除操作遵循“先进先出”的原则。可以在任何时候向队列中插入一个对象,但是只有处在队列最前面的元素才能被删除。队列中允许被插入的一端称为队尾,允许删除的一端称为队头。

2.2 队列的基本操作:

假设队列为 Q,队列的基本操作:

  1. Q.enqueue(e):将一个元素 e 添加到队列 Q 的队尾;
  2. Q.dequeue():从队列 Q 中返回队头的元素,如果队列为空,这个操作将提示错误。
  3. Q.first():不移除队头元素的情况下,返回队头元素;
  4. Q.is_empty():判断队列是否为空
  5. len(Q):返回队列中元素的数量

2.3 队列的 python list

这时候一个简单的思路就是和栈一样,插入使用 append,删除使用 pop(0) ,但是这有一个问题,就是 pop(0) 的时间复杂度是 O(n),那么有没有和栈一样时间复杂度都是 O(1) 的实现方法呢?

这个时候可能会想到这样做,我们可以使用一个变量来保存第一个元素(即队头)的索引,每次删除元素之后,这个变量就指向下一个元素,先前的队头元素变为 None,这样就不用了每次出队之后用后面的元素覆盖前面的元素。这样时间复杂度确实是 O(1),但是空间呢,这个时候底层数组的大小就变成了追加元素的总和,即使我们出队了很多元素。举个例子:比如一个队列入队了 10000 次,出队了 9990 次(假设每次出队时队中有元素),那么最后队列中有 10 个元素,但是实际底层列表长度为 10000,而更底层的数组可能更大(动态数组),这就有很大的空间浪费。

那么到底应该怎么办呢?

可以使用循环数组解决,基本操作和上面一样,只是我们让队列的元素在底部循环。

  1. 假设底层数组的长度初始为 N
  2. 出队的时候,前面的位置就空出来了
  3. 入队的时候,如果到了数组的尾部,那么看前面位置是否用空,如果有,那么放在数组的前面,如果没有,申请更大的数组来保存。

这样,最后队列的空间利用率就高多了。
具体实现如下:

class Error(Exception):
    pass


class ArrayQueue:
    DEFAULT_CAPACITY = 3

    def __init__(self):
        self.__data = [None] * ArrayQueue.DEFAULT_CAPACITY
        self.__size = 0
        self.__font = 0

    def __len__(self):
        return self.__size

    def is_empty(self):
        return self.__size == 0

    def first(self):
        if self.is_empty():
            raise Error("Queue is Empty")
        else:
            return self.__data[self.__font]

    def dequeue(self):
        if self.is_empty():
            raise Error("Queue is Empty")
        else:
            res = self.__data[self.__font]
            self.__data[self.__font] = None
            self.__font = (self.__font + 1) % len(self.__data)
            self.__size -= 1
            return res

    def enqueue(self, e):
        if self.__size == len(self.__data):
            self.__resize(2 * len(self.__data))
        avail = (self.__font + self.__size) % len(self.__data)
        self.__data[avail] = e
        self.__size += 1

    def __resize(self, cap):
        old = self.__data
        self.__data = [None] * cap
        walk = self.__font
        for k in range(self.__size):
            self.__data[k] = old[walk]
            walk = (walk + 1) % len(old)
        self.__font = 0


if __name__ == '__main__':
    Q = ArrayQueue()
    Q.enqueue(5)
    Q.enqueue(3)
    print(len(Q))
    print(Q.dequeue())
    print(Q.is_empty())
    print(Q.dequeue())
    print(Q.is_empty())
    Q.enqueue(7)
    Q.enqueue(9)
    Q.enqueue(10)
    print(Q.first())
    Q.enqueue(4)
    print(len(Q))
    print(Q.dequeue())
    Q.enqueue(6)
    print(Q.dequeue())
    print(Q.dequeue())
    print(Q.dequeue())
    print(Q.dequeue())

结果如下:
在这里插入图片描述
我们设置的初始大小为 2 是为了方便测试,一般可以设得大一点。我们的扩容方法是扩大为原来的 2 倍,这样摊销的时间复杂度为 1,具体原因参考 Python 序列:列表 (list),元组(tuple),字符串(str)深入分析(包括扩容和摊销)。

def __init__(self):
    self.__data = [None] * ArrayQueue.DEFAULT_CAPACITY
    self.__size = 0
    self.__font = 0

初始化中,__size 是队列实际元素的多少,__font 始终指向队头元素。

def dequeue(self):
    if self.is_empty():
        raise Error("Queue is Empty")
    else:
        res = self.__data[self.__font]
        self.__data[self.__font] = None
        self.__font = (self.__font + 1) % len(self.__data)
        self.__size -= 1
        return res

再来看出列操作:如果为空报错,反之进入 else。然后由于 __font 指向队头,那么返回值为 res。后续把出队位置的值设为 None,然后 __font 变量后移,由于是循环使用,所以是 + 1 之后模底层数组的长度。最后队列元素的总和 -1。

def enqueue(self, e):
    if self.__size == len(self.__data):
        self.__resize(2 * len(self.__data))
    avail = (self.__font + self.__size) % len(self.__data)
    self.__data[avail] = e
    self.__size += 1

再来看入队操作:如果底层数组满了,那么就申请一个 2 倍数的空间。然后可用位置索引为队头加上队列长度去模数组的长度(还是由于循环使用)。最后赋值,队列长度 + 1。

def __resize(self, cap):
    old = self.__data
    self.__data = [None] * cap
    walk = self.__font
    for k in range(self.__size):
        self.__data[k] = old[walk]
        walk = (walk + 1) % len(old)
    self.__font = 0

最后是申请的时候,回申请一个 2 倍的空间,然后将原来的队列元素放在新数组的开头位置。相当于重新摆放,让前面的空位置没有了,然后重新设置指向队头的变量 __font。

2.4 时间复杂度:

最开始简单的类似于栈一样的实现:

操作时间复杂度
Q.enqueue(e)O(1)
Q.dequeue()O(n)
Q.first()O(1)
Q.is_empty()O(1)
len(Q)O(1)

按照我们修改之后的实现:

操作时间复杂度
Q.enqueue(e)O(1)
Q.dequeue()O(1)
Q.first()O(1)
Q.is_empty()O(1)
len(Q)O(1)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值