1.堆栈基础知识
堆栈:简称为栈。一种线性表数据结构,是一种只允许在表的一端进行插入和删除操作的线性表。
把栈中允许插入和删除的一端称为 「栈顶(top)」;另一端则称为 「栈底(bottom)」。当表中没有任何数据元素时,称之为 「空栈」。
两种基本操作:「插入操作」 和 「删除操作」。
- 栈的插入操作又称为「入栈」或者「进栈」。
- 栈的删除操作又称为「出栈」或者「退栈」。
简单来说,栈是一种 「后进先出(Last In First Out)」 的线性表,简称为 「LIFO 结构」。
线性表:栈首先是一个线性表,栈中元素具有前驱后继的线性关系。栈中元素按照a1、a2…an 的次序依次进栈。栈顶元素为 。栈顶元素为an。
后进先出原则:根据堆栈的定义,每次删除的总是堆栈中当前的栈顶元素,即最后进入堆栈的元素。而在进栈时,最先进入堆栈的元素一定在栈底,最后进入堆栈的元素一定在栈顶。所以,元素进入堆栈或者退出退栈是按照【后进先出】的原则进行的。
1.1 堆栈的顺序存储与链式存储
栈有两种存储表示方法:顺序栈 和 链式栈
顺序栈:即堆栈的顺序存储结构。利用一组地址连续的存储单元依次存放自栈底到栈顶的元素,同时使用指针top指示栈顶元素在顺序栈中的位置。
链式栈: 即堆栈的链式存储结构。利用单链表的方式来实现堆栈。栈中元素按照插入顺序依次插入到链表的第一个节点之前,并使用栈顶指针 top 指示栈顶元素,top 永远指向链表的头节点位置。
1.2 堆栈的基本操作
栈作为一种线性表来说,理论上应该具备线性表所有的操作特性,但由于「后进先出」的特殊性,所以针对栈的操作进行了一些变化。尤其是插入操作和删除操作,改为了入栈(push)和出栈(pop)。
1.2.1 堆栈的顺序存储基本操作
基本操作描述(大概看下整体情况,不清楚地方结合实例)
假设 self.top 指向栈顶元素所在位置
初始化空栈:使用列表创建一个空栈,定义栈的大小 self.size,并令栈顶元素指针 self.top 指向 -1,即 self.top = -1。
判断栈是否为空:当 self.top == -1 时,说明堆栈为空,返回 True,否则返回 False。
获取栈顶元素:先判断队列是否为空,为空直接抛出异常。不为空则返回 self.top 指向的栈顶元素,即 self.stack[self.top]。
插入元素(进栈、入栈):先判断队列是否已满,已满直接抛出异常。如果队列未满,则在 self.stack 末尾插入新的数据元素,并令 self.top 向右移动 1 位。
删除元素(出栈、退栈):先判断队列是否为空,为空直接抛出异常。如果队列不为空,则令 self.top 向左移动 1 位,并返回 self.stack[self.top]。
代码
class Stack:
# 初始化空栈,大小为100
def __init__(self, size=100):
self.stack = []
self.size = size
self.top = -1
# 判断栈是否为空
def is_empty(self):
return self.top == -1
# 判断栈是否已满
def is_full(self):
return self.top + 1 == self.size
# 入栈操作
def push(self, value):
if self.is_full():
raise Exception('Stack is full')
else:
self.stack.append(value)
self.top += 1
# 出栈操作
def pop(self):
if self.is_empty():
raise Exception('Stack is empty')
else:
self.top -= 1
self.stack.pop()
# 获取栈顶元素
def peek(self):
if self.is_empty():
raise Exception('Stack is empty')
else:
return self.stack[self.top]
1.2.2 堆栈的链式存储基本操作
堆栈的顺序存储结构存在的问题:在栈满或者其他需要重新调整存储空间时需要移动大量元素。堆栈可以采用链式存储方式可以解决上述问题,在 Python 中我们通过构造链表节点 Node 的方式来实现。这种采用链式存储结构的堆栈也被称为 「链式栈」。
基本操作描述(大概看下整体情况,不清楚地方结合实例)
假设self.top指向栈顶元素所在位置
初始化空栈:使用列表创建一个空栈,并令栈顶元素指针 self.top 指向 None,即 self.top = None。
判断栈是否为空:当 self.top == None 时,说明堆栈为空,返回 True,否则返回 False。
获取栈顶元素:先判断队列是否为空,为空直接抛出异常。不为空则返回 self.top 指向的栈顶节点,即 self.top.value。
插入元素(进栈、入栈):创建值为 value 的链表节点,插入到链表头节点之前,并令栈顶指针 self.top 指向新的头节点。
删除元素(出栈、退栈):先判断队列是否为空,为空直接抛出异常。如果队列不为空,则令 self.top 链表移动 1 位,并返回 self.top.value。
代码
class Node:
def __init__(self, value):
self.value = value
self.next = None
class Stack:
# 初始化空栈
def __init__(self):
self.top = None
# 判断栈是否为空
def is_empty(self):
return self.top == None
# 入栈操作
def push(self, value):
cur = Node(value)
cur.next = self.top
self.top = cur
# 出栈操作
def pop(self):
if self.is_empty():
raise Exception('Stack is empty')
else:
cur = self.top
self.top = self.top.next
del cur
# 获取栈顶元素
def peek(self):
if self.is_empty():
raise Exception('Stack is empty')
else:
return self.top.value
1.3 堆栈的案例
堆栈的优势:
- 使用堆栈可以很方便的保存和取用信息,因此长被用作算法和程序中的辅助存储结构,临时保存信息,供后面操作中使用。
例如:操作系统中的函数调用栈,浏览器中的前进、后退功能。 - 堆栈的后进先出规则,可以保证特定的存取顺序
例如:翻转一组元素的顺序、铁路列车车辆调度。
1.3.1 题一:最小栈
部分过程
代码
class MinStack:
def __init__(self):
# stack元素栈 用于存储元素
self.stack = []
# min_stack辅助栈,与元素栈同步插入与删除,用于存储与每个元素对应的最小值
self.min_stack = [math.inf]
def push(self, x: int) -> None:
self.stack.append(x)
self.min_stack.append(min(x, self.min_stack[-1]))
def pop(self) -> None:
self.stack.pop()
self.min_stack.pop()
def top(self) -> int:
return self.stack[-1]
def getMin(self) -> int:
return self.min_stack[-1]
1.3.2 题二:有效的括号
思路
假如输入是 [{]},每种括号的左右数量分别相等,但不是有效的括号。这是因为结果还与括号的位置有关。
仔细分析我们发现,对于有效的括号,它的部分子表达式仍然是有效的括号,比如 {()[()]} 是一个有效的括号,()[{}] 是有效的括号,[()] 也是有效的括号。并且当我们每次删除一个最小的括号对时,我们会逐渐将括号删除完。比如下面的例子。
这个思考的过程其实就是栈的实现过程。因此我们考虑使用栈,当遇到匹配的最小括号对时,我们将这对括号从栈中删除(即出栈),如果最后栈为空,那么它是有效的括号,反之不是。
代码
class Solution:
def isValid(self, s: str) -> bool:
if len(s) % 2 == 1:
return False
pairs = {
")": "(",
"]": "[",
"}": "{",
}
stack = list()
for ch in s:
if ch