一、栈
定义:当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,我们就应该首选“栈”这种数据结构。
用数组实现的栈,叫顺序栈;用链表实现的栈,叫链式栈。
顺序栈的实现:
// 基于数组实现的顺序栈
public 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 ( 1 ) O(1) O(1)。
二、栈的动态扩容
当栈满后需要动态扩容,申请一块更大的内存,再将原来的数据搬移到新数组中。
动态扩容的时间复杂度分析
出栈:
O
(
1
)
O(1)
O(1)。
入栈:
- 当栈有空闲空间时,入栈操作的时间复杂度为 O ( 1 ) O(1) O(1),为最好时间复杂度;
- 当空间不够时,要申请内存和数据搬移,此时时间复杂度为 O ( n ) O(n) O(n),为最坏时间复杂度;
- 均摊时间复杂度:
假设栈空间不够时,重新申请原来大小两倍的数组,每一步时间复杂度画图如下:
k次入栈操作,总共涉及k个数据的搬移,以及k次的简单入栈操作,平均下来,每个入栈操作只需要一个数据搬移和一次简单入栈操作,均摊时间复杂度为 O ( 1 ) O(1) O(1)。
三、应用
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. 浏览器前进和后退功能应用
使用两个栈X,Y实现。首次浏览的页面一次压入X,点击后退按钮时从X取数据,一次压入Y;
当X栈中没有数据时,说明没有页面可以继续后退浏览了;当Y栈中没有数据时,说明没有页面可以继续前进浏览了。
当后退后查看新页面时,页面c无法通过前进、后退按钮重复查看,所以需要清空栈Y;即X中压入非Y栈中元素时,清空Y。