递归是一种方法(函数)调用自己的编程技术。
知道递归这两个字的时候是在大学生活时代的C语言的课堂上,当时老师提到了递归,说这是一种一个函数自己调用自己的方法,然后就非常感兴趣,感觉非常的神奇,但是当自己实际用到的时候,却怎么都理解不了他的好处,它是怎么实现之类的,就不像 if-else 语句这种,学的时候就可以在大脑中刻画一个流程图出来,然后可以很快的理解,所以当时出于对知识的敬畏,就没有在深入研究下去了。现在重新接触到它,决定重新啃啃着硬骨头了
递归算法的特征:
- 调用自身
- 当它调用自身的时候,它这样做是为了解决更小的问题
- 存在某个足够简单的问题的层次,在这一层算法不需要调用自己就可以直接解答,且返回结果。
首先要从一个最基础的三角数字问题说起:简单的表示就是一个数字序列具有以下的特点:就是该数列中的第 n 项是由第 n-1 项加 n 得到的。
1. 1
2. 1 + 2 = 3
3. 1 + 2 + 3 = 6
4. 1 + 2 + 3 + 4 = 10
…
现在我们要求输入一个整数n,输出其在三角数列中的值:
使用递归解决的代码:
/* 递归解决最基本的三角数字问题 */
public class Triangle {
static int theNumber;
public static int triangle(int n) {
if (n == 1) { // 当递归到1的时候 停止迭代 直接返回1
System.out.println("Returning 1");
return 1;
} else { // 没到1的话 继续递归
int temp = (n + triangle(n-1));
System.out.println("Returning " + temp);
return temp;
}
}
public static void main(String[] args) {
theNumber = 7;
System.out.println("Entering: n=" + theNumber);
int theAnswer = triangle(theNumber);
System.out.println(theAnswer);
}
}
上面的代码有一些输出语句,如果输入的数字比较小的话,我们就可以比较清楚的看到其内部的执行过程。
关于上面的代码的执行过程,我的理解是:(假如 n=4)
1. n = 4, return (n + triangle(n-1))的时候,等待--------第一次进入 triangle
2. n = 3, return (n + triangle(n-1))的时候, 等待--------第二次进入 triangle
3. n = 2, return (n + triangle(n-1))的时候, 等待--------第三次进入 triangle
4. n = 1, return (n + triangle(n-1))的时候, 不等待,返回1--------第四次进入 triangle
5. 返回至第三次的函数,n = 2 + 1 = 3;
6. 返回至第二次的函数,n = 3 + 3 = 6;
7. 返回至第一次的函数,n = 6 + 4 = 10;
8. 结束!!!
也就是说你输入 n;代码就得 n 次进入迭代函数,进行 n 次返回,n次判断,才得到你的结果,所以其实细看它内部的实现,就会发现:
!!!递归只是在概念上简化了算法,而其内部并不是很有效率。但是其仍然是一种非常有意思的算法!
递归应用的另外一种情况 – 变位字:
所谓变位字的概念就是将一个单词全部分割为字符,然后将他们全排列:
解决这个问题的思路是:首先得让这个单词的中的每一个字符都有排在首位的机会,然后再让剩下的字符全排列。比如我们输入的单词拥有四个字符,那么就让着四个字母都有在首位的机会,然后剩下的三个字母全排列,三个字母全排列也不是那么简单的,怎么办呢?继续呀,让这三个字母也和上面的情况一样,给他们其中的每个字母都拥有在首位的机会,然后剩下的两个字母全排列,两个字母全排列应该不是很复杂吧,互换位置就是两种不同的情况,这样问题就解决了!!!
先上代码:
/* 迭代-变位字 */
public class anagram {
static int size;
static int count;
static char[] arrChar = new char[100];
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("Please entering a word:");
String input = sc.next();
size = input.length(); // 初始化输入字符串的长度
count = 0; // 作用:输出计数
for (int j=0; j<size; j++)
arrChar[j] = input.charAt(j); // 将输入的字符串转化为数组
doAnagram(size); // 进行迭代操作 达到字符变位目的
sc.close();
}
// 变位业务
public static void doAnagram(int newSize) {
if (newSize == 1)
return;
for (int j=0; j<newSize; j++) {
doAnagram(newSize - 1); // 迭代
if (newSize == 2) // 如果需要交换的子字符串或者原字符串的长度为2的话 就输出交换的结果
// 因为下一次的迭代已经不会发生字符的交换了
displayWord();
rotate(newSize); // 原字符串 或者 子字符串的字符交换函数
}
}
// 字符交替子函数
public static void rotate(int newSize) {
int j ;
int position = size - newSize; // position 表示从哪一位开始交换位置 (这里的position表示是要进行字符交换的原字符串或者子字符串的首字符索引值)
char temp = arrChar[position]; // (将要交换的原字符串或子字符串的首字符做备份)
for (j = position+1; j<size; j++) // 进行交换
arrChar[j-1] = arrChar[j]; // 交换的结果就是 (原字符串或者子字符串)整体向左移动一个单位
arrChar[j-1] = temp; // 再将原来备份好的第一个字符放到最后一位
}
// 打印函数
public static void displayWord() {
if (count < 99)
System.out.print(" ");
if (count < 9)
System.out.print(" ");
System.out.print(++count + " "); // 计数作用在这里实现
for (int j=0; j<size; j++)
System.out.print( arrChar[j] ); // 打印当前字符数组的状态
System.out.print(" ");
if (count%6 == 0) // 规定每行输出 6 个结果
System.out.println();
}
}
假如输入 ter 的话这个输出的结果是:
1 ter 2 tre 3 ert 4 etr 5 rte 6 ret
代码执行过程解析:
首先经过初始化的字符数组 现在的内容是ter
1. 第一次进入 doAnagram,开始循环1第一次循环 循环3次 -------ter
2. 第二次进入 doAnagram,此时size已经变为2,开始循环2 循环2次 -------ter
3. 开始循环2的第一次循环,第三次进入 doAnagram ,此时size已经变为了1,会直接返回 ------- ter
4. 继续循环2的第一次循环,判断 size 为2,输出当前的 字符串 ter,进行子字符串的字符交换 ----tre
5. 开始循环2的第二次循环,第四次进入 doAnagram ,此时size已经变为了1,会直接返回 ------- tre
6. 继续循环2的第二次循环,判断 size 为2,输出当前的 字符串 tre,进行子字符串的交换 ,循环2结束 ------ ter
7. 继续循环1第一次循环,判断 size 为3,直接进行原字符串的字符交换,循环1第一次循环结束,循环1第二次循环开始 ------------ ert
8. 第五次进入 doAnagram,此时 size 已经变为2,开始循环3 循环2次 ------ ert
9. 开始循环3的第一次循环,第l六次进入 doAnagram ,此时size已经变为了1,会直接返回 ------ ert
10. 继续循环3的第一次循环,判断 size 为2,输出当前的 字符串 ert,进行子字符串的字符交换 --------etr
11. 开始循环3的第二次循环,第七次进入 doAnagram ,此时size已经变为了1,会直接返回 ------ etr
12. 继续循环3的第二次循环,判断 size 为2,输出当前的 字符串 etr,进行子字符串的交换 ,循环2结束 ------ ert
13. 继续循环1第二次循环,判断 size 为3,直接进行原字符串的字符交换,循环1第一次循环结束,循环1第三次循环开始 ----- ret
14. 。。。。。(由于码字太累了 实在不行了 没有思路画个图出来 只能用文字表述了 剩下的 循环1的第三次循环和上面的类似)
递归的二分查找
解决这个问题的思路就是:
先看二分查找的思维,它对自己每次查找的范围都用一个 当前范围的最大下标和最小下标来表示,然后用两个下标的平均数去做一个判断的依据,其实大体的模式就是这样的。然后他也有对应的结束的条件,要是找到了就返回对应的下标,没有找到就返回数组的大小,每次查找的范围也在逐步的缩减,从这么看,它完全符合 递归的所有特征,所以实现起来按照递归的正常实现去实现就行了。
/* 使用递归实现二分查找 */
public class binarySearch2 {
private int[] arr;
private int nItems;
public binarySearch2(int max) {
arr = new int[max];
nItems = 0;
}
int find( int searchkey, int lowerBound, int upperBound) { // 递归的二分查找函数
int curIn = (lowerBound + upperBound)/2;
if (arr[curIn] == searchkey) // 找到要找的元素
return curIn;
else if (lowerBound > upperBound) // 数组中没有要找的元素
return nItems;
else {
if (curIn > searchkey) // 当目标在左半部分的时候
return find(searchkey, lowerBound, curIn-1);
else // 当目标在右办部分的时候
return find(searchkey, curIn+1, upperBound);
}
}
public static void main(String[] args) {
int max = 50;
binarySearch2 Arr = new binarySearch2(max);
for (int i=0; i<max; i++) {
Arr.arr[i] = (int)Math.ceil(Math.random()*99);
Arr.nItems ++;
}
Arrays.sort(Arr.arr); // 数组工具类将无序数组,进行排序
int a = Arr.find(4, 0, max-1);
if (a == max) {
System.out.println("生成的随机数中,没有 4 ");
} else {
System.out.println("生成的随机数组中有4 且在有序数组中的下标为:" + a);
}
}
这里的用递归实现二分查找,其实就是和递归的基本的那个例子差不多:其实仔细看递归就两点:
1. 自己调用自己
2. 在函数中有对应的结束条件,结束之后将对应的值返回至上一级函数,再逐级返回,最后到达最外层的函数,得到结果!但是这个例子中和上面两个例子不一样的地方在于,在的递归的方法中含有两个对自身的递归的调用,分别对应于问题的两个部分,虽然只有一个是真正的执行了。
经典的汉诺塔问题:
诺塔问题是一个比较经典的问题,具体汉诺塔是什么东西,就不在细说了,直接说用递归解决该问题的思路。
现在有 A B C 三个柱子,要求是将在 A 柱上的 N 个盘子,全部移到 C 上,且每次只能移动一个盘子,每个柱子上的盘子只能是大的在下,小的在上。
假设我们现在有 4 个盘子在 A 上,那我们可以先将上面比较小的三个盘子,先放在 B 上,然后将最大的盘子放在 C 上,然后再将 B 上的三个较小的盘子放在 C上 ,游戏结束!
那么我会想,还能这么玩吗?直接将三个盘子一起移动 ,不是一次只能移动一个吗?当然,一次只能移动一个,那是因为移动三个盘子到 B 的过程,已经被递归完成了。我们假设要移动的盘子有两个,按照上面的思路,你还会觉得有问题吗?
/*汉诺塔问题(递归解决)*/
public class towers {
static int nDisks = 3;
public static void doTowers(int topN, char from, char inter, char to) { // topN 就代表着目前的子塔还有几个碟子
if (topN == 1) {
System.out.println("Disk 1 from " + from + " to " + to);
} else {
doTowers(topN-1, from, to, inter);
System.out.println("Disk " + topN + " from " + from + " to " + to);
doTowers(topN-1, inter, from, to);
}
}
public static void main(String[] args) {
doTowers(nDisks, 'A', 'B', 'C');
}
}