先进后出,后进先出,只允许在一端插入和删除数据。
第一次接触栈,会很疑惑,因为相对与数组和和链表,感觉栈给我们的只有限制,没任何优势。
其实,特定的数据结构是对特定场景的抽象,操作上越灵活,使用就越不可控,也就更容易出错。
当某种浏览器的前进后退,就可以使用栈这种数据结构来处理。
一、如何实现一个栈
栈主要包含2个操作,在栈顶插入和删除元素。栈既可以用数组实现,也可以用链表实现,数组实现的叫做顺序栈,链表实现的叫做链式栈。
以下使用数组实现一个顺序栈:
class ArrayStack {
private String[] items;
private int count;
private int n;
// 初始化数组,申请一个大小为n的数组空间
public ArrayStack(int n) {
this.items = new String[n];
this.n = n;
this.count = 0;
}
// 入栈操作
public boolean push(String item) {
// 数组空间不够了,直接返回false,入栈失败。
if (count == n) {
return false;
}
// 将item放到下标为count的位置,并且count加一
items[count] = item;
++count;
return true;
}
// 出栈操作
public String pop() {
// 栈为空,则直接返回null
if (count == 0) {
return null;
}
// 返回下标为count-1的数组元素,并且栈中元素个数count减一
String tmp = items[count-1];
--count;
return tmp;
}
}
时间复杂度:入栈和出栈都是O(1)
二、支持动态扩容的顺序栈
按之前的代码,原理就是,当栈满了之后,我们就申请一个更大的数组,把之前的数组元素复制到新的数组中。
时间复杂度:
对于出栈,复杂度是O(1)。对于入栈,如果没有满,那么也是O(1)。如果满了,那么就涉及到数据的迁移,就是O(n)。
按照之前复杂度分析中将的摊还分析法来分析:
先做一些假设:
- 每次扩容为原来的2倍
- 假设只有入栈没有出栈
- 不涉及扩容的入栈操作时间复杂度为O(1)
当前栈大小为k,k次O(1)操作之后,需要经过O(k)次操作扩容为原来的2倍,变为2k,还可以有k次O(1)操作,然后经过o(2k)次操作变为4K,接下来可以O(1)时间入栈2k次。
第一次费时O(K)操作后,可以使用O(1)时间k次,第二次费时O(2k)操作后,可以使用O(1)时间操作2k次。
那么我们将O(K)摊分到接下来的k次操作上,O(2K)摊分到2k操作上,以此类推。。。
总的时间复杂度就是就是O(1)
三、栈在函数调用中的应用
在软件工程中,函数的调用使用的就是栈。
操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈,比如:
int main() {
int a = 1;
int ret = 0;
int res = 0;
ret = add(3, 5);
res = a + ret;
printf("%d", res);
reuturn 0;
}
int add(int x, int y) {
int sum = 0;
sum = x + y;
return sum;
}
四、栈在表达式求值中的应用
我们再来看看栈的另一个常见的场景,编译器是如何利用栈来实现表达式求值的。这里我们只考虑加减乘除运算。
对于编译器来说,是利用2个栈来实现的,其中一个栈用来保存数字,另一个栈用来保存运算符。具体做法是:
我们从左往右遍历,遇到数字,我们直接压入保存数字的栈,遇到运算符,与保存运算符栈顶的符号比较。
如果当前运算符比栈顶的运算符优先级高,则将当前运算符压入栈中,如果比栈顶的运算符优先级低或者相同,则从栈顶取出操作符,并从数字栈中取出2个数字进行运算,计算后将数字压入数字栈,然后继续比较。知道遍历结束:
3+5*8-6如图:
五、栈在括号匹配中的应用
我们还能用栈来检测表达式中的括号是否匹配。也就是(),{}必须成对出现。(){},({})是合法的,({)}则是不合法的。
具体做法:我们用一个栈来存储左括号( { [
从左到右遍历字符串,如果遇到左括号,入栈,遇到右括号,则与栈顶元素比较,如果不相等或者栈为空,则括号不匹配。如果相等,那么弹出栈顶元素,继续比较。知道遍历完全,并且次吃栈为空,则表示括号匹配。
六、总结
栈只支持入栈和出栈2个操作。后进先出是他的特点。栈可以用数组实现,也可以用链表实现。