python数据结构教程第三课
在常用的数据结构中,有一批结构被称为容器,用于支持对所存储的元素进行存储、管理和使用,栈和队列是两类最常使用的容器。
一.简介
二.栈与队列的抽象数据类型(ADT)
三.栈的python实现
1.栈的顺序表实现
2.栈的链表实现
3.栈的应用——括号匹配、中缀表达式、Ackerman函数
四.队列的python实现
1.队列的循环线性表实现
2.队列的应用——舞伴配对问题
一.简介
栈和队列主要用于计算过程中保存临时数据,这些数据是计算中发现或产生的,在后面的计算中可能会用到它们。这种情况在计算中很常见:工作中产生的数据暂时不用或者用不完,就有必要把当时不能立刻用掉的数据存起来。如果需要存储的数据项不能事先就确定,就必须采用一定的机制存储和管理,这样的存储机制称为缓冲存储或者缓存,栈和队列就是使用最多的缓冲存储结构
栈是保证元素后进先出(Last In First Out,LIFO)关系的结构,简称为LIFO结构
队列是保证元素先进先出(First In First Out,FIFO)关系的结构,简称为FIFO结构
二.栈与队列的抽象数据类型(ADT)
栈的基本方法应该有创建空栈、入栈、出栈、空栈判断㩐,其基本ADT如下:
ADT Stack:
Stack(self) #创建空栈
is_empty(self) #空栈判断
top(self) #返回栈顶元素
pop(self) #返回并删除栈顶元素
push(self,elem) #元素入栈
len(self) #返回栈深度
队列的属性和方法与栈类似,只是在出入栈的方法上会有所区别,其基本的ADT如下:
ADT Queue:
Queue(self) #创建空队列
is_empty(self) #空队列判断
enqueue(self,elem) #元素入队
dequeue(self) #元素出队
peek(self) #返回队首元素
len(self) #返回队长
三.栈的python实现
使用线性表实现栈是最自然的方式,对于顺序表的实现在后端进行插入和删除是高效的操作,应该用这一端作为栈顶,在使用链接表实现时则正好相反
1.栈的顺序表实现
#避免空栈访问
class StackUnderflow(ValueError):
pass
class SStack():
def __init__(self): #栈初始化
self._elems = []
def is_empty(self): #空栈判断
return self._elems == []
def push(self,elem): #元素入栈
self._elems.append(elem)
def pop(self): #返回并删除栈顶元素
if self._elems == []:
raise StackUnderflow('in SStack.pop()')
return self._elems.pop()
def peek(self): #返回最新进入栈的元素
if self._elems == []:
return None
return self._elems[-1]
def deep(self): #返回栈深度
return len(self._elems)
测试代码:
st1 = SStack()
st1.push(3)
st1.push(5)
while not st1.is_empty():
print(st1.pop())
结果:
5
3
2.栈的链表实现
'''
基于链接表技术实现的栈类,用LNode作为结点
表头一段作为栈顶,表尾一端作为栈尾
'''
#LNode结点类
class LNode():
def __init__(self,elem = None,next_ = None):
self.elem = elem
self.next = next_
#基于链接表技术实现的栈类
class LStack():
def __init__(self): #初始化
self._top = None
def is_empty(self): #空栈判断
return self._top is None
def top(self): #返回栈顶元素
if self._top is None:
raise StackUnderflow('in LStack.top()')
return self._top.elem
def push(self,elem): #元素进栈
self._top = LNode(elem,self._top)
def pop(self): #返回并删除栈顶元素
if self._top is None:
raise StackUnderflow('in LStack.pop()')
p = self._top
self._top = p.next
return p.elem
测试代码:
#将序列中的元素反序
list1 = list([1,2,3,4,5,6,7,8])
print(list1)
st1 = SStack()
for x in list1:
st1.push(x)
list2 = []
while not st1.is_empty():
list2.append(st1.pop())
print(list2)
结果:
[1, 2, 3, 4, 5, 6, 7, 8]
[8, 7, 6, 5, 4, 3, 2, 1]
3.栈的应用——括号匹配、中缀表达式、Ackerman函数
1)检测 python程序当中括号不匹配的情况:
检测文本中的括号不匹配情况是栈的一个较为简单的应用,当发现开括号时,直接将开括号入栈,遇到闭括号时则从栈中弹出一个元素,如果出现空栈或括号不匹配的情况,则说明文本中的括号存在问题,报错。这里需要注意的是,在python程序中,还存在注释的情况,因此,有一个子函数parentheses专门用于检测合格的括号
'''检测python程序中的括号不匹配情况(考虑注释与字符串情况),发现不匹配时输出不匹配的括号、行号和位置'''
def check_py_parens(py_name):
parens = '()[]{}'
open_parens = '([{'
opposite = {')':'(',']':'[','}':'{'}
def parentheses(py_name): #括号生成器
row = 0 #传出python文件中
st0 = SStack() #不在注释与字符串
flag = 1 #里的括号
for line in open(py_name):
row += 1
column = 0
for char in line:
if char == '#':
break
elif (char == '\'' or char == '\"') and st0.is_empty():
st0.push(char)
column += 1
flag = 0
elif (not st0.is_empty()) and (char == '\'' or char == '\"') and char == st0.pop():
flag = 1
column += 1
else:
column += 1
if flag == 1:
if char in parens:
yield char,row,column
st = SStack() #存储括号的栈
for pr,row,column in parentheses(py_name):
if pr in open_parens: #开括号入栈
st.push(pr)
elif st.is_empty() or st.pop() != opposite[pr]: #空栈或者不匹配时返回失败
print(pr,row,column)
return False
return True
测试代码:
#这里的hello.py可以是任意py程序文本,这里不附文本了
print(check_py_parens('hello.py'))
2)中缀表达式的计算
中缀表达式即我们通常使用的运算表达式写法,运算符写在运算对象的中间,如“a+b”,这种形式是最符合人的自然认知的,但是对于计算机来说,它的计算是相对复杂的,如计算“(3+5)*2+5”,这里的计算顺序受到了括号与运算优先度的影响,计算机无法直接扫描计算,需要配合栈的存储,接下来给出算法逻辑:
@建立两个栈,一个用于存储运算符,一个用于存储数字
@从左到右逐个读取文本,遇到数字直接入栈,遇到操作符的情况较为复杂,在中缀表达式中遇到一个操作符时是无法直接计算的,因为程序此时只读取了二元运算符的一个运算对象,还需要读取另一个运算对象,所以,这时需要先将操作符入栈,接下来分情况讨论
@当读到一个操作符,操作符栈为空或该操作符是‘(’时,需要先将该操作符入栈
@当读到的操作符是‘)’时,需要不断弹出一个操作符与两个操作数进行运算,将运算结果重新入栈,直到遇到‘(’,将其弹出
@当遇到的操作符是四则运算时,需要比较该操作符与栈顶操作符的优先级,如果小于等于栈顶的优先级则应该先将栈顶的操作符和两个操作数弹出运算,结果压栈,再将读到的操作读入栈,当大于栈顶的优先级时,则简单的将其压栈,至此,则解决了所有括号与优先度的问题
@不断弹出一个操作符与两个数字,进行运算,将运算结果压栈,直至运算符栈为空
#协助函数,输入两个数与一个操作符,得到结果
def op_calculate(a,b,op):
a = float(a)
b = float(b)
if op == '+':
return a+b
elif op == '-':
return b-a
elif op == '*':
return a*b
elif op == '/':
return b/a
#直接的中缀表达式求值函数,只考虑四则运算和括号的情况,输入以空格分开
def infix_calculator(line):
st_num = SStack() #数字栈
st_op = SStack() #运算符栈
operator = '+-*/()'
priority = {'+':1,'-':1,'*':2,'/':2,'(':0}
exp = line.split() #将输入字符串化为列表
for char in exp: #开始处理表达式
if char not in operator:
st_num.push(char)
elif char in operator:
if st_op.is_empty() or char == '(':
st_op.push(char)
elif char == ')':
while st_op.peek() is not '(':
op = st_op.pop()
a = st_num.pop()
b = st_num.pop()
st_num.push(op_calculate(a,b,op))
st_op.pop()
elif char in '+-*/':
if priority[char] <= priority[st_op.peek()]:
op = st_op.pop()
a = st_num.pop()
b = st_num.pop()
st_num.push(op_calculate(a,b,op))
st_op.push(char)
elif priority[char] > priority[st_op.peek()]:
st_op.push(char)
while not st_op.is_empty(): #处理无括号
op = st_op.pop() #的剩余运算
a = st_num.pop()
b = st_num.pop()
st_num.push(op_calculate(a,b,op))
return st_num.pop() #返回结果
测试代码:
print(infix_calculator(' ( 3 + 5 ) + 2 * ( 5 - ( 1 + 2 ) )'))
print(infix_calculator(' 3 + 5 * 2'))
结果:
12.0
13.0
3)Ackerman函数求解
Ackermen函数是栈运用问题中非常经典的一个,举这个例子的目的是为了说明,任一个递归定义的函数,都可以引入一个栈保存中间结果的方式,翻译为一个非递归过程,与此对应,任何一个包含循环的程序都可以翻译为一个不包含循环的递归函数
Ackerman函数如下:
Ack(m,n)
= n+1 , m = 0
= Ack(m-1,1) , m != 0 and n = 0
= Ack(m-1,Ack(n-1)) , m != 0 and n != 0
这里给出该函数实现的迭代方法与栈的非迭代方法,不作逻辑分析了,感兴趣的同学可以自己根据代码自己画出栈的管理情况进行分析,编程的难点在于这是一个双重迭代的,迭代的深度会非常高,输入参数不能太大,否则程序会很容易崩溃,非迭代方法需要两个栈与双重循环
迭代方法实现:
def Ackerman(m,n):
if m == 0:
return n+1
elif n == 0:
return Ackerman(m-1,1)
elif n != 0:
return Ackerman(m-1,Ackerman(m,n-1))
非迭代方法实现:
def Ackerman_stack(m,n):
st1 = SStack()
st2 = SStack()
st1.push(m)
st2.push(n)
while m != 0:
if n != 0:
st1.push(m)
n -= 1
st2.push(n)
elif n == 0:
m = st1.pop() - 1
n = 1
st2.pop()
st1.push(m)
st2.push(1)
while m == 0 and st2.deep() > 1:
st1.pop()
n = st2.pop()
m = st1.pop()
st2.pop()
m -= 1
n += 1
st1.push(m)
st2.push(n)
return (st2.pop() + 1)
测试代码:
print(Ackerman(2,3))
print(Ackerman_stack(2,3))
print(Ackerman(3,3))
print(Ackerman_stack(3,4))
结果:
9
9
61
125
四.队列的python实现
在用python的线性表实现队列类时会发现,由于队列一端入一端出的特性,使用list或链表时需要大量的搬运、移动操作,这会导致代码的效率较低,因此,使用循环线性表实现队列
1.队列的循环线性表实现
将list对象看成一个圈,尾部添加与首部弹出时,都在这个圈内进行,这里需要定义一个head变量,一直跟随存储对象多少的变化,当长度不够时,需要扩展存储空间
'''基于循环顺序表的队列实现'''
#首先定义由于空而无法完成出队操作造成的异常
class QueueUnderflow(ValueError):
pass
class SQueue():
def __init__(self,init_len = 8): #初始化
self._len = init_len
self._elems = [0]*init_len #初始长度
self._head = 0
self._num = 0
def __len__(self): #返回队列长度
return self._num
def is_empty(self): #空队判断
return self._num == 0
def peek(self): #返回队首元素
if self._num == 0:
raise QueueUnderflow
return self._elems[self._head]
def dequeue(self): #返回并删除队首元素
if self._num == 0: #并更新队首位置
raise QueueUnderflow
e = self._elems[self._head]
self._head = (self._head + 1) % self._len
self._num -= 1
return e
def dequeue_tail(self): #返回并删除队尾元素
if self._num == 0:
raise QueueUnderflow
e = self._elems[(self._head+self._num-1) % self._len]
self._num -= 1
return e
def enqueue(self,e): #元素入队
if self._num == self._len:
self.__extend()
self._elems[(self._head+self._num) % self._len] = e
self._num += 1
def enqueue_head(self,e): #元素从队首入队
if self._num == self._len:
self.__extend()
self._head = (self._head + 1) % self._len
self._elems[self._head] = e
def __extend(self): #扩展存储空间
old_len = self._len
self._len *= 2
new_elems = [0]*self._len
for i in range(old_len):
new_elems[i] = self._elems[(self._head+i)%old_len]
self._elems,self._head = new_elems,0
2.队列的应用——舞伴配对问题
循环队列的一个小应用,舞会配对问题
现一晚会,男女各排一队,两队人数不一定相同,第一支歌响起时,男女配对入场,如果一队中有人没有配对,则下一首歌第一个入场,接着配对,要求输出每一场的配对情况
#舞伴配对问题
def party(men_num,women_num,times):
men = SQueue(men_num)
men_dance = SQueue()
women = SQueue(women_num)
women_dance = SQueue()
for i in range(men_num):
men.enqueue(i+1)
for i in range(women_num):
women.enqueue(i+1)
t = 0
while t < times:
print('第'+str(t+1)+'轮舞会配对:')
n = 0
m = 0
while n < men_num and m < women_num:
print('男序号: '+str(men._head+1)+','+'女序号: '+str(women._head+1))
men_dance.enqueue(men.dequeue())
women_dance.enqueue(women.dequeue())
n += 1
m += 1
if women_num == men_num:
print('全部入场')
elif women_num == m:
print('下一场次第一个出场的本次未配对男性的序号: ' + str(men._head+1))
elif men_num == n:
print('下一场次第一个出场的本次未配对女性的序号: ' + str(women._head+1))
while not men_dance.is_empty():
men.enqueue(men_dance.dequeue())
women.enqueue(women_dance.dequeue())
t += 1
测试代码:
party(10,12,3)
结果:
第1轮舞会配对:
男序号: 1,女序号: 1
男序号: 2,女序号: 2
男序号: 3,女序号: 3
男序号: 4,女序号: 4
男序号: 5,女序号: 5
男序号: 6,女序号: 6
男序号: 7,女序号: 7
男序号: 8,女序号: 8
男序号: 9,女序号: 9
男序号: 10,女序号: 10
下一场次第一个出场的本次未配对女性的序号: 11
第2轮舞会配对:
男序号: 1,女序号: 11
男序号: 2,女序号: 12
男序号: 3,女序号: 1
男序号: 4,女序号: 2
男序号: 5,女序号: 3
男序号: 6,女序号: 4
男序号: 7,女序号: 5
男序号: 8,女序号: 6
男序号: 9,女序号: 7
男序号: 10,女序号: 8
下一场次第一个出场的本次未配对女性的序号: 9
第3轮舞会配对:
男序号: 1,女序号: 9
男序号: 2,女序号: 10
男序号: 3,女序号: 11
男序号: 4,女序号: 12
男序号: 5,女序号: 1
男序号: 6,女序号: 2
男序号: 7,女序号: 3
男序号: 8,女序号: 4
男序号: 9,女序号: 5
男序号: 10,女序号: 6
下一场次第一个出场的本次未配对女性的序号: 7