2.1 什么是栈
有一种艺术叫做沙画。把沙子一层一层的叠在一起,从而形成美丽的图案。从图中可以看到,最底层的深蓝色沙子是最先放进去的。
继续放沙子,我们可以看到,最先放入的蓝色沙子在底部,而最后放入的紫色沙子在顶部。
如果把顶部的沙子取出,又是怎样的效果呢?
在此过程中,紫色沙子是最后放入的,却是最先取出的,继续取沙子,会观察到下面的情况:
蓝色沙子是最后取出的。只有一个出口,最先放入的元素(蓝色沙子)最后取出(FIRST IN LAST OUT 先进后出),最后放入的元素(紫色沙子)最先取出(LAST IN FIRST OUT 后进先出)的方式,这就是本章要讲的栈。
把沙子放入桶的过程,叫做入栈,把沙子从桶中取出的过程,叫做出栈。
2.2 栈有什么用
2.2.1 历史记录(步骤回退)
CTRL+Z是最常见的文本处理命令之一。甚至不止出现在文本软件上,有编辑作用的软件,例如图片处理(PS),视频剪辑(会声会影,PE)软件,通常也会有这个命令。简单格式的软件通常只支持一步回退动作,例如windows自带的txt文本文件notepad.exe。
在益智小游戏,比如推箱子,五子棋,围棋,井字棋中,往往会有个撤销按钮,这种时候,栈就可以派上用场了。下图是井字棋下棋,以及栈操作撤销的步骤:
从图中可以看到,撤销的过程和下棋的过程恰好是相反的,符合栈中“先入后出”的规律。
附代码:
public class Main {
public static void main(String[] args){
Main solution = new Main();
System.out.println("开始井字棋游戏!");
char[][] cs= new char[3][3];
for(int i = 0; i < 3; i++){
Arrays.fill(cs[i],'_');
}
solution.print(cs);
Stack<char[][]> stack = new Stack<>();
int type = 1;
Scanner scanner = new Scanner(System.in);
while(!solution.winOrLose(cs)){
if(type==1){
System.out.println("您现在是 X 棋");
}else{
System.out.println("您现在是 O 棋");
}
String s;
if(!stack.isEmpty()){
System.out.println("您需要进行什么操作?1 下棋 其他按键 撤销");
s = scanner.nextLine();
}else{
System.out.println();
s = "1";
}
if("1".equals(s)){
System.out.println("请输入下棋位置,用逗号分隔:");
int[] pos = new int[0];
String line = scanner.nextLine();
pos = solution.getPos(line);
while (pos.length==0){
System.out.println("您下棋的位置无效!请输入下棋位置,用逗号分隔:");
line = scanner.nextLine();
pos = solution.getPos(line);
}
//压栈
stack.push(solution.copy(cs));
cs[pos[0]][pos[1]] = type==1?'X':'O';
type = -type;
}else {
//弹出
cs = stack.pop();
type = -type;
}
solution.print(cs);
}
System.out.println("赢家是"+ solution.winner+"的下棋者");
}
private char[][] copy(char[][] origin){
char[][] copy = new char[3][3];
for(int i = 0; i < 3;i++){
for(int j = 0; j < 3; j++){
copy[i][j] = origin[i][j];
}
}
return copy;
}
char winner = ' ';
private boolean winOrLose(char[][] cs){
for(int i = 0; i < 3; i++){
if(cs[i][0]!='_'&&cs[i][0]==cs[i][1]&&cs[i][1]==cs[i][2]){
winner = cs[i][0];
return true;
}
}
for(int i = 0; i < 3; i++){
if(cs[0][i]!='_'&&cs[0][i]==cs[1][i]&&cs[1][i]==cs[2][i]){
winner = cs[0][i];
return true;
}
}
if(cs[0][0]!='_'&&cs[0][0]==cs[1][1] &&cs[1][1]==cs[2][2]){
winner = cs[0][0];
return true;
}
if(cs[0][2]!='_'&&cs[0][2]==cs[1][1] &&cs[1][1]==cs[2][0]){
winner = cs[0][2];
return true;
}
return false;
}
private int[] getPos(String str){
if(!str.contains(","))
return new int[0];
String[] strs = str.split(",");
if(isDigit(strs[0]) && isDigit(strs[1])){
int a = Integer.parseInt(strs[0]);
int b = Integer.parseInt(strs[1]);
if(a>=0&&a<3&&b>=0&&b<3)
return new int[]{a,b};
}
return new int[0];
}
private boolean isDigit(String s){
try{
int t = Integer.parseInt(s);
}catch (Exception e){
return false;
}
return true;
}
private void print(char[][] cs){
for(int i = 0; i < 3; i++){
for(int j = 0; j < 3;j++){
System.out.print(cs[i][j]+" ");
}System.out.println();
}
}
}
2.2.2 配对
在使用计算器,文本,输入代码过程中,如果括号不匹配,会立刻给出提示。例如:
我们忽略文本中的字母部分,仅保留括号,可得到内容: {{([]){(){}}
已知 ’{‘ 和 ’}‘ 是匹配的, ’[‘ 和 ’]‘ 是匹配的, '(' 和 ’)‘ 是匹配的。
在遇到右侧括号时,如果没有足够的左侧括号与其进行抵消,那说明是非法的;在遍历完所有的括号之后,如果左侧括号依然无法找到与之匹配的右侧括号,那么说明左侧括号是非法的。代码如下,此处暂不深究,后续将做进一步说明:
public class Main {
public static void main(String[] args) {
Main main = new Main();
System.out.println( main.checkCmp("public class Main{\n" +
"\tpublic static void main(String[] args){\n" +
"\t\tMain solution = new Main();{\n" +
"\t}\n" +
"}"));//多了一个左括号
System.out.println( main.checkCmp("public class Main{\n" +
"\tpublic static void main(String[] args){\n" +
"\t\tMain solution = new Main();\n" +
"\t}\n" +
"}"));//正确的
System.out.println( main.checkCmp("public class Main{\n" +
"\tpublic static void main(String[] args){\n" +
"\t\tMain solution = new Main();\n" +
"\t}\n" +
"}}"));//多了一个右括号
}
public boolean checkCmp(String str){
if(str == null || str.isEmpty())
return true;
int n = str.length();
Stack<Character> stack = new Stack<>();
for(int i = 0; i < n; i++){
char c = str.charAt(i);
//左括号入栈
if(c == '{' || c == '[' || c == '('){
stack.push(c);
//右括号出栈
}else if(c == '}' || c == ']' || c ==')'){
if(stack.isEmpty())
return false;
char p = stack.pop();
if(c=='}' && p !='{')
return false;
if(c == ']' && p != '[')
return false;
if(c == ')' && p != '(')
return false;
}
}
return stack.isEmpty();
}
}
2.2.3 计算器
计算机最早的用途就是用于计算数值的。以一段简单的
为例。当遇到左括号的时候,把前面的累计结果对数字栈进行入栈 [2] ,符号栈也进行入栈 [x] ,算完括号的
的结果后,把两个栈的结果弹出拿到 2 和符号 x ,然后用拿到的数字和符合,和后面计算过的数字进行运算,得到
。
2.2.4 递归
比如下面这段程序,是一段递归。完成了从 start 到 end 的累加过程。
public class Main {
public static void main(String[] args){
Main solution = new Main();
System.out.println(solution.getSum(1,3));
}
private int getSum(int start, int end){
if(start>end)
return dfs(end,start);
return dfs(start,end);
}
private int dfs(int start, int end){
if(start == end)
return start;
return start+dfs(start+1,end);
}
}
下图,是计算机实际的执行过程:
2.3 栈的操作
栈顶:栈最顶部的元素叫做 栈顶,指向栈顶元素的指针,叫做 栈顶指针。
出栈:把栈顶指针的元素取出的过程,把栈顶指针向下移动的过程,叫做 出栈。
入栈:把元素放入栈顶,并将栈顶指针向下移动的过程,叫做 入栈。
查看栈顶元素:查看栈顶指针对应的元素内容,这一过程只查看不进行出栈入栈操作。
栈空:栈中没有元素,栈顶指针指向 NULL,无法进行出栈操作
栈满:栈中数据已满,无法再容纳新的元素,无法进行入栈操作
这样描述呢比较抽象,下面用比较形象的方式再进行一次叙述。
2.3.1 概念解读
一串糖葫芦最上面的那颗,我们下一颗可以吃掉的糖葫芦,就是栈顶。它的特点是在顶部,是下一颗准备吃掉的目标。
最后一颗能吃掉的糖葫芦就是 栈底。
吃掉糖葫芦之前需要用自己铜铃般水汪汪的大眼睛盯着,防止把糖葫芦吃到鼻子里。这个“盯”,就是 栈顶指针 啦!
入栈 和 出栈 在本章开头已经有了较为详尽的解释,此处不再赘述。
栈满 呢,就是刚拿到这串糖葫芦的时候,它上面的尖头已经不足以再串一个新的糖葫芦了,换句话说,就是串糖葫芦的竹签不够长度再串一颗新的糖葫芦了。
栈空,就是把这串糖葫芦吃完了,没有新的糖葫芦可以吃,也没有任何一颗糖葫芦需要你继续盯着。
2.3.2 栈的缺点
- 操作受限
栈只能做出查看 栈顶、入栈、出栈 三种操作,而且仅能对栈顶元素进行操作,实际能做的非常有限,需要一些特定场景才能起到作用。
- 递归层次有限
如图所示,递归调用不到10000层就发生了内存溢出。
2.4 单调栈
栈内的元素按照从栈底到栈顶的顺序,所有元素满足单调递增或者单调递减或单调非递增,单调非递减时,可以称之为单调栈。
2.4.1 什么时候可以用单调栈
事实上,在写这篇文章之前,我也不知道什么是单调栈。但是既然决定了把单调栈作为书写内容之一,就有责任有义务把这个东西它什么时候能用具体的进行描述。只有这样,单调栈这样的神级工具,才能在合适的位置发挥最大作用。
如果一个问题可以转化为,找到(上一个/下一个)(大于/小于)【等于】当前值或者对应的索引,就可以求出答案,那么单调栈就是你的最佳帮手。
如果是上一个,则从前往后遍历。
如果是下一个,则从后往前遍历。
其中等于这个条件可要可不要。
2.4.2 单调栈实践
1475. 商品折扣后的最终价格
浏览了众多有关单调栈的题目,力扣的这一题,感觉特别合适作为单调栈的讲解题目。
为了方便读者阅读,本篇文章内也引用一下题目内容:
给你一个数组 prices ,其中 prices[i] 是商店里第 i 件商品的价格。
商店里正在进行促销活动,如果你要买第 i 件商品,那么你可以得到与 prices[j] 相等的折扣,其中 j 是满足 j > i 且 prices[j] <= prices[i] 的 最小下标 ,如果没有满足条件的 j ,你将没有任何折扣。
请你返回一个数组,数组中第 i 个元素是折扣后你购买商品 i 最终需要支付的价格。
首先进行一波题目解析:
输入:数组 prices,每个元素代表价格
输出:数组 ans,每个元素代表折扣后你购买商品 i 最终需要支付的价格
条件:
1. 商品可以得到折扣
2. 优惠价格是在当前索引后面的位置查找到第一个小于等于它的价格作为折扣
3. 没有折扣的情况,折扣为0
4. 实际支付金额=原价-折扣
这题可以转化为:找到下一个小于等于当前价格的值,用当前的价格找到下一个比它大的价格即为答案。
思路:
1. 根据前面给的解题方式,描述时使用了 下一个,因此从后往前遍历
2. 应为非递增栈,从后往前遍历,假设 j1<=j2,且 prices[j1] 的值更小,那么对于任意一个比 j1 的索引更前的索引i,必然选择 j1 的值,那么栈中所有比它大的元素都应出栈
喜欢的话,点个赞吧~!平时做题,以及笔记内容将更新到公众号。
关注公众号,互相学习:钰娘娘知识汇总