從一個題說起
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
}
}
首先題目要求返回的類型爲List<List<Integer>>,那麼我們就新建一個List<List<Integer>>作爲全局變量,最後將其返回。
class Solution {
List<List<Integer>> lists = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
return lists;
}
}
再看看返回的結構,List<List<Integer>>。因此我們需要寫一個包含List<Integer>的輔助函數,加上一些判斷條件,此時結構變成了
class Solution {
List<List<Integer>> lists = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if (candidates == null || candidates.length == 0 || target < 0) {
return lists;
}
List<Integer> list = new ArrayList<>();
process(candidates, target, list);
return lists;
}
private void process(int[] candidates, int target, List<Integer> list) {
}
}
重點就是如何進行遞歸。遞歸的第一步,當然是寫遞歸的終止條件啦,沒有終止條件的遞歸會進入死循環。那麼有 哪些終止條件呢?由於條件中說了都是正整數。因此,如果target<0,當然是要終止了,如果target==0,說明此時找到了一組數的和爲target,將其加進去。此時代碼結構變成了這樣。
class Solution {
List<List<Integer>> lists = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if (candidates == null || candidates.length == 0 || target < 0) {
return lists;
}
List<Integer> list = new ArrayList<>();
process(candidates, target, list);
return lists;
}
private void process(int[] candidates, int target, List<Integer> list) {
if (target < 0) {
return;
}
if (target == 0) {
lists.add(new ArrayList<>(list));
}
}
}
我們是要求組成target的組合。因此需要一個循環來進行遍歷。每遍歷一次,將此數加入list,然後進行下一輪遞歸。代碼結構如下。
class Solution {
List<List<Integer>> lists = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if (candidates == null || candidates.length == 0 || target < 0) {
return lists;
}
List<Integer> list = new ArrayList<>();
process(candidates, target, list);
return lists;
}
private void process(int[] candidates, int target, List<Integer> list) {
if (target < 0) {
return;
}
if (target == 0) {
lists.add(new ArrayList<>(list));
} else {
for (int i = 0; i < candidates.length; i++) {
list.add(candidates[i]);
//因爲每個數字都可以使用無數次,所以遞歸還可以從當前元素開始
process( candidates, target - candidates[i], list);
}
}
}
}
似乎初具規模,測試一把結果如下
結果差距有點大,爲何會出現如此大的反差。而且發現一個規律,後面的一個組合會包含前面一個組合的所有的數字,而且這些數加起來和target也不相等啊。原因出在哪呢?java中除了幾個基本類型,其他的類型可以算作引用傳遞。這就是導致list數字一直變多的原因。因此,在每次遞歸完成,我們要進行一次回溯。把最新加的那個數刪除。此時代碼結構變成這樣。
class Solution {
List<List<Integer>> lists = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if (candidates == null || candidates.length == 0 || target < 0) {
return lists;
}
List<Integer> list = new ArrayList<>();
process(candidates, target, list);
return lists;
}
private void process(int[] candidates, int target, List<Integer> list) {
if (target < 0) {
return;
}
if (target == 0) {
lists.add(new ArrayList<>(list));
} else {
for (int i = 0; i < candidates.length; i++) {
list.add(candidates[i]);
//因爲每個數字都可以使用無數次,所以遞歸還可以從當前元素開始
process( candidates, target - candidates[i], list);
list.remove(list.size() - 1);
}
}
}
}
再測一把,結果如下,
還是不對。這次加起來都等於7了,和上次結果相比算是一個很大的進步了。分析下測試結果。不難能看出,本次結果的主要問題包含了重複的組合。爲什麼會有重複的組合呢?因爲每次遞歸我們都是從0開始,所有數字都遍歷一遍。所以會出現重複的組合。改進一下,只需加一個start變量即可。 talk is cheap, show me the code。代碼如下。
List<List<Integer>> lists = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if (candidates == null || candidates.length == 0 || target < 0) {
return lists;
}
List<Integer> list = new ArrayList<>();
process(0, candidates, target, list);
return lists;
}
private void process(int start, int[] candidates, int target, List<Integer> list) {
//遞歸的終止條件
if (target < 0) {
return;
}
if (target == 0) {
lists.add(new ArrayList<>(list));
} else {
for (int i = start; i < candidates.length; i++) {
list.add(candidates[i]);
//因爲每個數字都可以使用無數次,所以遞歸還可以從當前元素開始
process(i, candidates, target - candidates[i], list);
list.remove(list.size() - 1);
}
}
}
最後梭哈一把。
代碼通過,但是效率並不高。本題有效果更好的動態規劃的解法。本文主要展示遞歸回溯,就不做具體介紹了。