Chapter03 基本数据结构——栈篇
因为这一章内容有点多,所以拆分一下,这里就先把栈讲了。
然后最近还在写硕士论文,就可能更新没那么快,见谅~~~
什么叫线性数据结构
栈、队列、双端队列和列表都是有序的数据集合,其元素的顺序取决于添加顺序或移除顺序。一旦某个元素被添加进来,它与前后元素的相对元素将保持不变。这样的数据集合被称为线性数据结构
线性数据结构可以看作有两端。这两端有时候被称作“左端”和“右端”,有时候也被称作“前端”和“后端”。真正区分线性数据结构的是元素的添加方式和移除方式,尤其是添加操作和移除操作发生的位置。举例来说,某个数据结构可能只允许在一端添加新元素,有些则允许从任意一端移除元素。
栈
栈的原理
它是有序集合,添加和移除操作只会发生在一端,“顶端”。
栈中的元素离底端越近,代表其在栈中的时间越长,因此栈的底端具有非常重要的意义。最新添加的元素将被最先移除。这种排序原则被称作 LIFO (last-in first-out),即后进先出。它提供了一种基于在集合中的时间来排序的方式。最近添加的元素靠近顶端,旧元素则靠近底端。
栈抽象数据类型由下面的结构和操作定义。如前所述,栈是元素的有序集合,添加操作与移除操作都发生在其顶端。栈的操作顺序是LIFO,它支持以下操作。
-
Stack() 创建一个空栈。它不需要参数,且会返回一个空栈。
-
push(item) 将一个元素添加到栈的顶端。它需要一个参数 item ,且无返回值。
-
pop() 将栈顶端的元素移除。它不需要参数,但会返回顶端的元素,并且修改栈的内容。
-
peek() 返回栈顶端的元素,但是并不移除该元素。它不需要参数,也不会修改栈的内容。
-
isEmpty() 检查栈是否为空。它不需要参数,且会返回一个布尔值。
-
size() 返回栈中元素的数目。它不需要参数,且会返回一个整数。
栈的实现
这个可以通过python的列表自己实现,因为在python中,pop()和append()的操作都是O(1)的。注意pop和append只有对最后一个操作才是O(1),其他是O(n),具体python内部实现的复杂度,看上一篇的结尾的链接。
class Stack():
def __init__(self) -> None:
self.item = []
def push(self, item):
self.item.append(item)
def pop(self):
return self.item.pop()
def peek(self):
return self.item[-1]
def size(self):
return len(self.item)
def isEmpty(self):
return self.item == []
实际上,在python内部有queue模块,有栈的实现,但是这里由于复杂度都是O(1),就直接自己定义了,下面是使用pythononds实现的,其实直接使用我上面的类就可以了,或者使用queue中的queue.LifoQueue实现,有些区别,我后面讲队列的时候用这个。
栈的应用——匹配括号
需求:编写一个算法,它从左到右读取一个括号串,然后判断其中的括号是否匹配。
实现原理:当出现右边括号时,必须现在前面有对应的左括号,否则就不是匹配的。
具体实现如下:(和书上不一致)
from pythonds.basic import Stack
def match(inputs):
"""
这个函数实现括号匹配
"""
s = Stack()
for item in inputs:
# 如果是右括号则栈中则必须为对应的左括号
if item == ']':
if not s.isEmpty(): # 若s里面是空的则s.pop会直接崩溃
check = s.pop()
if check != '[':
return False
else:
return False
elif item == ')':
if not s.isEmpty():
check = s.pop()
if check != '(':
return False
else:
return False
elif item =='}':
if not s.isEmpty():
check = s.pop()
if check != '{':
return False
else:
return False
else: #左括号添加元素
s.push(item)
else:
# 匹配完成之后左括号应该全是空的了
if s.isEmpty():
return True
else:
return False
if __name__ == '__main__':
test_input_1 = '{{([][])}()}'
test_input_2 = '[[{{(())}}]]'
test_input_3 = '[][][](){}'
test_input_4 = '([)]'
test_input_5 = '((()]))'
test_input_6 = '[{()]'
print(match(test_input_1))
print(match(test_input_2))
print(match(test_input_3))
print(match(test_input_4))
print(match(test_input_5))
print(match(test_input_6))
这个小例子说明,栈在实际应用中是十分重要的。
栈的应用——进制转换
需求:编写一个算法,实现十进制转换成其他进制
实现原理:除余,用栈去记录结果,最后将栈进行结果拼接
实现如下:(这里16进制的话其实有问题,没有考虑使用abc去表示10等数字)
from pythonds.basic import Stack
def int_trans(num, format=2):
"""
这个函数实现十进制转成'format'进制
"""
s = Stack()
# # 为了while循环判断,先额外进行一次
# s.push(num % format) # 将余数进行记录
# num = num // format
while num != 0:
s.push(num % format)
num = num // format
result = ''.join([str(s.pop()) for i in range(s.size())])
return result
if __name__ == '__main__':
test = 244
print(bin(test))
print(int_trans(test))
print(oct(test))
print(int_trans(test, format=8))
栈的应用——前序、中序和后序表达式
运算符出现在两个操作数的中间 , 所以这种表达式被称作中序表达式,如A + B * C。这种表达式人一看就懂,但是机器是不懂的,因为他不知道要*先运算然后在通过+,所以在具体实现时,会采用添加括号的方法(A + (B * C)),这种被称为完全括号表达式,显然当运算数据变大时,机器语言表达中序表达式是非常复杂的。
前序表达式是将运算符号写在数据的前面,例如刚才的就可以重新写成+A*BC。同样可以采用将运算符号写在数据后面后序表达式ABC*+来表示刚才的数据。
转换的原理:
- 首先把算式变完全括号表达式,如A+B*C变成(A+(B*C))。
- 转变 (A+(B*C)) ----> A(B*C)+ ------> ABC*+
解释一下,其实后序表达式就是把右括号用符号代替,前序表达式就是左括号代替。括号的位置其实就是运算符的最终位置
举一个复杂的例子来看一下
将(A+B)*C-(D-E)*(F+G)写成后序表达式 —> ((A+B)*C)-((D-E)*(F+G))) —>AB+C*DE-FG+*-
我们在计算时的顺序正好为后序表达式中的符号从做到右的顺序顺序+*-+*-。
运行顺序为:
- A+B
- (A+B) * C
- D-E
- F+G
- (D-E)*(F+G)
- (A+B)*C-(D-E)*(F+G)
下面举一些互相转化的例子方便理解并确保掌握:
中序表达式 | 前序表达式 | 后序表达式 |
---|---|---|
A+B*C+D | ++A*BCD | ABC*+D+ |
(A+B)*(C+D) | *+AB+CD | AB+CD+* |
A*B+C*D | +*AB*CD | AB*CD*+ |
A+B+C+D | +++ABCD | AB+C+D+ |
上述的转化说明了,中序、前序以及后序存在一定的关系,如此,我们便可以通过算法来实现这个功能了。
需求:将任意中序表达式转换成后序表达式的算法
方法规则
- 准备两个栈,一个栈(action)存运算符号,一个栈(number)存数字
- 若是数字直接放进number中
- 若是符号,如果action为空,则存入action,若不为空,判断栈顶端与当前符号的运算优先级,若优先级高于顶端,则存入action,否则将action的顶端拿出来放进number,当前的操作放进action中
- 如果遇到“(”,则直接放进action,因为括号优先级最高,若遇到“)”,将action中的拿出来放进number直到“(”
- 中序表达式结束后,将action的存入number
代码实现(与书上出入比较大)
from pythonds.basic import Stack
import string
def test(expr):
# 先存一个运算符优先级的字典
prec = {}
prec['*'] = 3
prec['/'] = 3
prec['+'] = 2
prec['-'] = 2
prec['('] = 1 # 这个设置起来是为了下面判断栈内的和当前的比较 因为栈内会存( 如果这个没有优先级会报错
action = Stack()
number = Stack()
for token in expr:
if token in string.ascii_uppercase: # 大写字母
number.push(token)
elif token == '(':
action.push(token)
elif token == ')':
temp = action.peek() # 查看一下 不能直接使用pop
while temp != '(':
number.push(action.pop())
temp = action.peek()
elif token in prec.keys(): # 操作运算符
# 如果是空,则直接压入
if action.isEmpty():
action.push(token)
else: #如果不是空就要判断当前和栈内的优先级了
temp = action.peek()
if temp == '(':
action.push(token)
elif prec[token] > prec[temp]:
# 当前的优先级大
action.push(token)
else:
# 如果不是的话,就要把里面小于等于当前的拿光
while prec[token] <= prec[temp] and not action.isEmpty():
# 因为(的优先级很小 可能会被直接拿掉 会导致结果存在( 所以要过滤掉
current = action.pop()
if current != '(':
number.push(current)
action.push(token)
while action.size() != 0: # 中序表达结束后把action放入number
temp = action.pop()
if temp != '(':
number.push(temp)
return ''.join(number.items)
if __name__ == '__main__':
print(test("(A+B) * (C+D)"))
print(test('(A+B)*C'))
print(test("A+B*C"))
print(test('(A+B)*C-(D-E)*(F+G)'))
print(test('A+B*C+D'))
print(test('(A+B)*(C+D)'))
print(test('A*B+C*D'))
print(test('A+B+C+D'))
这里举了八个例子,应该比较全面了,结果都是对的,代码应该没问题了