在常用的数据结构中,有一批结构被称为容器——栈与队列。
本篇博客主要学习一下栈这种结构的特性,以及用python实现它的相关操作。
内容
- 顺序栈
- 链栈
- 栈的实际应用
在这之前,我们需要了解一下什么是栈,以及栈这种结构有什么用处?
定义:
前面说到,栈相对于是一个容器,而这个容器里包含的是一些元素(这些元素的数据类型可以整型、浮点型、字符型等)。
同时,栈是保证元素后进先出关系的结构。例如,我们把几本书叠在一起,最上面的书也就是最后放上去的,我们要拿最下的面的书也只有先拿最上面的书,这满足了栈这种“后放先拿”的特性;又例如,做数学题时,遇到推导进行不下去的时候,我们通常是退回一步去考虑其它的可能性。
作用:
栈主要用来保存计算过程中的临时数据,这些数据是计算中发现或者产生的,在后面的计算中可能需要使用到它们。
栈也常常用来作为一种缓冲存储结构,用来存储工作中产生的暂时不用或者用不完的数据。
操作:
压栈:把数据压入栈
弹栈:把数据弹出栈
在Python中,我们可以用list来实现顺序栈,由于list才用动态顺序表技术,用它作为栈的表不会满,同时,python的内置函数append()和pop()也可以完美实现压栈和弹栈的操作。
因为用C语言刷过一遍数据结构,在这里呢,我就用两个指针来完成压栈和弹栈的操作。
base:栈底指针,始终指向栈底元素,一直不变。
top:栈顶指针,始终指向栈顶元素的后面。
请结合下图理解:
class stack:
def __init__(self, maxsize):
self.max = maxsize # 栈的最大容量
self.elem = [None] * self.max # 用list数据类型来表示顺序栈
self.top = 0 # 栈顶指针
self.base = 0 # 栈底指针
def push(self,elem):
if self.top - self.base == self.max: # 判断栈是否已满
raise Exception("The stack is full!")
self.elem[self.top] = elem # 元素入栈
self.top += 1 # top指针指向栈顶元素的下一个
def pop(self):
if self.top == self.base:
raise Exception("The stack is empty") # 判断栈是否已空
self.top -= 1 # top指向栈顶元素(牢记top指针的定义:top始终指向“栈顶元素”的后面)
e = self.elem[self.top] # 用e接收弹出的元素
return e
def get_top(self):
return self.elem[self.top - 1]
data = list(map(int,input("Please input a series of datas:").split(" ")))
s = stack(len(data)) # 栈的初始化
for i in range(len(data)): # 将元素压栈
s.push(data[i])
for i in range(s.top - s.base): # 输入栈内元素
print(s.elem[i],end=" ")
print("\n")
e = s.get_top()
print("The top elem of the stack is: %d" % e) # 栈顶元素是:
print("\n")
for i in range(s.top - s.base): # 弹栈
s.pop() # 弹一次栈
print("After popping the stack %d times,the stack is:" % (i + 1),end=" ")
for j in range(s.top - s.base): # 输出每次弹栈后,栈内剩余的元素
print(s.elem[j],end=" ")
print("\n")
在顺序栈中,一个栈的大小是固定死了的,为了实现栈的空间的动态增减,我们下面引入链栈。
链栈相当于是将前面的顺序栈中的每个元素拆分开来,从一个大的整体变成一个一个的小部分,然后再用某种方式将这些小部分链接起来,如下图所示:
其中,“某种链接方式”就相当于是C语言里的指针,用指针来链接每个“小部分”。link_top为链栈的栈顶指针,始终指向栈顶元素(注意与顺序栈中的栈顶指针作区分)。
那么为了实现链栈,我们需另设一个“结点类”来存储每个结点的信息,由上面的叙述可知,一个结点应该包含数据域(存放东西的地方)和指针域(链接方式,用于实现结点之间的链接)。
class stacknode: # 结点类
def __init__(self):
self.elem = None # 数据域
self.next = None # 指针域
class link_stack: # 链栈类
def __init__(self):
self.link_top = None # 链栈的指针,指向栈顶元素
关于链栈的压栈,它的方法本质上跟链表的头插法是一样的。
# 压栈(头插法)
def link_push(self,elem):
s = stacknode() # 生成一个新结点
s.elem = elem # 填充结点的值
s.next = self.link_top # 该结点指向下一个结点
self.link_top = s # 指针移动,指向栈顶元素
# 弹栈
def link_pop(self):
e = self.link_top.elem # 记录弹栈的那个结点
self.link_top = self.link_top.next # 栈顶指针指向下一个结点
return e # 返回弹栈的那个结点
def get_length(self):
top = self.link_top # 用top来代替link_top移动
count = 0 # 计数器
while top is not None:
count += 1 # 栈顶指针不为空,则加1
top = top.next # 指针指向下一个结点
return count # 返回count
def get_top(self):
if self.link_top == None:
return None
return self.link_top.elem
s = link_stack() # 链栈的初始化
data = list(map(int,input("Please input a series of datas:").split(" "))) # 输入数据
for i in range(len(data)): # 将data中的数据依次压栈,压栈顺序为1->2->3->4->5
s.link_push(data[i])
top = s.link_top # 用top代替link_top移动
print("The sequence of the datas in stack is:",end=" ") # 按先进后出的顺序输出链栈中的数据
while top is not None: # 依次输出链栈中每个结点的值
print(top.elem,end=" ")
top = top.next
print("\n")
for i in range(s.get_length()): # 按先进后出的顺序,输出每一次弹栈后链栈中的数据
s.link_pop() # 弹栈
print("After popping the stack %d times,the stack is:" % (i + 1),end=" ")
top = s.link_top
while top is not None:
print(top.elem,end=" ")
top = top.next
print("\n")
其实在前面验证顺序栈、链栈的操作时,我们已经完成了一个小小的应用,也就是颠倒一组元素的顺序。只需把所有元素按原来的顺序全部压栈,再依次弹栈,就能得到反序后的序列。
对于栈的应用,我们先从最简单的非递归应用开始。
先看一个关于阶乘的递归函数
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n - 1)
0的阶乘是1。
可以看到,在函数中这个递归过程就是一个“层层包围”的过程,是由里向外的一个计算过程。就像这个算式“(1+(2+(3+(4+5))))”一样,它总是先计算最里面的括号,然后一步一步地向外计算。
在程序执行时,函数的嵌套调用是按“后调用先返回”的规则进行,这种规则符合栈的使用模式,因此栈可以很自然地支持函数调用的实现。
下面给出用栈实现阶乘的算法:
def factorial_stack(n):
res = 1 # 保留阶乘的结果
st = link_stack()
while n > 0: # 先将需要计算的对象压栈
st.link_push(n)
n -= 1
while st.get_length() != 0: # 然后开始阶乘的计算
res *= st.link_pop()
return res
当然这只是列举了一个简单的例子,大家可自行用栈去实行其它复杂的过程
什么是括号匹配?
首先在这个问题中,我们只考虑三种括号,分别是:() [] {}。并命名“(”、“[”、“{”为开括号,“)”、“]”、“}”为闭括号。
所谓括号匹配,就是将开括号与闭括号匹配,也就是每对括号的开括号一定要在闭括号前,就算匹配成功。例如:
() 匹配成功
[()] 匹配成功
({[]}) 匹配成功
()[]{} 匹配成功
[) 匹配失败
{(]} 匹配失败
如何利用栈解决这个问题?
它的一个主要思想就是逐个处理遇到的括号,遇到开括号就直接进栈,遇到闭括号时与栈顶元素比较,若这俩是一对就将栈顶元素出栈,然后继续往下处理;若这俩不是一对,就说明匹配失败了。
由前面我们可以看到,匹配成功的情况有两种,一种是一对括号包围着另一对括号,就像([])一样;另一种就是一对括号接另一对括号,例如()[]。其实这个就跟“消消乐”差不多,第一种情况也就是由内向外消除,第二种情况就是依次消除。
在python中,“匹配”两个字是非常敏感的,因为字典这种数据结构不正好符合“匹配”的含义吗?所以,我们可以利用字典的特性来解决这个问题。下面上代码:
# 这里用的是链栈
def check(s):
brackets = {")": "(", "]": "[", "}": "{"} # 字典,用来当作判断的条件
open_bracket = "([{" # 用来逐个判断字符是否为开括号
length = len(s) # 获取字符串长度
if length % 2 == 1: # 因为括号一定成对出现,所以出现奇数个括号时一定不匹配
return False
st = link_stack() # 链栈初始化
for i in range(length): # 进入循环
if s[i] in open_bracket: # 逐个判断字符是否为开括号
st.link_push(s[i]) # 若为开括号就进栈
else: # 若不为开括号,那么就是闭括号,进入else
if brackets[s[i]] == st.get_top(): # 若该闭括号与栈顶元素相匹配
st.link_pop() # 将栈顶元素出栈
else: # 若不匹配
return False # 则整个字符串就不匹配
if st.get_length() != 0: # 如果匹配成功,栈里此时一定没有元素,如果有,那么一定匹配失败
return False
return True # 执行到此处,说明不匹配的条件已全部排除,所以匹配成功
测试代码:
# 这里需要导入random包
import random
pairs = "()[]{}" # 定义括号集
for i in range(5): # 执行5个实例
s = [] # 测试集
pos = random.randint(1,6) # 随机生成测试集的个数
for j in range(pos):
pr = random.randint(0,5) # 随机产生一个括号集的序号
while pairs[pr] in s: # 排除相同的括号,也就是说测试集中的括号一定是不相同的!
pr = random.randint(0,5)
s += pairs[pr] # 将随机产生的括号加入测试集
print(s)
print(check(s))
下面截取了几次的执行结果(不得不说,匹配成功的概率还是相当的低呀0.0):
这可是我运行了好多次才有的结果
中缀表达式:二元运算符写在两个运算对象中间的表达式。就是我们在日常生活中的数学表达式,例如“3+5*(8-4)”,上面这个表达式就是将运算符写在两个运算对象的中间。
那么由此,可以联想到后缀表达式是什么样。
后缀表达式:二元运算符写在两个运算对象后面的表达式。如果我们将上面的例子,转化为后缀表达式,那么就是“ 3 5 8 4 - * + ”。它的运算顺序就是:
1)8 4 - 也就是8 - 4,结果为4
2)5 4 * 也就是5 * 4,结果为20
3)3 20 + 也就是3 + 20,结果为23
自然而然地,也会有前缀表达式,那么前缀表达式呢就是将运算符号写在两个运算对象的前面。前缀表达式同时也被称为波兰表达式,那么后缀表达式也被称为逆波兰表达式。
为什么要将中缀表达式转换为后缀表达式?
首先,后缀表达式的出现一定是它在某些地方比中缀表达式更好。
中缀表达式的主要缺点就是:它不足以表示所有可能的运算顺序,需要通过辅助符号、约定等辅助描述机制。且中缀表达式必须引进括号的概念,规定括号里的运算先做,以便于一些特定顺序的计算。但这种处理方式非常麻烦,不适合计算机处理。
而后缀表达式中省去了括号,一个表达式只由运算对象和运算符组成,利用栈就可以实现一个后缀表达式的顺序计算。
所以类似于后缀表达式等很多不同于常规的东西,都是为了方便计算机处理而出现的。
如何利用栈进行后缀表达式的转换?
前缀 | 判断 | 栈内元素 | 后缀 |
---|---|---|---|
3+5*(8-4) | 为运算对象,直接输出 | 3 | |
3+5*(8-4) | 为除括号外的运算符,这里栈为空,不用与栈顶符号比较,直接进栈 | + | 3 |
3+5*(8-4) | 为运算对象,直接输出 | + | 35 |
3+5*(8-4) | 为除括号外的其它运算符,与栈顶符号“-”比较,x的优先级等于栈顶符号,直接进栈 | +* | 35 |
3+5*(8-4) | 为左括号,直接进栈 | +*( | 35 |
3+5*(8-4) | 为运算对象,直接输出 | +*( | 358 |
3+5*(8-4) | 为除括号外的运算符,这里栈不为空,与栈顶符号“(”比较,“-”的优先级大于栈顶符号,直接进栈 | +*(- | 358 |
3+5*(8-4) | 为运算对象,直接输出 | +*(- | 3584 |
3+5*(8-4) | 为右括号,不进栈,开始挨个弹栈并输出,直到遇见左括号 | +*( | 3584- |
3+5*(8-4) | 弹栈后,栈顶符号此时为左括号,再继续将左括号弹出,不输出 | +* | 3584- |
3+5*(8-4) | 此时对前缀表达式的读取已经结束,挨个弹出栈内剩余元素并输出 | 3584-*+ |
这就是一个完整的转换过程,大家可自行写一个中缀表达式来试着转换,加深一下对这个过程的印象。下面还是用前面说的链栈来实现这个过程,上代码:
priority = {'(':1,'+':2,'-':2,'*':3,'/':3} # 符号之间的优先级
operators = "+-*/()" # 符号集
def infix_to_suffix(infix):
st = link_stack()
suffix = []
for x in infix:
if x not in operators: # 若x是运算对象
suffix.append(x)
elif x == '(': # 若x是左括号
st.link_push(x)
elif x == ')': # 若x为右括号
while st.get_length() != 0 and st.get_top() != '(': # 依次弹栈,直到遇到左括号为止
suffix.append(st.link_pop())
st.link_pop() # 左括号出栈
else: # 若x是除括号外的其它运算符
while st.get_length() != 0 and priority[st.get_top()] >= priority[x]: # 比较栈顶符号与x的优先级
suffix.append(st.link_pop()) # 若栈顶符号的优先级大于x,则应先将栈顶元素出栈并加入到后缀表达式中
st.link_push(x) # 直到栈顶符号优先级小于x,将x压栈
while st.get_length() != 0: # 弹出并输入栈内剩余的符号
suffix.append(st.link_pop())
return suffix # 返回后缀表达式
示例
infix = "3+5*(8-4)"
print(infix_to_suffix(infix))
infix1 = "2+4*(5+3/(9+2))/5"
print(infix_to_suffix(infix1))
学会了中缀表达式转后缀表达式后,那么后缀表达式的计算就十分简单了。
因为后缀表达式只由运算符号和运算对象组成,没有了括号,所以它整体的一个思路就是遇到运算对象直接压栈,遇到运算符后不压栈,两次弹出栈顶元素,然后对它们作一个运算,运算完成后又压栈继续运算。
需要注意的是一下两点:
代码如下:
def calculate_suffix(suffix):
operators = "+-*/"
st = link_stack()
for x in suffix:
if x not in operators: # 若x为运算对象,直接进栈
st.link_push(int(x)) # 注意这里要强制转换x的类型,不然后续的计算无法进行,因为不转换的话就是字符型
continue # 退出本次循环读取下一个x
# 后面的x一定是运算符号,遇到运算符号我们就需要将栈顶元素依次取出两个
if st.get_length() >= 2:
b = st.link_pop() # b是右边的运算对象
a = st.link_pop() # a是左边的运算对象
else:
raise Exception("ERROR!The length of the stack is less than 2")
if x == "+":
c = a + b
elif x == "-":
c = a - b
elif x == "*":
c = a * b
else:
c = a / b
st.link_push(c) # 计算后将c压栈并继续后续的计算
return st.link_pop() # 计算的最后栈内一定只剩一个元素
示例:
infix1 = "2+4*(5+3/(9+2))/5"
print(calculate_suffix(infix_to_suffix(infix1)))
infix2 = "3+5*(8-4)"
print(calculate_suffix(infix_to_suffix(infix2)))
栈的学习到这里就结束了啦,如果大家有不懂的地方、或是我有不足的地方,欢迎在评论区留言!