后进者先出,先进者后出,这就是典型的“栈”结构。
栈是一种“操作受限”的线性表,只允许在一端插入和删除数据
从功能上来说,数组或链表确实可以替代栈,但特定的数据结构是对特定场景的抽象,而且,数组或链表暴露了太多的操作接口,操作上的确灵活自由,但使用时就比较不可控,自然也就更容易出错。
当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,就应该首选“栈”这种数据结构。
栈主要包含两个操作:
栈既可以用数组来实现,也可以用链表来实现。
不管是顺序栈还是链式栈,存储数据只需要一个大小为 n
的数组就够了。
O(1)
。O(1)
。注意,存储数据需要一个大小为
n
的数组,并不是说空间复杂度就是O(n)
。因为,这n
个空间是必须的,无法省掉。所以说空间复杂度的时候,是指除了原本的数据存储空间外,算法运行还需要额外的存储空间。
next
指针,内存消耗相对较多数组动态扩容:当数组空间不够时,我们就重新申请一块更大的内存,将原来数组中数据统统拷贝过去。
如果要实现一个支持动态扩容的栈,只需要底层依赖一个支持动态扩容的数组就可以了。实际上,支持动态扩容的顺序栈,平时开发中并不常用到。此时出栈的时间复杂度是O(1)
,入栈的时间复杂度分为有空间O(1)
和需要扩容O(n)
,入栈的均摊时间复杂度为O(1)
。
栈比较经典的一个应用场景就是函数调用栈。
操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。
func main() {
a, ret, res := 1, 0, 0
ret = add(3, 5)
res = a + ret
fmt.Printf("%d", res)
}
func add(x, y int) int {
sum := 0
sum = x + y
return sum
}
上面代码mian()
函数调用add()
函数,获取计算结果,并且与临时变量a
相加,最后打印res
的值。在执行add()
函数时,函数调用栈的情况如下图:
编译器通过两个栈实现表达式求值,其中一个栈保存操作数,另一个栈保存运算符。
如下示例为计算3+5*8-6
:
假设表达式中只包含三种括号:
()
[]
{}
它们可以任意嵌套。
比如:
{[] ()[{}]}
或[{()}([])]
等{[}()]
或[({)]
等用栈来判断一个包含三种括号的表达式字符串是否合法。
(
”跟“)
”匹配,“[
”跟“]
”匹配,“{
”跟“}
”匹配,则继续扫描剩下的字符串。使用两个栈实现浏览器中前进后退的功能,假设两个栈分别为X
和Y
。
X
X
中出栈,并将出栈的数据依次压入栈Y
中Y
中出栈,并将出栈的数据依次压入栈X
中X
中没有数据,说明没有可以继续后退浏览的页面Y
中没有数据,说明没有可以继续前进浏览的页面