前言:
在这个系列的第一篇博客中,我主要强调了编程思维的重要性以及如何高效准确的编写出能解决问题的代码,这一篇博客开始就正式开始python数据结构相关的知识。
有一种数据结构,它的元素顺序取决于添加的顺序或者删除的顺序,一旦某个元素被添加进来,它与前后元素的相对位置也就保持不变了,这样的数据集合就被称为线性数据结构。
常见的线性数据结构主要有:栈、队列、双端队列、列表。接下来的几篇博客会来详细讲讲这几种线性数据结构和它们的实现
今天的主角:栈
栈,它的添加和删除操作总发生在同一端,即顶端。它的特性呢就是“先进后出”(或者说“后进先出”,都是一个意思),也就是说最先进入栈的元素在栈中会呆最久,那么顶端的元素总是最近才添加的,而底端的元素总是最开始添加的。
生活中有很多栈的例子,比如:我们洗好的盘子叠起来放,总是先洗好的放在最下面;一堆摞起来的书,我们要拿到其中某一本,就需要先移开上面的那些书;或者有没有想过,我们浏览网页之所以可以返回之前浏览的网页,那是不是也是因为我们浏览的网页记录被放到栈中了呢?
栈抽象数据类型的操作:
- stack() 初始化一个空栈,返回值一个空栈
- push(item) 入栈操作,将一个元素item放入栈中
- pop() 出栈操作,将栈顶元素删除,并且返回栈顶元素的值
- peek() 查看栈顶元素,与pop不同,它不会删除栈顶元素,只是将栈顶元素的值返回
- isEmpty() 检查栈是否为空
- size() 返回栈中的元素数量
- show() 显示栈中所有元素
有了这些基本操作,那我们就尝试着实现一个栈
1.栈的python实现
class Stack():
def __init__(self):
self.items=[]
def isEmpty(self):
return '栈为空'if self.items==[] else '栈不为空'
def push(self,item):
self.items.append(item)
def pop(self):
return self.items.pop()
def peek(self):
return self.items[len(self.items)-1]
def size(self):
return len(self.items)
def show(self):
return self.items
# 实例化测试一下
s=Stack()
# 初始应该为空
print(s.isEmpty())
# 压入几个元素试试
s.push(4)
s.push('abc')
s.push(18)
s.push('DL')
# 再来看看是否为空,以及栈的大小和栈顶元素
print(s.isEmpty())
print(s.size())
print(s.peek())
# 然后试试删除栈顶元素
print(s.pop())
print(s.size())
# 显示所有元素
print(s.show())
通过上面的代码,我们可以很好的用代码实现栈的各种操作。同时大家肯定也注意到了,我说过python写数据结构比c、c++简单,原因就在于python的列表是动态的,没有固定的大小,有元素添加直接append就可以啦,相比于当时学习c的数据结构,一开始就要分配数组大小,然后添加元素还得判断栈是否满了,省去了大量的精力。不过,也许这样确实简单,但是我们还是需要去考虑python的动态列表是怎么实现的呢?或者有兴趣也可以去看看go语言切片的实现原理,了解一下长度和容量是如何变化的,为什么推荐go,因为go更类似于C语言,但是会比C语言简单,在这里我就不拓展太多了,还是本着最低学习成本的思想来讲
思考
如果我们用列表头作为栈顶,如何实现栈?与上面用列表尾实现效率上有什么不同
简单解释:用列表头作为栈,入栈和出栈都是对items[0]进行操作,入栈用insert(0,item),出栈用pop(0);从效率上来说,insert(0),pop(0)时间复杂度是O(n),而append(),pop()时间复杂度是O(1),所以栈中元素越多,列表尾作为栈顶的优势越明显。
2.栈的应用
今天先来说说常见的两个应用:匹配符号,表达式转换
-
我们知道(),[],{}总是成对出现,并且有一定的顺序,所以我们可以利用栈实现一个检测器,看输入的符号是否符合规范。
-
对于表达式,我们有中序表达式也有前序,后序,它们之间的转换也可以利用栈实现
思路:
-
我们遍历输入的表达式,将([{ 压入栈,然后一旦遍历到右边的符号就去做一次判断:首先把栈顶元素出栈,然后与它现在右边的符号做一次匹配,看是否符合规范;依次这样,如果最后栈为空(全部左边符号做过匹配),而且每一次匹配的结果(flag)都是正确的,那么也就是符号匹配正确。
-
以中序转后续为例,我们先遍历表达式,用栈保存运算符,并且每次遇到新的运算符都要先比较它与栈中元素的优先级,从而实现转换
这里呢,我们会使用pythonds这个库里面的数据结构,需要先pip install pythonds
from pythonds.basic import Stack
def check(string):
s=Stack()
flag=True
index=0
while(index<len(string) and flag):
symbol=string[index]
if symbol in "([{":
s.push(symbol)
else:
if s.isEmpty():
flag=False
else:
top=s.pop()
if not matches(top,symbol):
flag=False
index+=1
if flag and s.isEmpty():
return "符号匹配无错误"
else:
return "符号匹配有错误"
def matches(start,end):
starts='([{'
ends=')]}'
return starts.index(start)==ends.index(end)
print(check('(][){}'))
print(check('([{}])'))
print(check('[][][]'))
print(check('[{())}]'))
from pythonds.basic import Stack
import string
def infix_to_postfix(expr):
#先定义一下运算符的优先级
prec={}
prec['*']=3
prec['/']=3
prec['+']=2
prec['-']=2
prec['(']=1
op=Stack()
postfix_list=[]
# 以表达式中的空格分开,得到表达式中的每一个元素,返回列表
tokenlist=expr.split()
# 开始遍历
for token in tokenlist:
# 如果匹配到大写的英文字母直接添加到后续表达式
if token in string.ascii_uppercase:
postfix_list.append(token)
# 如果匹配到左括号,那就直接入栈
elif token=='(':
op.push(token)
# 匹配到右括号,就把栈中左括号之后添加的所有符号都拿出来放进后续表达式中
elif token==')':
topToken=op.pop()
while topToken!='(':
postfix_list.append(topToken)
topToken=op.pop()
# 栈不为空,且当前符号优先级小于栈顶元素优先级,
# 那么就把栈顶元素添加到后序表达式,同时把优先级小的运算符入栈
else:
while (not op.isEmpty()) and (prec[op.peek()]>=prec[token]):
postfix_list.append(op.pop())
op.push(token)
while not op.isEmpty():
postfix_list.append(op.pop())
return " ".join(postfix_list)
print(infix_to_postfix("( A + B ) * ( C + D )"))
print(infix_to_postfix("A + B * C"))
print(infix_to_postfix("A * B + C"))
总结
这篇博客,了解了栈的特点以及基本操作,然后还知道了可以利用栈去解决实现哪些问题,下一篇就讲讲队列。这个系列的博客其实每篇内容都不多,也是为了一点一点的能更好吸收。