被一塊石頭絆倒兩次是真的丟人——不寫引子不舒服斯基
遞歸思想
遞歸的思想中有幾個很重要的特性,對於使用遞歸求解的問題,把握好這幾個因素就能把代碼寫好了。
1. 終止條件。遞歸的方法有一個最終的終止條件,這個條件滿足題目所提出的要求,而且不會無限的循環下去。
2. 子問題。問題中的子問題可以再次遞歸調用方法求解。這就是分治思想的運用了。在沒有滿足返回條件之前,每一步和前一步都只是在狀態上有不同。
不帶重複元素的
題目
給定一個數字列表,返回其所有可能的排列。
注意事項
你可以假設沒有重複數字。
樣例
給出一個列表[1,2,3],其全排列爲:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
解題思路
不帶重複元素這個題是很早之前做的了。思路也大概就是寫一個遞歸方法,在方法中判斷是否已經全排列,是的話集合加一併返回,不是的話就循環。
很簡單的遞歸全排列思路,甚至可以直接點地記下來。
public class Permute {
public static List<List<Integer>> permute(int[] nums){
List<List<Integer>> res = new ArrayList<>();
ArrayList<Integer> list = new ArrayList<>();
per(res,list,nums);
return res;
}
public static void per(List res,List list,int[] nums){
if(list.size() == nums.length){
res.add(new ArrayList(list));
return ;
}
for(int i = 0;i < nums.length; i++){
if(list.contains(nums[i]))
continue;
list.add(nums[i]);
per(res,list,nums);
list.remove(list.size()-1);
}
}
}
帶重複元素的
題目
給出一個具有重複數字的列表,找出列表所有不同的排列。
給出列表 [1,2,2],不同的排列有:
[
[1,2,2],
[2,1,2],
[2,2,1]
]
遞歸解法
和上面的解法基本上一樣,只是要多加一個visited數組來記錄這個數據在這一次逐漸深入的遞歸中是否被使用過,遞歸返回時改回未被使用。因爲允許列表元素重複,所以進行一次普通的全排列後,可能會出現兩個相同的排列,爲了避免出現重複的排列,所以要進行一次contains的判斷。
public class PermuteUnique {
public List<List<Integer>> permuteUnique(int[] nums){
List<List<Integer>> ret = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
int[] visited = new int[nums.length];
permute(0,nums.length,ret,temp,nums,visited);
return ret;
}
/** * 用來做遞歸的子方法,可以當模版參考 * * @param k 當前長度 * @param n 每個的長度,nums.length * @param ret 返回的那個list * @param temp 每一次遞歸用的list * @param nums 初始數組 * @param visited 元素是否被訪問過 */
public void permute(int k,int n,List ret,List temp,int[] nums,int[] visited){
if(k == n && !ret.contains(temp)){
ret.add(new ArrayList(temp));
}
if(k > n){
return;
}
else{
for(int i = 0;i < n;i++){
int pre = nums[i];
if(visited[i] == 0){
temp.add(pre);
visited[i] = 1;
permute(k + 1,n,ret,temp,nums,visited);
visited[i] = 0;
temp.remove(temp.size() - 1);
}
}
}
}
}
這一份代碼中還有一些地方是可以優化的,比如那個參數n就是完全多於的東西,按照上一份代碼其實這個參數是可以不用的,包括k也是,明明就是一個list.size的事,但是當時寫代碼的時候本着跑起來再說的想法,沒有考慮這些問題,雖然之後發現了,但是本着提醒自己的想法,就把沒改的版本貼到了這個博客中。
非遞歸解法
這個,偷個懶,以後再說。
指針問題
說到指針,按理說學了這麼幾年已經不應該再在這種事上糊塗了,但是還是犯了錯,做之前全排列的那個題時愣是沒想通,好在今天做這個題的時候反應過來了,所以藉此機會記錄一下。
上面的遞歸代碼中,當排序好的List要放入總的集合中時,之前的我是這樣操作的。
if(k == n && !ret.contains(temp)){
//stupid
ret.add(temp);
}
用這種add,執行後你會發現結果會大錯特錯。而應該換用:
if(k == n && !ret.contains(temp)){
ret.add(new ArrayList(temp));
}
對,沒錯。
使用上面那個錯誤操作所存進集合中的並不是一個對象,而是指向這個List對象的指針。這就引起了問題:add進集合中的永遠是同一個元素,而且,在帶重複元素這道題中,需要進行一次是否已經contains的判斷,這個會一直爲true,讓你甚至連重複元素都加不進去。寫代碼驗證一下這個想法:
List<List<Integer>> ret = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
temp.add(5);
ret.add(temp);
System.out.println(ret.contains(temp));
temp.add(7);
System.out.println(ret.contains(temp));
ret.add(temp);
Iterator it = ret.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
執行結果爲:
true
true
[5, 7]
[5, 7]
證明確實如此。所以每次進行add操作時,要new一個新的對象用來存放已經排好序的部分。
整體感想
多看,多寫,多總結。