Java8函數式編程中的尾調用優化遞歸性能問題

這是我參與新手入門的第3篇文章。java

使用函數式編程求一個List的所有子集

實現方法來自《Java8 in Action》,思路是和其餘遞歸解決方法一致,但不一樣的地方在concat方法編程

public static List<List<Integer>> subsets(List<Integer> list){
		if(list.isEmpty()) {
			List<List<Integer>> ans = new ArrayList<>();
			ans.add(Collections.emptyList());
			return ans;
		}
		
		Integer first = list.get(0);
		List<Integer> rest = list.subList(1, list.size());
		
 		List<List<Integer>> subans = subsets(rest);
		List<List<Integer>> subans2 = insertAll(first,subans);
		return concat(subans,subans2);
	}
	
	
	public static List<List<Integer>> insertAll(Integer first,List<List<Integer>> lists){
		List<List<Integer>> result = new ArrayList<List<Integer>>();
		for (List<Integer> list : lists) {
			List<Integer> copyList = new ArrayList<Integer>();
			copyList.add(first);
			copyList.addAll(list);
			result.add(copyList);
		}
		return result;
	}
	
	public static List<List<Integer>> concat(List<List<Integer>> a,List<List<Integer>> b)            
   {
		List<List<Integer>> r = new ArrayList<List<Integer>>(a);
		r.addAll(b);
		return r;
	}
複製代碼

乍一看,concat方法也沒啥,可是這種寫法涉及到 解題思想的轉變markdown

在Java8中,最主要的改變就是函數式編程,它接受零個或多個參數,生成一個或多個結果,而且不會有任何反作用,這裏的反作用能夠理解爲對傳入參數/其餘數據源作了修改。併發

這裏稍稍擴展下,爲何函數式編程不修改數據源,在併發編程中,每每是對某一數據源進行多方修改,因此要用鎖,而鎖的維護是很難的,不少程序的bug都出在這裏。app

函數式編程就提出不修改數據源,這樣,出bug的可能性就不存在了。函數式編程默認變量是不可變的,若是要改變變量,須要把變量copy而後修改,就像上面的concat方法同樣。函數式編程

按照以往的思路concat方法可能會這麼寫:函數

public static List<List<Integer>> concat(List<List<Integer>> a,List<List<Integer>> b){
		//List<List<Integer>> r = new ArrayList<List<Integer>>(a);
		a.addAll(b);
		return a;
	}
複製代碼

尾調用優化解決遞歸性能問題

總體思路仍是遞歸那一套,使用遞歸必然帶來性能問題,甚至StackOverflowError。性能

因此,咱們一般是寫迭代而不是遞歸,可是Java8提倡用遞歸取代迭代,由於迭代每每會修改數據源,而函數式編程不修改。優化

那麼遞歸的性能問題怎麼解決?ui

先來講下什麼叫作尾調用。

尾調用就是在方法的最後調用。

這就是尾調。

public int getResult(int a,int b) {
	return  add(a,b);
}
複製代碼

而下面這兩個方法都不是尾調。

public int getResult(int a,int b) {
	return  add(a,b)+5;
}

public int getResult(int a,int b) {
	int c = add(a,b);
	return c;
}
複製代碼

我們再來回顧下遞歸性能問題產生的緣由:

函數調用會在內存中生成調用幀,用以保存位置和內部變量,直到程序徹底結束。遞歸一次就生成一個棧幀,若是輸入量很大,直接就StackOverflowError了。

​​

image.png 圖片來自阮一峯博客

拿階乘舉個例子:

日常的遞歸方法這麼寫:

public static int factorial(int n) {
	if(n==1) 
		return n;
	return n*factorial(n-1);
}
複製代碼

調用過程是這樣的:

image.png

把方法改爲尾調用是這樣的:

public static int factorial(int a,int n) {
	if(n==1) 
		return a;
	return factorial(a*n,n-1);
}
複製代碼

調用過程是這樣的:

image.png

尾調用,由於是 方法的最後一步調用,不須要保留以前的位置和狀態,直接用內層函數的調用記錄替換了外層調用記錄

相關文章
相關標籤/搜索