栈和队列
一、栈
栈( s t a c k stack stack,有时也叫堆栈)是一种容器,可存入数据元素、访问元素、删除元素等。存入栈的元素之间没有任何具体的关系,只有时间的到来先后关系。栈是一种 L I F O LIFO LIFO的数据结构,栈的基本性质表明,在任何时刻可以操作的元素是在此之前最后存入的那个元素。
一、栈抽象数据类型
ADT Stack:
Stack(self)
is_empty(self)
push(self,elem)
pop(self)
top(self) #取得栈顶元素,不删除
二、栈的顺序表实现
栈是一个后进先出表( L I F O LIFO LIFO表),在表的实现中,执行插入和删除操作的一端称为栈顶,另一端称为栈底。对于顺序表而言,后端插入和删除是 O ( 1 ) O(1) O(1)操作,应该用这一端作为栈顶。
#实现栈结构之前,先考虑为操作失败定义一个异常类。
class StackUnderflow(ValueError): #栈下溢(空栈访问)
pass
#python中的List是动态顺序表,可以实现栈
class SStack():
def __init__(self):
self.elems = []
def is_empty(self):
return self.elems == []
def top(self):
if self.is_emmpty():
raise StackUnderflow("in SStack.top()")
return self.elems[-1]
def push(self,elem):
self.elems.append(item)
def pop(self):
if self.is_emmpty():
raise StackUnderflow("in SStack.pop()")
return self.elems.pop()
三、栈的链接表实现
对于单链表而言,前端插入和删除元素都是 O ( 1 ) O(1) O(1)操作,应该用这端作为栈顶。
class LNode:
def __init__(self,elem,next = None):
self.elem = elem
self.next = next
class LStack:
def __init__(self):
self.top = None
def is_empty(self):
return self.top == None
def top(self):
if self.is_emmpty():
raise StackUnderflow("in SStack.top()")
return self.top.elem
def push(self,elem):
self.top = LNode(elem,self.__top)
def pop(self):
if self.is_emmpty():
raise StackUnderflow("in SStack.pop()")
cur = self.top
self.top = cur.next
return cur.elem
四、栈的应用
一、括号匹配问题
不难得出,括号配对的原则:在扫描正文过程中,遇到的闭括号应该与此前最近遇到的且尚未匹配的开括号配对。
def check_parens(text):
parens = "()[]{}"
open_parens = "([{"
opposite = {"(":")","[":"]","{":"}"}
def parentheses(text):
i,text_len = 0,len(text)
while True:
while i < text_len and text[i] not in parens:
i += 1
if i >= text_len:
return
yield text[i], i
i += 1
st = SStack()
for pr,i in parentheses(text):
if pr in open_parens:
ss.push(pr)
elif pr != opposite[st.pop()]:
print("Unmatching is found at",i,"for",pr)
return False
print("All parentheses are correctly matched.")
return True
二、表达式计算
一、中缀表达式到后缀表达式的转换
不同项的处理:
- 在扫描中缀表达式的过程中,如果遇到运算对象,就应该将其直接送出作为后缀表达式的一个项。
- 将运算符放入后缀表达式也就是要求运算,需要仔细控制送出的时机。
- 中缀表达式中运算符的处理比较麻烦:扫描中遇到一个运算符不能将其直接输出,只有看到下一个运算符的优先级不高于本运算符时,才能够去做本运算符要求的计算,或者说这是才能将本运算符送入后缀表达式中。准确地来说,遇到运算符 o o o时,应该把它与前面还没处理的运算符(假设是 o , o^, o,)比较,如果 o o o的优先级不高于 o , o^, o,,就输出 o , o^, o,,而后记录 o o o(无论做没做 o , o^, o,的运算)。
- 在整个过程中,总要拿当前运算符与前面最近的且尚未送走的运算符比较,而且可能删除前面的运算符(将括号也看做运算符)。
- 表达式中,左括号标明了一个优先计算的表达式的起点,需要记录,右括号说明优先计算的表达式应该到此为止。因此,在遇到右括号时,需要逐个弹出栈里的运算符,直至遇到左括号时也将其弹出。
- 当扫描完整个中缀表达式时,栈里可能还剩下一些运算符,它们的计算都应该进行,因此要把它们一一弹出送到后缀表达式中。
priority = {"(":1,"+":3,"-":3,"*":5,"/":5}
infix_operators = "+-*/()"
def trans_infix_suffix(line):
st = SStack()
exp = []
for x in tokens(line):
if x not in infix_operators:
exp.append(x)
elif st.is_empty() or x == "(":
st.push(x)
elif x == ")":
while (not st.is_empty() and st.top() != "("):
exp.append(st.pop())
if st.is_empty():
raise SyntaxError("Missing'('.")
st.pop()
else:
while not st.is_empty() and priority[x] <= priority[st.top()]:
exp.append(st.pop())
st.push(x)
while not st.is_empty():
if st.top() == "(":
raise SyntaxError("Extra'('.")
exp.append(st.pop())
return exp
def tokens(line):
i, llen = 0, len(line)
while i < llen:
while line[i].isspace():
i += 1
if i >= llen:
break
if line[i] in infix_operators:
yield line[i]
i += 1
continue
j = i + 1
while j < llen and not line[j].isspace() and line[j] not in infix_operators:
if ((line[j] == 'e' or line[j] == 'E') and j + 1 < llen and line[j + 1] == '-'):
j += 1
j += 1
yield line[i:j]
i = j
二、后缀表达式计算
计算规则:
- 遇到运算对象时,应该记录下来以备后面使用。
- 遇到运算符时,应该取出相关的对象并进行运算并保存计算结果。
#由于计算过程需要检查栈的深度,因此需要用继承的方式定义一个扩充的栈类
class ESStack(SStack):
def depth(self):
return len(self.elems)
def suf_exp_evaluator(exp):
operators = "+-*/"
st = ESStack()
for x in exp:
if x not in operators:
st.push(float(x))
continue
if st.depth() < 2:
raise SyntaxError("short of operand(s).")
a = st.pop()
b = st.pop()
if x == "+":
c = b + a
elif x == "-":
c = b - a
elif x == "*":
c = b * a
elif x == "/":
c = b / a
else:
break
st.push(c)
if st.depth() == 1:
return st.pop()
raise SyntaxError("Extra operand(s).")
二、队列
队列( q u e u e queue queue)也是一种容器,可存入元素、访问元素、删除元素。队列中也没有位置的概念,只支持默认方式的元素存入和取出,其特点是保证在任何时刻可访问或删除的元素都是在此之前最早存入队列而至今未删除的那个元素。因此,队列是一种 F I F O FIFO FIFO的结构。
一、队列抽象数据类型
ADT Queue:
Queue(self)
is_empty(self)
enqueue(self,elem) #入队
dequeue(self) #出队
peek(self) #查看队列里最早进入的元素,不删除
二、队列的线性表实现
带表尾指针的单链表,支持 O ( 1 ) O(1) O(1)时间的尾端插入操作,再加上表首端的高效访问和删除,因此非常适用于实现队列。
三、队列的顺序表实现
顺序表首端删除元素需要移动元素,操作的时间复杂度为 O ( n ) O(n) O(n),如果删除时不移动元素,那么表首端会慢慢变空,会出现假溢出的现象。所以,这两种简单的设计都不能够使人满意。那么就需要考虑新的想法:如果入队时队尾已经到达存储区末尾,应该考虑转到存储区开始的位置去入队新元素。
一、循环顺序表
将顺序表看成是一种环形结构,认为其最后存储位置之后是最前的位置,从而形成一个环形。当然,这种环形结构只是逻辑上的环,并非物理结构上的环。用循环顺序表实现的队列应该具有如下性质:
- 在队列使用中,顺序表的开始位置并不改变,例如可以用 q . e l e m s q.elems q.elems始终指向表元素区的开始。
- 队头变量 q . h e a d q.head q.head记录当前队列里第一个元素的位置;队尾变量 q . r e a r q.rear q.rear记录当前队列里最后一个元素之后的第一个空位。
- 队列元素保存在顺序表的一块连续的区域里,用 P y t h o n Python Python的写法是 [ q . h e a d : q . r e a r ] [q.head:q.rear] [q.head:q.rear]。两个变量的差(取模存储区长度)就是队列里元素的个数。
相关操作:
- 出队操作: q . h e a d = ( q . h e a d + 1 ) % q . l e n q.head = (q.head+1)\%q.len q.head=(q.head+1)%q.len
- 入队操作: q . r e a r = ( q . r e a r + 1 ) % q . l e n q.rear = (q.rear+1) \% q.len q.rear=(q.rear+1)%q.len
- 判断队列为空: q . h e a d = = q . r e a r q.head == q.rear q.head==q.rear
- 判断队满: ( q . r e a r + 1 ) % q . l e n = = q . h e a d (q.rear+1)\%q.len == q.head (q.rear+1)%q.len==q.head(说明:采用这种方法,将在表里留下一个不用的空位。这样做的目的是为了区分队空和队满。)
四、队列的 l i s t list list实现
要想用 l i s t list list实现队列,如果采用循环队列的想法,就要定义一个关于 l i s t list list的扩容,而不能使用 l i s t list list的自动扩容机制,原因有:
- 队列元素的存储方式与 l i s t list list的默认存储方式不一致。 l i s t list list的元素总总存储在存储区开始的位置,而队列中的元素可存在任何位置。如果 l i s t list list自动扩容,可能出现元素失控。
- l i s t list list没有提供检查存储区容量的机制,队列操作无法啊判断何时扩容。
一、基本设计
首先,队列可能为空而无法 d e q u e u e dequeue dequeue,为此定义一个异常。
class QueueUnderflow(ValueError):
pass
将定义的顺寻表队列命名为 S Q u e u e SQueue SQueue,其基本设计应该为:
- 在 S Q u e u e SQueue SQueue对象里用一个 l i s t list list类型的成分 e l e m s elems elems存放队列元素。
- 用 h e a d head head记录队列首元素位置所在下标,用 n u m num num记录表中元素个数。
- 用 P y t h o n Python Python中的 l i s t list list作为元素存储区,需要检查当前的表是否已满,必要时换一个存储表,因此需要用属性 l e n len len记录当前表的长度。
二、数据不变式
实现一种数据结构里的操作时,最基本的问题就是这些操作需要维护对象属性之间的正确关系,这样的一套关系被称为这种数据结构的不变关系式。数据不变式说明对象的不同属性的性质,描述它们应该满足的逻辑约束关系。
- 所有构造对象的操作,都必须把对象成分设置为满足数据不变式的状态。也就是说,对象的初始状态应该满足数据不变式。
- 每个对象的操作都应该保证不破坏数据不变式。也就是说,如果对一个状态完好的对象应用一个操作,该操作完成时,还必须保证对象处于完好的状态。
三、队列类的实现
class SQueue:
def __init__(self,init_len = 8):
self.len = init_len
self.elems = [0]*self.len
self.num = 0
self.head = 0
def is_empty(self):
return self.num == 0
def dequeue(self):
if self.is_empty():
raise QueueUnderflow("in SQueue.deque")
e = self.elems[self.head]
self.head = (self.head + 1) % self.len
self.num -= 1
return e
def peek(self):
if self.is_empty():
raise QueueUnderflow("in SQueue.peek")
return self.elems[self.head]
def enqueue(self,elem):
if self.num == self.len :
self.__extend()
self.elems[(self.head + self.num) % self.len] = elem
self.num += 1
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