数据结构:栈 讲解
一、概念:
后进者先出,先进者后出,这就是典型的“栈”结构。
从栈的操作特性上来看,栈是一种操作受限
的线性表,只允许在一端插入和删除数据。
数组或链表确实可以替代栈。
特定的数据结构是对特定场景的抽象,而且,数组或链表暴露了太多的操作接口,操作上的确灵活自由,但使用时就比较不可控,自然也就更容易出错。
二、栈的实现:
栈主要包含两个操作,入栈和出栈,也就是在栈顶插入一个数据和从栈顶删除一个数据
。
栈可以用数组来实现,也可以用链表来实现。
- 用数组实现的栈,我们叫作顺序栈。
- 用链表实现的栈,我们叫作链式栈。
1.基于数组实现顺序栈:
class Stack(object):
"""用列表, 实现顺序栈"""
def __init__(self):
# Python中,list列表就是顺序表
self.__list = []
def push(self, item):
"""压栈:加入元素"""
self.__list.append(item)
def pop(self):
"""弹出元素"""
return self.__list.pop()
def peek(self):
"""返回栈顶元素"""
if self.is_empty():
return None
else:
return self.__list[-1]
def is_empty(self):
"""判断是否为空"""
return not self.__list
def length(self):
"""返回栈的长度"""
return len(self.__list)
if __name__ == '__main__':
stack = Stack()
print(stack.is_empty())
stack.push(1)
stack.push(2)
stack.push(3)
stack.push(4)
stack.push(5)
print(stack.pop())
print(stack.pop())
print(stack.peek())
print(stack.is_empty())
print(stack.length())
# 输出结果:
True # 表示栈中为空
5 # 出栈
4 # 出战
3 # 栈顶元素
False # 栈中不为空
3 # 栈中的元素
2.基于链表实现链式栈:
class Node(object):
"""链表中的节点"""
def __init__(self, data=None):
self.data = data
self.next = None
class LKStack(object):
"""链式栈"""
def __init__(self):
self.top = Node(None)
self.count = 0
def get_length(self):
"""获取栈的数量"""
return self.count
def get_top(self):
"""返回栈顶的元素"""
return self.top.data
def is_empty(self):
"""判断是否为空"""
return self.count == 0
def push(self, elem):
"""进栈操作"""
tmp = Node(elem)
if self.is_empty():
self.top = tmp
else:
tmp.next = self.top
self.top = tmp
self.count += 1
def pop(self):
"""出栈操作"""
if self.is_empty():
raise IndexError("Stack is empty!")
else:
self.count -= 1
elem = self.top.data
self.top = self.top.next
return elem
def show_stack(self):
"""从栈顶显示每个节点的值"""
if self.is_empty():
raise IndexError("Stack is empty!")
else:
j = self.count
tmp = self.top
while j > 0 and tmp:
print(tmp.data)
tmp = tmp.next
j -= 1
if __name__ == '__main__':
lks = LKStack()
for i in range(1, 5):
lks.push(i)
lks.show_stack()
lks.pop()
lks.show_stack()
3.总结:
- 不管是顺序栈还是链式栈,我们存储数据只需要一个大小为 n 的数组就够了。
- 在入栈和出栈过程中,只需要一两个临时变量存储空间,所以
空间复杂度是 O(1)
。 - 无论顺序栈还是链式栈,
入栈、出栈
只涉及栈顶个别数据的操作,所以时间复杂度都是 O(1)
。
空间复杂度:
这里存储数据需要一个大小为 n 的数组,并不是说空间复杂度就是 O(n)。因为,这 n 个空间是必须的,无法省掉。所以我们说空间复杂度的时候,是指除了原本的数据存储空间外,算法运行还需要额外的存储空间。
三、支持动态扩容的顺序栈:
数组实现的栈,是一个固定大小的栈
,也就是说,在初始化栈时需要事先指定栈的大小
。当栈满之后,就无法再往栈里添加数据了。尽管链式栈的大小不受限,但要存储 next 指针
,内存消耗
相对较多。
要实现一个支持动态扩容的栈,我们需底层依赖一个支持动态扩容的数组就可以了。当栈满了之后,我们就申请一个更大的数组,将原来的数据搬移到新数组中。
如图所示:
分析支持动态扩容的顺序栈的入栈、出栈操作时间复杂度:
- 出栈操作来说,不会涉及内存的重新申请和数据的搬移,所以出栈的时间复杂度仍然是O(1)。
- 入栈操作,当栈中
有空闲空间时
,入栈操作的时间复杂度为 O(1)。但当空间不够
时,就需要重新申请内存和数据搬移,所以时间复杂度就变成了O(n)。
四、栈的使用场景:
1.函数调用栈:
操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成栈这种结构, 用来存储函数调用时的临时变量
。
每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。
举个栗子:
def main():
a = 1
ret = 0
result = add(3, 5)
res = a + result
return res
def add(x, y):
sum = 0
sum = x + y
return sum
ret = main()
print(ret) # 输出结果 9
main()函数调用了 add()函数,获取结果,并且与临时变量a相加,最后返回res 的值。
分析这个过程对应的函数栈里的出栈、入栈操作,图示。在执行add()函数时,函数调用栈的情况。
2.栈在表达式求值:
编译器如何利用栈来实现表达式求值。
- 编译器就是通过两个栈来实现的;
- 一个保存
操作数的栈
,另一个是保存运算符的栈
; - 我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。
- 如果比运算符栈顶元素的优先级高,就将当前运算符压入栈;
- 如果比运算符栈顶元素的优先级
低或者相同
,从运算符栈中取栈顶运算符,从操作数栈的栈顶取 2 个操作数
,然后进行计算,再把计算完的结果压入操作数栈,继续比较。
将 3 + 5 * 8 - 6 表达式的计算过程如图所示:
3.栈在括号匹配中应用:
我们还可以借助栈来检查表达式中的括号是否匹配。
比如:表达式中只包含三种括号,圆括号 ()、方括号 [] 和花括号{},并且它们可以任意嵌套。
比如,{[] ()[{}]}或 [{()}([])] 等都为合法格式,而{[}()] 或 [({)] 为不合法的格式。
用栈来解决:
- 用栈来保存未匹配的左括号,从左到右依次扫描字符串。
- 当扫描到左括号时,则将其压入栈中;
- 当扫描到右括号时,从栈顶取出一个左括号。
- 如果扫描的过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。
- 当所有的括号都扫描完成之后,如果栈为空,则说明字符串为合法格式;否则,说明有未匹配的左括号,为非法格式。
比如,{[“A”] (“B”)[{“C”}]};
五、如何实现浏览器前进、后退功能?
用两个栈就可以解决这个问题。
- 使用两个栈,X、Y,【首次浏览】的页面依次压入栈 X,点击【后退按钮】,依次从栈 X 中 出栈,并将出栈的数据依次放入栈 Y 。
- 点击【前进按钮】,在依次从栈 Y 中 取出数据,放入栈 X 中。
- 当栈 X 中没有数据时,那就说明没有页面可以继续后退浏览了。
- 当栈 Y 中没有数据,那就说明没有页面可以点击前进按钮浏览了。
举个栗子:
-
你顺序查看了 a,b,c 三个页面,我们就依次把 a,b,c 压入栈,这个时候,两个栈的数据就是这个样子:
-
当你通过浏览器的后退按钮,从页面 c 后退到页面 a 之后,我们就依次把 c 和 b 从栈 X 中弹出,并且依次放入到栈 Y。这个时候,两个栈的数据就是这个样子:
-
又想看页面 b,于是你又点击前进按钮回到 b 页面,我们就把 b 再从栈 Y 中出栈,放入栈 X 中。此时两个栈的数据是这个样子:
-
这个时候,你通过页面 b 又跳转到新的页面 d 了,页面 c 就无法再通过前进、后退按钮重复查看了,所以需要清空栈 Y。此时两个栈的数据这个样子:
参考资料:
《数据结构与算法之美》