栈
后进者先出,先进者后出,这就是典型的“栈”结构。栈是一种“操作受限”的线性表,只允许在一端插入和删除数据。
为什么要使用到“栈”这种操作受限的数据结构?
事实上,从功能上来说,数组或链表确实可以替代栈,但你要知道,特定的数据结构是对特定场景的抽象,而且,数组或链表暴露了太多的操作接口,操作上的确灵活自由,但使用时就比较不可控,自然也就更容易出错。
当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,这时我们就应该首选“栈”这种数据结构。
如何实现一个栈?
- 栈既可以用数组来实现,也可以用链表来实现。
- 用数组实现的栈,我们叫作顺序栈,
- 用链表实现的栈,我们叫作链式栈
顺序栈的实现:
// 基于数组实现的顺序栈
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() {// 栈为空,则直接返回nullif (count == 0) return null;// 返回下标为count-1的数组元素,并且栈中元素个数count减一String tmp = items[count-1];--count;return tmp;}
}
栈操作的复杂度分析
存储数据只需要一个大小为 n 的数组就够了。在入栈和出栈过程中,只需要一两个临时变量存储空间,所以空间复杂度是 O(1);
入栈、出栈只涉及栈顶个别数据的操作,所以时间复杂度都是 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;
}
编译器通过栈实现求值表达式的求值
四则运算法则:34+13*9+44-12/3
编译器就是通过两个栈来实现的。其中一个保存操作数的栈,另一个是保存运算符的栈。我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较;如果比运算符栈顶元素的优先级高,就将当前运算符压入栈;如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取 2 个操作数,然后进行计算,再把计算完的结果压入操作数栈,继续比较;
如何实现浏览器的后退和前进功能?
- 定义两个栈X和Y,将首次浏览的页面压入栈X,当点击后退按钮时,再依次从栈X中出栈,并将出栈的依次压入栈Y,当点击前进时,就从Y中取出数据放入X中;
- 当X中没有数据时,表示页面不可后退,当Y中没有数据时表示页面不可前进;
- 当跳转了新的页面,新的页面压入栈X中,同时需要清空栈Y,也即当前页面不可以前进了;