一 栈
1.1 栈的一个实际需求
请输入一个表达式
计算式:[722-5+1-5+3-3] 点击计算
1.2 栈的基本原理
- 基本概念
(1)栈是一个先入后出(FILO-First In Last Out)的有序列表。
(2)栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。
(3)出栈(pop)和入栈(push)的概念
1.3 栈的应用场景
- 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
- 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
- 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
- 二叉树的遍历。
- 图形的深度优先(depth 一 first)搜索法。
1.4 用数组模拟栈
- 思路分析
实现 栈的 思路分析
(1)使用数组来模拟栈
(2)定义一个 top 来表示栈顶,初始化 为 -1
(3)入栈的操作,当有数据加入到栈时, top++; stack[top] = data;
(4)出栈的操作, int value = stack[top]; top–, return value - 代码是实现
package com.atguigu.stack;
import java.util.Scanner;
public class ArrayStackDemo {
public static void main(String[] args) {
ArrayStack stack = new ArrayStack(4);
String key = "";
boolean loop = true;
Scanner scanner = new Scanner(System.in);
while (loop) {
System.out.println("show:显示栈");
System.out.println("exit:退出栈");
System.out.println("push:入栈");
System.out.println("pop:出栈");
System.out.println("输入选择:");
key = scanner.next();
switch (key) {
case "show":
stack.list();
break;
case "exit":
scanner.close();
loop=false;
break;
case "push":
System.out.println("请输入一个数:");
int value= scanner.nextInt();
stack.push(value);
break;
case "pop":
try {
int res=stack.pop();
System.out.printf("出栈的数据为:%d\n",res);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
default:
break;
}
}
}
}
// 定义一个数组ArrayStack表示栈
class ArrayStack {
private int maxSize; // 栈的大小
private int[] stack; // 数组,模拟栈
private int top = -1; // 表示栈顶
public ArrayStack(int maxSize) {
this.maxSize = maxSize;
stack = new int[this.maxSize];
}
// 栈满
public boolean isFull() {
return top == maxSize - 1;
}
// 栈空
public boolean isEmpty() {
return top == -1;
}
// 入栈
public void push(int value) {
if (isFull()) {
System.out.println("此栈已满");
return;
}
top++;
stack[top] = value;
}
// 出栈
public int pop() {
if (isEmpty()) {
throw new RuntimeException("栈空");
}
int value = stack[top];
top--;
return value;
}
// 显示栈的内容
public void list() {
if (isEmpty()) {
System.out.println("栈空");
return;
}
// 从栈空开始显示数据
for (int i = top; i >= 0; i--) {
System.out.printf("stack[%d]=%d\n", i, stack[i]);
}
}
}
1.5 栈实现综合计算器
1.5.1. 思路分析
使用栈完成表达式的计算 思路
- 通过一个 index 值(索引),来遍历我们的表达式
- 如果我们发现是一个数字, 就直接入数栈。
2.1 当发现是一位的数时,可以直接入栈,但是如果是多位数时(两位及以上),不能直接入栈。需要判断后面一位是数字还是符号
2.2 如果是数字,需要再次将其入数栈。 - 如果发现扫描到是一个符号, 就分如下情况
3.1 如果发现当前的符号栈为 空,就直接入栈
3.2 如果符号栈有操作符,就进行比较,如果当前的操作符的优先级小于或者等于栈中的操作符, 就需要从数栈中pop出两个数,在从符号栈中pop出一个符号,进行运算,将得到结果,入数栈,然后将当前的操作符入符号栈, 如果当前的操作符的优先级大于栈中的操作符, 就直接入符号栈. - 当表达式扫描完毕,就顺序的从 数栈和符号栈中pop出相应的数和符号,并运行.
- 最后在数栈只有一个数字,就是表达式的结果
1.5.2 代码实现(中缀表达式)
package com.atguigu.stack;
public class Calculator {
public static void main(String[] args) {
String expression="500+2*6-20"; //目前不能处理多位数。
//创建两个栈,
ArrayStack2 operStack = new ArrayStack2(10);
ArrayStack2 numStack = new ArrayStack2(10);
//定义需要的相关变量
int index=0; //用于扫描
int num1=0;
int num2=0;
int oper=0;
int res=0;
char ch=' '; //将每次扫描得到的char保存到ch
String keepNum=""; //用于检查数组是否是多位数。
//开始while循环,扫描expression
while(true) {
//依次得到expression的每一个字符
ch=expression.substring(index,index+1).charAt(0);
//判断ch是什么,然后做相应的处理
if (operStack.isOper(ch)) { //是运算符
//判断当前符号栈是否为空
if (!operStack.isEmpty()) {
if (operStack.priority(ch)<=operStack.priority(operStack.peek())) {
num1=numStack.pop();
num2=numStack.pop();
oper=operStack.pop();
res=numStack.cal(num1, num2, oper);
numStack.push(res); //运算结果入数栈
operStack.push(ch);
}else {
operStack.push(ch); //当前符号优先级大于符号栈符号优先级,直接入栈。
}
}else {
operStack.push(ch); //符号栈为空
}
}else {
keepNum+=ch;
if (index==expression.length()-1) { //如果index是最后一位,直接入栈
numStack.push(Integer.parseInt(keepNum));
}else {
//此时需要考虑是单位数还是多位数;;
//在处理数时,我们需要向experiession的index后面再看一位,如果后一位是数,就继续扫描,如果是符号才入栈
//因此我们需要一个变量字符串,用于拼接。
if (operStack.isOper(expression.subSequence(index+1, index+2).charAt(0))) {
//如果后一位是运算符,则入栈keepNum=1或者“123”
numStack.push(Integer.parseInt(keepNum));
//重要的是!!! keepNum清空
keepNum="";
}
}
}
//让index+1,并判断是否扫描的最后
index++;
if (index >=expression.length()) {
break;
}
}
//当表达式扫描完毕,就顺序的从 数栈和符号栈中pop出相应的数和符号,并运行.
while (true) {
//如果符号栈为空,则计算到最后的结果,数栈中只有一个数组,即为结果
if (operStack.isEmpty()) {
break;
}
num1=numStack.pop();
num2=numStack.pop();
oper=operStack.pop();
res=numStack.cal(num1, num2, oper);
numStack.push(res);
}
System.out.printf("表达式%s=%d", expression, numStack.pop());
}
}
// 定义一个数组ArrayStack2表示栈
class ArrayStack2 {
private int maxSize; // 栈的大小
private int[] stack; // 数组,模拟栈
private int top = -1; // 表示栈顶
public ArrayStack2(int maxSize) {
this.maxSize = maxSize;
stack = new int[this.maxSize];
}
//返回当前的栈的值,但是不是真正的pop
public int peek() {
return stack[top];
}
// 栈满
public boolean isFull() {
return top == maxSize - 1;
}
// 栈空
public boolean isEmpty() {
return top == -1;
}
// 入栈
public void push(int value) {
if (isFull()) {
System.out.println("此栈已满");
return;
}
top++;
stack[top] = value;
}
// 出栈
public int pop() {
if (isEmpty()) {
throw new RuntimeException("栈空");
}
int value = stack[top];
top--;
return value;
}
// 显示栈的内容
public void list() {
if (isEmpty()) {
System.out.println("栈空");
return;
}
// 从栈空开始显示数据
for (int i = top; i >= 0; i--) {
System.out.printf("stack[%d]=%d\n", i, stack[i]);
}
}
// 返回运算符的优先级,优先级有程序员来确定,优先级使用数字来表示,数组越大,优先级越高
public int priority(int oper) {
if (oper == '*' || oper == '/') {
return 1;
} else if (oper == '+' || oper == '-') {
return 0;
} else {
return -1;
}
}
// 判断是不是运算符
public boolean isOper(char val) {
return val == '+' || val == '-' || val == '*' || val == '/';
}
// 计算方法
public int cal(int num1,int num2,int oper) {
int res=0;
switch (oper) {
case '+':
res=num1+num2;
break;
case '-':
res=num2-num1;
break;
case '*':
res=num1*num2;
break;
case '/':
if (num1!=0) {
res=num2/num1;
}else {
System.out.println("除数不能为0");
}
break;
default:
break;
}
return res;
}
}
1.6 前缀、中缀、后缀表达式
1.6.1 基本原理
- 前缀表达式:波兰表达式
(1)原理
从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 和 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果。
(2)实例:
例如: (3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6 , 针对前缀表达式求值步骤如下:
1)从右至左扫描,将6、5、4、3压入堆栈
2)遇到+运算符,因此弹出3和4(3为栈顶元素,4为次顶元素),计算出3+4的值,得7,再将7入栈
3)接下来是×运算符,因此弹出7和5,计算出7×5=35,将35入栈
4)最后是-运算符,计算出35-6的值,即29,由此得出最终结果 - 中缀表达式**(符合人的习惯,但不符合计算机习惯)**
即为常见的表达式。 - 后缀表达式:逆波兰表达式
运算符位于操作数之后,
(1)原理
从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果
(2)实例
例如: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 - , 针对后缀表达式求值步骤如下:
1)从左至右扫描,将3和4压入堆栈;
2)遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
3))将5入栈;
4)接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
5)将6入栈;
6)最后是-运算符,计算出35-6的值,即29,由此得出最终结果
1.6.2 逆波兰表达式实现计算器
- 要求
(1)输入一个逆波兰表达式(后缀表达式),使用栈(Stack), 计算其结果
(2)支持小括号和多位数整数,因为这里我们主要讲的是数据结构,因此计算器进行简化,只支持对整数的计算。
(3)思路分析
(4)代码完成
package com.atguigu.stack;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class PolandNotation {
public static void main(String[] args) {
//先定义给逆波兰表达式:(3+4)*5-6 => 3 4 + 5 * 6 -
//为了说明方便,你波兰表达式的数字和符号之间用空格隔开
String suffixExpression="3 4 + 5 * 6 -";
List<String> rpnList=getListString(suffixExpression);
System.out.println("rpnList="+rpnList);
int res=calculate(rpnList);
System.out.println(res);
}
//将一个逆波兰表达式,一次将数据和运算符放入到ArrayLIst中
public static List<String> getListString(String suffixExpression){
//将suffixExpression分隔
String[] split=suffixExpression.split(" ");
ArrayList<String> list = new ArrayList<String>();
for(String ele:split) {
list.add(ele);
}
return list;
}
//完成对逆波兰表达式的计算
public static int calculate(List<String> ls) {
//创建栈,只需要一个
Stack<String> stack = new Stack<String>();
for (String item:ls) {
//这里使用正则表达式判断是否为符号
if (item.matches("\\d+")) { //匹配的是多位数
stack.push(item); //入栈
}else {
//pop出两个数 并运算,并将结果再次入栈
int num2=Integer.parseInt(stack.pop());
int num1=Integer.parseInt(stack.pop());
int res=0;
if (item.equals("+")) {
res=num1+num2;
}else if (item.equals("-")) {
res=num1-num2;
}else if (item.equals("*")) {
res=num1*num2;
}else if (item.equals("/")) {
res=num1/num2;
}else {
throw new RuntimeException("输入运算法有误");
}
stack.push(res+"");
}
}
return Integer.parseInt(stack.pop());
}
}
1.7 中缀表达式转后缀表达式
1.7.1 中缀表达式转后缀表达式的步骤
- 初始化两个栈:运算符栈 s1 和储存中间结果的栈s2;
- 从左至右扫描中缀表达式;
- 遇到操作数时,将其压 s2;
- 遇到运算符时,比较其与 s1 栈顶运算符的优先级:
(1)如果 s1 为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;
(2)否则,若优先级比栈顶运算符的高,也将运算符压入s1;
(3)否则,将 s1 栈顶的运算符弹出并压入到 s2 中,再次转到(4-1)与s1 中新的栈顶运算符相比较; - 遇到括号时:
(1) 如果是左括号“(”,则直接压入s1
(2) 如果是右括号“)”,则依次弹出s1 栈顶的运算符,并压入 s2,直到遇到左括号为止,此时将这一对括号丢弃 6) 重复步骤 2 至 5,直到表达式的最右边 - 重复步骤 2 至 5,直到表达式的最右边
- 将 s1 中剩余的运算符依次弹出并压入 s2
- 依次弹出 s2 中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
例如:对表达式“1+(((2+3)*4)-5”转为后缀表达式为:“1 2 3 + 4 * + 5 -”
1.7.2 中缀表达式转后缀表达式的实现
- 因为直接对str进行操作不方便,因此先将“1+(((2+3)*4)-5”转成对应的List,
即“1+(((2+3)4)-5”=>ArrayList[1,+,(,(,(,2,+,3,),,4,),-,5] - 将得到的中缀表达式对应的list => 后缀表达式对应的list
即:ArrayList[1,+,(,(,(,2,+,3,),,4,),-,5]
=>ArrayList[1,2,3,+,4,,+,5,-]
3.代码:
package com.atguigu.stack;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class PolandNotation {
public static void main(String[] args) {
String expression="1+((2+3)*4)-5";
List<String> infixExpressionList=toInfixExpressionList(expression);
System.out.println("中缀表达式为"+infixExpressionList);
//得到对应的中缀表达式
List<String> suffixExpreesionList = parseSuffixExpreesionList(infixExpressionList);
System.out.println("对应的后缀表达式为:"+suffixExpreesionList);
System.out.printf("expression=%d\n", calculate(suffixExpreesionList));
}
//方法一:将给出的中缀表达式转为对应的list
public static List<String> toInfixExpressionList(String s){
//定义一个List,存放中缀表达式,对应的内容
ArrayList<String> ls = new ArrayList<String>();
int i=0; //指针,用于遍历中缀表达式字符串
String str; //对多位数的拼接
char c; //每遍历一个字符,存放到c
do {
//如果c是一个非数字,需要加到ls中,
if ((c=s.charAt(i))<48 || (c=s.charAt(i))>57 ) {
ls.add(""+c);
i++;
}else { //如果是一个数,需要考虑多位数
str="";
while (i<s.length() && ((c=s.charAt(i))>=48 && (c=s.charAt(i))<=57)) {
str+=c; //拼接 //多个数拼接成一个多位数
i++;
}
ls.add(str);
}
} while (i<s.length());
return ls;
}
//方法二:将中缀表达式的ArrayList转化为 后缀表达式的ArrayList
public static List<String> parseSuffixExpreesionList(List<String> ls){
//定义两个栈
Stack<String> s1=new Stack<String>();
//因为s2这个栈,在整个过程中没有pop操作,而且我们还需要逆序输出,比较麻烦
//因此我们就不用Stack<String>,直接使用List<String>,
List<String> s2=new ArrayList<String>();
//遍历ls
for (String item:ls) {
//如果是一个数,加入s2
if (item.matches("\\d+")) {
s2.add(item);
}else if (item.equals("(")) {
s1.push(item);
}else if (item.equals(")")) {
while (!s1.peek().equals("(")) {
s2.add(s1.pop()); //弹出s1中“(”之后的数弹出
}
s1.pop(); //将“(”弹出
} else {
//当item的优先级小于s1栈顶运算符,将s1栈顶运算法弹出到s2中,并继续转到4.1与s1新的栈顶运算符相比较
//问题:我们缺少一个比较优先级高低的方法。
while (s1.size()!=0 && Opeeration.getValue(s1.peek()) >= Opeeration.getValue(item) ) {
s2.add(s1.pop());
}
//还需要将item压入栈
s1.push(item);
}
}
//将s1中剩余的运算符依次弹出并加入s2
while (s1.size()!=0) {
s2.add(s1.pop());
}
return s2; //因为存放的是list,因此按顺序输出就是对应的后缀表达式
}
//方法三:将一个逆波兰表达式,转为对应的list
public static List<String> getListString(String suffixExpression){
//将suffixExpression分隔
String[] split=suffixExpression.split(" ");
ArrayList<String> list = new ArrayList<String>();
for(String ele:split) {
list.add(ele);
}
return list;
}
//方法四:完成对逆波兰表达式的计算
public static int calculate(List<String> ls) {
//创建栈,只需要一个
Stack<String> stack = new Stack<String>();
for (String item:ls) {
//这里使用正则表达式判断是否为符号
if (item.matches("\\d+")) { //匹配的是多位数
stack.push(item); //入栈
}else {
//pop出两个数 并运算,并将结果再次入栈
int num2=Integer.parseInt(stack.pop());
int num1=Integer.parseInt(stack.pop());
int res=0;
if (item.equals("+")) {
res=num1+num2;
}else if (item.equals("-")) {
res=num1-num2;
}else if (item.equals("*")) {
res=num1*num2;
}else if (item.equals("/")) {
res=num1/num2;
}else {
throw new RuntimeException("输入运算法有误");
}
stack.push(res+"");
}
}
return Integer.parseInt(stack.pop());
}
}
//优先级定义类。
class Opeeration{
private static int Add=1;
private static int SUB=1;
private static int Mul=2;
private static int DIV=1;
//写一个方法,返回对应的优先级数字
public static int getValue(String operation) {
int result=0;
switch (operation) {
case "+":
result=Add;
break;
case "-":
result=SUB;
break;
case "*":
result=Mul;
break;
case "/":
result=DIV;
break;
default:
System.out.println("不存在该运算符");
break;
}
return result;
}
}
1.7.3 考虑小数点和空格的完整代码
package com.atguigu.reversepolishcal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
import java.util.regex.Pattern;
public class ReversePolishMultiCalc {
/**
* 匹配 + - * / ( ) 运算符
*/
static final String SYMBOL = "\\+|-|\\*|/|\\(|\\)";
static final String LEFT = "(";
static final String RIGHT = ")";
static final String ADD = "+";
static final String MINUS= "-";
static final String TIMES = "*";
static final String DIVISION = "/";
/** * 加減 + - */
static final int LEVEL_01 = 1;
/** * 乘除 * / */
static final int LEVEL_02 = 2;
/** 括号 */
static final int LEVEL_HIGH = Integer.MAX_VALUE;
static Stack<String> stack = new Stack<>();
static List<String> data = Collections.synchronizedList(new ArrayList<String>());
/**
* 去除所有空白符
* @param s
* @return
*/
public static String replaceAllBlank(String s ){
// \\s+ 匹配任何空白字符,包括空格、制表符、换页符等等, 等价于[ \f\n\r\t\v]
return s.replaceAll("\\s+","");
}
/**
* 判断是不是数字 int double long float
* @param s
* @return
*/
public static boolean isNumber(String s){
Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$");
return pattern.matcher(s).matches();
}
/**
* 判断是不是运算符
* @param s
* @return
*/
public static boolean isSymbol(String s){
return s.matches(SYMBOL);
}
/**
* 匹配运算等级
* @param s
* @return
*/
public static int calcLevel(String s){
if("+".equals(s) || "-".equals(s)){
return LEVEL_01;
} else if("*".equals(s) || "/".equals(s)){
return LEVEL_02;
}
return LEVEL_HIGH;
}
/**
* 匹配
* @param s
* @throws Exception
*/
public static List<String> doMatch (String s) throws Exception{
if(s == null || "".equals(s.trim())) throw new RuntimeException("data is empty");
if(!isNumber(s.charAt(0)+"")) throw new RuntimeException("data illeagle,start not with a number");
s = replaceAllBlank(s);
String each;
int start = 0;
for (int i = 0; i < s.length(); i++) {
if(isSymbol(s.charAt(i)+"")){
each = s.charAt(i)+"";
//栈为空,(操作符,或者 操作符优先级大于栈顶优先级 && 操作符优先级不是( )的优先级 及是 ) 不能直接入栈
if(stack.isEmpty() || LEFT.equals(each)
|| ((calcLevel(each) > calcLevel(stack.peek())) && calcLevel(each) < LEVEL_HIGH)){
stack.push(each);
}else if( !stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())){
//栈非空,操作符优先级小于等于栈顶优先级时出栈入列,直到栈为空,或者遇到了(,最后操作符入栈
while (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek()) ){
if(calcLevel(stack.peek()) == LEVEL_HIGH){
break;
}
data.add(stack.pop());
}
stack.push(each);
}else if(RIGHT.equals(each)){
// ) 操作符,依次出栈入列直到空栈或者遇到了第一个)操作符,此时)出栈
while (!stack.isEmpty() && LEVEL_HIGH >= calcLevel(stack.peek())){
if(LEVEL_HIGH == calcLevel(stack.peek())){
stack.pop();
break;
}
data.add(stack.pop());
}
}
start = i ; //前一个运算符的位置
}else if( i == s.length()-1 || isSymbol(s.charAt(i+1)+"") ){
each = start == 0 ? s.substring(start,i+1) : s.substring(start+1,i+1);
if(isNumber(each)) {
data.add(each);
continue;
}
throw new RuntimeException("data not match number");
}
}
//如果栈里还有元素,此时元素需要依次出栈入列,可以想象栈里剩下栈顶为/,栈底为+,应该依次出栈入列,可以直接翻转整个stack 添加到队列
Collections.reverse(stack);
data.addAll(new ArrayList<>(stack));
System.out.println(data);
return data;
}
/**
* 算出结果
* @param list
* @return
*/
public static Double doCalc(List<String> list){
Double d = 0d;
if(list == null || list.isEmpty()){
return null;
}
if (list.size() == 1){
System.out.println(list);
d = Double.valueOf(list.get(0));
return d;
}
ArrayList<String> list1 = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
list1.add(list.get(i));
if(isSymbol(list.get(i))){
Double d1 = doTheMath(list.get(i - 2), list.get(i - 1), list.get(i));
list1.remove(i);
list1.remove(i-1);
list1.set(i-2,d1+"");
list1.addAll(list.subList(i+1,list.size()));
break;
}
}
doCalc(list1);
return d;
}
/**
* 运算
* @param s1
* @param s2
* @param symbol
* @return
*/
public static Double doTheMath(String s1,String s2,String symbol){
Double result ;
switch (symbol){
case ADD : result = Double.valueOf(s1) + Double.valueOf(s2); break;
case MINUS : result = Double.valueOf(s1) - Double.valueOf(s2); break;
case TIMES : result = Double.valueOf(s1) * Double.valueOf(s2); break;
case DIVISION : result = Double.valueOf(s1) / Double.valueOf(s2); break;
default : result = null;
}
return result;
}
public static void main(String[] args) {
//String math = "9+(3-1)*3+10/2";
String math = "12.8 + (2 - 3.55)*4+10/5.0";
try {
doCalc(doMatch(math));
} catch (Exception e) {
e.printStackTrace();
}
}
}
二 递归
2.1 递归的概念
递归(recursion)就是方法自己调用自己,每次调用时传入不同的变量.递归有助于编程者解决复杂的问题,同时可以让代码变得简洁。
2.2 递归调用机制
先递归,再回溯,所以打印结果是2,3,4,而不是4,3,2.
2.3 应用场景
- 打印问题
- 阶乘问题
- 各种数学问题如: 8 皇后问题 , 汉诺塔, 阶乘问题, 迷宫问题, 球和篮子的问题(google 编程大赛)
- 各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等.
- 将用栈解决的问题–>第归代码比较简洁
2.4 递归需要遵守的重要规则
- 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
- 方法的局部变量是独立的,不会相互影响, 比如 n 变量
- 如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据.
- 递归必须向退出递归的条件逼近,否则就是无限递归,出现死归: StackOverflowError
- 当一个方法执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕。
2.5 递归的实际应用-迷宫回溯
- 使用递归回溯给小球找路
找到从起点到终点的路径 - 说明
(1)map表示地图;其中map[i][j]=0,表示该店没有走过,1:表示墙;2:表示通路,可以走;3:表示该点已经走过,但是走不通。
(2)小球如果能到map[6][5]表示可以找到迷宫的路,否则找不到。
(3)走迷宫时,需要确定一个策略,:下->右->上->左,如果该点走不通,再回溯。 - 如何寻找最短路径
(1)小球得到的路径,和程序员设置的找路策略有关即:找路的上下左右的顺序相关
(2)再得到小球路径时,可以先使用(下右上左),再改成(上右下左),看看路径是不是有变化 3) 测试回溯现象
(3)改变不同的策略,得到不同的结果,将结果保存,比较结果中2最少的那条,就是最短路径。
2.6 迷宫问题的代码实现
package com.atguigu.recursion;
public class MiGong {
public static void main(String[] args) {
//先创建一个二维数组,模拟迷宫
int[][] map=new int[8][7];
//使用1表示墙
for(int i=0;i<7;i++) { //设置第一行和最后一行为1
map[0][i]=1;
map[7][i]=1;
}
for(int i=1;i<8;i++) { //设置第一列和最后一列为1
map[i][0]=1;
map[i][6]=1;
}
//设置墙
map[3][1]=1;
map[3][2]=1;
//遍历map
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 7; j++) {
System.out.print(map[i][j]+" ");
}
System.out.println();
}
//使用递归回溯给小球找路
//setWay1(map, 1, 1);
setWay2(map, 1, 1);
//输出新的地图,
System.out.println("小球走过并标识的地图情况");
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 7; j++) {
System.out.print(map[i][j]+" ");
}
System.out.println();
}
}
/**
* 寻路策略2:上->右->下->左
* @param map
* @param i
* @param j
* @return
*/
public static boolean setWay2(int[][] map,int i,int j) {
if (map[6][5]==2) { //通路已找到
return true;
}else {
if (map[i][j]==0) { //表示当前点还没有走过
//按照策略下->右->上->左
map[i][j]=2; //假设该点可以走通
if (setWay1(map, i-1, j)) { //向上走
return true;
}else if (setWay2(map, i, j+1)) { //向右走
return true;
}else if (setWay2(map, i+1, j)) { //向下走
return true;
}else if (setWay2(map, i, j-1)) { //向左走
return true;
}else { //说明走不通,是死路
map[i][j]=3;
return false ;
}
}else { //map[i][j]为,1,2,3,都不用再考虑
return false ;
}
}
}
/**
* 寻路策略1:下->右->上->左
* @param map
* @param i
* @param j
* @return
*/
public static boolean setWay1(int[][] map,int i,int j) {
if (map[6][5]==2) { //通路已找到
return true;
}else {
if (map[i][j]==0) { //表示当前点还没有走过
//按照策略下->右->上->左
map[i][j]=2; //假设该点可以走通
if (setWay1(map, i+1, j)) { //向下走
return true;
}else if (setWay1(map, i, j+1)) { //向右走
return true;
}else if (setWay1(map, i-1, j)) { //向上走
return true;
}else if (setWay1(map, i, j-1)) { //向左走
return true;
}else { //说明走不通,是死路
map[i][j]=3;
return false ;
}
}else { //map[i][j]为,1,2,3,都不用再考虑
return false ;
}
}
}
}
2.7 递归的实际应用-8皇后问题
- 问题简述
在 8×8 格的国际象棋上摆放八个皇后,使其不能互相攻击,即:任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法(92)。 - 算法思路分析
(1)第一个皇后先放第一行第一列
(2)第二个皇后放在第二行第一列、然后判断是否 OK, 如果不 OK,继续放在第二列、第三列、依次把所有列都放完,找到一个合适
(3)继续第三个皇后,还是第一列、第二列……直到第 8 个皇后也能放在一个不冲突的位置,算是找到了一个正确解
(4) 当得到一个正确解时,在栈回退到上一个栈时,就会开始回溯,即将第一个皇后,放到第一列的所有正确解,全部得到.
(5)然后回头继续第一个皇后放第二列,后面继续循环执行 1,2,3,4 的步骤
(6)说明
理论上应该创建一个二维数组来表示棋盘,但是实际上可以通过算法,用一个一维数组即可解决问题. arr[8] =
{0 , 4, 7, 5, 2, 6, 1, 3} //对应 arr 下标 表示第几行,即第几个皇后,arr[i] = val , val 表示第 i+1 个皇后,放在第 i+1 行的第 val+1 列。
2.8 8皇后问题的代码实现
package com.atguigu.recursion;
public class Queue8 {
//定义一个max,表示共有多少个皇后
int max=8;
//定义数组array,保存皇后防止位置的结果,比如:arr[8] = {0 , 4, 7, 5, 2, 6, 1, 3}
int[] array=new int[max];
static int count=0;
public static void main(String[] args) {
Queue8 queue8 = new Queue8();
queue8.check(0); //从0开始放。
System.out.println(count);
}
//方法1:放置第n个皇后
//注意,check是每一次递归时,进入到check中都有for循环,因此会有回溯。
private void check(int n) {
if (n==max) { //说明8个皇后已放好
print();
return;
}
//依次放入皇后,并判断是否冲突
for (int i = 0; i < max; i++) {
//先把当前n皇后,放到该行的第1列
array[n]=i;
//判断当前列放置第n个皇后时,是否冲突
if (judge(n)) { //不冲突
check(n+1);
}
//如果冲突,就继续执行array[n]=i;即将第n个皇后放置在本行的后一个位置
}
}
//方法2:查看放置第n个皇后,就去检测该黄狗是否和前面已经摆放的皇后冲突
//n:表示第n个皇后
private boolean judge(int n) {
for (int i = 0; i < n; i++) {
/*说明
* 1. 下标代表行,因此:
* (1)array[i]==array[n]:表示不同行的列值相同,及在不同行的皇后在同一列
* (2)Math.abs(n=i)==Math.abs(array[n]-array[i]):n皇后和i皇后在横坐标和纵坐标的距离相等,即在同一条下线上
* 2. 没必要判断是否在同一行,因为每次n递增,即行号递增,即不会处于同一行。
* */
if (array[i]==array[n] || Math.abs(n-i)==Math.abs(array[n]-array[i])) {
return false;
}
}
return true;
}
//方法3:可以将皇后摆放的位置输出
private void print() {
count++;
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]+" ");
}
System.out.println();
}
}