目录
栈有一个特点,先进后出,或者说,后进先出
有一个很经典的面试题,用栈可以比较容易解决,那就是判断括号是不是一对,比如
{[()]}这样就是一对,{{[)}}不是一对
这个题,可以用正则啊,可以拆分匹配等等,其实都是可以的,但是不如栈方便
再比如,做一个历史记录功能,最贴切的,就是浏览器的后退功能,也完全可以用栈实现,点击一步,那就入栈,后退,那就出栈,当然,后退这个地方也需要用一个栈记录,毕竟浏览器有前进有后退,可以来回切换
理解栈的结构,其实也很简单,我们进行一个类比,你现在有很多书,摞起来,你放的时候肯定不是放最下面,是放最上面,你拿的时候,也是从最上面开始拿,不是最下面,这也就是上面说的站点特征,后进先出,last in first out,简称LIFO
栈,其实就是仅仅在表尾进行插入和删除操作的线性表,这一端被称为栈顶,相对的,另一边就是栈底
往一个栈里插入元素,称为进栈、压栈、入栈,它会把新元素放在栈顶元素的上面,成为新的栈顶元素
从一个栈里删除元素,称为出栈、退栈,它会把栈顶元素删除,相邻的元素成为新的栈顶元素
栈,其实就是一个特殊的数组或者链表
既然栈是一个特殊的数组或者链表,那为什么还要用栈,栈还有这么些个限制
仔细想想,其实栈的限制也是一种保护,限制了只能操作这个范围,而不能操作其他的
栈分为两种,基于数组的栈和基于链表的栈
基于数组的栈,通常以数组头为栈底,数组头到数组尾的方向为栈顶的延展方向
基于单链表的栈,通常以链表头为栈顶,这样方便节点的插入和删除,压栈产生的节点一直出现在链表头
这两种实现方式最大的区别,就是扩容,基于链表,那就有链表的特性,意味着天然可以扩容,而数组不行,并且因为是从头访问,所以数组和链表的访问复杂度没区别
但是实际在使用中,我们用更多的还是数组实现的,链表的实现存在栈溢出的风险
因为栈有多种实现,我们先写一个接口
-
- /**
- * @author create by YanXi on 2022/9/3 11:00
- * @email best.you@icloud.com
- * @Description: 数组实现的栈
- */
- public class AyyayStack implements MyStack{
-
- // 要用数组实现,一定有一个数组
- private I[] stack = (I[]) new Object[1];;
- // 元素个数
- private int itemSize = 0;
-
- public AyyayStack(int initSize) {
- if (0 > initSize) {
- this.stack = (I[]) new Object[initSize];
- }
- }
-
- /**
- * @Description: 入栈
- * @Author: create by YanXi on 2022/9/4 14:36
- * @Email: best.you@icloud.com
- *
- * @param item 插入的参数
- * @exception
- * @Return: void
- */
- @Override
- public void push(I item) {
- // 判断是否需要扩容
- if (stack.length <= itemSize) {
- resize(itemSize * 2);
- }
- stack[itemSize++] = item;
- }
-
- /**
- * @Description: 扩容
- * @Author: create by YanXi on 2022/9/4 14:36
- * @Email: best.you@icloud.com
- *
- * @param size
- * @exception
- * @Return: void
- */
- public void resize(int size) {
- I[] tmp = (I[]) new Object[size];
- for (int i = 0; i < itemSize; i++) {
- tmp[i] = stack[i];
- }
- stack = tmp;
- }
-
- /**
- * @Description: 出栈
- * @Author: create by YanXi on 2022/9/4 14:36
- * @Email: best.you@icloud.com
- *
- * @param
- * @exception
- * @Return: I
- */
- @Override
- public I pop() {
- if (isEmpty()) {
- return null;
- }
- // 注意,是 --size,不是size--,一定要先减去在用,因为入栈的时候加了
- I item = stack[--itemSize];
- stack[itemSize] = null;
- // 判断是否需要缩减容量
- if (itemSize > 0 && itemSize <= stack.length >> 1){
- resize(stack.length >> 1);
- }
- return item;
- }
-
- /**
- * @Description: 获取栈大小
- * @Author: create by YanXi on 2022/9/4 14:37
- * @Email: best.you@icloud.com
- *
- * @param
- * @exception
- * @Return: int
- */
- @Override
- public int size() {
- return itemSize;
- }
-
- /**
- * @Description: 栈是否为空
- * @Author: create by YanXi on 2022/9/4 14:37
- * @Email: best.you@icloud.com
- *
- * @param
- * @exception
- * @Return: boolean
- */
- @Override
- public boolean isEmpty() {
- return itemSize == 0;
- }
-
- }
看一个算法题,也是我们提到的括号匹配问题,是力扣的原题哦~
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。
我们用栈来实现一下,就用我们自己实现的栈来实现
-
- /**
- * @author create by YanXi on 2022/9/6 6:18
- * @email best.you@icloud.com
- * @Description: 括号匹配测试
- */
- public class StackTest {
-
- public static void main(String[] args) {
- String need = "(){}[]";
- System.out.println(String.format("isMatch(\"%s\") = %s" , need, isMatch(need)));
- String need1 = "[(){}[]";
- System.out.println(String.format("isMatch(\"%s\") = %s" , need1, isMatch(need1)));
- }
-
- public static boolean isMatch(String reg) {
- AyyayStack
stack = new AyyayStack<>(15); - char[] chars = reg.toCharArray();
- Character pop;
-
- for (char aChar : chars) {
- switch (aChar){
- case '(':
- case '[':
- case '{':
- stack.push(aChar);
- break;
- case ')':
- if (stack.isEmpty()) {
- return false;
- }
- pop = stack.pop();
- if (null == pop) {
- return false;
- } else if ('(' == (pop.charValue())) {
- break;
- } else {
- return false;
- }
- case ']':
- if (stack.isEmpty()) {
- return false;
- }
- pop = stack.pop();
- if (null == pop) {
- return false;
- } else if ('[' == (pop.charValue())) {
- break;
- } else {
- return false;
- }
- case '}':
- if (stack.isEmpty()) {
- return false;
- }
- pop = stack.pop();
- if (null == pop) {
- return false;
- } else if ('{' == (pop.charValue())) {
- break;
- } else {
- return false;
- }
- default:
- return false;
- }
- }
- return stack.isEmpty();
- }
-
- }
时间复杂度为O(n)
想一想,哪些地方和栈类似
最常用的,是不是方法调用,比如A方法调用了B,然后再调用了C,是不是A方法是最后结束的?
在数学上,计算是不是也是这样,比如 1 + 2*2 - 4/2
所以说,也可以用栈实现一个简易的计算器
创建两个栈,一个存数字,一个存运算符
只要是数字,就入栈,如果是运算符,分为两种情况
1、当前运算符和运算符栈顶运算符比较,如果比栈顶运算符的优先级高,那就直接入栈
2、当前运算符和运算符栈顶运算符比较,如果比栈顶运算符的优先级低或者相同,不入栈,并且数字栈出栈两个,这两个数字按照当前运算符运算,运算完成,再将数字结果和后续符号入栈
仔细想想,是这么个道理不?代入上面写的例子
1,入数字栈
+,入符号栈
2,入数字栈
*,优先级比+高,入符号栈
2,入数字栈
-,比*优先级低,不入栈,取出数字栈两个,也就是2-2,执行,2*2=4,4入数字栈,-入符号栈
往后的类似
是不是?
我们上面还说过,用栈也可以实现浏览器的前进和后退功能,如何实现呢?
假设浏览网页A,点击B,再点击C,点击D
可以每次点击都入栈,后退的时候,入另一个栈
比如我现在在C,栈1应该是ABC,我现在点击后退,C从栈1挪到栈2,页面就到了B
当我在B,点击前进,栈2出栈,C
当然,还有一个现象,那就是在B页面,直接点击D,D返回不是C,而是B,所以,当点击了新的地方, 就应该清空栈2
说白了, 就是用两个栈,来严格维护网页的前面和后面
这就是栈大概的总结,栈是一个很神奇的结构,也是一个很好用的结构,用的好的话,能给我们一些场景简化很多,带来很多便利