您的位置:首页 > 职场人生

黑马程序员——java基础——递归专题

2014-11-23 17:45 176 查看
------<a href="http://www.itheima.com" target="blank">Java培训、Android培训、iOS培训、.Net培训</a>、期待与您交流! -------

 一、递归思想

        递归是程序设计中的常用思想,其特点是在一个单独的函数中,会出现自身调用自身的现象。为什么会出现这样的现象呢?其实这也是来源于需求,我们在编写某些功能代码时,发现在代码编写过程在又有同样的需求出现,这时就出现了自身调用自身的现象,也说是说,功能上的重复产生了递归的应用。这是一种很自然的思维方法,类似于数学中数列的递推公式,就是数列的前一项通过某种数学运算,得出后一项,由前一项得后一项的这种运算,可以作用于数列后面的任一项的获取。这就是递归。

        递归是相对于非递归而言的,也拿数列来说明。数列中求第n项,除了用从第一项,通过递推公式以外,还有一个求任一项的通项公式,自变量是项数,因变量是第n项。这就类似于递归算法与非递归算法的区别,通过下面的小代码,更能说明它们之间的区别:

        这个小程序是实现十进制转二进制的功能,分别用到了非递归与递归算法,本程序只用来说明问题,故不考虑负数、0等其他情况。

public class BinaryTrans {

/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println(toBin(60));
System.out.println(transBin(60, new StringBuilder()));
}
<span style="white-space:pre">	</span>//非递归实现方式
public static String toBin(int num) {
StringBuilder sb = new StringBuilder();//存储各二进制位的一个容器
while(num>0){
sb.append(num % 2);
num = num >>> 1;
}
return sb.reverse().toString();//由于二进制各位的获取是从低位到高位的,故返回时需要进行反转
}
//递归实现方式
public static String transBin(int num, StringBuilder sb) {//这里必须传入一个外部容器对象
if (num == 0){
return sb.reverse().toString();
}
sb.append(num % 2);
return transBin(num / 2, sb);//这里就用到了自身调用自身的手法
}
}


从上面的例子可以看出,程序功能的实现,既可以用递归的方式,也可用非递归的方式,不必拘泥于某一种方式。

二、递归结束条件

        用递归进行编程时,很重要的一点就是递归结束条件的确定,否则递归的动作将是无止尽的,只会不断往方法体中深入而没有返回操作,这将导致内存的溢出。其实递归中,虽然递归用到的功能函数是同一个,但函数的参数列表其实是不同的,它们各自在栈内存中有各自的引用,若不明确递归结束条件,在栈内存中就会不断产生参数的引用,后果可想而知。

三、递归的一些其它重要特征

        我们先简单的说明一下,还是看一中的代码,有这样一个细节,为什么一中递归函数transBin中参数列表中除了待转换的参数num外,还会传入一个StringBuilder外部对象。在初步接触递归编程手法时,很难一步就想到在递归函数列表中一下写入这样一个外部参数,我们往往会很自然的采取在函数休内去创建一个数据存储对象,或者在发现需要一个这样的存储对象后,在函数外部定义一个成员变量,然后再在函数体类进行调用,而往往不会采用一个参数列表这样的方式。这里我们先简单说明一下,在后面会结合代祥细进行说明的。

        首先,第一种方式,在函数体中进行数据存储对象的创建,这种方式在运用递归手法的函数中往往不可取,因为递归会调用到自身,而且实际需求中往往里层的递归函数中往往会用到外层函数中一些对象,如果采用第一种方式会新创建一个对象,并没有用到外层函数的数据。再者,即使里外层递归函数没有数据的直接交互,由于考虑到递归的层数往往较多,如果每递归一次就创建一个新的对象,对内存资源的消耗也是不利的,所以第一种方式往往不太可取;再说说第二种方式,在类的成员变量,即递归函数外部进行数据存储对象的创建会怎么样呢?这看上去还行,里外层递归函数没有创建新的对象,用的是同一个对象,数据是共享的。这里也结合实际中的一些应用来说明,在实际应用中,被自身调用的递归函数若是被嵌入到一个循环体中,如for循环体,而每次for循环中的数据又没用很强的联系,或者说要保持一定的独立性,如果用第二种方式,for循环中第一轮循环和第二轮循环用到的存储对象都是类成员变量,没有独立性,而是同一份数据,这是不可取的。

        综上,之所以在递归中提倡将存储对象等作用的对象封装在递归函数的参数列表中,就是为了既能保证第一种方式的独立性,又能保持第二种方式的共享性。这也是实际开发中常用的需求。这里说明一点,不是说在递归中就一定要用第三种方式,也在一些需求会用到第一种和第二种方式的情况,还是那句话,不能太拘泥,要视需求而定。

四、递归的一些实际应用

        下面整理了一些实际应用中非常适合用递归手法实现特定功能的例子:

例1:螺旋方阵

        这里有一段输出螺旋方阵的的代码,所谓螺旋方阵是这样的:

1       2        3         4

12     13      14       5

11     16      15       6

10       9        8       7

        由外向内从1开始顺时针旋转的一个螺旋形。打印此结构正好可用递归的思想:将填充一圈的代码写到一个函数里,然后内圈的填充即是递归该函数。

参看如下代码:

class  Test
{
public static void main(String[] args)
{
spiral(5);
}
/**
* 此方法用于描述当前点在arr方阵中的坐标值(x,y),被打印的当前圈的长度len,以及变化的值value
* */
private static void spiral(int[][] arr,int value,int len,int x,int y)
{
if(len<=0)//这里为递归结束条件,即圈维度小于0时,结束递归
return;
for(int j=0;j<len;j++)//横向填充
{
arr[x][y]=value;
y++;
value++;
}
y--;
value--;
for(int i=0;i<len;i++)//纵向填充
{
arr[x][y]=value;
x++;
value++;
}
x--;
y--;
len--;

for(int j=0;j<len;j++)//横向填充
{
arr[x][y]=value;
y--;
value++;
}
y++;
value--;
for(int i=0;i<len;i++)//纵向填充
{
arr[x][y]=value;
x--;
value++;
}
x++;
y++;
len--;
spiral(arr,value,len,x,y);//进入下一轮递归
}
public static void spiral(int n)
{
int[][] arr=new int

;
spiral(arr,1,arr.length,0,0);
printArr2(arr);
}
public static void printArr2(int[][] arr)
{
for(int[] array:arr)
{
for(int value:array)
{
System.out.print(value+"\t");
}
System.out.println();
System.out.println();
}
}
}


        递归必须确定好递归的结束条件,否则会造成栈内存溢出的现象。例如在上面的代码中,if(len<=0)就是递归的条件判断语句,若只写len==0则会在当方阵长度为奇数时出现内存溢出的现象,而方阵长度为偶数则没有这种提示,这一点非常重要。

例2:文件树的打印

参看下面的代码:

import java.io.*;
class FileTree
{
public static void main(String[] args)
{
File dir=new File("G:\\Java学习资料");
try
{
System.setOut(new PrintStream("filetree.txt"));//数据重定向
}
catch (IOException e)
{
System.exit(0);
}
showFileTree(dir);
}
public static void showFileTree(File dir)
{
fileTree(dir,0);
}
private static void fileTree(File dir,int level)
{
if(level!=0)
{
fileLevel(level);
System.out.println(dir.getName());
}
if(dir.isDirectory())
{
File[] files=dir.listFiles();
for(File file:files)
fileTree(file,level+1);//进行递归
}
}
private static void fileLevel(int level)
{
System.out.print("|");
for(int i=1;i<level;i++)
System.out.print("---");
}
}


        这个程序中体现了递归另外需要注意的一点:递归函数参数的传递。体现文件层次的变量level是用参数列表的方式而不是函数内部的内部变量,用fileTree(file,level+1);而不是用fileTree(file,level++);等这些细节都是在进行递归时必须注意的问题:level+1能保证同层文件的深度变量的一致性,而level++;会进行深度的累加,导致同层文件深度不一致,破坏了同层文件深度数值的独立性;而level参数作为参数列表进行参数传递又保证了调用函数间的数据一致性。

例3:字符串全组合

       编程列出一个字符串的全字符组合情况,原始字符串中没有重复字符,例如:

       原始字符串是"abc",打印得到下列所有组合情况:
"a" "b" "c" 
"ab" "bc" "ca" "ba" "cb" "ac"
"abc" "acb" "bac" "bca" "cab" "cba"

/*
* 分析:首先如题中列出的三组情况,可看出各种组合按组合的长度看,共有n组(其中n为原始字符串的长度),而各组的排列情况本质上是很类似的,
*     故不难想到在功能函数中,可传入一个组合的长度参数,当然还要传入原始字符串。现在专门分析第m组,首先可在原始字符串中取出第一个字符,
*     第一个字符共有n种可能,故不难想到定义一个for循环,之后除去原始字符中第一个取走的字符,剩下n-1个字符,之后又得取m-1个字符,
*     这样便不难想到递归的思想了。
* 步骤:1.既然用到递归,首先定义一个单独的递归函数showSubstring,参数为原始字符串、组合的长度len以及用于存储这len个字符的StringBuilder对象;
*     2.由于是递归,故结束条件是必须确定的,不难想到结束条件就是存储完len个字符,故结束条件可为判断len是否为0;
*     3.用一个for循环来遍历原字符串,用参数StringBuilder对象来存储第一个字符,之后在字符串中除去该字符,进行下一个字符的存储,故递归传入的是除去已存入
*     StringBuilder中的字符之后的长度为n-1的字符串,以及len-1,表示在长度为n-1的字符串中,还要取len-1个字符;
*     4.递归函数基本写完,之后用另外一个函数displaySub,调用该递归函数,传入从1到n不同的长度参数;
* */
public class AllStrCollection {

/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
String str = "abc";
checkRepeatedChar(str);
displaySub(str);
}

/**
* 用于显示传入字符串的所有子串
* @param str 要显示所有子串的字符串
* */
public static void displaySub(String str){
//由于子串长度是从1到str.length(),所以这里用到了for循环
for(int len = 1; len <= str.length(); len++){
showSubstring(str, len, new StringBuilder());//显示长度为len的所有子串
System.out.println();
}
}

/**
* 用于显示字符串中固定长度的所有子串
* @param str 原字符串
* @param len 指定要显示len长度的所有子串
* @param store 用于存储长度为len的子串
* */
private static void showSubstring(String str, int len, StringBuilder store){
if(len == 0){//循环结束的条件,当StringBuilder中存够len个字符时,退出循环
System.out.print(store.toString()+ " ");//显示一个长度为len的子串
return;
}
char[] chs = str.toCharArray();//由字符串转换为字符数组
for(char c: chs){//foreach语句进行遍历
StringBuilder sb = new StringBuilder(str);//这个StringBuilder只用于为原字符串操作提供方便,方便后面删除里面的字符
StringBuilder sBuilder = new StringBuilder(store);//这个StringBuilder用于继承上层循环所保存的字符序列
sBuilder.append(c);//从上层循环所保留下来的字符序列的基础上,添加新的字符
sb.deleteCharAt(sb.indexOf(c + ""));//删除已添加的字符后,进行其他子串的存储
showSubstring(sb.toString(), len-1, sBuilder);//添加余下len-1个字符
}
}

/**
* 检验原字符串中是否有重复字符
* @param str 被检验的字符串
* */
public static void checkRepeatedChar(String str){
char[] chs = str.toCharArray();
//检验过程运用了选择排序的思想
for(int i = 0; i < chs.length-1; i++){
for(int j = i+1; j < chs.length; j++){
char c = chs[i];
if(c == chs[j])//若存在重复字符,则抛出异常
throw new RuntimeException("存在重复字符!");
}
}
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: