一,如何理解“栈”
关于“栈”,有一个非常贴切的例子,就是一摞叠在一起的盘子。我们平时放盘子的时候,都是从下往上一个一个放;取的时候,我们也是从上往下一个一个地 依次取,不能从中间任意抽出。后进者先出,先进者后出,这就是典型的“栈”结构。
从栈的操作特性上看,栈是一种“操作受限”的线性表,只允许一个在插入和删除数据。
二,如何实现一个“栈”
栈的注意操作包含两个操作,入栈和出栈,即在栈顶插入/删除一个数据。栈用数组实现叫做顺序栈,用链表实现叫做链式栈。其时间复杂度和空间复杂度都是,需要说明的是空间复杂度并不是指原本的数据存储空间,而是算法运行还需要额外的存储空间。
三,支持动态扩容的顺序栈
刚才那个基于数组实现的栈,是一个固定大小的栈,也就是说,在初始化栈时需要事先指定栈的大小。当栈满之后,就无法再往栈里添加数据了。尽管链式栈的 大小不受限,但要存储next指针,内存消耗相对较多。所以,如果要实现一个支持动态扩容的栈,我们只需要底层依赖一个支持动态扩容的数组就可以了。当栈满了之后,我们就申请一个更大的数组,将原来的数据 搬移到新数组中。如果当前栈大小为K,并且已满,当再有新的数据要入栈时,就需要重新申请2倍大小的内存,并且做K个数据的搬移操作,然后再入栈。
实际上,支持动态扩容的顺序栈,我们平时开发中并不常用到。支持动态扩容的顺序栈其时间复杂度如图:
也印证了前面讲到的,均摊时间复杂度一般都等于最好情况时间复杂度。因为在大部分情况下,入栈操作的时间复杂度O都是,只 有在个别时刻才会退化为O(n),所以把耗时多的入栈操作的时间均摊到其他入栈操作上,平均情况下的耗时就接近
。
四,栈在函数调用中的应用
栈作为一个比较基础的数据结构,应用场景还是蛮多的。其中,比较经典的一个应用 场景就是函数调用栈。 操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构,用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。例如如下代码:
int main(){
int a=1;
int ret=0;
int res=0;
ret=add(3,5);
res=a+ret;
return 0;
}
int add(int x, int y){
int sum=0;
sum=x+y;
return sum;
}
为了清晰地看到这个过程对应的函数栈里出栈、入栈的操作,如下图。图中显示的是,在执行到add()函数时,函数调用栈的情况:
五,栈在表达式求值中的应用
另一个常见的应用场景,编译器利用栈来实现表达式的求值。比如:34+13*9+44-12/3。实际上,编译器就是通过两个栈来实现的。其中一个保存操作数的栈,另一个保存运算符的栈。从左向右遍历表达式,当遇到数字,我们就直接压入操作 数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。 如果比运算符栈顶元素的优先级高,就将当前运算符压入栈;如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取2个操作数,然后进行计算,再把计算完的结果压入操作数栈,继续比较。 我将3+5*8-6这个表达式的计算过程画成了一张图来理解。