该部分为学习笔记,具体内容详见:《数据结构(Python语言描述)》一书
第4章 栈
栈是操作受限的线性表,是最简单的数据结构之一,在程序设计和计算机系统中,栈都是非常重要的数据结构。数据的逆置、网页浏览时的后退以及文本编辑器中的撤销等功能的实现,都可借助栈来完成。
第一节 栈的基本概念
栈是一种特殊的线性表,它的特殊性在于操作受到了限制,只允许在序列的尾端进行插入和删除,而不像普通线性表那样可以在任意合法位置进行插入和删除。栈中允许插入、删除的一端称为栈顶,另一端为固定端,称为栈底。元素的插入称为入栈或进栈,元素的删除称为出栈或退栈。因此,栈也被称为后进先出表。
第二节 栈的抽象数据类型
T 类型元素构成的栈是由 T 类型元素构成的有限序列,并且具有以下基本操作:
- 创建一个空栈(__init__)
- 判断栈是否为空(empty)
- 求栈的长度,即栈中元素的个数(__len__)
- 入栈一个元素(push)
- 出栈一个元素(pop)
- 读取栈顶元素(get_top)
from abc import ABCMeta, abstractmethod
# 抽象栈类
class AbstractStack(metaclass = ABCMeta):
# 创建一个空栈
@abstractmethod
def __init__(self):
# 判断栈是否为空
@abstractmethod
def empty(self):
# 求栈的长度
@abstractmethod
def __len__(self):
# 入栈一个元素
@abstractmethod
def push(self):
# 出栈一个元素
@abstractmethod
def pop(self):
# 读取栈顶元素
@abstractmethod
def get_top(self):
第三节 栈的顺序存储及实现
栈的顺序存储方案将栈中从栈底和栈顶的所有元素依次存储在一块连续的存储空间中,简称顺序栈。
一、利用 Python 列表实现
# 抽象栈类的一种具体实现
class ArrayStack(AbstractStack):
def __init__(self):
super().__init__()
self._entry = []
def empty(self):
return self._entry == []
def __len__(self):
return len(self._entry)
def push(self, item):
self._entry.append(item)
def pop(self):
if self.empty():
return None
return self._entry.pop()
def get_top(self):
if self.empty():
return None
return self._entry[-1]
二、记录容量和栈顶位置的实现
# 定义 DynamicArrayStack 类来实现上述顺序栈
class DynamicArrayStack(AbstractStack):
INCREMENT = 10 # 栈的容量增量
def __init__(self, init_size = 0):
super().__init__()
self._capacity = init_size
# 生成容量为 _capacity 的列表
self._entry = [None for x in range(0, self._capacity)]
self._top = -1
def empty(self):
return self._top == -1
def __len__(self):
return self._top + 1
def push(self, item):
if self._top >= self._capacity - 1:
self._resize()
self._top += 1
self._entry[self._top] = item
def _resize(self):
self._capacity += self.INCREMENT
temp = [None for x in range(0, self._capacity)]
for i in range(0, self._capacity - self.INCREMENT):
temp[i] = self._entry[i]
self._entry = temp
def pop(self):
if self.empty():
raise Exception("栈为空")
else:
item = self._entry[self._top]
self._top -= 1
return item
def get_top(self):
if self.empty():
raise Exception("栈为空")
else:
return self._entry[self._top]
第四节 栈的链式存储及实现
- 链栈结点类
class StackNode:
def __init__(self, data, link = None):
self.entry = data
self.next = link
- 链栈类
class LinkedStack:
def __init__(self):
self._top = None
def empty(self):
return self._top is None
def __len__(self):
p = self._top
count = 0
while p:
count += 1
p = p.next
return count
def push(self, item):
new_top = StackNode(item, self._top)
self._top = new_top
def pop(self):
if self.empty():
return None
else:
old_top = self._top
self._top = self._top.next
item = old_top.entry
del old_top
return item
def get_top(self):
if self.empty():
return None
else:
return self._top.entry
第五节 栈的典型应用
一、括号匹配检验
语法检查程序在对源代码进行语法检查时,判断其中的符号是否正确匹配是一个基本环节,假设文本字符串中允许有 3 种符号——圆括号、方括号和花括号哦,具体的相关算法如下。
- 算法思想
- 初始化空栈 st
- 从左到右依次读入文本字符串中的每个字符,假设当前读到的符号为 pr。若 pr 为左括号,则入栈 st。若 pr 为右括号:检查栈 st 是否为空。若 st 为空,则表明该右括号多余,给出不匹配结论,返回。出栈 st 的栈顶左括号,将 pr 和该栈顶左括号进行配对。
- 文本字符串中的所有符号全部读完时:若栈 st 非空,表明左括号是有多余,给出不匹配结论。否则,给出匹配结论。
- 括号匹配函数
根据以上算法思想,结合 Python 语言的特点,用字典来表示括号之间的配对关系,通过调用生成器函数 parentheses 依次获得 text 串中的括号及其位置。以下 bracket_match 算法完成括号的匹配检查,并在不匹配时给出具体原因和位置。
# 括号匹配函数
def bracket_match(text):
open_pares = {'(', '[', '{'}
opposite = {")": "(", "]": "[", "}": "{"}
st = LinkedStack()
for pr, i in parentheses(text):
if pr in open_pares:
st.push((pr, i))
elif st.empty():
print("多余右括号,位置", i, "对应括号为", pr)
return False
else:
prepr, j = st.pop()
if prepr != opposite[pr]:
print("发现不匹配位置", j, i, "对应括号为", prepr, pr)
return False
if not st.empty():
prepr, j = st.pop()
print("多余左括号,位置为", j, "括号为", prepr)
return False
else:
print("所有括号全部配对")
return True
- 括号生成器函数
# 括号生成器函数
def parentheses(text):
pares = {'(', ')', '[', ']', '{', '}'}
i, text_len = 0, len(text)
for i in range(0, text_len):
if text[i] in pares:
yield text[i], i
- 应用示例
# 应用示例
if __name__ == "__main__":
text = "{3 * [A + (b * cd)]}"
print(text)
bracket_match(text)
print("==============================")
text = "3 * A + (b * cd] + {5 * a}"
print(text)
bracket_match(text)
二、计算后缀表达式的值
- 表达式的 3 种形式
- 中缀表达式:运算符位于它所运算的操作数中间
- 后缀表达式:运算符紧接着其运算的两个操作数出现
- 前缀表达式:运算符位于它所运算的两个操作数之前。
- 后缀表达式求值算法的思想
从左到右扫描后缀表达式,可依次分离出其中的操作数和运算符。如果分离出的是操作数,由于对该操作数执行的运算还未知,所以暂时将它存储起来;如果分离出的是运算符,则应取出最近保存的两个操作数,进行该运算符对应的运算,并存储运算结果。满足后进先出的原则,所以适合用栈存储操作数。如后缀表达式:1 2 4 * + 5 - = 4 - 后缀表达式求值算法
operator = {'+', '-', '*', '/', '#'}
def postfix(postfix_ex):
st = ArrayStack()
for m in tokens(postfix_ex):
if m not in operator:
st.push(float(m))
else:
if m == '#':
return st.pop()
opnd2 = st.pop()
opnd1 = st.pop()
op = m
result = 0
if op == '+':
result = opnd1 + opnd2
elif op == '-':
result = opnd1 - opnd2
elif op == '*':
result = opnd1 * opnd2
elif op == '/':
result = opnd1 / opnd2
st.push(result)
- 生成器函数
def tokens(text):
i, t_len = 0, len(text)
while i < t_len:
if text[i].isspace():
i += 1
elif text[i] in operator:
yield text[i]
i += 1
else:
j = i + 1
while j < t_len and not text[j].isspace() and text[j] not in operator:
if (text[j] == 'e' or text[j] == 'E') and j + 1 < t_len and text[j + 1] == '-':
j += 1
j += 1
yield text[i:j]
i = j
yield '#' # 串读完时生成结束符
- 应用示例
if __name__ == "__main__":
postfix_ex = "1 2 4 * + 5 -"
print(postfix_ex, '===>', postfix(postfix_ex))
postfix_ex = "1 2 3.1 * + 5e-2/"
print(postfix_ex, '===>', postfix(postfix_ex))
三、计算中缀表达式的值
中缀表达式转换为后缀表达式
- 算符的优先级
算符 | *、/ | +、- | ( | ) | # |
---|---|---|---|---|---|
优先级数值 | 4 | 3 | 2 | 2 | 1 |
- 算法思想
为简化起见,假设表达式一定是合法的中缀形式。
(1)初始化空栈 st 用于存放算符,初始存入 “#”;初始化空列表 exp 用于存放后缀表达式。
(2)循环(外层)从左到右依次扫描中缀表达式,根据所读到的逻辑符号 m 的不同情况分别处理:
① 若 m 是操作数,则将其直接加到 exp 的末尾,继续循环;
② 若 m 是左括号,左括号入栈,继续循环;
③ 读取栈顶的符号,设为 theta1,将当前符号 m 设为 theta2;
④ 循环(内层)比较 theta2 与栈顶 theta1 的优先级数值大小,直至 theta1 和 theta2 都为 “#”。
(3)返回得到的后缀表达式列表 exp - 算法实现
operator = {'+', '-', '*', '/', '(', ')', '#'} # 算符集合
priority = {'*': 4, '/': 4, '+': 3, '-': 3, '(': 2, ')': 2, '#': 1} # 算符优先级字典
def trans_infix_suffix(infix_ex):
st = ArrayStack()
st.push('#')
exp = []
for m in tokens(infix_ex):
if m not in operator:
exp.append(m)
elif m == '(':
st.push(m)
else:
theta1 = st.get_top()
theta2 = m
while theta1 != '#' or theta2 != '#':
if priority[theta1] < priority[theta2]:
st.push(theta2)
break
elif theta1 == "(" and theta2 == ")":
st.pop()
break
else:
st.pop()
exp.append(theta1)
theta1 = st.get_top()
return exp
def tokens(text):
i, t_len = 0, len(text)
while i < t_len:
if text[i].isspace():
i += 1
elif text[i] in operator:
yield text[i]
i += 1
else:
j = i + 1
while j < t_len and not text[j].isspace() and text[j] not in operator:
if (text[j] == 'e' or text[j] == 'E') and j + 1 < t_len and text[j + 1] == '-':
j += 1
j += 1
yield text[i:j]
i = j
yield '#' # 串读完时生成结束符
def postfix2(postfix_ex):
st = ArrayStack()
postfix_ex.append('#')
for m in postfix_ex:
if m not in operator:
st.push(float(m))
else:
if m == '#':
return st.pop()
opnd2 = st.pop()
opnd1 = st.pop()
op = m
result = 0
if op == '+':
result = opnd1 + opnd2
elif op == '-':
result = opnd1 - opnd2
elif op == '*':
result = opnd1 * opnd2
elif op == '/':
result = opnd1 / opnd2
st.push(result)
- 应用示例
if __name__ == "__main__":
infix_ex1 = "5 + 6 * (1 + 2) - 4"
infix_ex2 = "(3 + 5 * 1e2/40) * 2"
post_lst = trans_infix_suffix(infix_ex1)
print(infix_ex1, "=>", post_lst, end=" ")
print(" => ", postfix2(post_lst))
post_lst = trans_infix_suffix(infix_ex2)
print(infix_ex2, "=>", post_lst, end=" ")
print(" => ", postfix2(post_lst))
四、迷宫求解
迷宫求解是一类常见的智力游戏,也是许多实际问题的抽象,具有实用意义。例如,在公路网上查找可行或最优的路线、电子地图中的路径检索等。
- 迷宫的存储表示
迷宫可以看作一个二维的矩阵,假设用一个二维的列表存储迷宫,设为 maze,设 maze[i][j]==0 时表示该坐标位置可以通行。另外,为了防止在某位置上重复绕圈,需要对已走过的位置设置标记,如设 maze[i][j] = 2 表示该位置已经走过。 为处理方便,通常在迷宫的周围加一层围墙,并全部设置为不可通行,这样就可以将所有位置的邻居数统一为 4 个。 - 用回溯法求解迷宫
求解迷宫路径的一种基本方法是从入口位置开始探索,按照东、西、南、北的优先顺序探索到一个可通的邻居位置,则移动到该邻居位置继续探索,直到找到出口;如果当前位置无可通邻居或其他邻居都已经走过,则用回溯法退回到上一个位置,从该位置的其他方向邻居开始搜索。 - 算法步骤
- 初始化当前位置 position 为入口位置,如果 Position 不通,给出相应提示并结束。
- 初始化 st 栈,用于存放走过的可通位置坐标及该位置的下一个可探索方向,并设置当前位置 position 的当前探索方向 nextDirection 为 0。
- 当 position 不是出口位置时循环(外层)执行
- 迷宫求解算法
根据上述思想,设计 Maze 的核心算法 findRouteByStack 用于搜索当前迷宫从入口到出口的路径,并将路径存放在 st 栈中;它调用 updatePosition 方法,根据坐标位置的不同状态做不同颜色和大小的圆点标记, isPassable 方法用于判断对应坐标位置是否可通且未走过。
def findRouteByStack(self):
position = self.startPosition
if self[position[0]][position[1]] == OBSTACLE:
print("入口不通")
return
st = ArrayStack()
nextDirection = 0
while not self.isExit(position):
self.updatePosition(position[0], position[1], TRIED)
for i in range(nextDirection, 4):
nextPosition = (position[0] + DIRECTIONS[i][0], position[1] + DIRECTIONS[i][1])
if self.isPassable(nextPosition):
st.push((position, i + 1))
position = nextPosition
nextDirection = 0
break
else:
self.updatePosition(position[0], position[1], DEAD_END)
if not st.empty():
position, nextDirection = st.pop()
else:
print("没有找到通过迷路的路径")
return False
st.push((position, 0))
while not st.empty():
position = st.pop()[0]
self.updatePosition(position[0], position[1], PATH_PART)
self.wn.exitonclick()
return
- 迷宫求解的可视化实现
class Maze:
def __init__(self, source, start, end):
rowsInMaze = len(source)
columnsInMaze = len(source[0])
self.mazeList = [[None for j in range(columnsInMaze)] for i in range(rowsInMaze)]
for i in range(rowsInMaze):
for j in range(columnsInMaze):
self.mazeList[i][j] = source[i][j]
self.startPosition = start
self.endPosition = end
self.rowsInMaze = rowsInMaze
self.columnsInMaze = columnsInMaze
self.t = turtle.Turtle()
self.t.shape('turtle')
self.wn = turtle.Screen()
self.wn.setup(800, 800)
self.wn.setworldcoordinates(0, self.rowsInMaze, self.columnsInMaze, 0)
def updatePosition(self, row, col, val):
self.mazeList[row][col] = val
self.moveTurtle(col, row)
size = 10
if val == PATH_PART:
color = 'gray'
size = 15
elif val == OBSTACLE:
color = 'red'
elif val == DEAD_END:
color = 'red'
elif val == TRIED:
color = 'black'
self.drawDot(color, size)
def moveTurtle(self, x, y):
self.t.down()
self.t.setheading(self.t.towards(x + 0.5, y + 0.5))
self.t.speed(5)
self.t.goto(x + 0.5, y + 0.5)
def drawDot(self, color, size = 10):
self.t.dot(size, color)
def isExit(self, position):
return position == self.endPosition
def isPassable(self, position):
return self.mazeList[position[0]][position[1]] == PASSABLE
def __getitem__(self, idx):
return self.mazeList[idx]
def drawMaze(self):
self.t.speed(100)
for y in range(self.rowsInMaze):
for x in range(self.columnsInMaze):
if self.mazeList[y][x] == OBSTACLE:
self.drawCenteredBox(x, y, 'red')
self.t.color('black')
self.t.fillcolor('blue')
def drawCenteredBox(self, x, y, color):
self.t.up()
self.t.goto(x, y)
self.t.color(color)
self.t.fillcolor(color)
self.t.down()
self.t.begin_fill()
for i in range(4):
self.t.setheading(90 * i)
self.t.forward(1)
self.t.end_fill()
def gotoStart(self, x, y):
self.t.up()
self.t.hideturtle()
self.t.setposition(y + 0.5, x + 0.5)
self.t.showturtle()
DIRECTIONS = [(0, 1), (1, 0), (0, -1), (-1, 0)]
PASSABLE = 0
OBSTACLE = 1
TRIED = 2
DEAD_END = 1
PATH_PART = 0
- 运行示例
if __name__ == "__main__":
maze = [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1],
[1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1],
[1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1],
[1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1],
[1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1],
[1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1],
[1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1],
[1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
]
start = (1, 4)
end = (10, 10)
myMaze = Maze(maze, start, end)
myMaze.drawMaze()
myMaze.gotoStart(start[0], start[1])
myMaze.findRouteByStack()
第六节 上机实验
一、栈的实现和应用
- 实现顺序栈类
- 实现链栈类
- 设计测试程序验证所设计的顺序栈类和链栈类的正确性
- 假如你是一场棒球比赛的记分员。你拿到的比赛数据是一个字符串列表,其中每个字符串可能是以下 4 种情形之一。
(1)整数:直接表示该轮的分数
(2)“+”:表示在这一轮得到的分数是最后两轮有效分数的总和
(3)“D”:表示这一轮中获得的分数是上一轮有效分数的两倍
(4)“C”:表示之前最后一轮的分数是无效的,应该被删除。
设计程序,给出所有轮次的总得分
二、HTML 文档标签匹配
超文本标记语言中标签的匹配问题与括号匹配问题类似。以下是一个非常简单的 HTML 文件。编写一个程序,用于检查 HTML 文档中的开始和结束标签是否匹配。
<html>
<head>
<title>example</tiitle>
</head>
<body>
<center>
<h3> Welcome! </h3>
<font size = 10>
stay hungry, stay foolish!
</font>
</center>
</body>
</html>
三、表达式求值
(1)输入一个以浮点数为操作数的后缀表达式,并计算该表达式的值。例如:输入为 3.5 20 4 + /* 5 /,计算结果应为 16.8。要求:单步显示运算过程中栈的状态,当表达式输入错误时给出错误提示。
(2)输入一个以浮点数为操作数的中缀表达式,并计算该表达式的值。如:3.5 /* (20 + 4) - 20 / 4,则程序的运行结果应为 79。
四、四色地图着色
对于行政区划图,可以用不多于 4 种的颜色进行着色区分,使相邻行政区域不重色。对任意输入的行政区划图验证这一定理。提示: n 个行政区域之间的相邻关系可以用关系矩阵 r[n][n] 来表示。如果两个区域相邻,则 r[i][j] = r[j][i] = 1,反之,等于 0 。
五、用回溯法求解 n 皇后问题
假设有 n 个皇后,如果将所有 n 个皇后放置到 n * n 的棋盘上,使得没有两个皇后在同一行,没有两各皇后在同一列,并且没有两个皇后在同一对角线上?例如:当 n 为 6 时,六皇后问题共有 4 个解。