回溯算法
回溯理论
回溯法也可以叫做回溯搜索法,它是一种搜索的方式,回溯是递归的副产品,只要有递归就会有回溯。
回溯法解决的问题都可以抽象为树形结构,因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。
大家可以从图中看出for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果了。
一般模板如下:
void backtracking(参数){
if(终止条件){
存放结果;
return;
}
for(选择:本层集合中元素(树中节点孩子的数量就是集合的大小)){
处理节点;
backtracking(路径,选择列表);//递归
回溯,撤销处理结果;
}
}
时间复杂度
子集问题分析:
- 时间复杂度:O(n × 2n),因为每一个元素的状态无外乎取与不取,一共2n种状态,所以时间复杂度为O(2^n),构造每一组子集都需要填进数组,又有需要O(n),最终时间复杂度:O(n × 2^n)。
- 空间复杂度:O(n),递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)。
排列问题分析:
- 时间复杂度:O(n!),这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * … 1 = n!。每个叶子节点都会有一个构造全排列填进数组的操作(对应的代码:
result.push_back(path)
),该操作的复杂度为O(n)。所以,最终时间复杂度为:n * n!,简化为O(n!)。 - 空间复杂度:O(n),和子集问题同理。
组合问题分析:
- 时间复杂度:O(n × 2^n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
- 空间复杂度:O(n),和子集问题同理。
一般说道回溯算法的复杂度,都说是指数级别的时间复杂度,算是一个概括。
77. 组合
递归传入结果和操作数组
通过for循环每层从左到右遍历nums数组,添加到结果集result中。
nums数组中删除此时遍历的节点。
将此nums数组的复制传入子函数递归,因为子函数中节点的删除不能影响父函数。
递归深度为k,每次递归时间复杂度O(n(n+k))
时间复杂度O(k*n^2) = O(n^2),2ms,69%
class Solution {
//Cnk
List<List<Integer>> answer;
public List<List<Integer>> combine(int n, int k) {
//初始化结果集
answer = new ArrayList<>();
List<Integer> result = new ArrayList<>();
//构造nums数组,从1到n方便遍历
List<Integer> allnums = new ArrayList<>();
for(int i = 1; i<=n;i++){
allnums.add(i);
}
//调用递归函数
findCombine(allnums,result,k);
return answer;
}
private void findCombine(List<Integer> nums, List<Integer> result, int k){
//k=0,说明结果集添加完毕,返回
if(k <= 0){
return;
}
//每层,从左到右遍历,又因为是组合,所以将前面遍历的节点删除即可
//t不用动,删除头节点即可
for(int t = 0; nums.size() >= k;){
int addnum = nums.get(t);
result.add(addnum);//此结果添加
nums.remove(t);//删除此遍历过的节点
//传入nums的复制,因为下层的删除不能干预上层。
List<Integer> newNums = new ArrayList<>(nums);
findCombine(newNums,result,k-1);
//如果在最底层,将此result装入结果集
if(k == 1){
List<Integer> temp = new ArrayList<>(result);
answer.add(temp);
}
//result,进入下一个数。
result.remove(result.size()-1);
}
}
}
递归传入结果和操作下标
因为第一种方法在递归时需要传入nums的复制,涉及对nums的频繁构造,每层递归相对耗时。
可以采用传入下标,递归传入从nums的哪一个下标开始操作,省去了nums的复制。
递归深度依然是k,时间复杂度O(k^2*n)=O(n),1ms,99.97%
class Solution {
//Cnk
List<List<Integer>> answer;
public List<List<Integer>> combine(int n, int k) {
answer = new ArrayList<>();
List<Integer> allnums = new ArrayList<>();
for(int i = 1; i<=n;i++){
allnums.add(i);
}
List<Integer> result = new ArrayList<>();
findCombine(allnums,result,k,0);
return answer;
}
private void findCombine(List<Integer> nums, List<Integer> result, int k, int begin){
if(k <= 0){
return;
}
for(int t = begin; t < nums.size()-k+1;t++){
int addnum = nums.get(t);
result.add(addnum);
//传入下个递归开始的操作数下标
findCombine(nums,result,k-1,t+1);//隐藏回溯
//如果在最底层,将此result装入结果集
if(k == 1){
List<Integer> temp = new ArrayList<>(result);
answer.add(temp);
}
//result,进入下一个数
result.remove(result.size()-1);
}
}
}
优化
将result也设为全局变量,少传入一个参数,将叶子节点判断条件加入k=0里,简洁多了。
class Solution {
//Cnk
List<List<Integer>> answer = new ArrayList<>();;
List<Integer> result = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
List<Integer> allnums = new ArrayList<>();
for(int i = 1; i<=n;i++){
allnums.add(i);
}
findCombine(allnums,k,0);
return answer;
}
private void findCombine(List<Integer> nums, int k, int begin){
//如果在最底层,将此result装入结果集
if(k <= 0){
answer.add(new ArrayList<>(result));
return;
}
for(int t = begin; t < nums.size()-k+1;t++){
result.add(nums.get(t););
//传入下个递归开始的操作数下标
findCombine(nums,k-1,t+1);//隐藏回溯
//result,进入下一个数
result.remove(result.size()-1);
}
}
}
代码随想录版本
思想跟传入下标一样,既然nums不会改变,而且是从1到n递增的,那么其实nums.get(t) = t,可以不用nums数组,而且上个版本其实 k = 原k - result.size() ; 也可以让k不变,根据result大小跟k的关系判断叶子节点。
将下标版本改造下可得
class Solution {
List<List<Integer>> answer = new ArrayList<>();
LinkedList<Integer> result = new LinkedList<>();//removelast方便
public List<List<Integer>> combine(int n, int k) {
//不用nums[t]了,直接startIndex = 1开始;
combineHelper(n, k, 1);
return answer;
}
/**
* 每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠startIndex
* @param startIndex 用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )。
*/
private void combineHelper(int n, int k, int startIndex){
//终止条件,k不变,根据result判断
if (result.size() == k){
answer.add(new ArrayList<>(result));
return;
}
//如果传入的个数小于还需要填的个数,不符。剪枝操作
for (int i = startIndex; i <= n - (k - result.size()) + 1; i++){
//直接添加i即可
result.add(i);
combineHelper(n, k, i + 1);
result.removeLast();
}
}
}
216.组合总和III
递归回溯
把组合加入结果集的条件加个判断,即可。0ms,100%,剪枝判断如下。
class Solution {
List<List<Integer>> answer = new ArrayList<>();
LinkedList<Integer> result = new LinkedList<>();//removelast方便
Integer sum = 0;
public List<List<Integer>> combinationSum3(int k, int n) {
//combineHelper(9,k,1,n);
combineHelper(k,1,n);
return answer;
}
private void combineHelper(int k, int startIndex,int target){
//终止条件,k不变,根据result判断
if (result.size() == k){
if(sum == target) answer.add(new ArrayList<>(result));
return;
}
//如果传入的个数小于还需要填的个数,不符。剪枝操作
for (int i = startIndex; i <= 9 - (k - result.size()) + 1 && sum<=target; i++){
//直接添加i即可
result.add(i);
sum = sum + i;
combineHelper( k, i + 1,target);
sum = sum - i;
result.removeLast();
}
}
}
代码随想路版本
多传了一个当时sum的值。基本一样
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
backTracking(n, k, 1, 0);
return result;
}
private void backTracking(int targetSum, int k, int startIndex, int sum) {
// 减枝
if (sum > targetSum) {
return;
}
if (path.size() == k) {
if (sum == targetSum) result.add(new ArrayList<>(path));
return;
}
// 减枝 9 - (k - path.size()) + 1
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) {
path.add(i);
sum += i;
backTracking(targetSum, k, i + 1, sum);
//回溯
path.removeLast();
//回溯
sum -= i;
}
}
}
17.电话号码的字母组合
递归回溯
还是组合问题,改变k和n的表现形式即可。0ms,100%。
class Solution {
StringBuilder path = new StringBuilder();
List<String> result = new ArrayList<>();
public List<String> letterCombinations(String digits) {
if(digits.isEmpty()){
return result;
}
char[] c = digits.toCharArray();
combinateHelper(c,0);
return result;
}
public void combinateHelper(char[] s,int startIndex){
if(startIndex > s.length -1){
result.add(path.toString());
return;
}
//设置起始值与遍历数
char num = s[startIndex];
char begin ='a';
int length = 0;
if(num == '7'){
begin = 'p';
length = 3;
}else if(num == '9'){
begin = 'w';
length = 3;
}else if(num == '8'){
begin = 't';
length = 2;
}else{
begin = (char)((num-'2')* 3 + 'a');
length = 2;
}
//递归算法
for(char t = begin; t<= begin+length; t++){
path.append(t);
combinateHelper(s,startIndex+1);
path.deleteCharAt(path.length()-1);
}
}
}
代码随想录版本
使用一个String数组来对应2-9的情况,对于0,1误输入不进入循环,增加了异常情况处理。
class Solution {
//设置全局列表存储最后的结果
List<String> list = new ArrayList<>();
//每次迭代获取一个字符串,所以会设计大量的字符串拼接,所以这里选择更为高效的 StringBuild
StringBuilder temp = new StringBuilder();
public List<String> letterCombinations(String digits) {
if (digits == null || digits.length() == 0) {
return list;
}
//初始对应所有的数字,为了直接对应2-9,新增了两个无效的字符串""
String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
//迭代处理
backTracking(digits, numString, 0);
return list;
}
//比如digits如果为"23",num 为0,则str表示2对应的 abc
public void backTracking(String digits, String[] numString, int num) {
//遍历全部一次记录一次得到的字符串
if (num == digits.length()) {
list.add(temp.toString());
return;
}
//str 表示当前num对应的字符串
String str = numString[digits.charAt(num) - '0'];
for (int i = 0; i < str.length(); i++) {
temp.append(str.charAt(i));
//c
backTracking(digits, numString, num + 1);
//剔除末尾的继续尝试
temp.deleteCharAt(temp.length() - 1);
}
}
}
39. 组合总和
递归回溯
同一数字可多次被选择,所以下一层的开始索引一样,并且可由任意个数字构成,所以每次都判断path。
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
Integer sum = 0;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
combinateHelper(candidates,target,0);
return result;
}
private void combinateHelper(int[] candidates,int target,int startIndex){
if(sum > target){
return;
}
if(sum == target){
result.add(new ArrayList<Integer>(path));
return;
}
for(int i = startIndex; i< candidates.length;i++){
path.add(candidates[i]);
sum = sum + candidates[i];
combinateHelper(candidates,target,i);
sum = sum - candidates[i];
path.removeLast();
}
}
}
代码随想录
还可以进行剪枝操作,对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历。
// 剪枝优化
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
Integer sum = 0;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates); // 先进行排序
backtracking(candidates, target, 0);
return result;
}
public void backtracking(int[] candidates, int target, int idx) {
// 找到了数字和为 target 的组合
if (sum == target) {
result.add(new ArrayList<>(path));
return;
}
// 如果 sum + candidates[i] > target 就终止遍历
for (int i = idx; i < candidates.length && sum + candidates[i]<=target; i++) {
path.add(candidates[i]);
sum = sum + candidates[i];
//如果用以下隐式回溯target,就不用sum了。
//backtracking(candidates, target - candidates[i], i+1);
backtracking(candidates, target, i);
sum = sum - candidates[i];
path.remove(path.size() - 1); // 回溯,移除路径 path 最后一个元素
}
}
}
40.组合总和II
递归回溯
Cause Each number in candidates
may only be used once in the combination. 所以下次的递归需要使用i+1,又因为不能出现重复的组合,所以要去重。2ms,99,75%
去重的方法我使用的是
- 先进行排序,在横向遍历时,如果一个数已经组合遍历完毕。[1,2,2,2,3],target = 8;
- 当下一个数字与此数字相等时,他就不用再进入递归组合了,直接continue跳过。
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
Integer sum = 0;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates); // 先进行排序
backtracking(candidates, target, 0);
return result;
}
public void backtracking(int[] candidates, int target, int idx) {
// 找到了数字和为 target 的组合
if (sum == target) {
result.add(new ArrayList<>(path));
return;
}
// 如果 sum + candidates[i] > target 就终止遍历
for (int i = idx; i < candidates.length && sum + candidates[i]<=target; i++) {
if(i>idx && candidates[i] == candidates[i-1]){
continue;
}
path.add(candidates[i]);
sum = sum + candidates[i];
backtracking(candidates, target, i+1);
sum = sum - candidates[i];
path.remove(path.size() - 1); // 回溯,移除路径 path 最后一个元素
}
}
}
131.分割回文串
递归回溯
8ms,60%
使用StringBuilder记录从startIndex到此index的字符串,与组合类似,把startIndex前的部分切割保存而不是只用一个位置。
class Solution {
LinkedList<String> path = new LinkedList<>();
List<List<String>> result = new ArrayList<>();
public List<List<String>> partition(String s) {
char[] c = s.toCharArray();
backtracking(c,0);
return result;
}
private void backtracking(char[] charArray, int startIndex) {
if (startIndex == charArray.length) {
result.add(new ArrayList<>(path));
return;
}
StringBuilder builder = new StringBuilder();
// 如果startIndex到i是回文数,继续
for (int i = startIndex; i < charArray.length; i++) {
//在检查回文数之前,因为即使此时不满足,也要把这时候的字母加到字符串上。
builder.append(charArray[i]);
//String builder = new String(charArray,startIndex,i-startIndex+1);
if(!checkPalindrome(charArray,startIndex,i)){
continue;
}
path.add(builder.toString());
backtracking(charArray, i+1);
path.removeLast(); // 回溯,移除路径 path 最后一个元素
}
}
//左闭右闭,检查是否为回文数
private boolean checkPalindrome(char[] charArray,int startIndex,int endIndex){
for(int i = startIndex ; i <= (startIndex+endIndex)/2;i++){
if(charArray[i] != charArray[endIndex - i + startIndex]){
return false;
}
}
return true;
}
}
代码随想录
6-7ms,80-99%
例如对于字符串abcdef:
- 组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个…。
- 切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段…。
所以切割问题,也可以抽象为一棵树形结构,如图:
回文数判断时会产生重复判断的问题。
比如 aab,当分割前两个字符aa时,有两种分割的方法:[aa] 和 [a,a],当这两种分割方法遇到b时,都需要再判断aab是否是回文串,产生了重复计算,所以可以将字符串S的每个子串是否是回文串进行预处理,再进行判断时就不用进行计算了。
所以回文串预处理可用如下方法
boolean[][] f;
public List<List<String>> partition(String s) {
int n = s.length();
f = new boolean[n][n];
for (int i = 0; i < n; ++i) {
Arrays.fill(f[i], true);
}
for (int i = n - 1; i >= 0; --i) {
for (int j = i + 1; j < n; ++j) {
f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i + 1][j - 1];
}
}
}
也可以使用记忆化搜索,求过回文串的都记住
// 记忆化搜索中,f[i][j] = 0 表示未搜索,1 表示是回文串,-1 表示不是回文串
public int isPalindrome(String s, int i, int j) {
if (f[i][j] != 0) {
return f[i][j];
}
if (i >= j) {
f[i][j] = 1;
} else if (s.charAt(i) == s.charAt(j)) {
f[i][j] = isPalindrome(s, i + 1, j - 1);
} else {
f[i][j] = -1;
}
return f[i][j];
}
优化后的代码
class Solution {
LinkedList<String> path = new LinkedList<>();
List<List<String>> result = new ArrayList<>();
boolean[][] f;
public List<List<String>> partition(String s) {
char[] c = s.toCharArray();
int n = s.length();
//预处理回文判断
f = new boolean[n][n];
for (int i = 0; i < n; ++i) {
Arrays.fill(f[i], true);
}
for (int i = n - 1; i >= 0; --i) {
for (int j = i + 1; j < n; ++j) {
f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i + 1][j - 1];
}
}
//回溯
backtracking(c,0);
return result;
}
private void backtracking(char[] charArray, int startIndex) {
if (startIndex == charArray.length) {
result.add(new ArrayList<>(path));
return;
}
// 如果startIndex到i是回文数,继续
for (int i = startIndex; i < charArray.length; i++) {
//在检查回文数之前,因为即使此时不满足,也要把这时候的字母加到字符串上。
if(f[startIndex][i]){
path.add(new String(charArray,startIndex,i-startIndex+1));
backtracking(charArray, i+1);
path.removeLast(); // 回溯,移除路径 path 最后一个元素
}
}
}
}
时间复杂度为O(n*2^n)。
空间复杂度:递归深度O(n),数组f需要O(n2),所以空间复杂度O(n2)。
93.复原IP地址
递归回溯
其实跟Cn4差不多,需要四个数,也可看作切割成四段,使用path存储路径。2ms,77.31%
class Solution {
List<String> result = new ArrayList<>();
LinkedList<String> path = new LinkedList<>();
int n;
//相当于Cn4
public List<String> restoreIpAddresses(String s) {
n = s.length();
char[] c = s.toCharArray();
backTracking(c,4,0);
return result;
}
private void backTracking(char[] c,int k,int startIndex){
//remain表示剩下的数字个数
//k表示还需要几段,3k是最大能占个数,k是最少需要个数,感觉是更精准的剪枝
int remain = n - startIndex;
if(k * 3 < remain || k > remain){
return;
}
//当遍历到队尾,且已经切割四段后。
if(k == 0 && n == startIndex){
//将path里的String路径组装一下。
StringBuilder temp = new StringBuilder();
int i;
for(i = 0 ;i< path.size()-1;i++){
temp.append(path.get(i)+".");
}
temp.append(path.get(i));
result.add(temp.toString());
//result.add(String.join(".",path));
return;
}
StringBuilder builder = new StringBuilder();
for(int i = startIndex; i< n;i++){
//先把访问的放入builder,再判断是否符合。
builder.append(c[i]);
String temp = builder.toString();
//此builder不符合,之后也不用再遍历了,比如02时跳过027,0277
if(!check(temp)){
break;
}
//符合条件,加入path,将子集再次进入递归
path.add(temp);
backTracking(c,k-1,i+1);
path.removeLast();
}
}
//检查是否以0作为开头,是否数字大于255
private boolean check(String s){
if(s.length() > 1 && s.charAt(0) == '0'){
return false;
}
int num = Integer.valueOf(s);
if(num>255){
return false;
}
return true;
}
}
代码随想录
把k变为了记录点数的number,记录到3个时直接判断后面的数字是否符合。7ms,77.31%
class Solution {
List<String> result = new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
if (s.length() > 12) return result; // 算是剪枝了
backTrack(s, 0, 0);
return result;
}
// startIndex: 搜索的起始位置, pointNum:添加逗点的数量
private void backTrack(String s, int startIndex, int pointNum) {
if (pointNum == 3) {// 逗点数量为3时,分隔结束
// 判断第四段⼦字符串是否合法,如果合法就放进result中
if (isValid(s,startIndex,s.length()-1)) {
result.add(s);
}
return;
}
for (int i = startIndex; i < s.length(); i++) {
if (isValid(s, startIndex, i)) {
s = s.substring(0, i + 1) + "." + s.substring(i + 1); //在str的后⾯插⼊⼀个逗点
pointNum++;
backTrack(s, i + 2, pointNum);// 插⼊逗点之后下⼀个⼦串的起始位置为i+2
pointNum--;// 回溯
s = s.substring(0, i + 1) + s.substring(i + 2);// 回溯删掉逗点
} else {
break;
}
}
}
// 判断字符串s在左闭⼜闭区间[start, end]所组成的数字是否合法
private Boolean isValid(String s, int start, int end) {
if (start > end) {
return false;
}
if (s.charAt(start) == '0' && start != end) { // 0开头的数字不合法
return false;
}
int num = 0;
for (int i = start; i <= end; i++) {
if (s.charAt(i) > '9' || s.charAt(i) < '0') { // 遇到⾮数字字符不合法
return false;
}
num = num * 10 + (s.charAt(i) - '0');
if (num > 255) { // 如果⼤于255了不合法
return false;
}
}
return true;
}
}
78.子集
递归回溯
子集是收集树形结构中树的所有节点的结果。0ms,100%
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
backTracking(nums,0);
return result;
}
private void backTracking(int[] nums, int startIndex){
result.add(new ArrayList(path));//遍历这个树的时候,把所有节点都记录下来,就是子集集合。
if(startIndex == nums.length){//终止条件可不加,因为进入循环也不符合
return;
}
for(int i = startIndex; i< nums.length;i++){
path.add(nums[i]);
backTracking(nums,i+1);
path.removeLast();
}
}
}
90.子集II
递归回溯
跟组合一样,所以题解与组合总和II一致,需要去重,通过排序把相同元素放在一起,然后当后值与前值相同则continue跳过。
class Solution {
LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
backTracking(nums,0);
return result;
}
private void backTracking(int[] nums,int startIndex){
result.add(new ArrayList<>(path));
for(int i = startIndex; i < nums.length; i++){
if(i> startIndex && nums[i] == nums[i-1]){
continue;
}
path.add(nums[i]);
backTracking(nums,i+1);
path.removeLast();
}
}
}
491.递增子序列
递归回溯
LinkedList
- getLast,如果列表为空返回NoSuchElementException
- peekLast,如果列表为空返回null
因为此题nums顺序有关,不能改变,所以不能sort去重,尝试使用set去重。10ms,8.62%
class Solution {
LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> result = new ArrayList<>();
Set<List<Integer>> check = new HashSet<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backTracking(nums,0);
return result;
}
private void backTracking(int[] nums, int startIndex){
if(path.size() >= 2){
List<Integer> temp = new ArrayList<>(path);
if(check.add(temp)){
result.add(temp);
}
}
for(int i = startIndex; i< nums.length; i++){
Integer max = path.peekLast();
if(max != null && nums[i] < max){
continue;
}
path.add(nums[i]);
backTracking(nums,i+1);
path.removeLast();
}
}
}
代码随想录
每次添加结果时都判断HashSet中是否存在同样的List,这个操作太过于复杂,一层的时间复杂度即为O(n)。
其实可以在每层判断下此时取得值之前取过没有,如果取过则跳过,这样时间复杂度缩小到O(1)。6ms,29%
class Solution {
LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backTracking(nums,0);
return result;
}
private void backTracking(int[] nums, int startIndex){
if(path.size() >= 2){
result.add(new ArrayList<>(path));
}
Set<Integer> check = new HashSet<>();
for(int i = startIndex; i< nums.length; i++){
Integer max = path.peekLast();
if((max != null && nums[i] < max) || !check.add(nums[i])){
continue;
}
path.add(nums[i]);
backTracking(nums,i+1);
path.removeLast();
}
}
}
优化
其实当每层符合条件的值往里面添加时,需要检查此值是否被使用过。当set频繁的add,而且需要做hash映射的时候,相对耗时。
因为-100 <= nums[i] <= 100
,所以可以用大小为201的数组来存储值是否被使用过,而不用Set,降低了时间复杂度。4ms,91.95%
class Solution {
LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backTracking(nums,0);
return result;
}
private void backTracking(int[] nums, int startIndex){
if(path.size() > 1){
result.add(new ArrayList<>(path));
}
boolean[] used = new boolean[201];
for(int i = startIndex; i< nums.length; i++){
Integer max = path.peekLast();
if( (max != null && nums[i] < max) || (used[nums[i]+100] == true)){
continue;
}
used[nums[i]+100] = true;
path.add(nums[i]);
backTracking(nums,i+1);
path.removeLast();
}
}
}
46. 全排列(Permutations)
递归回溯
通过used数组传递上一层所用过的数字,以免重复使用。
class Solution {
LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
int[] used = new int[6];//new int[nums.length];
backTracking(nums,used);
return result;
}
private void backTracking(int[] nums,int[] used){
if(path.size() == nums.length){
result.add(new ArrayList<>(path));
return;
}
for(int i = 0; i< nums.length;i++){
if(used[i] == 1){
continue;
}
used[i] = 1;
path.add(nums[i]);
backTracking(nums,used);
used[i] = 0;
path.removeLast();
}
}
}
代码随想录
可以通过path中是否包含nums[i]排除,一样,只是遍历path需要O(n)时间复杂度。
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> permute(int[] nums) {
if (nums.length == 0) return result;
backtrack(nums, path);
return result;
}
public void backtrack(int[] nums, LinkedList<Integer> path) {
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
}
for (int i =0; i < nums.length; i++) {
// 如果path中已有,则跳过
if (path.contains(nums[i])) {
continue;
}
path.add(nums[i]);
backtrack(nums, path);
path.removeLast();
}
}
}
47.全排列 II
递归回溯
跟全排列基本一样,但是存在重复值,此时需要去重,采用的如下
- 将nums排序
- 判断nums[i-1]与nums[i]是否相同
- 如果相同,判断nums[i-1]是否使用过
- 如果used[i-1]==true;说明他们在一条path上,此时不用去重
- 如果used[i-1]==false;说明他们在同一层,i-1已经被使用过,此时需要去重,跳过。
- 如果相同,判断nums[i-1]是否使用过
class Solution {
LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
int[] used = new int[nums.length];//new int[nums.length];
Arrays.sort(nums);
backTracking(nums,used);
return result;
}
private void backTracking(int[] nums,int[] used){
if(path.size() == nums.length){
result.add(new ArrayList<>(path));
return;
}
for(int i = 0; i< nums.length;i++){
//这个数字被使用过,或者
//这个数字跟前一个数字一样,且前一个数字此时没被使用,说明此数字已经被用过
//如果这个数字跟前一个数字一样,且前一个数字使用过,同一path此时不用跳过
if(used[i] == 1 || (i> 0 && nums[i] == nums[i-1] && used[i-1] == 0 )){
continue;
}
used[i] = 1;
path.add(nums[i]);
backTracking(nums,used);
used[i] = 0;
path.removeLast();
}
}
}
代码随想录
如果要对树层中前一位去重,就用used[i - 1] == false
,如果要对树枝前一位去重用used[i - 1] == true
。
class Solution {
//存放结果
List<List<Integer>> result = new ArrayList<>();
//暂存结果
List<Integer> path = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
boolean[] used = new boolean[nums.length];
Arrays.fill(used, false);
Arrays.sort(nums);
backTrack(nums, used);
return result;
}
private void backTrack(int[] nums, boolean[] used) {
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
// used[i - 1] == true,说明同⼀树⽀nums[i - 1]使⽤过
// used[i - 1] == false,说明同⼀树层nums[i - 1]使⽤过
// 如果同⼀树层nums[i - 1]使⽤过则直接跳过
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
//如果同⼀树⽀nums[i]没使⽤过开始处理
if (used[i] == false) {
used[i] = true;//标记同⼀树⽀nums[i]使⽤过,防止同一树枝重复使用
path.add(nums[i]);
backTrack(nums, used);
path.remove(path.size() - 1);//回溯,说明同⼀树层nums[i]使⽤过,防止下一树层重复
used[i] = false;//回溯
}
}
}
}
如果改成 used[i - 1] == true
, 也是正确的!,去重代码如下:
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) {
continue;
}
对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!
大家应该很清晰的看到,树层上对前一位去重非常彻底,效率很高,树枝上对前一位去重虽然最后可以得到答案,但是做了很多无用搜索。
332.重新安排行程
问题
问题1:数组存储机票的使用,重复元素
因为机票有重复元素,而List.indexOf(obj)在有重复元素时,只会返回第一个位置的索引。所以第二张机票的索引无法获得。然后自己写函数判断path与最小的result判断谁是最小行程。
class Solution {
LinkedList<String> path = new LinkedList<>();
List<String> result = new ArrayList<>();
//set去重
Set<String> set = new HashSet<>();
List<String> staionList = new ArrayList<>();
public List<String> findItinerary(List<List<String>> tickets) {
for(List<String> ticket : tickets){
if(set.add(ticket.get(0))){
staionList.add(ticket.get(0));
}
if(set.add(ticket.get(1))){
staionList.add(ticket.get(1));
}
}
int[] ticketUsed = new int[tickets.size()];
String pre = "JFK";
path.add(pre);
for(int i = 0; i < staionList.size();i++){
String station = staionList.get(i);
//check是否存在,或者是否被用
int index = checkTicket(tickets,ticketUsed,station,pre);
if(index == -1){
continue;
}
ticketUsed[index] = 1;
path.add(station);
//找半天,在这pre不变一直是JDK!!
backTracking(tickets,staionList,ticketUsed,station);
ticketUsed[index] = 0;
path.removeLast();
}
return result;
}
private void backTracking(List<List<String>> tickets,List<String> staionList,int[] ticketUsed,String pre){
if(path.size() == ticketUsed.length+1){
if(!result.isEmpty() && checkAnswer()){
return;
}
result = new ArrayList<>(path);
return;
}
for(int i = 0; i < staionList.size(); i++){
String station = staionList.get(i);
//check是否存在,或者是否被用
int index = checkTicket(tickets,ticketUsed,station,pre);
if(index == -1){
continue;
}
pre = station;
ticketUsed[index] = 1;
path.add(station);
backTracking(tickets,staionList,ticketUsed,pre);
ticketUsed[index] = 0;
path.removeLast();
}
}
private int checkTicket(List<List<String>> tickets,int[] ticketUsed, String station, String pre){
List<String> temp = new ArrayList<>();
temp.add(pre);
temp.add(station);
int index = tickets.indexOf(temp);
if(index == -1 || ticketUsed[index] == 1){
return -1;
}
return index;
}
//不符合返回false
private boolean checkAnswer(){
for(int i =0; i < path.size(); i++){
if(!check(path.get(i),result.get(i))){
return false;
}
}
return true;
}
private boolean check(String s1, String s2){
for(int i = 0; i < s1.length(); i++){
if(s1.charAt(i) == s2.charAt(i)){
continue;
}
if(s1.charAt(i) > s2.charAt(i)){
return false;
}
if(s1.charAt(i) < s2.charAt(i)){
return true;
}
}
//两个字符串相等
return true;
}
}
可以通过直接对List进行增删,或者hashMap存储机票数量来解决问题1。
问题2:HashMap改进,超时
使用HashMap存储机票的名字和张数,从张数判断是否机票是否能使用。但是依然会超时
class Solution {
LinkedList<String> path = new LinkedList<>();
HashMap<String,Integer> map = new HashMap<>();
List<String> result = new ArrayList<>();
//set去重
Set<String> set = new HashSet<>();
List<String> staionList = new ArrayList<>();
public List<String> findItinerary(List<List<String>> tickets) {
for(List<String> ticket : tickets){
if(set.add(ticket.get(0))){
staionList.add(ticket.get(0));
}
if(set.add(ticket.get(1))){
staionList.add(ticket.get(1));
}
}
int[] ticketUsed = new int[tickets.size()];
String pre = "JFK";
int flag = 0;
for(List<String> ticket : tickets){
String key = ticket.get(0)+ticket.get(1);
Integer value = map.getOrDefault(key,0);
map.put(key,value+1);
}
path.add(pre);
backTracking(tickets,staionList,pre);
return result;
}
private void backTracking(List<List<String>> tickets,List<String> staionList,String pre){
if(path.size() == tickets.size()+1){
//checkAnswer当path>result时,返回false
//当path<result时,返回true时,需要替换
if(result.isEmpty() || checkAnswer()){
result = new ArrayList<>(path);
}
return;
}
for(int i = 0; i < staionList.size(); i++){
String station = staionList.get(i);
//check是否存在,或者是否被用
String key = pre + station;
Integer value = map.get(key);
//不存在此车票,或车票已被用
if(value == null || value <= 0){
continue;
}
map.put(key,--value);
path.add(station);
backTracking(tickets,staionList,station);
map.put(key,++value);
path.removeLast();
}
}
.
.
.
.
}
改进
超时的原因,是因为需要遍历的次数太多,需要剪枝,可以通过Collection.sort方法将待选元素排序,此时找到的第一个path即可返回。所以backTracking需要设置返回值boolean
遍历机场
操作List
在ListTickets上面增删tickets,contains判断是否存在。136ms,5.13%
class Solution {
LinkedList<String> path = new LinkedList<>();
List<String> result = new ArrayList<>();
//机场列表
List<String> staionList = new ArrayList<>();
public List<String> findItinerary(List<List<String>> tickets) {
//set去重
Set<String> set = new HashSet<>();
for(List<String> ticket : tickets){
if(set.add(ticket.get(0))){
staionList.add(ticket.get(0));
}
if(set.add(ticket.get(1))){
staionList.add(ticket.get(1));
}
}
int[] ticketUsed = new int[tickets.size()];
String pre = "JFK";
Collections.sort(staionList);
path.add(pre);
backTracking(tickets,staionList,pre);
return result;
}
private boolean backTracking(List<List<String>> tickets,List<String> staionList,String pre){
if(tickets.size() == 0){
result = new ArrayList<>(path);
return true;
}
for(int i = 0; i < staionList.size(); i++){
String station = staionList.get(i);
List<String> ticket = new ArrayList<>();
ticket.add(pre);
ticket.add(station);
//check是否存在,或者是否被用
//不存在此车票,或车票已被用
if(!tickets.contains(ticket)){
continue;
}
path.add(station);
tickets.remove(ticket);
if(backTracking(tickets,staionList,station)){
return true;
}
tickets.add(ticket);
path.removeLast();
}
return false;
}
}
操作Map
使用HashMap存储机票的名字和张数,降低了时间复杂度。23ms,12.6%
class Solution {
LinkedList<String> path = new LinkedList<>();
HashMap<String,Integer> map = new HashMap<>();
List<String> result = new ArrayList<>();
//set去重
Set<String> set = new HashSet<>();
List<String> staionList = new ArrayList<>();
public List<String> findItinerary(List<List<String>> tickets) {
for(List<String> ticket : tickets){
if(set.add(ticket.get(0))){
staionList.add(ticket.get(0));
}
if(set.add(ticket.get(1))){
staionList.add(ticket.get(1));
}
}
int[] ticketUsed = new int[tickets.size()];
String pre = "JFK";
int flag = 0;
for(List<String> ticket : tickets){
String key = ticket.get(0)+ticket.get(1);
Integer value = map.getOrDefault(key,0);
map.put(key,value+1);
}
Collections.sort(staionList);
path.add(pre);
backTracking(tickets,staionList,pre);
return result;
}
private boolean backTracking(List<List<String>> tickets,List<String> staionList,String pre){
if(path.size() == tickets.size()+1){
result = new ArrayList<>(path);
return true;
}
for(int i = 0; i < staionList.size(); i++){
String station = staionList.get(i);
//check是否存在,或者是否被用
String key = pre + station;
Integer value = map.get(key);
//不存在此车票,或车票已被用
if(value == null || value <= 0){
continue;
}
map.put(key,--value);
path.add(station);
if(backTracking(tickets,staionList,station)){
return true;
}
map.put(key,++value);
path.removeLast();
}
return false;
}
}
遍历机票
操作Used数组
通过数组存储机票的使用,同时遍历机票,此时就不会有后面的机票无法访问的问题。17ms,19.50%
class Solution {
LinkedList<String> path = new LinkedList<>();
List<String> result = new ArrayList<>();
public List<String> findItinerary(List<List<String>> tickets) {
//根据目的地排序后,最先选择的结果一定是最小行程组合
Collections.sort(tickets,(a,b) -> a.get(1).compareTo(b.get(1)) );
boolean[] ticketUsed = new boolean[tickets.size()];
path.add("JFK");
backTracking(tickets,ticketUsed);
return result;
}
private boolean backTracking(List<List<String>> tickets,boolean[] ticketUsed){
//当长度等于票数+1时,即找到路径
if(path.size() == tickets.size()+1){
result = new ArrayList<>(path);
return true;
}
//不遍历车站,遍历各票
for(int i = 0; i < tickets.size(); i++){
//第i张车票,车票的终点站按顺序排列
List<String> ticket = tickets.get(i);
//出发站符合,而且车票没被用过,则按顺序遍历终点站
//ticketUsed[i]放前面,容易访问降低时间复杂度
if(!ticketUsed[i] && ticket.get(0).equals(path.getLast()) ){
path.add(ticket.get(1));
ticketUsed[i] = true;
if(backTracking(tickets,ticketUsed)){
return true;
}
ticketUsed[i] = false;
path.removeLast();
}
}
return false;
}
}
操作Map
还可以通过Map<出发机场,Map<到达机场,航班次数>>来优化for循环,使其不用每个到达机场都检测一遍,而是在初始化后大大的缩小了遍历的范围。
并且将Map<到达机场,航班次数>设置为升序的TreeSet即可找到最小行程。7ms,84.98%
class Solution {
private LinkedList<String> result;
private Map<String, Map<String, Integer>> map;
public List<String> findItinerary(List<List<String>> tickets) {
map = new HashMap<String, Map<String, Integer>>();
result = new LinkedList<>();
//Map<出发机场,Map<到达机场,航班次数>>
for(List<String> t : tickets){
Map<String, Integer> arrival;
//t.get(0)是出发机场,如果已经有值
if(map.containsKey(t.get(0))){
//获得值
arrival = map.get(t.get(0));
//更改值
arrival.put(t.get(1), arrival.getOrDefault(t.get(1), 0) + 1);
}else{
//如果该出发机场没有值,初始化
arrival = new TreeMap<>();//升序Map
arrival.put(t.get(1), 1);
//放入值
map.put(t.get(0), arrival);
}
}
result.add("JFK");
backTracking(tickets.size());
return result;
}
private boolean backTracking(int ticketNum){
if(result.size() == ticketNum + 1){
return true;
}
//出发机场
String last = result.getLast();
if(map.containsKey(last)){//防止出现null,有的降落机场单向
//得到出发机场对应的到达机场键值对
for(Map.Entry<String, Integer> target : map.get(last).entrySet()){
int count = target.getValue();
//还有机票
if(count > 0){
result.add(target.getKey());
target.setValue(count - 1);
if(backTracking(ticketNum)) return true;
result.removeLast();
target.setValue(count);
}
}
}
return false;
}
}
51. N皇后
递归回溯
使用char [n] [n] 存储board棋盘情况,check函数检查盘面是否符合,每行遍历,竖向回溯。
public class Solution {
List<List<String>> result = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
char[][] board = initBoard(n);
backTracking(board,0);
return result;
}
private void backTracking(char[][] board, int rowBegin){
if(rowBegin >= board.length){
List<String> path = generResult(board);
result.add(path);
return;
}
for(int j = 0; j < board.length;j++){
board[rowBegin][j] = 'Q';
if(!check(board)){
board[rowBegin][j] = '.';
continue;
}
backTracking(board,rowBegin+1);
board[rowBegin][j] = '.';
}
}
//返回false,不符合,返回true,符合
private boolean check(char[][] board){
for(int i = 0; i < board.length; i++){
for(int j = 0; j< board[0].length;j++){
if(board[i][j]=='Q'){
//如果棋盘返回false,不符合
if(!checkQueen(board,i,j)){
return false;
}
}
}
}
return true;
}
//符合true,不符合false
private boolean checkQueen(char[][] board,int row, int column){
int n = board.length;
//不用检查一行的数,回溯时每行一个Queen
//检查一列的数
for(int j = 0; j < n;j++){
if(j == row){
continue;
}
if(board[j][column] == 'Q'){
return false;
}
}
//检查斜线的数
//左上到右下
//节点到右下
for(int i = row+1, j = column+1;i<n && j<n; i++,j++){
if(board[i][j] == 'Q'){
return false;
}
}
//节点到左上
for(int i = row-1, j = column -1;i>=0 && j>=0; i--,j--){
if(board[i][j] == 'Q'){
return false;
}
}
//右上到左下
//节点到左下
for(int i = row+1, j = column-1; i< n && j>= 0; i++,j--){
if(board[i][j] == 'Q'){
return false;
}
}
//节点到右上
for(int i = row-1, j = column+1; i>= 0 && j<n; i--,j++){
if(board[i][j] == 'Q'){
return false;
}
}
return true;
}
//将board转换为path存入result
private List<String> generResult(char[][] board){
List<String> path = new ArrayList<>();
for(int i = 0; i < board.length;i++){
StringBuilder sb = new StringBuilder();
for(int j = 0; j < board[i].length; j++){
sb.append(board[i][j]);
}
path.add(sb.toString());
}
return path;
}
//初始化棋盘
private char[][] initBoard(int n){
char[][] board = new char[n][n];
for(int i = 0; i< n; i++){
for(int j = 0; j<n; j++){
board[i][j]='.';
}
}
return board;
}
}
代码随想录
优化
- 因为棋盘总体从上到下,从左到右,所以 节点到左下 与 节点到右下可以省略
- 还可以使用Arrays.fill(c,‘.’) 和 String.copyValueOf© 省去循环 ,复杂度一样。
- check与checkQueen重复,每次棋盘不用重新判断,只需要把新添加的Queen的位置重新判断即可。
class Solution {
List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
char[][] chessboard = new char[n][n];
for (char[] c : chessboard) {
Arrays.fill(c, '.');
}
backTrack(n, 0, chessboard);
return res;
}
public void backTrack(int n, int row, char[][] chessboard) {
if (row == n) {
res.add(Array2List(chessboard));
return;
}
for (int col = 0;col < n; ++col) {
if (isValid (row, col, n, chessboard)) {
chessboard[row][col] = 'Q';
backTrack(n, row+1, chessboard);
chessboard[row][col] = '.';
}
}
}
public List Array2List(char[][] chessboard) {
List<String> list = new ArrayList<>();
for (char[] c : chessboard) {
list.add(String.copyValueOf(c));
}
return list;
}
public boolean isValid(int row, int col, int n, char[][] chessboard) {
// 检查列
for (int i=0; i<row; ++i) { // 相当于剪枝
if (chessboard[i][col] == 'Q') {
return false;
}
}
// 检查45度对角线
for (int i=row-1, j=col-1; i>=0 && j>=0; i--, j--) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
// 检查135度对角线
for (int i=row-1, j=col+1; i>=0 && j<=n-1; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
}
递归回溯改进
把我的代码进行上述的点改进,得到如下。
public class Solution {
List<List<String>> result = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
char[][] board = initBoard(n);
backTracking(board,0);
return result;
}
private void backTracking(char[][] board, int rowBegin){
if(rowBegin >= board.length){
List<String> path = generResult(board);
result.add(path);
return;
}
for(int j = 0; j < board.length;j++){
if(!checkQueen(board,rowBegin,j)){
continue;
}
board[rowBegin][j] = 'Q';
backTracking(board,rowBegin+1);
board[rowBegin][j] = '.';
}
}
//符合true,不符合false
private boolean checkQueen(char[][] board,int row, int column){
int n = board.length;
//不用检查一行的数,回溯时每行一个Queen
//检查一列的数
for(int j = 0; j < row;j++){
if(board[j][column] == 'Q'){
return false;
}
}
//检查斜线的数,节点到左上,节点到右下省略
for(int i = row-1, j = column -1;i>=0 && j>=0; i--,j--){
if(board[i][j] == 'Q'){
return false;
}
}
//节点到右上,节点到左下,省略
for(int i = row-1, j = column+1; i>= 0 && j<n; i--,j++){
if(board[i][j] == 'Q'){
return false;
}
}
return true;
}
//将board转换为path存入result
private List<String> generResult(char[][] board){
List<String> path = new ArrayList<>();
for (char[] c : board) {
path.add(String.copyValueOf(c));
}
return path;
}
//初始化棋盘
private char[][] initBoard(int n){
char[][] board = new char[n][n];
for (char[] c : board) {
Arrays.fill(c, '.');
}
return board;
}
}
37. 解数独
递归回溯
每一格从1到9进行遍历,然后每格向后回溯。3ms,72.83%
class Solution {
public void solveSudoku(char[][] board) {
backTracking(board,0);
}
private boolean backTracking(char[][] board,int beginIndex){
if(beginIndex >= 9*9){
return true;
}
int row = beginIndex / 9;
int column = beginIndex % 9;
if(board[row][column] != '.'){
return backTracking(board, beginIndex+1);
}
boolean[] used = unChosen(board,row,column);
//注意1-9,所以从1开始
for(int i = 1; i< 10;i++){
//此数值被使用过
if(used[i] == true){
continue;
}
board[row][column] = (char)('0'+i);
if(backTracking(board, beginIndex+1)){
return true;
}
board[row][column] = '.';
}
return false;
}
private boolean[] unChosen(char[][]board, int row, int column){
boolean[] used = new boolean[10];
//列
for(int i = 0; i < 9; i++){
int n = board[i][column] - '0';
if(n == -2){
continue;
}
used[n] = true;
}
//横
for(int j = 0; j < 9; j++){
int n = board[row][j] - '0';
if(n == -2){
continue;
}
used[n] = true;
}
//九宫格
//注意需要*3,找到开始索引
int boxColumnBegin = (column / 3) *3;
int boxRowBegin = (row / 3) *3;
for(int i = boxRowBegin; i< boxRowBegin+3;i++){
for(int j = boxColumnBegin; j < boxColumnBegin+3;j++){
int n = board[i][j] - '0';
if(n == -2){
continue;
}
used[n] = true;
}
}
return used;
}
}
代码随想录
二维递归,把递归函数放在二维的循环中,每次都从头开始判断是否有数字(遍历过)。6ms,62.19%
class Solution {
public void solveSudoku(char[][] board) {
solveSudokuHelper(board);
}
private boolean solveSudokuHelper(char[][] board){
//「一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,
// 一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!」
for (int i = 0; i < 9; i++){ // 遍历行
for (int j = 0; j < 9; j++){ // 遍历列
if (board[i][j] != '.'){ // 跳过原始数字
continue;
}
for (char k = '1'; k <= '9'; k++){ // (i, j) 这个位置放k是否合适
if (isValidSudoku(i, j, k, board)){
board[i][j] = k;
if (solveSudokuHelper(board)){ // 如果找到合适一组立刻返回
return true;
}
board[i][j] = '.';
}
}
// 9个数都试完了,都不行,那么就返回false
return false;
// 因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解!
// 那么会直接返回, 「这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!」
}
}
// 遍历完没有返回false,说明找到了合适棋盘位置了
return true;
}
/**
* 判断棋盘是否合法有如下三个维度:
* 同行是否重复
* 同列是否重复
* 9宫格里是否重复
*/
private boolean isValidSudoku(int row, int col, char val, char[][] board){
// 同行是否重复
for (int i = 0; i < 9; i++){
if (board[row][i] == val){
return false;
}
}
// 同列是否重复
for (int j = 0; j < 9; j++){
if (board[j][col] == val){
return false;
}
}
// 9宫格里是否重复
int startRow = (row / 3) * 3;
int startCol = (col / 3) * 3;
for (int i = startRow; i < startRow + 3; i++){
for (int j = startCol; j < startCol + 3; j++){
if (board[i][j] == val){
return false;
}
}
}
return true;
}
}
每次填入数字都判断此数字是否冲突,从1-9判断,所以不如used数组快。