Java 学习 - Day04

方法递归

指方法自己调用自己,每次递归都会对参数进行处理,实现问题的不断简化,最终将结果合并以解决实际问题。举例如下:

  1. 数学问题:阶乘、迷宫、汉诺塔、八皇后...
  2. 算法:快排、二分、分治...

使用递归时需要遵循以下规则:

  1. 每一个方法执行都会入栈
  2. 递归过程中方法的局部变量相互独立(引用类型例外)
  3. 方法执行的结果会返回给调用者
  4. 递归必须具有终止的趋势,避免无限递归

使用递归的三个条件:

  1. 原问题可以分解为若干个子问题
  2. 原问题与分解之后的问题求解思路一致,只有规模不同
  3. 存在递归终止条件

阶乘

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;
    }
}

作用域

主要针对变量,依据其生效范围(作用域)将其分为全局变量和局部变量,前者属于类、后者属于方法,并且局部变量必须赋值后才能使用(因为其没有默认值)

关于局部变量没有默认值,有以下四点原因:

  1. 安全性:避免依赖默认值导致的潜在错误,强制要求使用前进行初始化
  2. 性能:局部变量的使用是频繁的,如果每次声明都要进行默认初始化,会造成额外性能开销
  3. 可读性:声明的同时赋值可以使代码更加清晰,提高可读性
  4. 作用域:局部变量仅在方法内有效,方法出栈之后局部变量就会被销毁,因此没有必要

关于局部变量的一些细节如下:

  1. 局部变量作用域小于全局变量,因此局部变量与全局变量可以重名,访问时遵循就近原则
  2. 同样由于作用域,局部变量的生命周期与方法同步,而全局变量与对象同步
  3. 局部变量只能在本类方法中使用,无法被其他类访问,不加修饰符

构造方法

Java 当中要想完成某些操作,就需要调用对应类的方法,那么在完成对象创建后进行初始化时同样如此。而与普通方法相比构造方法又有以下不同:

  1. 对象创建后的初始化操作是自动的,因此构造方法需要一个独特的名称,即与类名相同(可以被JVM 找到)
  2. 构造方法的作用仅是初始化对象而不需要返回任何值,因此构造方法没有返回值(与void 做区分)
  3. 类的属性具有默认值,实际可能需要DIY 对象,因此构造方法需要重载(针对不同参数进行初始化)
  4. 即使我们不定义构造方法,依旧可以通过new 去创建对象,因此构造方法存在一个默认的版本以供JVM 调用

未定义有参构造器的情况下,变异器提供有默认的无参构造器,而在已定义的情况下,会将其覆盖

基于此,对象的创建流程如下:

  1. 加载类信息至方法区
  2. 堆中分配空间
  3. 完成对象初始化(默认初始化 -> 显示初始化 -> 构造方法)

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 如果要访问构造器,只能在构造器中基于重载根据参数来确定具体调用哪个构造器,不能在其他地方访问构造器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值