方法递归
指方法自己调用自己,每次递归都会对参数进行处理,实现问题的不断简化,最终将结果合并以解决实际问题。举例如下:
- 数学问题:阶乘、迷宫、汉诺塔、八皇后...
- 算法:快排、二分、分治...
使用递归时需要遵循以下规则:
- 每一个方法执行都会入栈
- 递归过程中方法的局部变量相互独立(引用类型例外)
- 方法执行的结果会返回给调用者
- 递归必须具有终止的趋势,避免无限递归
使用递归的三个条件:
- 原问题可以分解为若干个子问题
- 原问题与分解之后的问题求解思路一致,只有规模不同
- 存在递归终止条件
阶乘
import java.util.Scanner;
public class Test{
public static void main(String[] args) {
Scanner scn = new Scanner(System.in);
System.out.print("请输入一个整数:");
int num = scn.nextInt();
System.out.println(num + " 的阶乘为:" + new Test().recursion(num));
}
/**
* 使用递归计算 n 的阶乘
* @param n 输入的整数
* @return 阶乘结果
*/
public int recursion(int n){
// 当 n 大于 1 时,执行递归调用
if(n > 1) {
// 递归调用,参数递减 -> 5 * 4 * 3 * 2 * 1
// 每一个值都对应一次递归方法的执行结果
return n * recursion(n - 1);
} else {
// 当 n 不大于 1 时(包括 1 和 0),直接返回 1
// 这是递归的终止条件
return 1;
}
}
}
斐波那契
import java.util.Scanner;
public class Test{
public static void main(String[] args) {
Scanner scn = new Scanner(System.in);
System.out.print("请输入一个整数:");
int num = scn.nextInt();
System.out.println("第" + num + "个斐波那契数是:"
+ new Test().fibonacci(num));
}
/**
* 计算斐波那契数列的第n个数
* 斐波那契数列是一个每个数字都是前两个数字之和的数列
* 从1开始,前几个数字为1, 1, 2, 3, 5, 8, ...
*
* @param n 斐波那契数列的位置,n为正整数
* @return 返回斐波那契数列中第n个位置的数字
*/
public int fibonacci(int n) {
// 当n为1或2时,直接返回1,因为斐波那契数列的前两个数字都是1
if (n == 1 || n == 2) {
return 1;
} else {
// 递归调用fibonacci方法,计算第n个数字为前两个数字之和
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
}
猴子吃桃
import java.util.Scanner;
/**
* 有一堆桃子,猴子每天吃一半加一个桃子,最后一天还剩1 个,求第一天有多少个桃子?
*/
public class Test{
public static void main(String[] args) {
Scanner scn = new Scanner(System.in);
System.out.print("请输入一个整数:");
int days = scn.nextInt();
System.out.println("第1 天有" + new Test().peachNum(days) + "个桃子");
}
/**
* 计算第1 天的桃子数量
* 通过递归方式实现桃子数量的计算,每一天桃子的数量都是前一天桃子数量的一半减一
*
* @param days 输入的天数,用于计算对应天数的桃子数量
* @return 返回第前一天的桃子数量
*/
public int peachNum(int days) {
// 当天数大于1时,递归调用peachNum方法计算前一天的桃子数量
if (days > 1) {
return (peachNum(days - 1) + 1) * 2;
} else {
// 当天数为1时,直接返回1,即最后一天的桃子数量为1
return 1;
}
}
}
走迷宫
注意回溯现象,即除去移动前的位置,其余位置都走不通,就回到之前的位置并换一个方向移动
import java.util.Random;
import java.util.Scanner;
/**
* 迷宫入口固定为左上角,即maze[1][1]
* 迷宫出口固定为右下角,即maze[height - 2][width - 2]
* 需要注意的是宽对应的是列,高对应的是行
*/
public class Test {
public static void main(String[] args) {
Test test = new Test();
int[][] maze = test.buildAMaze();
System.out.println("迷宫生成完成!");
printMaze(maze);
System.out.println("---------开始查找路线-----------");
if (test.findWay(maze, 1, 1)) {
System.out.println("迷宫路线已找到!");
printMaze(maze);
} else {
System.out.println("没有找到路线!");
printMaze(maze);
}
}
/**
* 打印迷宫数组
* 该方法将一个二维数组(代表迷宫)按照行优先的顺序打印出来
*
* @param maze 一个二维整数数组,代表迷宫的布局
*/
public static void printMaze(int[][] maze) {
// 遍历迷宫的每一行
for (int i = 0; i < maze.length; i++) {
// 遍历当前行的每一个元素
for (int j = 0; j < maze[0].length; j++) {
// 打印当前元素,元素之间用空格分隔
System.out.print(maze[i][j] + " ");
}
// 每完成一行的打印后,换行开始打印下一行
System.out.println();
}
}
/**
* 构建一个迷宫
*
* @return 返回一个二维数组表示的迷宫,其中1表示墙,0表示通路
* 详细说明:
* 这个方法用于生成一个二维数组表示的迷宫。迷宫的大小由参数width和height决定。
* 迷宫的边缘全部由墙(用数字1表示)组成,内部区域则为通路(用数字0表示)。
*/
public int[][] buildAMaze() {
// 获取用户输入的迷宫的宽度和高度
Scanner scanner = new Scanner(System.in);
System.out.print("请输入迷宫的宽度:");
int width = scanner.nextInt();
System.out.print("请输入迷宫的高度:");
int height = scanner.nextInt();
// 初始化一个二维数组来表示迷宫
int[][] maze = new int[height][width];
// 设置迷宫的上边界和下边界为墙
for (int i = 0; i < width; i++) {
maze[0][i] = 1; // 上边界
maze[height - 1][i] = 1; // 下边界
}
// 设置迷宫的左边界和右边界为墙
for (int i = 0; i < height; i++) {
maze[i][0] = 1; // 左边界
maze[i][width - 1] = 1; // 右边界
}
// 将迷宫内部区域设置为通路
for (int i = 1; i < height - 1; i++) {
for (int j = 1; j < width - 1; j++) {
maze[i][j] = 0; // 内部区域设置为通路
}
}
// 随机设置障碍物
setUpBarriers(maze, width, height);
// 返回生成的迷宫
return maze;
}
/**
* 随机在迷宫中设置障碍物
* <p>
* 该方法的主要目的是为了增加迷宫的复杂性通过随机生成障碍物(maze[i][j] == 1),
*
* @param maze 二维数组表示的迷宫,0代表空白区域,1代表障碍物
* @param width 迷宫的宽度
* @param height 迷宫的高度
*/
public void setUpBarriers(int[][] maze, int width, int height) {
// 创建一个随机数生成器
Random rand = new Random();
// 遍历迷宫的每一个位置
for (int i = 1; i < height; i++) {
for (int j = 1; j < width; j++) {
// 跳过迷宫的入口(左上角)和出口(右下角)
if (i == 1 & j == 1 || i == height - 1 & j == width - 1) {
continue;
}
// 如果随机数小于3,并且当前位置是空白区域,则将其设置为障碍物
if (rand.nextInt(10) < 3 && maze[i][j] == 0) {
maze[i][j] = 1;
}
}
}
}
/**
* 在迷宫中查找路径
* 该方法使用深度优先搜索(DFS)算法递归地探索迷宫中的路径
* 它尝试寻找从指定位置 (x, y) 到迷宫出口的路径
*
* @param maze 二维整数数组,表示迷宫
* 其中0表示可以通过的路,1表示障碍物,2表示已经探索过的路,3表示死路
* @param x 当前探索位置的横坐标
* @param y 当前探索位置的纵坐标
* @return 如果找到路径则返回true,否则返回false
*/
public boolean findWay(int[][] maze, int x, int y) {
int width = maze[0].length;
int height = maze.length;
// 检查当前位置是否已经是迷宫的出口
if (maze[height - 2][width - 2] == 2) {
return true;
} else {
// 检查当前位置是否尚未被探索过
if (maze[x][y] == 0) {
// 标记当前位置为已探索
maze[x][y] = 2;
// 按照下、右、上、左的顺序尝试探索下一个位置
// 如果任何一个方向找到了路径,则返回true
if (findWay(maze, x + 1, y)) {
return true;
} else if (findWay(maze, x, y + 1)) {
return true;
} else if (findWay(maze, x - 1, y)) {
return true;
} else if (findWay(maze, x, y - 1)) {
return true;
} else {
// 如果所有方向都找不到路径,标记当前位置为死路,并返回false
maze[x][y] = 3;
return false;
}
} else {
// 如果当前位置不是可以通过的路(已经是墙、已经探索过或者死路),直接返回false
return false;
}
}
}
}
汉诺塔
/**
* 将三个盘子从A 柱移动到C 柱,保证大在下、小在上
*/
public class Test {
public static void main(String[] args) {
new Test().move(3, 'A', 'B', 'C');
}
/**
* 汉诺塔问题的解决方案
* 本方法递归地移动盘子从起始柱子a到目标柱子c,期间使用辅助柱子b
* 通过这个问题的经典解决思路,展示了如何通过递归调用解决复杂问题
*
* @param nums 盘子的数量,表示需要移动的盘子总数
* @param a 起始柱子,盘子初始所在的柱子
* @param b 辅助柱子,在移动过程中临时存放盘子
* @param c 目标柱子,盘子最终移动到的目标柱子
*/
public void move(int nums, char a, char b, char c) {
// 当只有一个盘子时,直接移动到目标柱子,这是递归的终止条件
if (nums == 1) {
System.out.println("move " + a + " to " + c);
} else {
// 先将nums-1个盘子从起始柱子移动到辅助柱子,以便最终可以将第1个盘子直接移动到目标柱子
move(nums - 1, a, c, b);
// 移动第1个盘子到目标柱子
System.out.println("move " + a + " to " + c);
// 将nums-1个盘子从辅助柱子移动到目标柱子,完成整个移动过程
move(nums - 1, b, a, c);
}
}
}
八皇后
/**
* 8 * 8 的棋盘中摆放8 个皇后,要求横竖斜都只能摆放一个皇后
*/
public class Test {
public static void main(String[] args) {
solveNQueens(new boolean[8][8], 0, 8);
}
/**
* 使用递归求解八皇后问题
* 该方法尝试在棋盘上放置皇后,并确保没有两个皇后在同一行、列或对角线上
*
* @param board 棋盘状态,记录了当前已经放置的皇后位置
* @param col 当前尝试放置皇后的列
* @param size 棋盘大小,默认为8
*/
public static void solveNQueens(boolean[][] board, int col, int size) {
// 如果已经成功放置了所有皇后
if (col >= size) {
printSolution(board); // 打印解决方案
return;
}
// 尝试在当前列的每一行放置皇后
for (int i = 0; i < size; i++) {
// 检查当前位置是否安全
if (isSafe(board, i, col, size)) {
// 放置皇后
board[i][col] = true;
// 递归地尝试下一行
solveNQueens(board, col + 1, size);
// 回溯,撤销上一步放置的皇后
board[i][col] = false;
}
}
}
/**
* 检查当前位置是否可以放置皇后
*
* @param board 棋盘状态
* @param row 行号
* @param col 列号
* @param size 棋盘大小
* @return 如果当前位置安全,则返回true;否则返回false
*/
private static boolean isSafe(boolean[][] board, int row, int col, int size) {
// 检查列是否有皇后互相冲突
for (int i = 0; i < col; i++) {
if (board[row][i]) {
return false;
}
}
// 检查左上对角线
for (int i = row, j = col; i >= 0 && j >= 0; i--, j--) {
if (board[i][j]) {
return false;
}
}
// 检查左下对角线
for (int i = row, j = col; j >= 0 && i < size; i++, j--) {
if (board[i][j]) {
return false;
}
}
return true;
}
/**
* 打印解决方案
*
* @param board 棋盘状态
*/
private static void printSolution(boolean[][] board) {
for (boolean[] row : board) {
for (int i = 0; i < board.length; i++) {
if (row[i]) {
System.out.print("Q ");
} else {
System.out.print(". ");
}
}
System.out.println();
}
System.out.println();
}
}
重载
OverLoad,表示在同一个类中存在方法名相同,但参数列表不同的方法,实现了方法名称的复用(本质上就是调用同意方法名,依据传递的参数实现不同的需求)
参数列表不同包括参数类型、个数甚至是位置
public class Test {
public static void main(String[] args) {
System.out.println("2 + 3 = " + new Test().sum(2, 3));
System.out.println("2.0 + 3.0 = " + new Test().sum(2.0, 3.0));
}
public int sum(int a, int b) {
return a + b;
}
// 是否重载与返回值类型无关
public double sum(double a, double b) {
return a + b;
}
}
可变参数
针对方法的参数列表,即一个方法允许拥有不定数量的参数
使用可变参数的前提是这些参数都具有同类型,而这与数组的概念一致(本质就是数组)
同一个方法只能出现一个可变参数,并且必须出现在最后,避免出现混淆
public class Test {
public static void main(String[] args) {
System.out.println("2 + 3 = " + new Test().sum(2, 3));
System.out.println("2 + 3 + 4 = " + new Test().sum(2, 3, 4));
}
public int sum(int... nums) {
int sum = 0;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
return sum;
}
}
作用域
主要针对变量,依据其生效范围(作用域)将其分为全局变量和局部变量,前者属于类、后者属于方法,并且局部变量必须赋值后才能使用(因为其没有默认值)
关于局部变量没有默认值,有以下四点原因:
- 安全性:避免依赖默认值导致的潜在错误,强制要求使用前进行初始化
- 性能:局部变量的使用是频繁的,如果每次声明都要进行默认初始化,会造成额外性能开销
- 可读性:声明的同时赋值可以使代码更加清晰,提高可读性
- 作用域:局部变量仅在方法内有效,方法出栈之后局部变量就会被销毁,因此没有必要
关于局部变量的一些细节如下:
- 局部变量作用域小于全局变量,因此局部变量与全局变量可以重名,访问时遵循就近原则
- 同样由于作用域,局部变量的生命周期与方法同步,而全局变量与对象同步
- 局部变量只能在本类方法中使用,无法被其他类访问,不加修饰符
构造方法
Java 当中要想完成某些操作,就需要调用对应类的方法,那么在完成对象创建后进行初始化时同样如此。而与普通方法相比构造方法又有以下不同:
- 对象创建后的初始化操作是自动的,因此构造方法需要一个独特的名称,即与类名相同(可以被JVM 找到)
- 构造方法的作用仅是初始化对象而不需要返回任何值,因此构造方法没有返回值(与void 做区分)
- 类的属性具有默认值,实际可能需要DIY 对象,因此构造方法需要重载(针对不同参数进行初始化)
- 即使我们不定义构造方法,依旧可以通过new 去创建对象,因此构造方法存在一个默认的版本以供JVM 调用
未定义有参构造器的情况下,变异器提供有默认的无参构造器,而在已定义的情况下,会将其覆盖
基于此,对象的创建流程如下:
- 加载类信息至方法区
- 堆中分配空间
- 完成对象初始化(默认初始化 -> 显示初始化 -> 构造方法)
this
this 指向当前对象,用于解决变量命名问题
public class Test {
int age;
String name;
public Test(int age, String name) {
// 前者表示属性age, 后者表示形参age
this.age = age;
this.name = name;
}
}
总的来说,在哪个对象中调用this,this 就指向哪个对象
this 如果要访问构造器,只能在构造器中基于重载根据参数来确定具体调用哪个构造器,不能在其他地方访问构造器