目录
前言
在编程学习过程中,我们常常会遇到难以解决的问题。
- 同学A遇到了一个问题,他向老师请教。老师指导他解决这个问题。
- 后来,同学B、C和D也遇到了同样的问题。他们分别向老师请教,并得到了相同的解决方案。
- 老师意识到这是一个共性问题。为了提高效率并帮助其他学生,老师将解决方案整理成一份文档。
- 老师将该文档发送到班级群,让所有学生都能够访问和阅读。文档中详细描述了问题的解决方法、常见错误以及推荐的最佳实践。
通过将解决方案整理成文档:
-
老师减少了重复性工作;
-
同学可以随机直接进行阅读,解决了问题,不需要和老师做重复的事情。
在我们编写代码时也是如此,有许多代码需要在程序中多次的重复执行,如果每次执行都需要重新编写一次代码,会使整个代码变得繁琐低效,也不利于我们后期检查维护。
这时候,我们就可以通过构建方法来实现对于代码的重复利用 ,只需要将重复利用的代码封装成一个方法,在后续使用中就可以通过方法名直接调用这段代码,避免重复大大提高了书写与使用效率。
1.方法的定义
Java中的方法与C中的函数类似,都是一个存放功能代码的片段,是一段可执行的代码块,可以接收输入参数并返回结果。
定义方式:
修饰符 返回值类型 方法名 ([ 参数类型 形参 ]){方法体代码 ;[ return 返回值 ];}
简例:
// 两整数求和方法1:返回结果
public static int add1 (int a, int b) {
int c = a + b;
return c;
}
// 两整数求和方法2:无返回,直接打印
public static void add2 (int a, int b) {
System.out.println(a + b);
}
注意: 方法必须写在类当中返回值:方法返回值必须符合返回值类型,如果没有返回值,必须写成 void参数列表:如果方法没有参数,()中什么都不写,如果有参数,需指定参数类型,多个参数之间使用逗号隔开方法体:方法内部要执行的语句
2.方法的调用
方法在定义好后并不会立即执行,只有当代码中调用此方法时,才会执行。
调用方式:
方法位于当前类中:
方法名(接收参数);
例:
public static int add1 (int a, int b) {
int c = a + b;
return c;
}
public static void add2 (int a, int b) {
System.out.println(a + b);
}
public static void main(String[] args) {
int x = 5;
int y = 10;
System.out.println(add1(x,y)); //打印返回结果
add2(x,y); //方法直接打印无需接收返回值
}
3.形参与实参
在方法定义中我们提到了形参的定义,形参类似于我们在数学函数中的自变量,在程序中形式参数(形参)是方法在定义时声明的变量,用于接收调用该方法时传入的实际参数(实参)的值。
形参的作用类似于函数的输入,它们允许我们在方法内部使用传递给方法的值来执行特定的操作或计算。形参允许方法根据不同的输入进行不同的处理,增加了方法的灵活性和可重用性。
在方法定义中,形参有以下特点:
-
形参的类型:形参可以指定特定的数据类型,以限制方法接受的参数的类型。例如,可以使用
int
、String
或自定义的类作为形参的类型。 -
形参的名称:形参需要一个变量名来标识,在方法体内可以使用该变量名来引用传递给方法的实参的值。
-
形参的数量:方法可以定义多个形参,每个形参之间使用逗号进行分隔。
形参的定义类似于变量的定义,但注意它们只在方法的作用域内有效。在方法被调用时,传递给方法的实参会被赋值给对应的形参,然后方法体内可以使用这些形参进行相应的操作。
例如,下面是一个简单的方法定义,其中包含两个形参:
public void printSum(int num1, int num2) {
int sum = num1 + num2;
System.out.println("Sum: " + sum);
}
在上述示例中,printSum
方法有两个形参num1
和num2
,它们的类型为int
。在方法体内,可以使用这两个形参计算它们的和,并进行输出。
当调用上述示例方法时,需要提供实际的参数值作为方法调用的实参。例如:
printSum(3, 4); // 调用方法,并传递实参值
在方法调用中,3 和 4 即为实参,他们被传递给对应的形参num1
和num2
,在方法体内进行计算后输出结果。
总之,形参在方法定义中充当了类似于数学函数中自变量的角色,它们接收方法调用时传入的实参值,并在方法内部使用这些参数进行相应的操作。
二者关系
public static void change (int a, int b){
int c = a;
a = b;
b = c;
System.out.println("a = "+a+","+"b = "+b);
}
public static void main(String[] args) {
int a = 10;
int b = 20;
change(a,b);
System.out.println("a = "+a+","+"b = "+b);
}
运行结果
根据上述代码的运行结果,我们可以看出在change方法中形参a和b的值被交换了,而在main方法中的变量a和b的值并没有被实际改变。
因为在Java中,方法参数传递采用的是按值传递。当我们将变量作为参数传递给方法时,实际上是将变量的值复制给了方法的形参。在change方法中,交换的是形参a和b的值,而不是main方法中的变量a和b。
因此,即使在change方法内部交换了形参的值,原始的变量a和b的值并没有改变。这说明对形参的修改只在方法的作用域内有效,不会对外部的变量产生影响。
4.方法的重载
在我们的实际生活中,许多的词在不同的语境、运用场景上有着不同的含义。
例如“坤”:
在八卦中,"坤"是八个卦爻之一,代表着地、阴、柔、顺从、母性等概念。坤卦象征着大地的形象,代表了温和、包容和养育万物的特性。
在风水学中,"坤"是指南针的朝向之一,代表了南方或正南。
在娱乐生活中,"坤"可以抽象指代动物鸡、可以充当2.5个单位的量词......
同样在Java中我们也会遇到类似的情况,例如在求和的sum方法中,我们要求3+2与3.5+2.4时所需形参不同,所需返回值类型也不相同,此时就需要用到方法的重载,在编程中,方法重载是指在同一个类中定义多个具有相同名字但参数类型和数量不同的方法。通过方法重载,可以根据不同的参数来执行不同的操作,提供更灵活的方法调用方式。
例:
// sum1
public static int add (int a, int b){
int c = a + b;
return c;
}
// sum2
public static int add (int a, int b, int c){
int d = a + b + c;
return d;
}
// sum3
public static float add (float a, float b) {
float c = a + b;
return c;
}
public static void main(String[] args) {
System.out.println(add(1, 2));
System.out.println(add(1, 2, 3));
System.out.println(add(1.5f, 2.5f));
}
运行结果:
尽管这三个方法具有不同的参数列表和返回值类型,但它们的方法名相同。在Java中,方法重载通过方法名相同但返回值或参数列表不同来实现。编译器根据调用时提供的参数类型和数量来选择合适的方法重载进行调用。
5.递归方法
5.1 什么是递归
从小我们就听过这样一个故事:
“从前有座山,山里有座庙,庙里有个老和尚再给小和尚讲故事,讲的是什么呢?
从前有座山,山里有座庙,庙里有个老和尚再给小和尚讲故事,讲的是什么呢?
从前有座山,山里有座庙……”
也会见到许多无限循环的包装盒:
蚊香盒里的赵本山老师,拿着印有(赵本山老师拿着蚊香盒)图片的蚊香盒,图片上赵本山老师手里的蚊香盒上印着(赵本山老师拿着蚊香盒)图片……
在编程中我们讲这种自身之中又包含自身的形式称为——递归。
递归可以被视为一种特殊的循环。但与传统的迭代循环(例如for循环和while循环)不同的是,递归是一种通过调用自身来解决问题的方法。
递归和循环都是在算法和程序设计中用于重复执行一段代码或解决重复的问题。它们可以用来实现相同的功能,但采用不同的方式。
在递归中,函数或方法会通过调用自身来重复执行某个步骤或解决子问题。递归函数会不断地调用自身,并通过某种终止条件来结束递归。每次递归调用都会解决问题的一个较小部分,直到达到终止条件。
5.2 递归的构建方式
递归可以类比我们数学中常用到的数学归纳法相,要实现递归,必须找到一个起始条件 (也可以说是终止条件) 和递推公式。
例如在求 n 的阶乘过程中:n!可以转化为 n*(n-1)!而其中 (n-1)!又可以转化为(n-1)(n-2)!,依次类推知道最终可以推得1!= 1;而1!= 1 便是递归的起始。
在这个过程中,我们通过递归调用将问题分解为更小的子问题,直到达到起始条件1!= 1,然后逐步回溯并计算出每个子问题的解,最终得到n!
具体实现:
public static int factorial(int n){
int ret=0;
if (n == 1){ //起始条件
ret = 1;
//当n=1时n!结果为1
}
if (n > 1) {
ret = n * factorial(n - 1); //递推公式
//当n>1时n!结果为n*(n-1)!,此时再次调用当前方法求(n-1)!
}
return ret;
}
public static void main(String[] args) {
System.out.println(factorial(5));
//测试执行运算5!
}
运行结果:
5.3 递归过程分析
以上述阶乘的实现为例:
递归过程是通过不断调用自身来实现的,直到达到起始条件为止。这个过程可以形成一个递归树,如下图示,当程序进行按红色箭头方向逐级向下递推,当到达起始条件 n=1 时得出起始值后,延绿色箭头逐级返回。
6.实践——递归解决汉诺塔问题
要解决这个问题首先需要我们了解什么是汉诺塔,汉诺塔(Tower of Hanoi),又称河内塔,是一个源于印度古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
汉诺塔是一项古老而经典的益智游戏,电影《猩球崛起》中凯撒的妈妈Bright Eyes只用15步就完成了一次4阶汉诺塔游戏(4阶最短步骤)。
6.1 解决思路
首先在定义方法前我们需要获取一些必要的信息,例如:圆盘的个数 n,起始位置 s ,中转位置 i ,最终位置 e 。
接下来就要确定方法的起始条件和递推公式;
当只有一个圆盘 (n = 1) 时,可以直接将其从起始位置 s 移动到目标位置 e ,从而完成任务,因此我们很容易就能获取到起始条件即为 n = 1;
当圆盘个数大于1 (n > 1) 时,由于圆盘必须按顺序排列,所以我们需要将除最大圆盘外的其他圆盘全部转移到中转位置 i ,在将最大圆盘按照与只有一个圆盘 (n = 1) 时相同的方式移动到目标位置 e ,最后再将其他圆盘借助起始位置 s 移动到终止位置 e ;此时我们也就成功找到了方法的递推公式,这个过程可以分为三个步骤:
- 将 n-1 个圆盘从初始柱子 s 通过目标柱子 e 移动到辅助柱子 i ;
- 将最大的圆盘从初始柱子 s 移动到目标柱子 i ;
- 将 n-1 个圆盘从辅助柱子 i 通过初始柱子移动到目标柱子 e 。
通过递归调用这个递推公式,可以将大问题转化为小问题,并最终得到整体的解决方案。递归的思想在这里将问题分解成了子问题,并依次解决每个子问题,从而解决整个问题。
6.2 具体实现
public class Hanoi {
public static int hanoi(int n, String s, String i, String e){
if (n == 1){
System.out.print(s + "⇒" + e + " ");
//初始位置到目标位置
}
if (n > 1){
hanoi(n-1, s, e, i);
//将最大圆盘以外所有圆盘通过目标位置转移至中转位置
System.out.print(s + "⇒" + e + " ");
//将最大圆移入最终位置
hanoi(n-1, i, s, e);
//将剩余圆盘从中转位置上借助起始位置移至目标位置
}
return n=n-1;
//确定未归位圆盘数量
}
public static void main(String[] args) {
hanoi(4,"A","B","C");
//设置圆盘数为4,起始位置为A,中转位置与目标位置为B、C
}
}
运行结果
总结
本文介绍了方法的定义、调用、形参与实参的关系,以及方法的重载和递归方法的概念。其中,递归方法是通过将大问题分解为子问题并递归解决的方法,有效应用于复杂问题的求解。汉诺塔问题是递归方法的一个经典应用,通过圆盘的移动方式,展示了递归思想的实际运用。通过本文的学习,可以理解并应用递归方法来解决不同类型的问题。