《代码随想录》刷题笔记——回溯篇【java实现】
文章目录
- 组合
- 组合总和 III
- 电话号码的字母组合
- 组合总和
- 组合总和II
- 思路
- 代码实现
- 分割回文串※
- 思路
- 字符串分割
- 回文串判断
- 效率优化※
- 复原 IP 地址
- 优化版本
- 子集
- 子集 II
- 使用usedArr辅助去重
- 不使用usedArr辅助去重
- 递增子序列※
- 全排列
- 全排列 II
- 重新安排行程
- 题意
- 代码
- N 皇后
- 解数独
- 直接遍历
- 优化
组合
https://leetcode.cn/problems/combinations/description/
public List<List<Integer>> combine(int n, int k) {List<List<Integer>> result = new ArrayList<>();// 存储搜索路径List<Integer> path = new ArrayList<>();// 从1开始递归backtrack(n, k, 1, path, result);return result;
}void backtrack(int n, int k, int startNum, List<Integer> path, List<List<Integer>> result) {if (path.size() == k) {// --if-- 组合的元素个数=k,说明找到了新的解,存储到result中List<Integer> clone = new ArrayList<>(k);for (Integer num : path) {clone.add(num);}result.add(clone);return;}for (int num = startNum; num <= n; num++) {// --for--遍历加下来的元素// 将遍历到的元素添加到搜索路径中path.add(num);// 递归,开始数量+1backtrack(n, k, num + 1, path, result);// 移除刚刚添加的数量path.remove(path.size() - 1);}
}
List<Integer> clone = new ArrayList<>(k);
for (Integer num : path) {clone.add(num);
}
result.add(clone);
上面的实现可以简化为
result.add(new ArrayList<>(path));
通过查看源码可以发现,构造方法传入集合的时候,JDK会自动帮我们做克隆操作
/*** Constructs a list containing the elements of the specified* collection, in the order they are returned by the collection's* iterator.** @param c the collection whose elements are to be placed into this list* @throws NullPointerException if the specified collection is null*/
public ArrayList(Collection<? extends E> c) {Object[] a = c.toArray();if ((size = a.length) != 0) {if (c.getClass() == ArrayList.class) {elementData = a;} else {elementData = Arrays.copyOf(a, size, Object[].class);}} else {// replace with empty array.elementData = EMPTY_ELEMENTDATA;}
}
通过提交运行结果可以发现,上面的实现执行用时比较长,这是为什么呢?
假设n=5,k=3,组合可选元素为1,2,3,4,5
- 当path=[]时,组合的第一个元素为4,后面只有一个5,继续递归遍历,也只能找到两个元素的组合,无法找找到达到长度要求的组合(4、5无需遍历);
- 当path=[1]时,组合的第二个元素为5,后面没有元素,无法找找到达到长度要求的组合(5无需遍历);
因此通过减少部分遍历来加快代码速度
public List<List<Integer>> combine(int n, int k) {List<List<Integer>> result = new ArrayList<>();// 存储搜索路径List<Integer> path = new ArrayList<>();// 从1开始递归backtrack(n, k, 1, path, result);return result;
}void backtrack(int n, int k, int startNum, List<Integer> path, List<List<Integer>> result) {if (path.size() == k) {// --if-- 组合的元素个数=k,说明找到了新的解,存储到result中result.add(new ArrayList<>(path));return;}for (int num = startNum; num <= n - (k - path.size()) + 1; num++) {// --for--遍历加下来的元素// 将遍历到的元素添加到搜索路径中path.add(num);// 递归,开始数量+1backtrack(n, k, num + 1, path, result);// 移除刚刚添加的数量path.remove(path.size() - 1);}
}
在创建List<Integer> path = new ArrayList<>();
,因为path的长度已知,因此可以在初始化集合的时候传入集合长度,避免后面发生集合扩容,List<Integer> path = new ArrayList<>(k);
组合总和 III
https://leetcode.cn/problems/combination-sum-iii/description/
public List<List<Integer>> combinationSum3(int k, int n) {List<List<Integer>> result = new ArrayList<>();List<Integer> path = new ArrayList<>();backtrack(k, n, 1, result, path, 0);return result;
}private void backtrack(int k, int n, int startNum, List<List<Integer>> result, List<Integer> path, int sum) {if (path.size() == k) {// --if-- 搜索到的组合元素个数为kif (sum == n) {// --if-- sum等于n,存储组合result.add(new ArrayList<>(path));} else {// --if-- 元素个数已经为3,无需再往下递归,否则,元素个数超出k了,再搜索也是白费return;}}for (int i = startNum; i <= 9; i++) {path.add(i);backtrack(k, n, i + 1, result, path, sum + i);path.remove(path.size() - 1);}
}
【剪枝优化】
- 剪枝1:path.size()>k
- 剪枝2:sum>n
- 剪枝3:无效组合起点,类似上一题,
i <= 10- (k - path.size())
public List<List<Integer>> combinationSum3(int k, int n) {List<List<Integer>> result = new ArrayList<>();List<Integer> path = new ArrayList<>();backtrack(k, n, 1, result, path, 0);return result;
}private void backtrack(int k, int n, int startNum, List<List<Integer>> result, List<Integer> path, int sum) {if (path.size() == k) {// --if-- 搜索到的组合元素个数为kif (sum == n) {// --if-- sum等于n,存储组合result.add(new ArrayList<>(path));} else {// --if-- 元素个数已经为3,无需再往下递归,否则,元素个数超出k了,再搜索也是白费return;}} else if (sum > n) {// --if-- 搜索到的组sum已经大于n,无需再往下搜索return;}for (int i = startNum; i <= 10- (k - path.size()) ; i++) {path.add(i);backtrack(k, n, i + 1, result, path, sum + i);path.remove(path.size() - 1);}
}
电话号码的字母组合
https://leetcode.cn/problems/letter-combinations-of-a-phone-number/description/
public List<String> letterCombinations(String digits) {if (digits.length() == 0) {return new ArrayList<>();}// 先将字符串转化为char数组,方便遍历操作char[] charArray = digits.toCharArray();// 存储结果List<String> result = new ArrayList<>();// 存储搜索组合(为什么使用数组呢,因为组合的元素数量是没有变化的,而且后面需要将元素组合转化为字符串,使用char数组更加方便)char[] path = new char[digits.length()];// 从1开始递归backtrack(charArray, 0, path, result);return result;
}void backtrack(char[] charArray, int charIndex, char[] path, List<String> result) {if (charIndex == charArray.length) {// --if-- 数字已经遍历完成,存储结果result.add(new String(path));return;}char numChar = charArray[charIndex];// 根据输入的数字,转化为对应的字符数组char[] chooseCharArr = null;switch (numChar) {case '2':chooseCharArr = new char[]{'a', 'b', 'c'};break;case '3':chooseCharArr = new char[]{'d', 'e', 'f'};break;case '4':chooseCharArr = new char[]{'g', 'h', 'i'};break;case '5':chooseCharArr = new char[]{'j', 'k', 'l'};break;case '6':chooseCharArr = new char[]{'m', 'n', 'o'};break;case '7':chooseCharArr = new char[]{'p', 'q', 'r', 's'};break;case '8':chooseCharArr = new char[]{'t', 'u', 'v'};break;case '9':chooseCharArr = new char[]{'w', 'x', 'y', 'z'};break;}// 循环字符数组,挑选一个,并进入下一个数字对应的字符挑选for (int i = 0; i < chooseCharArr.length; i++) {path[charIndex] = chooseCharArr[i];backtrack(charArray, charIndex + 1, path, result);// 这里不需要回溯操作,因为下一次循环会主动将 path[charIndex] 替换成另一个元素}
}
【代码随想录实现】
//设置全局列表存储最后的结果
List<String> list = new ArrayList<>();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;}//每次迭代获取一个字符串,所以会涉及大量的字符串拼接,所以这里选择更为高效的 StringBuilder
StringBuilder temp = new StringBuilder();//比如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));//递归,处理下一层backTracking(digits, numString, num + 1);//剔除末尾的继续尝试temp.deleteCharAt(temp.length() - 1);}
}
组合总和
https://leetcode.cn/problems/combination-sum/description/
public List<List<Integer>> combinationSum(int[] candidates, int target) {List<List<Integer>> result = new ArrayList<>();List<Integer> path = new ArrayList<>();backtrack(candidates, target, 0, result, path);return result;
}private void backtrack(int[] candidates, int target, int startIndex,List<List<Integer>> result, List<Integer> path) {if (target == 0) {result.add(new ArrayList<>(path));return;}for (int i = startIndex; i < candidates.length; i++) {// 计算剩下的target最多还可以容纳多少个 candidates[i] int num = target / candidates[i];// 循环取不同个数 candidates[i] 的情况// 注意j从1开始,j=0的情况其实在i的循环中已经考虑到了,例如当i等于startIndex+1时,实际上就是startIndex对应的元素j=0for (int j = 1; j <= num; j++) {// 将j个元素添加到搜索路径中for (int k = 0; k < j; k++) {path.add(candidates[i]);}backtrack(candidates, target - candidates[i] * j, i + 1, result, path);// 将j个元素从搜索路径中移除for (int k = 0; k < j; k++) {path.remove(path.size() - 1);}}}
}
从结果可以分析出来,上面的代码还有效率问题
其中的一个原因就是如下代码,每次需要添加j个元素,然后移除j个元素
// 将j个元素添加到搜索路径中
for (int k = 0; k < j; k++) {path.add(candidates[i]);
}// 将j个元素从搜索路径中移除
for (int k = 0; k < j; k++) {path.remove(path.size() - 1);
}
改成下面的代码,效率会更好一点
public List<List<Integer>> combinationSum(int[] candidates, int target) {List<List<Integer>> result = new ArrayList<>();List<Integer> path = new ArrayList<>();backtrack(candidates, target, 0, result, path);return result;
}private void backtrack(int[] candidates, int target, int startIndex,List<List<Integer>> result, List<Integer> path) {if (target == 0) {result.add(new ArrayList<>(path));return;}for (int i = startIndex; i < candidates.length; i++) {if (target >= candidates[i]) {path.add(candidates[i]);backtrack(candidates, target - candidates[i], i, result, path);path.remove(path.size() - 1);}}
}
最后还存在一个剪枝策略,就是先将candidates升序排序,当遇到一个candidates[i]>target
时,直接break出循环即可
public List<List<Integer>> combinationSum(int[] candidates, int target) {List<List<Integer>> result = new ArrayList<>();List<Integer> path = new ArrayList<>();Arrays.sort(candidates);backtrack(candidates, target, 0, result, path);return result;
}private void backtrack(int[] candidates, int target, int startIndex,List<List<Integer>> result, List<Integer> path) {if (target == 0) {result.add(new ArrayList<>(path));return;}for (int i = startIndex; i < candidates.length; i++) {if (target >= candidates[i]) {path.add(candidates[i]);backtrack(candidates, target - candidates[i], i, result, path);path.remove(path.size() - 1);} else {break;}}
}
不过跑了很多次,还没有前面的效率高,可能是排序也比较耗时
组合总和II
https://leetcode.cn/problems/combination-sum-ii/description/
思路
如下图所示,常规的回溯方法会导致搜索到重复的组合,题目中要求不能出现重复组合。可以做一些去重操作来解决这个问题,但是性能肯定不好。我们应该在搜索的时候进行剪枝。
如下图所示,分支2 其实已经被 分支1 包含,因此算法在搜索过程,无需搜索分支2
如何识别出分支2呢,一个明显的特征是,candidates[1] = candidates[0] = 2
,那当candidates[i] = candidates[i-1]
时,就把i对应分支剪掉?
答案是不允许,因为这样的话,会导致分支1也没有搜索完全。如下图所示,分支a 和 分支b 都会被剪掉。如果target=7
的话,组合[2,2,3]就搜索不到了。因此需要区分 分支a 和 分支b ,分支b才是要被剪枝的。
那么如何区分 分支a
和 分支b
呢?我使用一个数组useArr
来帮助区分,useArr[i]
表示candidates[i]
在当前搜索组合中是否被使用。如下图所示,搜索过程中useArr
的变化如下图所示。从图中可以很容易发现,分支a和分支b的useArr
不一样,准确来说,是useArr[i-1]
不一样,分支a对应的useArr[i-1]=1
,而分支b对应的useArr[i-1]=0
。因此当i > 0 && useArr[i - 1] == 0 && candidates[i - 1] == candidates[i]
时,进行剪枝。注意,在搜索之前需要先对candidates
进行升序排序,才通过判断i
和i-1
对应的元素是否相同。
代码实现
public List<List<Integer>> combinationSum2(int[] candidates, int target) {List<List<Integer>> result = new ArrayList<>();List<Integer> path = new ArrayList<>();// 存储组合使用的元素,1说明元素被使用,0说明元素没有被使用int[] useArr = new int[candidates.length];// 先将数组的值升序排序Arrays.sort(candidates);backtrack(candidates, useArr, target, 0, result, path);return result;
}private void backtrack(int[] candidates, int[] useArr, int target, int startIndex,List<List<Integer>> result, List<Integer> path) {if (target == 0) {result.add(new ArrayList<>(path));return;}for (int i = startIndex; i < candidates.length; i++) {if (target < candidates[i]) {// target很小了,candidates后的数只会更大,所以可以breakbreak;}// 如果 candidates[i - 1] == candidates[i] ,说明同一层遍历到和之前的元素一样。如果useArr[i - 1] == 0,进行剪枝if (i > 0 && useArr[i - 1] == 0 && candidates[i - 1] == candidates[i]) {continue;}// 使用了candidates[i]useArr[i] = 1;path.add(candidates[i]);backtrack(candidates, useArr, target - candidates[i], i + 1, result, path);path.remove(path.size() - 1);// 去除candidates[i]的使用useArr[i] = 0;}
}
分割回文串※
https://leetcode.cn/problems/palindrome-partitioning/description/
思路
字符串分割
public List<List<String>> partition(String s) {List<List<String>> result = new ArrayList<>();List<String> path = new ArrayList<>();char[] charArray = s.toCharArray();backtrack(result, path, charArray, 0);return result;
}private void backtrack(List<List<String>> result, List<String> path, char[] charArray, int startIndex) {if (startIndex == charArray.length) {// --if-- 字符串切割完成了result.add(new ArrayList<>(path));return;}for (int i = startIndex; i < charArray.length; i++) {String s = "";// 截取[startIndex,i]对应子串for (int j = startIndex; j <= i; j++) {s += charArray[j];}// 将子串添加到path中path.add(s);// 递归处理剩下的子串backtrack(result, path, charArray, i + 1);// 回溯path.remove(path.size() - 1);}
}
当然上面的代码是生成所有的切割方案,并没有判断切割出来的子串是否符合回文串要求
回文串判断
判断一个字符串非常简单,使用双指针法。一个指针从头开始,一个指针从尾开始,判断两个指针对应的字符是否相同即可,如果出现不相同,说明字符串不是回文串。
private boolean isPalindromeSubStr(String s) {int start = 0;int end = s.length() - 1;while (start < end) {if (s.charAt(start++) != s.charAt(end--)) {return false;}}return true;
}
【总代码】
public List<List<String>> partition(String s) {List<List<String>> result = new ArrayList<>();List<String> path = new ArrayList<>();char[] charArray = s.toCharArray();backtrack(result, path, charArray, 0);return result;
}private void backtrack(List<List<String>> result, List<String> path, char[] charArray, int startIndex) {if (startIndex == charArray.length) {// --if-- 字符串切割完成了result.add(new ArrayList<>(path));return;}for (int i = startIndex; i < charArray.length; i++) {String s = "";for (int j = startIndex; j <= i; j++) {s += charArray[j];}// 判断子串是否为回文串,如果不是的,遍历下一个回文串if (isPalindromeSubStr(s) == false) {continue;}path.add(s);backtrack(result, path, charArray, i + 1);path.remove(path.size() - 1);}
}private boolean isPalindromeSubStr(String s) {int start = 0;int end = s.length() - 1;while (start < end) {if (s.charAt(start++) != s.charAt(end--)) {return false;}}return true;
}
通过结果,可以发现计算效率非常低,因为在搜索过程中,不断使用字符串拼接
效率优化※
直接使用substring
public List<List<String>> partition(String s) {List<List<String>> result = new ArrayList<>();List<String> path = new ArrayList<>();backtrack(result, path, s, 0);return result;
}private void backtrack(List<List<String>> result, List<String> path, String s, int startIndex) {if (startIndex == s.length()) {// --if-- 字符串切割完成了result.add(new ArrayList<>(path));return;}for (int i = startIndex; i < s.length(); i++) {// 判断子串是否为回文串,如果不是的,遍历下一个回文串String str = s.substring(startIndex, i + 1);if (isPalindromeSubStr(str) == false) {continue;}path.add(str);backtrack(result, path, s, i + 1);path.remove(path.size() - 1);}
}private boolean isPalindromeSubStr(String s) {int start = 0;int end = s.length() - 1;while (start < end) {if (s.charAt(start++) != s.charAt(end--)) {return false;}}return true;
}
效率提高了一点点
多跑一次,结果完全不同,所以大家不能太相信leetcode的数据统计。同一段代码在计算机中跑,运行时间有波动非常正常
性能还有优化的空间,还可以使用动态规划来进行优化,等学完动态规划,再回来改进
复原 IP 地址
https://leetcode.cn/problems/restore-ip-addresses/description/
public List<String> restoreIpAddresses(String s) {List<String> result = new ArrayList<>();List<String> path = new ArrayList<>();char[] charArray = s.toCharArray();backtrack(result, path, charArray, 0);return result;
}private void backtrack(List<String> result, List<String> path, char[] charArray, int startIndex) {if (startIndex == charArray.length && path.size() == 4) {// --if-- 字符串切割完成了,且符合ip形式,就存储ipStringBuilder ipSb = new StringBuilder();for (int i = 0; i < path.size() - 1; i++) {ipSb.append(path.get(i) + ".");}ipSb.append(path.get(path.size() - 1));result.add(ipSb.toString());return;} else if (path.size() > 4) {// --if-- 超过4个整数了,ip不合法return;}StringBuilder sb = new StringBuilder();for (int i = startIndex; i < charArray.length; i++) {sb.append(charArray[i]);// 判断子串是否为回文串,如果不是的,遍历下一个回文串if (isReasonable(sb.toString()) == false) {break;}path.add(sb.toString());backtrack(result, path, charArray, i + 1);path.remove(path.size() - 1);}
}private boolean isReasonable(String string) {// 不能含有前导 0if (string.length() > 1 && string.charAt(0) == '0') {return false;}// 每个整数位于 0 到 255 之间组成int num = Integer.parseInt(string);if (num > 255) {return false;}return true;
}
优化版本
统一使用一个StringBuilder
String temp;public List<String> restoreIpAddresses(String s) {List<String> result = new ArrayList<>();StringBuilder sb = new StringBuilder();backtrack(result, sb, s, 0, 0);return result;
}/*** 回溯方法** @param result 记录结果集* @param sb 记录当前ip* @param s 原字符串* @param startIndex 开始索引* @param splitNum 逗号分割数量*/
private void backtrack(List<String> result, StringBuilder sb, String s, int startIndex, int splitNum) {if (startIndex == s.length() && splitNum == 4) {// --if-- 字符串切割完成了,且符合ip形式,就存储ipresult.add(sb.toString());return;} else if (splitNum == 4) {// --if-- 超过4个整数了,ip不合法return;}for (int i = startIndex; i < s.length(); i++) {if (i - startIndex >= 1 && s.charAt(startIndex) == '0') {// --if-- 当前网段长度>1,且含有前导 0,不合法ip,breakbreak;}if (i - startIndex >= 3) {// --if-- 当前网段长度>3,不合法ip,breakbreak;}// 截取字符串作为网段,注意是左闭右开,所以i+1temp = s.substring(startIndex, i + 1);if (Integer.parseInt(temp) > 255) {// --if-- 当前网段值>255,不合法ip,breakbreak;}// 将当前网段添加到ip中sb.append(temp);// 如果逗号数量 < 3(一个ip,网段最多4个,逗号最多3个),拼接一个逗号if (splitNum < 3) {sb.append(".");}// 递归backtrack(result, sb, s, i + 1, splitNum + 1);// 回溯,删除最后一个网段,例如现在是255.255.11.13,回溯之后为 255.255.11.sb.delete(startIndex + splitNum, sb.length());}
}
子集
https://leetcode.cn/problems/subsets/description/
注意:题目说了nums中的所有元素各不相同,所以下面的代码没有必要去重
public List<List<Integer>> subsets(int[] nums) {List<List<Integer>> result = new ArrayList<>();List<Integer> path = new ArrayList<>();backtrack(result, path, nums, 0);return result;
}private void backtrack(List<List<Integer>> result, List<Integer> path, int[] nums, int startIndex) {// 任何集合都要存储,没有说需要满足什么条件才可以存储result.add(new ArrayList<>(path));// 当 startIndex = nums长度 的时候,本来需要return,但是下面的循环不会进行,所以无需把这部分代码写出来了for (int i = startIndex; i < nums.length; i++) {path.add(nums[i]);backtrack(result, path, nums, i + 1);path.remove(path.size() - 1);}
}
子集 II
https://leetcode.cn/problems/subsets-ii/description/
使用usedArr辅助去重
注意,这道题目和上面的不同,数组里面含有重复元素,需要去重操作,其实就和 组合总和II 的思路一样
public List<List<Integer>> subsetsWithDup(int[] nums) {List<List<Integer>> result = new ArrayList<>();List<Integer> path = new ArrayList<>();Arrays.sort(nums);int[] useArr = new int[nums.length];backtrack(result, path, nums, 0, useArr);return result;
}private void backtrack(List<List<Integer>> result, List<Integer> path, int[] nums, int startIndex, int[] useArr) {// 任何集合都要存储,没有说需要满足什么条件才可以存储result.add(new ArrayList<>(path));// 当 startIndex = nums长度 的时候,本来需要return,但是下面的循环不会进行,所以无需把这部分代码写出来了for (int i = startIndex; i < nums.length; i++) {// 去重if (i > 0 && useArr[i - 1] == 0 && nums[i - 1] == nums[i]) {continue;}path.add(nums[i]);useArr[i] = 1;backtrack(result, path, nums, i + 1, useArr);path.remove(path.size() - 1);useArr[i] = 0;}
}
不使用usedArr辅助去重
其实这道题还可以不用use数组
public List<List<Integer>> subsetsWithDup(int[] nums) {List<List<Integer>> result = new ArrayList<>();List<Integer> path = new ArrayList<>();Arrays.sort(nums);backtrack(result, path, nums, 0);return result;
}private void backtrack(List<List<Integer>> result, List<Integer> path, int[] nums, int startIndex) {// 任何集合都要存储,没有说需要满足什么条件才可以存储result.add(new ArrayList<>(path));// 当 startIndex = nums长度 的时候,本来需要return,但是下面的循环不会进行,所以无需把这部分代码写出来了for (int i = startIndex; i < nums.length; i++) {// 去重if (i > startIndex && nums[i - 1] == nums[i]) {continue;}path.add(nums[i]);backtrack(result, path, nums, i + 1);path.remove(path.size() - 1);}
}
注意,去重判断变成了如下代码。和上面代码的区别是一个为i > 0
,一个为i > startIndex
。以[1,2,2]为例子,当startIndex=2时。
- 如果代码为
i > 0
,就会进入去重,导致子集[1,2,2]无法找到,只能找到[]、[1]、[1,2]。 - 但是如果代码为
i > startIndex
,不会触发去重,因为i = startIndex
// 去重
if (i > startIndex && nums[i - 1] == nums[i]) {continue;
}
递增子序列※
public List<List<Integer>> findSubsequences(int[] nums) {List<List<Integer>> result = new ArrayList<>();List<Integer> path = new ArrayList<>();backtrack(result, path, nums, 0);return result;
}private void backtrack(List<List<Integer>> result, List<Integer> path, int[] nums, int startIndex) {// 任何集合都要存储,没有说需要满足什么条件才可以存储if (path.size() >= 2) {result.add(new ArrayList<>(path));}// 用来去重HashMap<Integer, Boolean> useMap = new HashMap<>();for (int i = startIndex; i < nums.length; i++) {// 如果不是递增,continueif (path.size() > 0 && nums[i] < path.get(path.size() - 1)) {continue;}// 去重,如果前面有用过相同的元素,直接continueif (useMap.getOrDefault(nums[i], false) == true) {continue;}useMap.put(nums[i], true);path.add(nums[i]);backtrack(result, path, nums, i + 1);path.remove(path.size() - 1);}
}
如果说数据为 [1, 2, 3, 4, 1, 1] ,不去重答案就会出问题
全排列
https://leetcode.cn/problems/permutations/
这道题只能暴力求解
public List<List<Integer>> permute(int[] nums) {List<List<Integer>> result = new ArrayList<>();List<Integer> path = new ArrayList<>();boolean[] useArr = new boolean[nums.length];backtrack(result, path, nums, useArr);return result;
}private void backtrack(List<List<Integer>> result, List<Integer> path, int[] nums, boolean[] useArr) {if (path.size() == nums.length) {result.add(new ArrayList<>(path));}for (int i = 0; i < nums.length; i++) {if (useArr[i] == true) {continue;}useArr[i] = true;path.add(nums[i]);backtrack(result, path, nums, useArr);useArr[i] = false;path.remove(path.size() - 1);}
}
不用数组useArr来辅助也可以,直接使用path的contain方法来判断元素是否已经存在即可
这道题和前面比较大的一个区别是不再需要startIndex这个变量,而是遍历整个数组,因为需要的所有排序
全排列 II
https://leetcode.cn/problems/permutations-ii/description/
public List<List<Integer>> permuteUnique(int[] nums) {List<List<Integer>> result = new ArrayList<>();List<Integer> path = new ArrayList<>();boolean[] useArr = new boolean[nums.length];// 升序排序Arrays.sort(nums);backtrack(result, path, nums, useArr);return result;
}private void backtrack(List<List<Integer>> result, List<Integer> path, int[] nums, boolean[] useArr) {if (path.size() == nums.length) {result.add(new ArrayList<>(path));}for (int i = 0; i < nums.length; i++) {if (useArr[i] == true) {continue;}// 去重,如果当前元素和前一个元素相同,而且useArr[i - 1] == false,前面一个元素肯定已经被同样的方式使用过,当前组合不需要再往下遍历了if (i > 0 && nums[i - 1] == nums[i] && useArr[i - 1] == false) {continue;}useArr[i] = true;path.add(nums[i]);backtrack(result, path, nums, useArr);useArr[i] = false;path.remove(path.size() - 1);}
}
重新安排行程
https://leetcode.cn/problems/reconstruct-itinerary/description/
题意
List<List<String>> tickets
中存储了多张票的起点和终点,我们要做的是:从起点JFK
开始旅行,然后把每张票使用一遍,注意每张票只能使用一次
字典排序的意思如下,例如有两个字符串 ACD 和 ABE
- 首先对比两个字符串的第一个字母,两者都是A打平手
- 然后对比两个字符串的第二个字母,第一个字符串的字母为C,第二个字符串的字母为B,因为B在字母表中的顺序更加靠前,因此排序时ABE要排在ACD前面
代码
public List<String> findItinerary(List<List<String>> tickets) {List<String> result = new ArrayList<>();List<String> path = new ArrayList<>();// 存储开始站点 及 开始站点所对应的票 索引Map<String, List<Integer>> startStationAndIndexListMap = new HashMap<>();// 填充 startStationAndIndexListMap 的数据for (int i = 0; i < tickets.size(); i++) {List<String> ticket = tickets.get(i);if (!startStationAndIndexListMap.containsKey(ticket.get(0))) {ArrayList<Integer> indexList = new ArrayList<>();indexList.add(i);startStationAndIndexListMap.put(ticket.get(0), indexList);} else {startStationAndIndexListMap.get(ticket.get(0)).add(i);}}// 将indexList按照字典序升序排序for (Map.Entry<String, List<Integer>> entry : startStationAndIndexListMap.entrySet()) {List<Integer> indexList = entry.getValue();Collections.sort(indexList, ((o1, o2) -> {return compare(tickets.get(o1).get(1), tickets.get(o2).get(1));}));}// 用来标识哪张机票已经被使用过boolean[] useArr = new boolean[tickets.size()];// 起点是固定的,添加起点path.add("JFK");backtrack(tickets, result, path, startStationAndIndexListMap, useArr, 0, "JFK");return result;
}private void backtrack(List<List<String>> tickets, List<String> result, List<String> path, Map<String, List<Integer>> startStationAndIndexListMap, boolean[] useArr, int useTicketNum, String startStation) {if (useTicketNum == tickets.size() && result.size() == 0) {// --if-- 所有机票已经用完了,存储当前路径result.addAll(path);return;}List<Integer> indexList = startStationAndIndexListMap.get(startStation);if (indexList == null) {// 当前起点站没有机票了,直接退出return;}// 遍历从当前起始站出发的机票for (int i = 0; i < indexList.size(); i++) {if (result.size() > 0) {// --if-- 因为题目要求只要字典序最小的一条路径,因此找到一个结果就可以结束整个搜索了,这里直接returnreturn;}// 机票索引Integer ticketIndex = indexList.get(i);if (useArr[ticketIndex] == true) {// --if-- 该机票已经被使用过continue;}// 去重,当前机票首站-尾站和上一张机票的相同,直接跳过即可if (i > 0 && tickets.get(indexList.get(i - 1)).get(1).equals(tickets.get(indexList.get(i)).get(1)) && useArr[indexList.get(i - 1)] == false) {continue;}// 标识当前机票已经被使用useArr[ticketIndex] = true;path.add(tickets.get(ticketIndex).get(1));backtrack(tickets, result, path, startStationAndIndexListMap, useArr, useTicketNum + 1, tickets.get(ticketIndex).get(1));path.remove(path.size() - 1);useArr[ticketIndex] = false;}
}/*** 字典排序** @param str1* @param str2* @return*/
private int compare(String str1, String str2) {for (int i = 0; i < str1.length(); i++) {int compare = Integer.compare(str1.charAt(i), str2.charAt(i));if (compare != 0) {// --if-- 当前字母没有分出胜负,继续比较下一个字母return compare;}}return 0;
}
当然**return
**compare(tickets.get(o1).get(1), tickets.get(o2).get(1));
可以使用
return
tickets.get(o1).get(1).compareTo(tickets.get(o2).get(1));
来代替
小小优化了一下,去掉path,只使用result
public List<String> findItinerary(List<List<String>> tickets) {List<String> result = new ArrayList<>();// 存储开始站点 及 开始站点所对应的票 索引Map<String, List<Integer>> startStationAndIndexListMap = new HashMap<>();// 填充 startStationAndIndexListMap 的数据for (int i = 0; i < tickets.size(); i++) {List<String> ticket = tickets.get(i);if (!startStationAndIndexListMap.containsKey(ticket.get(0))) {ArrayList<Integer> indexList = new ArrayList<>();indexList.add(i);startStationAndIndexListMap.put(ticket.get(0), indexList);} else {startStationAndIndexListMap.get(ticket.get(0)).add(i);}}// 将indexList按照字典序升序排序for (Map.Entry<String, List<Integer>> entry : startStationAndIndexListMap.entrySet()) {List<Integer> indexList = entry.getValue();Collections.sort(indexList, ((o1, o2) -> {return tickets.get(o1).get(1).compareTo(tickets.get(o2).get(1));}));}// 用来标识哪张机票已经被使用过boolean[] useArr = new boolean[tickets.size()];// 起点是固定的,添加起点result.add("JFK");backtrack(tickets, result, startStationAndIndexListMap, useArr, "JFK");return result;
}private void backtrack(List<List<String>> tickets, List<String> result, Map<String, List<Integer>> startStationAndIndexListMap, boolean[] useArr, String startStation) {if (result.size() == tickets.size() + 1) {// --if-- 所有机票已经用完了,存储当前路径return;}List<Integer> indexList = startStationAndIndexListMap.get(startStation);if (indexList == null) {// 当前起点站没有机票了,直接退出return;}// 遍历从当前起始站出发的机票for (int i = 0; i < indexList.size(); i++) {// 机票索引Integer ticketIndex = indexList.get(i);if (useArr[ticketIndex] == true) {// --if-- 该机票已经被使用过continue;}// 去重,当前机票首站-尾站和上一张机票的相同,直接跳过即可if (i > 0 && tickets.get(indexList.get(i - 1)).get(1).equals(tickets.get(ticketIndex).get(1)) && useArr[indexList.get(i - 1)] == false) {continue;}// 标识当前机票已经被使用useArr[ticketIndex] = true;result.add(tickets.get(ticketIndex).get(1));backtrack(tickets, result, startStationAndIndexListMap, useArr, tickets.get(ticketIndex).get(1));if (result.size() == tickets.size() + 1) {// --if-- 因为题目要求只要字典序最小的一条路径,因此找到一个结果就可以结束整个搜索了,这里直接returnreturn;}result.remove(result.size() - 1);useArr[ticketIndex] = false;}
}
字典直接存储一个站可以去往哪些站点,如果选择了该站点,将其从集合中删除,回溯的时候,再将该站点加回来
int totalStationNum;public List<String> findItinerary(List<List<String>> tickets) {totalStationNum = tickets.size() + 1;List<String> result = new ArrayList<>();// 存储开始站点 及 开始站点所对应的票 索引Map<String, List<String>> startStationAndEndStationListMap = new HashMap<>();// 填充 startStationAndIndexListMap 的数据for (int i = 0; i < tickets.size(); i++) {List<String> ticket = tickets.get(i);if (!startStationAndEndStationListMap.containsKey(ticket.get(0))) {List<String> endStationList = new ArrayList<>();endStationList.add(ticket.get(1));startStationAndEndStationListMap.put(ticket.get(0), endStationList);} else {startStationAndEndStationListMap.get(ticket.get(0)).add(ticket.get(1));}}// 将indexList按照字典序升序排序for (Map.Entry<String, List<String>> entry : startStationAndEndStationListMap.entrySet()) {Collections.sort(entry.getValue(), ((o1, o2) -> {return o1.compareTo(o2);}));}// 起点是固定的,添加起点result.add("JFK");backtrack(result, startStationAndEndStationListMap, "JFK");return result;
}private boolean backtrack(List<String> result, Map<String, List<String>> startStationAndEndStationListMap, String startStation) {if (result.size() == totalStationNum) {// --if-- 所有机票已经用完了,存储当前路径return true;}List<String> endStationList = startStationAndEndStationListMap.get(startStation);if (endStationList == null) {// 当前起点站没有机票了,直接退出return false;}// 遍历从当前起始站出发的机票int i = 0;while (i < endStationList.size()) {// 目标站点String endStation = endStationList.get(i);// 去重,当前机票首站-尾站和上一张机票的相同,直接跳过即可if (i > 0 && endStation.equals(endStationList.get(i - 1))) {i++;continue;}// 去往该站点,将其从集合中删除endStationList.remove(i);result.add(endStation);if (backtrack(result, startStationAndEndStationListMap, endStation) == true) {// --if-- 因为题目要求只要字典序最小的一条路径,因此找到一个结果就可以结束整个搜索了,这里直接returnreturn true;}result.remove(result.size() - 1);endStationList.add(i, endStation);i++;}return false;
}
代码随想录的代码效率更高
N 皇后
https://leetcode.cn/problems/n-queens/description/
思路:遍历每一行,然后看当行的每列,再判断当前位置是否可行
public List<List<String>> solveNQueens(int n) {List<List<String>> result = new ArrayList<>();// 初始化棋盘char[][] chessboard = new char[n][n];for (int i = 0; i < n; i++) {Arrays.fill(chessboard[i], '.');}backtrack(result, chessboard, 0, n);return result;
}private void backtrack(List<List<String>> result, char[][] chessboard, int layer, int n) {if (layer == n) {// --for-- 遍历到最后一行了,保存结果就结束List<String> path = new ArrayList<>();for (int i = 0; i < chessboard.length; i++) {path.add(new String(chessboard[i]));}result.add(path);}for (int j = 0; j < n; j++) {// --for-- 遍历棋盘中当前行的每一列boolean isCanPlace = true;if (layer > 0) {// --if-- 如果是第二行以上,需要判断上面行有没有皇后排斥。第一行不需要看for (int i = 0; i < layer; i++) {// 判断同列是否有皇后if (chessboard[i][j] == 'Q') {isCanPlace = false;break;}// 判断斜对角是否有皇后if (((j - (layer - i)) >= 0 && chessboard[i][j - (layer - i)] == 'Q') ||(j + (layer - i) < n && chessboard[i][j + (layer - i)] == 'Q')) {isCanPlace = false;break;}}}if (isCanPlace == true) {// --if-- 当前位置可以放皇后chessboard[layer][j] = 'Q';backtrack(result, chessboard, layer + 1, n);chessboard[layer][j] = '.';}}
}
代码随想录中有更快的方法
解数独
https://leetcode.cn/problems/sudoku-solver/description/
直接遍历
一个单元格一个单元格的遍历,然后遍历[1,9]的每个数字,判断该数字是否可设置。如果所有数字都不可以设置,回溯
char[] selectNumCharArr = new char[]{'1', '2', '3', '4', '5', '6', '7', '8', '9'};
int cellRowIndex;
int cellColumnIndex;
int cellRowIndexStart;
int cellColumnIndexStart;
int cellRowIndexEnd;
int cellColumnIndexEnd;
boolean reachEnd;
boolean isFindResult;
boolean isExit;public void solveSudoku(char[][] board) {backtrack(0, 0, board);
}/*** 思路:依次遍历每一个格子来填充数字** @param rowIndex* @param columnIndex* @param board* @return*/
private boolean backtrack(int rowIndex, int columnIndex, char[][] board) {if (rowIndex == board.length) {// --if-- 所有行都已经遍历完成,说明全部数字填充完成return true;}if (board[rowIndex][columnIndex] != '.') {// --if-- 当前格子已经有数字// 判断当前行的每个格子是否遍历完成reachEnd = (columnIndex + 1 == board.length);// 如果当前行遍历完成,就要换行,columnIndex从0开始isFindResult = backtrack(reachEnd ? rowIndex + 1 : rowIndex, reachEnd ? 0 : columnIndex + 1, board);if (isFindResult) {// --if-- 找到结果,直接返回return true;}} else {for (int k = 0; k < selectNumCharArr.length; k++) {char numChar = selectNumCharArr[k];// 检测同一行是否有重复值isExit = false;for (int j = 0; j < board.length; j++) {if (board[rowIndex][j] == numChar) {isExit = true;break;}}if (isExit) {continue;}// 检测同一列是否有重复值for (int i = 0; i < board.length; i++) {if (board[i][columnIndex] == numChar) {isExit = true;break;}}if (isExit) {continue;}// 检测框里面是否有重复值cellRowIndex = rowIndex / 3;cellColumnIndex = columnIndex / 3;cellRowIndexStart = cellRowIndex * 3;cellColumnIndexStart = cellColumnIndex * 3;cellRowIndexEnd = (cellRowIndex + 1) * 3;cellColumnIndexEnd = (cellColumnIndex + 1) * 3;for (int i = cellRowIndexStart; i < cellRowIndexEnd; i++) {for (int j = cellColumnIndexStart; j < cellColumnIndexEnd; j++) {if (board[i][j] == numChar) {isExit = true;break;}}if (isExit) {break;}}if (isExit) {continue;}// 通过上述校验,可以放下当前数字board[rowIndex][columnIndex] = numChar;// 递归到下一个格子reachEnd = (columnIndex + 1 == board.length);isFindResult = backtrack(reachEnd ? rowIndex + 1 : rowIndex, reachEnd ? 0 : columnIndex + 1, board);if (isFindResult) {return true;}// 回溯board[rowIndex][columnIndex] = '.';}}return false;
}
优化
使用boolean数组去重,减少行、列、单元格的循环判断
char[] selectNumCharArr = new char[]{'1', '2', '3', '4', '5', '6', '7', '8', '9'};
boolean reachEnd;
boolean isFindResult;
// 行去重
boolean[][] rowUsedArr = new boolean[9][9];
// 列去重
boolean[][] columnUsedArr = new boolean[9][9];
// 单元去重
boolean[][] cellUsedArr = new boolean[9][9];public void solveSudoku(char[][] board) {int num;for (int i = 0; i < board.length; i++) {for (int j = 0; j < board.length; j++) {if (board[i][j] != '.') {num = board[i][j] - '1';rowUsedArr[i][num] = true;columnUsedArr[j][num] = true;cellUsedArr[(i / 3) * 3 + (j / 3)][num] = true;}}}backtrack(0, 0, board);
}/*** 思路:依次遍历每一个格子来填充数字** @param rowIndex* @param columnIndex* @param board* @return*/
private boolean backtrack(int rowIndex, int columnIndex, char[][] board) {if (rowIndex == board.length) {// --if-- 所有行都已经遍历完成,说明全部数字填充完成return true;}if (board[rowIndex][columnIndex] != '.') {// --if-- 当前格子已经有数字// 判断当前行的每个格子是否遍历完成reachEnd = (columnIndex + 1 == board.length);// 如果当前行遍历完成,就要换行,columnIndex从0开始isFindResult = backtrack(reachEnd ? rowIndex + 1 : rowIndex, reachEnd ? 0 : columnIndex + 1, board);if (isFindResult) {// --if-- 找到结果,直接返回return true;}} else {for (int k = 0; k < selectNumCharArr.length; k++) {int num = selectNumCharArr[k] - '1';// 检测同一行是否有重复值if (rowUsedArr[rowIndex][num]) {continue;}// 检测同一列是否有重复值if (columnUsedArr[columnIndex][num] == true) {continue;}// 检测框里面是否有重复值int cellIndex = (rowIndex / 3) * 3 + (columnIndex / 3);if (cellUsedArr[cellIndex][num]) {continue;}// 通过上述校验,可以放下当前数字board[rowIndex][columnIndex] = selectNumCharArr[k];rowUsedArr[rowIndex][num] = true;columnUsedArr[columnIndex][num] = true;cellUsedArr[cellIndex][num] = true;// 递归到下一个格子reachEnd = (columnIndex + 1 == board.length);isFindResult = backtrack(reachEnd ? rowIndex + 1 : rowIndex, reachEnd ? 0 : columnIndex + 1, board);if (isFindResult) {return true;}// 回溯board[rowIndex][columnIndex] = '.';rowUsedArr[rowIndex][num] = false;columnUsedArr[columnIndex][num] = false;cellUsedArr[cellIndex][num] = false;}}return false;
}
相关文章:

《代码随想录》刷题笔记——回溯篇【java实现】
文章目录 组合组合总和 III电话号码的字母组合组合总和组合总和II思路代码实现 分割回文串※思路字符串分割回文串判断效率优化※ 复原 IP 地址优化版本 子集子集 II使用usedArr辅助去重不使用usedArr辅助去重 递增子序列※全排列全排列 II重新安排行程题意代码 N 皇后解数独直…...

数值积分:通过复合梯形法计算
在物理学和工程学中,很多问题都可以通过数值积分来求解,特别是当我们无法得到解析解时。数值积分是通过计算积分区间内离散点的函数值来近似积分的结果。在这篇博客中,我将讨论如何使用 复合梯形法 来进行数值积分,并以一个简单的…...

AcWing——3624. 三值字符串
双指针解法 #include<iostream> #include<unordered_map> using namespace std; int main() {int n; cin >> n;while(n--){unordered_map<char, int> tree;string s; cin >> s;int ans 0x7fffffff; for(int i 0, j 0; j < (int)s.size();…...

【JavaEE进阶】验证码案例
目 🌲实现说明 🎄Hutool介绍 🌳准备工作 🌴约定前后端交互接口 🚩接口定义 🚩实现服务器后端代码 🚩前端代码 🚩整体测试 🌲实现说明 随着安全性的要求越来越⾼…...

Uniapp 短视频去水印解析工具开发实现
最近搞了一个有意思的小工具——短视频去水印解析器!这玩意儿可以把短视频中的水印给抹掉,还能提取视频、封面等资源。整个项目用了 Uniapp 开发,做完后体验了一下,发现还挺顺手。今天就来跟大家聊聊实现思路和代码细节~ 需求分析…...

计算机网络-八股-学习摘要
一:HTTP的基本概念 全称: 超文本传输协议 从三个方面介绍HTTP协议 1,超文本:我们先来理解「文本」,在互联网早期的时候只是简单的字符文字,但现在「文本」的涵义已经可以扩展为图片、视频、压缩包等&am…...

编程速递-庆祝Delphi诞生30周年!
庆祝Delphi 30周年纪念是一个特别的时刻。 回到1995年,也就是30年前,在微软Windows和互联网时代的曙光初现之时,Borland Delphi的创建者们无法想象,当时使用Borland Delphi构建的应用程序至今仍在运行——为全世界数十亿人服务。…...

每天五分钟深度学习框架pytorch:搭建谷歌的Inception网络模块
本文重点 前面我们学习了VGG,从现在开始我们将学习谷歌公司推出的GoogLeNet。当年ImageNet竞赛的第二名是VGG,而第一名就是GoogLeNet,它的模型设计拥有很多的技巧,这个model证明了一件事:用更多的卷积,更深的层次可以得到更好的结构 GoogLeNet的网络结构 如图所示就是Go…...

性能测试流程、主流性能工具
性能测试流程 性能测试流程 测试测试需求分析 性能测试计划和方案 测什么: 测试背景 测试目的 测试范围 谁来测: 进度和分工 交付清单 怎么测: 测试策略 性能测试用例设计 性能测试测试执行 性能分析和调优 性能测试报告 测试报告是…...

DeepSeek4j 已开源,支持思维链,自定义参数,Spring Boot Starter 轻松集成,快速入门!建议收藏
DeepSeek4j Spring Boot Starter 快速入门 简介 DeepSeek4j 是一个专为 Spring Boot 设计的 AI 能力集成启动器,可快速接入 DeepSeek 大模型服务。通过简洁的配置和易用的 API,开发者可轻松实现对话交互功能。 环境要求 JDK 8Spring Boot 2.7Maven/Gr…...

无耳科技 Solon v3.0.8 发布,Java 企业级应用开发框架
Solon 框架! Solon 是新一代,Java 企业级应用开发框架。是杭州无耳科技有限公司的“根级”开源项目(最近“杭州六小龙”很火啊,我们也是杭州的哦)。从零开始构建(No Spring、No Java-EE、No Servlet&#…...

【吾爱出品】针对红警之类老游戏适用WIN10和11的补丁cnc-ddraw7.1汉化版
针对红警之类老游戏适用WIN10和11的补丁cnc-ddraw7.1汉化版 链接:https://pan.xunlei.com/s/VOJ8PZd4avMubnDzHQAeZDxWA1?pwdnjwm# 直接复制到游戏安装目录,保持与游戏主程序同目录下。...
使用 playwright 自定义 js 下载的路径和文件名
遇到一个问题,点击按钮自动下载文件,路径和文件名都不能自定义,可以用 playwright 来解决这个问题 from playwright.sync_api import sync_playwright import os import time class ExcelDownloader: def __init__(self, download_pat…...

Kafka分区管理大师指南:扩容、均衡、迁移与限流全解析
#作者:孙德新 文章目录 分区分配操作(kafka-reassign-partitions.sh)1.1 分区扩容、数据均衡、迁移(kafka-reassign-partitions.sh)1.2、修改topic分区partition的副本数(扩缩容副本)1.3、Partition Reassign场景限流1.4、节点内副本移动到不…...

3.从零开始学会Vue--{{生命周期,工程化,组件化}}
1.生命周期钩子 1.是什么 生命周期 概念:就是一个Vue实例从创建 到 销毁 的整个过程 生命周期包括:① 创建 ② 挂载 ③ 更新 ④ 销毁 四个阶段 1.创建阶段:创建响应式数据 2.挂载阶段:渲染模板 3.更新阶段:修改…...
Python--网络编程
3. 网络编程与Socket 3.1 Socket基础 创建Socket import socket# TCP Socket tcp_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM)# UDP Socket udp_socket socket.socket(socket.AF_INET, socket.SOCK_DGRAM)服务器端函数 函数描述bind((host, port))绑定…...
【java】方法的基本内存原理(栈和堆)
java内存主要分为栈和堆,方法相关的部分主要在栈内存里,每个方法调用时会在栈里创建一个栈帧,存放局部变量和方法执行的信息。执行完后栈帧被销毁,局部变量消失。而对象实例存在堆里,由垃圾回收器管理。 **Java方法内…...

SQLMesh 系列教程4- 详解模型特点及模型类型
SQLMesh 作为一款强大的数据建模工具,以其灵活的模型设计和高效的增量处理能力脱颖而出。本文将详细介绍 SQLMesh 模型的特点和类型,帮助读者快速了解其强大功能。我们将深入探讨不同模型类型(如增量模型、全量模型、SCD Type 2 等࿰…...

SpringBoot(接受参数相关注解)
文章目录 1.基本介绍2.PathVariable 路径参数获取信息 1.代码实例 1.index.html2.ParameterController.java3.测试 2.细节说明 3.RequestHeader 请求头获取信息 1.代码实例 1.index.html2.ParameterController.java3.测试 2.细节说明 4.RequestParameter 请求获取参数信息 1.…...
hbase合并队列超长问题分析
问题现象 hbase集群合并队列超长,有节点上合并任务已经运行超过1天未结束,合并队列总长不断增加。 问题分析 参数配置: 配置参数默认值含义hbase.hregion.memstore.flush.size128MMemStore达到该值会Flush成StoreFilehbase.hregion.memstore.block.multiplier4当region中…...

Docker 运行 Kafka 带 SASL 认证教程
Docker 运行 Kafka 带 SASL 认证教程 Docker 运行 Kafka 带 SASL 认证教程一、说明二、环境准备三、编写 Docker Compose 和 jaas文件docker-compose.yml代码说明:server_jaas.conf 四、启动服务五、验证服务六、连接kafka服务七、总结 Docker 运行 Kafka 带 SASL 认…...

【CSS position 属性】static、relative、fixed、absolute 、sticky详细介绍,多层嵌套定位示例
文章目录 ★ position 的五种类型及基本用法 ★ 一、position 属性概述 二、position 的五种类型详解(初学者版) 1. static(默认值) 2. relative(相对定位) 3. absolute(绝对定位) 4. fixed(固定定位) 5. sticky(粘性定位) 三、定位元素的层级关系(z-i…...
【android bluetooth 框架分析 04】【bt-framework 层详解 1】【BluetoothProperties介绍】
1. BluetoothProperties介绍 libsysprop/srcs/android/sysprop/BluetoothProperties.sysprop BluetoothProperties.sysprop 是 Android AOSP 中的一种 系统属性定义文件(System Property Definition File),用于声明和管理 Bluetooth 模块相…...
Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!
一、引言 在数据驱动的背景下,知识图谱凭借其高效的信息组织能力,正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合,探讨知识图谱开发的实现细节,帮助读者掌握该技术栈在实际项目中的落地方法。 …...
【C语言练习】080. 使用C语言实现简单的数据库操作
080. 使用C语言实现简单的数据库操作 080. 使用C语言实现简单的数据库操作使用原生APIODBC接口第三方库ORM框架文件模拟1. 安装SQLite2. 示例代码:使用SQLite创建数据库、表和插入数据3. 编译和运行4. 示例运行输出:5. 注意事项6. 总结080. 使用C语言实现简单的数据库操作 在…...
Unit 1 深度强化学习简介
Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库,例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体,比如 SnowballFight、Huggy the Do…...

智能分布式爬虫的数据处理流水线优化:基于深度强化学习的数据质量控制
在数字化浪潮席卷全球的今天,数据已成为企业和研究机构的核心资产。智能分布式爬虫作为高效的数据采集工具,在大规模数据获取中发挥着关键作用。然而,传统的数据处理流水线在面对复杂多变的网络环境和海量异构数据时,常出现数据质…...

安宝特方案丨船舶智造的“AR+AI+作业标准化管理解决方案”(装配)
船舶制造装配管理现状:装配工作依赖人工经验,装配工人凭借长期实践积累的操作技巧完成零部件组装。企业通常制定了装配作业指导书,但在实际执行中,工人对指导书的理解和遵循程度参差不齐。 船舶装配过程中的挑战与需求 挑战 (1…...
CSS | transition 和 transform的用处和区别
省流总结: transform用于变换/变形,transition是动画控制器 transform 用来对元素进行变形,常见的操作如下,它是立即生效的样式变形属性。 旋转 rotate(角度deg)、平移 translateX(像素px)、缩放 scale(倍数)、倾斜 skewX(角度…...

Linux nano命令的基本使用
参考资料 GNU nanoを使いこなすnano基础 目录 一. 简介二. 文件打开2.1 普通方式打开文件2.2 只读方式打开文件 三. 文件查看3.1 打开文件时,显示行号3.2 翻页查看 四. 文件编辑4.1 Ctrl K 复制 和 Ctrl U 粘贴4.2 Alt/Esc U 撤回 五. 文件保存与退出5.1 Ctrl …...