后进者先出,先进者后出,这就是典型的“栈”结构。
栈是一种“操作受限”的线性表,只允许在一端插入和删除数据
从功能上来说,数组或链表确实可以替代栈,但特定的数据结构是对特定场景的抽象,而且,数组或链表暴露了太多的操作接口,操作上的确灵活自由,但使用时就比较不可控,自然也就更容易出错。
当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,就应该首选“栈”这种数据结构。
栈主要包含两个操作:
栈既可以用数组来实现,也可以用链表来实现。
不管是顺序栈还是链式栈,存储数据只需要一个大小为 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。
XX中出栈,并将出栈的数据依次压入栈Y中Y中出栈,并将出栈的数据依次压入栈X中X中没有数据,说明没有可以继续后退浏览的页面Y中没有数据,说明没有可以继续前进浏览的页面