栈
栈的介绍:
补充:线性表:数据逻辑结构是线性的,但物理结构不一定是线性的,所以线性表有顺序表和链式表的区别。
Stack
- 栈是一个先入后出(FILO)的有序列表
- 栈是限制线性表中元素的出入和删除只能在线性表的同一端进行操作的一种特殊线性表,允许插入和删除的一端为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)
- 最先放入的元素在栈底,最后放入的元素在栈底。入栈push,出栈pop
栈的应用场景:
1.子程序的调用:在跳转到子程序的调用的时候,会将下一个指令存到堆栈中,直到子程序执行完后再将地址取出,回到原来的程序中
2.处理递归调用:在存储下一条指令的地址的时候,还存储了参数,区域变量等数据存入堆栈中
3.表达式的转换【中缀表达式转后缀表达式】与求值
4.二叉树的遍历
5.图形的深度优先遍历(depth-first)搜索法
用数组模拟栈代码实现:
这个比较简单,基本赏识知道就行
package stack;
public class ArrayStackDemo {
public static void main(String[] args) {
// TODO Auto-generated method stub
ArrayStack arrayStack = new ArrayStack(3);
arrayStack.push(1);
arrayStack.push(2);
arrayStack.push(3);
arrayStack.list();
}
}
//数组模拟栈
class ArrayStack {
public int size;
public int[] stack;
public int top=-1;
public ArrayStack(int size) {
this.size=size;
stack=new int[size];
}
//判断栈是否为空
public boolean isEmpty() {
return top==-1;
}
//判断栈是否满了
public boolean isFull() {
return top==size-1;
}
//添加数据到栈
public void push(int data) {
if(isFull()) {
System.out.println("栈已经满了,不能添加数据了");
return;}
top++;
stack[top]=data;
}
//出栈
public int pop() {
int data=0;
if(isEmpty()) {
System.out.println("栈是空的");
throw new RuntimeException();
}
data=stack[top];
top--;
return data;
}
//展示栈中的数据
public void list() {
if(isEmpty()) {
System.out.println("栈中没有数据");
return;
}
for(int i=top;i>=0;i--) {
System.out.println(stack[i]);
}
}
}
用链表来模拟栈:
思路:用的头插法,即新添加进来的元素是头,这样的话就很好将top指针和head指针用同一个表示,遍历和去数据的时候也很方便。
//链表来模拟栈
class ListStack{
public LinkNode top;
//栈空
public boolean isEmpty() {
return top==null;
}
//线性表可以不断的增加,因此无须判断栈满的情况
//入栈,采用头插法,这样链表头指针和栈的top指针,可以代表同一个意思,是不是很巧妙,否则,top指针可能并不需要
public void push(LinkNode node) {
//第一次插入栈底元素,不需要为top当前指定的元素指定一个节点,因为top此时为null,会报空指针异常的
if(isEmpty()) {
top=node;
}else {
//新添加的元素为头节点
node.next=top;
top=node;
}
}
//出栈
public int pop(LinkNode node) {
if(isEmpty()) {
throw new RuntimeException("当前栈内没有数据");
}
//头指针下移,要出栈的元素进行剔除
int data=top.no;
top.next=null;
top=top.next;
return data;
}
//展示栈内数据
public void list() {
if(isEmpty()) {
throw new RuntimeException("当前栈内没有数据");}
LinkNode cur=top;
while(cur!=null) {
System.out.println(cur.no);
cur=cur.next;
}
}
}
class LinkNode{
LinkNode(int no){
this.no=no;
}
public int no;
public LinkNode next;
}
栈的应用场景
- 计算表达式
思路:利用两个栈,一个用来存放数字,一个用来存放操作符,在入符号栈如果遇到当前操作符小于等于栈顶的元素,进行一次运算,即数字栈弹出两个数字,操作栈弹出一个操作符,再将结果入栈,但是因为栈是先进后出的,要注意- 和/的时候的顺序,思路挺简单,就是循环多,这里的表达式是一个中缀表达式:
package stack;
import java.util.Stack;
public class CalculateStack {
public static void main(String[] args) {
//数字栈
Stack numStack = new Stack();
//符号栈
Stack operStack=new Stack();
//定义一个表达式
String expression="300+2*6-1";
//遍历用的
int index=0;
int num1=0;
int num2=0;
char oper=0;
int result=0;
String numMore="";
//用来存放临时扫描的临时变量
char ch=' ';
while(true) {
ch=expression.substring(index, index+1).charAt(0);
if(isOper(ch)) {
//如果栈是空的
if(operStack.isEmpty()) {
operStack.push(ch);
}else{
//比较优先级
if(getPrority(ch)<=getPrority((char)operStack.peek())) {
num1 = (int)numStack.pop();
num2=(int)numStack.pop();
oper=(char)operStack.pop();
result = calculate(num1, num2, oper);
//将结果入栈,并且将当前遍历到的操作符也入栈
numStack.push(result);
operStack.push(ch);
}else {
operStack.push(ch);
}
}
}else {
numMore+=ch;
//如果ch是最后一位了,会引起数组越界异常
if(index==expression.length()-1) {
numStack.push(Integer.parseInt(numMore));
}else {
//如果是多位数的解决方案,看下一位是不是符号位,是的话,进行拼接,如果下一位还是数字,继续拼接,即继续执行numMore+=ch
if(isOper(expression.substring(index+1, index+2).charAt(0))) {
numStack.push(Integer.parseInt(numMore));
numMore="";
}
}
}
//index+1,判断是否扫描到最后
index++;
if(index>=expression.length()) {
break;
}
}
//栈内剩余的数据进行顺序操作
while(true) {
//如果符号栈为空,则计算到最后的结果了,数栈中只有一个数字,
if(operStack.isEmpty()) {
break;
}
num1=(int)numStack.pop();
num2=(int)numStack.pop();
oper=(char)operStack.pop();
result=calculate(num1, num2, oper);
numStack.push(result);
}
System.out.printf("表达式:"+expression+"=%d\n",numStack.pop());
}
//判断是不是运算符的优先级
public static int getPrority(char oper) {
if(oper=='*'||oper=='/') {
return 1;
}
else if(oper=='+'||oper=='-'){
return 0;
}
else {
return -1;
}
}
//判断是否是一个运算符
public static boolean isOper(char val) {
return val=='*'||val=='/'||val=='+'||val=='-';
}
//计算
public static int calculate(int num1,int num2,char oper) {
int res=0;//res用于存放计算的结果
switch(oper) {
case '+':
res=num1+num2;
break;
case '-':
res=num2-num1;
break;
case '*':
res=num1*num2;
break;
case '/':
res=num2/num1;
break;
default:
break;
}
return res;
}
}
逆波兰计算器:
后缀表达式:
从左到右扫描,遇到数字,讲数字压入堆栈,遇到运算符,弹出栈顶的两个数,用运算符对他们进行相应的运算,并讲结果入栈,重复过程直到到达表达式最右端。
package stack;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class PrefixExpression {
public static void main(String[] args) {
//给定波兰表达式(3+4)*5-6=>3 4 + 5 * 6 -
String suffixExpression="3 4 + 5 * 6 - ";
List<String>rpnlist=new ArrayList<String>();
String[] split = suffixExpression.split(" ");
for(String s:split) {
rpnlist.add(s);
}
System.out.println(caculate(rpnlist));
}
public static int caculate(List<String> list) {
String prefixExpression="";
Stack<String> stack=new Stack<String>();
for(String s:list) {
//如果匹配的是多位数,这里利用了正则,需要了解的看我的博客:https://blog.csdn.net/qq_45750255/article/details/108912638
if(s.matches("\\d+")) {
stack.push(s);
}else {
int num1 = Integer.parseInt(stack.pop());
int num2 = Integer.parseInt(stack.pop());
int res=0;
if(s.equals("+")) {
res=num1+num2;
}else if(s.equals("-")) {
res=num2-num1;
}else if(s.equals("*")) {
res=num1*num2;
}else if(s.equals("/")) {
res=num2/num1;
}else {
throw new RuntimeException("你输入的符号不正确");
}
//把res入栈
stack.push(""+res);
}
}
return Integer.parseInt(stack.pop());
}
}
中缀表达式转后缀表达式
思路:是死的,理解就好了,我认为就是把括号单独放出来作为一种情况来比较,几种操作符一种情况,遇到数是一种情况,借助中间栈和操作数栈不断地进行操作,直到操作数栈中没有数据。不要过于纠结原理,因为这是别人设计好的。
重点:1.初始化两个栈,运算符栈s1和存储中间结果的栈s2
2.从左至右扫描中缀表达式(记住了从左至右扫描)
3.遇到操作数时,讲其压s2;
4.遇到运算符时,比较其与s1栈顶运算符的优先级:
1.如果s1为空,或栈顶运算符为左括号"(",则直接将此运算符入栈
2.否则,若优先级比栈顶运算符的高,也讲运算符压入s1;
3.否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(4.1)与s1中新的栈顶运算符相比较;
5.遇到括号时:
1.如果是左括号"(",则直接压入s1
2.如果是右括号")",则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
6.重复步骤2~5,直到表达式的最右边
7.将s1中剩余的运算符依次弹出并压入s2
8.依次弹出s2中的并输出,结果的逆序即为中缀表达式对应的后缀表达式
普通表达式转中缀:
public static List<String> toInfixExpression(String s){
//定义一个List存放中缀表达式对应的内容
List<String>ls=new ArrayList<String>();
String str="";
char c;//存放每次遍历出来的字符的
for(int i=0;i<s.length();) {
//说明不是数字
if((c=s.charAt(i))<48||(c=s.charAt(i))>57){
ls.add(""+c);
i++;
}else {
//先将str置为空
str="";
while(i<s.length()&&(c=s.charAt(i))>=48&&(c=s.charAt(i))<=57) {
str+=c;
i++;
}
ls.add(str);
}
}
return ls;
}
中缀转后缀:
//中缀转后缀
public static List<String> toSuffixExpression(List<String> ls){
Stack<String> stack1=new Stack();//符号栈
//因为第二个栈根本没有出栈的操作,还要逆序输出使用ArrayList()就可以了
List<String> list1=new ArrayList<String>();
//遍历ls
for(String s:ls) {
//如果是一个数,就加入
if(s.matches("\\d+")) {
list1.add(s);
}else if(s.equals("(")){
stack1.push(s);
}else if(s.equals(")")) {
while(!stack1.peek().equals("(")) {
list1.add(stack1.pop());
}
stack1.pop();//弹出左括号
}
else {
//当s的优先级小于等于栈顶的优先级
while(stack1.size()!=0&&getPriority(stack1.peek())>=getPriority(s)) {
list1.add(stack1.pop());
}
//讲s压入栈中
stack1.push(s);
}
}
//将stack1中剩余的运算符加入list
while(stack1.size()>0) {
list1.add(stack1.pop());
}
return list1;
}
//比较优先级
public static int getPriority(String operation) {
int result=0;
switch(operation) {
case "+":
result=1;break;
case "-":
result=1;break;
case "*":
result=2;break;
case "/":
result=2;break;
default:
System.out.println("不存在该运算符");
break;
}
return result;
}
递归:
以前没学好,现在又学,递归就是开辟了一个新栈调用原来的自己嘛,这样理解豁然开朗
递归调用原则很重要:
1.当程序执行到一个方法时,就会开辟一个独立的空间(栈)
2.每个空间的数据(局部变量),是独立的
迷宫回朔问题:
真的很精妙,尤其是那个回朔,1,2,3的含义,不说了,上代码:
package recursion;
public class MiGongProblem {
public static void main(String[] args) {
// TODO Auto-generated method stub
int [][] map=new int[8][7];
for(int i=0;i<8;i++) {
map[i][0]=1;
map[i][6]=1;
}
for(int j=0;j<7;j++) {
map[0][j]=1;
map[7][j]=1;
}
map[3][1]=1;
map[3][2]=1;
getWay(map, 1, 1);
for(int i=0;i<8;i++) {
for(int j=0;j<7;j++) {
System.out.print(map[i][j]+" ");
}
System.out.println();
}
}
public static boolean getWay(int[][] map,int i,int j) {
//按照指定的策略去走,比如说下右上左
if(map[6][5]==2) {
//说明已经走通了
return true;
}else {
if(map[i][j]==0) {
//进行找路嘛,回朔,假定这条路能走
map[i][j]=2;
if(getWay(map,i+1,j)) {
return true;
}else if(getWay(map,i,j+1)) {
return true;
}else if(getWay(map,i-1,j)) {
return true;
}else if(getWay(map,i,j-1)) {
return true;
}else {
//说明假定失败,没有一个能够走通
map[i][j]=3;
return false;
}
}else {
//如果map[i][j]不为0,即为1,2,3
//1是挡板,2是走过了,3是走不通
return false;
}
}
}
}
八皇后问题:(非常经典)
虽然代码很简单,但是我却有点看不懂,我太笨了,又看了一下午
不会的原因:没有深入理解递归和回朔的意义,仔细一想,代码明白了。其实理解了真的很容易
public void check(int n){
if(n==maxsize){
print();
return;
}
for(int i=0;i<maxsize;i++){
array[n]=i;
if(judge(n)){
check(n+1);
}
}
}
//判断第n个皇后和第n-1个皇后是否有冲突
public boolean judge(int n){
for(int i=0;i<n;i++){
if(array[i]==n||(Math.abs(n-i)==Math.ads(array[n]-array[i])){
return false;
}
}
return true;
}
//print打印八个皇后的位置
public void print(){
for(int i=0;i<maxsize;i++){
System.out.printf(array[i]+" ");
}
System.out.println();
}
下面是完整的代码:
package recursion;
public class QueueEightProblem {
int maxsize=8;
public int array[]=new int[maxsize];
public static void main(String[] args) {
// TODO Auto-generated method stub
QueueEightProblem queue=new QueueEightProblem();
queue.check(0);
}
public void check(int n ) {
if(n==maxsize) {
print();
return;
}
//依次放入,并判断是否冲突,注意,这里是放的列了
for(int i=0;i<maxsize;i++) {
//先把这个皇后n,放到该行的第0列
array[n]=i;
//判断当前放置第n个皇后放到第i列时,是否冲突
if(judge(n)) {
//不冲突
//接着放n+1个皇后,开始递归
check(n+1);
}
}
}
public boolean judge(int n) {
for(int i=0;i<n;i++) {
//检查第n个皇后和第n-1个是否有冲突
if(array[i]==array[n]||Math.abs(n-i)==Math.abs(array[n]-array[i])) {
return false;
}
}
return true;
}
public void print(){
for(int i=0;i<array.length;i++) {
System.out.printf(array[i]+" ");
}
System.out.println();
}
}