Python数据结构之栈详解
作者:盼小辉丶 发布时间:2021-01-07 01:12:36
0. 学习目标
栈和队列是在程序设计中常见的数据类型,从数据结构的角度来讲,栈和队列也是线性表,是操作受限的线性表,它们的基本操作是线性表操作的子集,但从数据类型的角度来讲,它们与线性表又有着巨大的不同。本节将首先介绍栈的定义和其不同实现,并且给出栈的一些实际应用。
通过本节学习,应掌握以下内容:
栈的基本概念及不同实现方法
栈基本操作的实现及时间复杂度
利用栈的基本操作实现复杂算法
1. 栈的基本概念
1.1 栈的基本概念
栈 (Stack) 是限定仅在序列一端执行插入和删除操作的线性表,对于栈而言,可进行操作的一端称为栈顶 (top),而另一端称为栈底 (bottom)。如果栈中不含任何元素则称其为空栈。
栈提供了一种基于在集合中的时间来排序的方式,最近添加的元素靠近顶端,旧元素则靠近底端。最新添加的元素被最先移除,这种排序原则也称为后进先出 (last in first out, LIFO) 或先进后出 (fast in last out, FILO)。
栈在现实中的例子随处可见,如下图所示,球桶中的球构成了一个栈,每次只能从顶部取出一个,放回时也只能置于顶部。假设栈为S = ( a0 , a1 , … , en)为栈顶元素,栈中元素按的顺序入栈 (push),而栈顶元素是第一个退栈 (pop) 的元素。
通过观察元素的添加和移除顺序,就可以快速理解栈所蕴含的思想。下图展示了栈的入栈和出栈过程,栈中元素的插入顺序和移除顺序恰好是相反的。
1.2 栈抽象数据类型
除了主要的操作(入栈和出栈)外,栈还具有初始化、判空和取栈顶元素等辅助操作。具体而言,栈的抽象数据类型定义如下:
基本操作:
1. __itit__(): 初始化栈
创建一个空栈
2. size(): 求取并返回栈中所含元素的个数 n
若栈为空,则返回整数0
3. isempty(): 判断是否为空栈
判断栈中是否存储元素
4. push(data): 入栈
将元素 data 插入栈顶
5. pop(): 出栈
删除并返回栈顶元素
4. peek(): 取栈顶元素
返回栈顶元素值,但并不删除元素
1.3 栈的应用场景
栈具有广泛的应用场景,例如:
符号的匹配,具体描述参考第3.3小节;
函数调用,每个未结束调用的函数都会在函数栈中拥有一块数据区,保存了函数的重要信息,包括函数的局部变量、参数等;
后缀表达式求值,计算后缀表达式只需一个用于存放数值的栈,遍历表达式遇到数值则入栈,遇到运算符则出栈两个数值进行计算,并将计算结果入栈,最后栈中保留的唯一值即为表达式结果;
网页浏览中的返回按钮,当我们在网页间进行跳转时,这些网址都被存放在一个栈中;
编辑器中的撤销序列,与网页浏览中的返回按钮类似,栈保存每步的编辑操作。
除了以上应用外,我们在之后的学习中还将看到栈用作许多算法的辅助数据结构。
2. 栈的实现
和线性表一样,栈同样有两种存储表示方式。
2.1 顺序栈的实现
顺序栈是栈的顺序存储结构,其利用一组地址连续的存储单元从栈底到栈顶依次存放。同时使用指针top来指示栈顶元素在顺序栈中的索引,同样顺序栈可以是固定长度和动态长度,当栈满时,定长顺序栈会抛出栈满异常,动态顺序栈则会动态申请空闲空间。
2.1.1 栈的初始化
顺序栈的初始化需要三部分信息:stack 列表用于存储数据元素,max_size 用于存储 stack 列表的最大长度,以及 top 用于记录栈顶元素的索引:
class Stack:
def __init__(self, max_size=10):
self.max_size = max_size
self.stack = self.max_size * [None]
self.top = -1
2.1.2 求栈长
由于 top 表示栈顶元素的索引,我们可以据此方便的计算顺序栈中的数据元素数量,即栈长:
def size(self):
return self.top + 1
2.1.3 判栈空
根据栈的长度可以很容易的判断栈是否为空栈:
def isempty(self):
if self.size() == 0:
return True
else:
return False
2.1.4 判栈满
由于需要提前申请栈空间,因此我们需要能够判断栈是否还有空闲空间:
def isfully(self):
if self.size() == self.max_size:
return True
else:
return False
2.1.5 入栈
入栈时,需要首先判断栈中是否还有空闲空间,然后根据栈为定长顺序栈或动态顺序栈,入栈操作稍有不同:
[定长顺序栈的入栈操作] 如果栈满,则引发异常:
def push(self, data):
if self.isfully():
raise IndexError('Stack Overflow!')
else:
self.top += 1
self.stack[self.top_1] = data
[动态顺序栈的入栈操作] 如果栈满,则首先申请新空间:
def resize(self):
new_size = 2 * self.max_size
new_stack = [None] * new_size
for i in range(self.num_items):
new_stack[i] = self.items[i]
self.stack = new_stack
self.max_size = new_size
def push(self, data):
if self.isfully():
self.resize()
else:
self.top += 1
self.stack[self.top_1] = data
入栈的时间复杂度为O(1)。这里需要注意的是,虽然当动态顺序栈满时,原栈中的元素需要首先复制到新栈中,然后添加新元素,但根据《顺序表及其操作实现》中顺序表追加操作的介绍,由于n次入栈操作的总时间T(n) 与O(n) 成正比,因此入栈的摊销时间复杂度仍可以认为是O(1)。
2.1.6 出栈
若栈不空,则删除并返回栈顶元素:
def pop(self):
if self.isempty():
raise IndexError('Stack Underflow!')
else:
result = self.stack[self.top]
self.top -= 1
return result
2.1.7 求栈顶元素
若栈不空,则只需返回栈顶元素:
def peek(self):
if self.isempty():
raise IndexError('Stack Underflow!')
else:
return self.stack[self.top]
2.2 链栈的实现
栈的另一种存储表示方式是使用链式存储结构,因此也常称为链栈,其中 push 操作是通过在链表头部插入元素来实现的,pop 操作是通过从头部删除节点来实现的。
2.2.1 栈结点
栈的结点实现与链表并无差别:
class Node:
def __init__(self, data):
self.data = data
self.next = None
def __str__(self):
return str(self.data)
2.2.2 栈的初始化
栈的初始化函数中,使栈顶指针指向 None,并初始化栈长:
class Stack:
def __init__(self):
self.top = None
# 栈中元素数
self.length = 0
2.2.3 求栈长
返回 length 的值用于求取栈的长度,如果没有 length 属性,则需要遍历整个链表才能得到栈长:
def size(self):
return self.length
2.2.4 判栈空
根据栈的长度可以很容易的判断栈是否为空栈:
def isempty(self):
if self.length == 0:
return True
else:
return False
2.2.5 入栈
入栈时,在栈顶插入新元素即可:
def push(self, data):
p = Node(data)
p.next = self.top
self.top = p
self.length += 1
由于插入元素是在链表头部进行的,因此入栈的时间复杂度为O(1),在这种情况下链尾作为栈底 。
2.2.6 出栈
若栈不空,则删除并返回栈顶元素:
def pop(self):
if self.isempty():
raise IndexError("Stack Underflow!")
ele = self.top.data
self.top = self.top.next
self.length -= 1
return ele
由于删除元素仅需修改头指针指向其 next 域,因此出栈的时间复杂度同样为O(1)。
2.2.7 求栈顶元素
若栈不空,返回栈顶元素即可,但栈顶元素并不会被删除:
def peek(self):
if self.isempty():
raise IndexError("Stack Underflow!")
return self.top.data
2.3 栈的不同实现对比
本节我们将对比栈的不同实现之间的异同:
顺序栈
操作的时间复杂度均为O(1),列表的尾部作为栈顶
栈满时需要进行动态的扩展,复制原栈元素到新栈中
链栈
操作的时间复杂度均为O(1),链表的头部作为栈顶
优雅的扩展,无需考虑栈满,需要额外的空间存储指针
3. 栈应用
接下来,我们首先测试上述实现的链表,以验证操作的有效性,然后利用实现的基本操作来解决实际算法问题。
3.1 顺序栈的应用
首先初始化一个顺序栈 stack,然后测试相关操作:
# 初始化一个最大长度为4的栈
s = Stack(4)
print('栈空?', s.isempty())
for i in range(4):
print('入栈元素:', i)
s.push(i)
print('栈满?', s.isfully())
print('栈顶元素:', s.peek())
print('栈长度为:', s.size())
while not s.isempty():
print('出栈元素:', s.pop())
测试程序输出结果如下:
栈空? True
入栈元素: 0
入栈元素: 1
入栈元素: 2
入栈元素: 3
栈满? True
栈顶元素: 3
栈长度为: 4
出栈元素: 3
出栈元素: 2
出栈元素: 1
出栈元素: 0
3.2 链栈的应用
首先初始化一个链栈 stack,然后测试相关操作:
# 初始化新栈
s = Stack()
print('栈空?', s.isempty())
for i in range(4):
print('入栈元素:', i)
s.push(i)
print('栈顶元素:', s.peek())
print('栈长度为:', s.size())
while not s.isempty():
print('出栈元素:', s.pop())
测试程序输出结果如下:
栈空? True
入栈元素: 0
入栈元素: 1
入栈元素: 2
入栈元素: 3
栈顶元素: 3
栈长度为: 4
出栈元素: 3
出栈元素: 2
出栈元素: 1
出栈元素: 0
3.3 利用栈基本操作实现复杂算法
匹配符号是指正确地匹配左右对应的符号(符号允许进行嵌套),不仅每一个左符号都有一个右符号与之对应,而且两个符号的类型也是一致的,下标展示了一些符号串的匹配情况:
符号串 | 是否匹配 |
---|---|
[]()() | 匹配 |
[(())() | 不匹配 |
{([]())} | 匹配 |
(())[]} | 不匹配 |
为了检查符号串的匹配情况,需要遍历符号串,如果字符是 (、[ 或 { 之类的开始分隔符,则将其写入栈中;当遇到诸如 )、] 或 } 等结束分隔符时,则栈顶元素出栈,并将其与当前遍历元素进行比较,如果它们匹配,则继续解析符号串,否则表示不匹配。当遍历完成后,如果栈不为空,则同样表示不匹配:
def isvalid_expression(expression):
stack = Stack()
symbols = {')':'(', ']':'[', '}':'{'}
for s in expression:
if s in symbols:
if stack:
top_element = stack.pop()
else:
top_element = '#'
if symbols[s] != top_element:
return False
else:
stack.push(s)
return not stack
由于我们只需要遍历符号串一边,因此算法的时间复杂度为O(n),算法的空间复杂度同样为O(n)。
给定一链表(带有头结点) L : L0→L1→…→Ln ,将其重排为:L0→Ln→L1→Ln−1 … 。
例如链表中包含 9 个元素,则下图现实了重排前后的链表元素情况:
由于栈的先进后出原则,可以利用栈与原链表的配合进行重排,首次按遍历链表,将每个结点入栈;栈中元素的出栈顺序为原链表结点的逆序,然后交替遍历链表和栈,构建新链表。
def reorder_list(L):
p = L.head.next
if p == None:
return L
stack = Stack()
while p!= None:
stack.push(p)
p = p.next
l = L.head.next
from_head = L.head.next
from_stack = True
while (from_stack and l != stack.peek() or (not from_stack and l != from_head)):
if from_stack:
from_head = from_head.next
l.next = stack.pop()
from_stack = False
else:
l.next = from_head
from_stack = True
l = l.next
l.next = None
该算法的时间复杂度和空间复杂度均为O(n)。
来源:https://blog.csdn.net/LOVEmy134611/article/details/123327176


猜你喜欢
- 在javascript中原型(prototype)定义了特定类型的所有实例都可以访问的属性和方法,很多些情况下需要重新对原型中的属性赋值,如
- 这一次将使用pymysql来进行一次对MySQL的增删改查的全部操作,相当于对前五次的总结:先查阅数据库:现在编写源码进行增删改查操作,源码
- 1.检查重复元素下面的方法可以检查给定列表中是否有重复的元素。它使用了 set() 属性,该属性将会从列表中删除重复的元素。def all_
- 我就废话不多说了,直接上代码吧!import numpy as npimport torchimport torch.nn as nnimp
- /*不同服务器数据库之间的数据操作*/ --创建链接服务器 exec sp_addlinkedserver 'ITSV ',
- 对数据库的备份是网站管理人员的必修课,那么常用的数据库备份方式有哪些呢?应如何选择?数据库备份有四种类型,分别应用于不同的场合,下面简要介绍
- 先附上官方文档:https://pandas.pydata.org/pandas-docs/stable/reference/api/pan
- 用CSV格式来保存文件是个不错的主意,因为大部分程序设计语言和应用程序都能处理这种格式,所以交流起来非常方便。然而这种格式的存储效率不是很高
- 把这两个很普遍性的网友比较关心的问题总结回答一下。in和exist的区别从sql编程角度来说,in直观,exists不直观多一个select
- 以前提取这些文件用的是一同事些的批处理文件;用起来不怎么顺手,刚好最近在学些python,所有就自己动手写了一个python提取文件的小程序
- MySQL是一个开源的关系型数据库管理系统,支持多种操作语言,其中最基础、最常用的命令之一就是SELECT语句。在本篇文章中,这里将详细介绍
- 随着访问量的增加,对于一些比较耗时的数据库读取操作,一般采用将写入与读取操作分开来缓解数据库的压力,数据库引擎一般采用Master/Slav
- 主要就是通过jieba的posseg模块将一段文字分段并赋予不同字段不同意思。然后通过频率计算出热频词数据放在文章里面了,就不用花积分下载了
- The prompt command reconfigures the default mysql> prompt. The stri
- Python----OS 文件目录处理import osimport time# 获取当前文件的绝对路径dir_1 = os.path.ab
- 引言 本文通过python3、第三方python库Selenium和谷歌浏览器Chrome,完成WPS表单的自动填写。开发环境配置 py
- 测了一下django、flask、bottle、tornado 框架本身最简单的性能。对django的性能完全无语了。django、flas
- 高考在即,笔者想为孩子以后能够快乐学习数学、学习编程找到一个比较合适的项目,经过一番比较发现github上的万星项目manim(https:
- 1、先说结论:使用xml-rpc的机制可以很方便的实现服务器间的RPC调用。2、试验结果如下:3、源码如下:服务器端的源代码如下:impor
- Fabric 是使用 Python 开发的一个自动化运维和部署项目的一个好工具,可以通过 SSH 的方式与远程服务器进行自动化交互,例如将本