Java基本功練習十一(遞歸與迭代【漢諾塔、文件大小的顯示、遞歸的輔助方法、尾遞歸】)

        遞歸是程序控制的一種代替形式,實質上就是不用循環控制的重複。程序每調用一個方法,系統就要給方法中所有的局部變量和參數分配空間,這就要佔用大量的內存,還需要額外的時間來處理這些附加的空間。任何用遞歸解決的問題,都可以用迭代非遞歸地解決,所以如果可以較容易迭代實現的,就不要用遞歸。

        遞歸與迭代的選擇:根據要解決的問題的本質和我們對這個問題的理解來決定是用遞歸還是用迭代。根據經驗,選擇使用遞歸還是迭代的原則,就是看它能否給出一個反映問題本質的直觀解法。如果迭代的方案是顯而易見的,那就使用迭代。

        示例一:通過簡單的菲波那契數列的遞歸實現,並以圖片展示其實現過程。以此說明內存的耗用情況嚴重,所以如果不是很難實現的方法,最好不要用遞歸,而用迭代去實現。

實現的源代碼如下:

package Blog;

import java.util.Scanner;

public class blogTryProject{
	public static void main(String[]args){
		Scanner input = new Scanner(System.in);
		System.out.print("Enter a nonnegative integer: ");
		int n = input.nextInt();
		System.out.println("Fibonacci of "+n+" is "+fib(n));
	}
	public static long fib(int n){
		if(n == 0)
			return 0;
		else if(n == 1)
			return 1;
		else
			return fib(n -1) + fib(n - 2);
	}
	
}
實現fib(4)的調用過程如下圖所示:



        Java中,對操作數是從左到右計算的。所以調用Fibonacci數列時,先計算左邊的子調用直到返回最後結果,再計算右邊的子調用。從上圖可以看到,調用過程比較繁瑣,並且調用過程沒有結束,其佔用的棧內存不會被釋放,如果調用過程很多,有可能造成棧的溢出!實際上,菲波那契數列可以用循環迭代的方式很容易的實現。

        示例二:文件大小的顯示。運行程序,輸入想要計算文件大小的地址,如c:\Program Files\alipay,然後顯示此文件或文件夾的大小。

運行效果如圖所示:

        分析:此題如果使用迭代,或是其他方法,比較難實現,如果用遞歸的方法,則可以比較直觀的將方法寫出來。本問題是求出一個目錄的大小。一個目錄的大小是指該目錄下所有文件大小之和。目錄d可能會包含子目錄。假設一個目錄包含文件f1,f2,...,fm以及子目錄d1,d2,...,dn,則可以將文件大小遞歸的表示爲:size(d)=size(f1)+size(f2)+...size(fm)+size(d1)+...size(dn)。File類可以用來表示一個文件或一個目錄,並且獲取文件和目錄的屬性。如下是實現代碼:

package Blog;

import java.util.Scanner;
import java.io.File;

public class blogTryProject{
	public static void main(String[]args){
		System.out.print("Enter a directory or a file: ");
		Scanner input = new Scanner(System.in);
		String directory = input.nextLine();
		System.out.println(getSize(new File(directory))+" bytes");
	}
	public static long getSize(File file){
		long size = 0;
		if(file.isDirectory()){
			File[] files = file.listFiles();
			for(int i = 0;i < files.length;i++)
				size += getSize(files[i]);//遞歸調用
		}
		else
			size += file.length();
		return size;
	}
	
}
        示例三:漢諾塔的實現。漢諾塔是一個用非遞歸調用很難實現的例子。但是用遞歸,則思路清晰,代碼書寫容易。在這種情況下選擇用遞歸是明智的選擇。


編寫程序實現:藉助C將n個盤子從A移動到B上去。並將移動的步驟顯示出來。

運行效果如圖所示:
實現的源代碼如下:
package Blog;

import java.util.Scanner;
import java.io.File;

public class blogTryProject{
	public static void main(String[]args){
		Scanner input = new Scanner(System.in);
		System.out.print("Enter number of disks: ");
		int n = input.nextInt();
		System.out.println("The moves are:");
		moveDisks(n,'A','B','C');
	}
	public static void moveDisks(int n,char fromTower,char toTower,char auxTower){
		if(n == 1)
			System.out.println("Move disk "+n+" form "+fromTower+" to "+toTower);
		else{
			moveDisks(n - 1,fromTower,auxTower,toTower);
			System.out.println("Move disk "+n+" from "+fromTower+" to "+toTower);
			moveDisks(n - 1,auxTower,toTower,fromTower);
		}
	}
	
}

        雖然遞歸調用很佔用內存,在很多難以直接實現的場合,不得不用遞歸調用,這是無法避免的,但是我們可以儘量採用別的方法去減小內存耗用的影響。比如遞歸的輔助方法和尾遞歸。
        示例四:遞歸的輔助方法。以二分查找法的實現爲例。

運行效果如圖:

實現源代碼如下:

package Blog;

import java.util.Scanner;
import java.io.File;

public class blogTryProject{
	public static void main(String[]args){
		Scanner input = new Scanner(System.in);
		System.out.print("Enter the number of num : ");
		int n = input.nextInt();
		int[] num = new int[n];
		num = create(n);
		java.util.Arrays.sort(num);
		for(int u:num)
			System.out.print(u+" ");
		System.out.println();
		int key = num[n-2];
		int position = BinarySearch(num,key);//調用二分查找
		System.out.println(key+" 在數組中的位置是 "+position);
	}
	public static int BinarySearch(int[] num,int key){
		return BinarySearch(num,key,0,num.length - 1);//產生輔助的方法
	}
	public static int BinarySearch(int[] num,int key,int low,int high){//輔助方法的實現
		if(low > high)
			return - low - 1;
		int mid = (low + high) / 2;
		if(key < num[mid])
			return BinarySearch(num,key,low,mid - 1);
		else if(key == num[mid])
			return mid;
		else
			return BinarySearch(num,key,mid + 1,high);
	}
	public static int[] create(int n){
		int[] num = new int[n];
		Scanner input = new Scanner(System.in);
		for(int i = 0;i < num.length;i++)
			num[i] = (int)(Math.random()*100);
		return num;
	}
	
}

        遞歸的輔助方法:重載一個遞歸實現的方法,第一個方法實現功能的形式,第二個方法增加標誌位(輔助參數)來具體實現問題。這是一個很好的遞歸實現的方法,使得遞歸更加高效。

        示例五:尾遞歸。以階乘的實現爲例。

通常如果用遞歸來實現階乘算法,那麼很自然的就能寫出如下所示源代碼:

public static int jiecheng(int n){
		if(n == 0 || n== 1)
			return 1;
		else
			return n * jiecheng(n - 1);
	}

上述代碼的缺點就是每調用一次jiecheng函數都要創建新的棧內存空間去保存調用點的相關變量,很耗用內存。如果使用尾遞歸可以節省不少內存。

        尾遞歸:如果從遞歸調用返回時沒有待定的操作(如本例的jiecheng(n - 1))要完成,那麼這個遞歸方法就稱爲尾遞歸。

        某些編譯器可以優化尾遞歸以減小棧空間。可以使用輔助參數將非尾遞歸方法轉換爲遞歸方法。使用這些參數來控制結果,思路是將待定的操作和輔助參數以一種遞歸調用不再有待定操作的形式相結合。可能會定義一個帶輔助參數的新的輔助遞歸方法,這個方法可以重載名字相同但簽名不同的原始方法。

階乘的尾遞歸實現源代碼如下所示:

package Blog;

import java.util.Scanner;
import java.io.File;

public class blogTryProject{
	//尾遞歸的形式來實現階乘
		public static void main(String[]args){
			System.out.println("Enter the n for factorial :");
			Scanner input = new Scanner(System.in);
			int n = input.nextInt();
			long result = factorial(n,1);
			System.out.println(result);
		}
		public static long facotrial(int n){
			return factorial(n,1);
		}
		public static long factorial(int n,int result){
			if(n == 0)
				return result;
			else
				return factorial(n - 1,n * result);
		}
	
}


         總結:1)如果一個方法很難直接去實現,那麼用遞歸,並考慮是否可以用輔助參數或尾遞歸的形式減輕棧內存                          的壓力;

                    2)如果能夠較容易的使用迭代實現(如菲波那契可以很容易的用循環實現),就不要使用遞歸!

                    3)原則上,任何用遞歸解決的問題,都可以用迭代非遞歸地解決。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章