- 归并排序
- 汉诺塔问题
- 添加括号的所有方式
- 分析
- 代码
- 至少有 K 个重复字符的最长子串
归并排序是典型的分治思想
void sort(int[] nums, int lo, int hi) { int mid = (lo + hi) / 2; // 对数组的两部分分别排序 sort(nums, lo, mid); sort(nums, mid + 1, hi); // 合并两个排好序的子数组 merge(nums, lo, mid, hi); }汉诺塔问题
添加链接描述
解题思路:递归与分治
假设 n = 1,只有一个盘子,直接把它从 A 移到 C 上;
如果 n = 2 ,这时候我们就要借助 B 了,因为小盘子必须时刻都在大盘子上面,所以是:0到b,1到c,然后0从b到c。
如果 n > 2 呢?思路和上面是一样的,我们把 n 个盘子也看成两个部分,一部分是最下面的最大的那个盘子,另一部分是上面的 n - 1 个盘子。如下:
那 n - 1 个盘子是怎么从 A 移到 C 的呢?
注意,当你在思考这个问题的时候,就将最初的 n 个盘子从 A 移到 C 的问题,转化成了将 n - 1 个盘子从 A 移到 C 的问题, 依次类推,直至转化成 1 个盘子的问题时,问题也就解决了。这就是分治的思想。
而实现分治思想的常用方法就是递归。不难发现,如果原问题可以分解成若干个与原问题结构相同但规模较小的子问题时,往往可以用递归的方法解决。具体解决办法如下:
n = 1 时,直接把盘子从 A 移到 C; n > 1 时, 先把上面 n - 1 个盘子从 A 移到 B(子问题,递归); 再将最大的盘子从 A 移到 C; 再将 B 上 n - 1 个盘子从 B 移到 C(子问题,递归)。
leetcode上汉罗塔问题的ac代码:
class Solution { public void hanota(List添加括号的所有方式a, List b, List c) { int n=a.size(); move(a,b,c,n); } private void move(List a, List b, List c, int n) { if(n==1){ c.add(a.remove(a.size()-1)); return; } move(a,c,b,n-1); //借助c,将a上的前n-1个移到b上 c.add(a.remove(a.size()-1));//把a上的最后一个移到c上 move(b,a,c,n-1); //再借助a,将b上的n-1个移到c上,搞定 } }
添加链接描述
分析首先明确本题用分治法,分治分治,分而治之
举例分析
1 + 2 * 3 - 4 * 5
1、分
总共分下面四种:
(1) + (2 * 3 - 4 * 5) (1 + 2) * (3 - 4 * 5) (1 + 2 * 3) - (4 * 5) (1 + 2 * 3 - 4) * (5)
发现规律了么?其实就是按照运算符进行分割,给每个运算符的左右两部分加括
继续分
现在单独说上面的第三种情况:
(1 + 2 * 3) - (4 * 5)
我们用减号-作为分隔,把原算式分解成两个算式1 + 2 * 3和4 * 5。
1 + 2 * 3可以有两种加括号的方式,分别是:
(1) + (2 * 3) = 7 (1 + 2) * (3) = 9
分到底了
2、治
通过上述结果推导出(1 + 2 * 3) - (4 * 5)有两种结果,分别是:
9 - 20 = -11 7 - 20 = -13
伪代码:
ListF("(1 + 2 * 3) - (4 * 5)"){ left=F("1 + 2 * 3") right=F("4 * 5") for (int a : left) for (int b : right) res.add(a - b); return res; }
于是上面的分和治用代码描述如下:
每个运算符都可以把原问题分割成两个子问题,刚才已经列出了所有可能的分割方式:
(1) + (2 * 3 - 4 * 5) (1 + 2) * (3 - 4 * 5) (1 + 2 * 3) - (4 * 5) (1 + 2 * 3 - 4) * (5)
所以,我们需要穷举上述的每一种情况
3、剪枝优化
当输入的算式如下:
1 + 1 + 1 + 1 + 1
那么按照算法逻辑,按照运算符进行分割,一定存在下面两种分割情况:
(1 + 1) + (1 + 1 + 1)
(1 + 1 + 1) + (1 + 1)
算法会依次递归每一种情况,就会有冗余计算,此时可以记忆化剪枝!
class Solution { Map> map = new HashMap<>(); public List diffWaysToCompute(String expression) { return dfs(expression, 0, expression.length() - 1); } private List dfs(String expression, int left, int right) { List ans = new ArrayList<>(); String subStr = expression.substring(left, right + 1); if (map.containsKey(subStr)) { //记忆化剪枝 return map.get(subStr); } for (int i = left; i <= right; i++) { if (Character.isDigit(expression.charAt(i))) { //数字:跳过。 continue; } //运算符 List leftList = dfs(expression, left, i - 1); List rightList = dfs(expression, i + 1, right); for (Integer l : leftList) { for (Integer r : rightList) { switch (expression.charAt(i)) { case '+': ans.add(l + r); break; case '-': ans.add(l - r); break; case '*': ans.add(l * r); break; case '/': ans.add(l / r); break; } } } } if (ans.isEmpty()) { // 如果 ans 为空,说明subStr是一个数字,没有运算符. ans.add(Integer.parseInt(subStr)); } map.put(subStr, ans); return ans; } }
可以看到和构造bst树类似
至少有 K 个重复字符的最长子串添加链接描述
思路:
先整体考虑,如果整个字符串中的所有字符出现次数都>=k,则这个字符串就是所求
如果某个字符在整个字符串中的出现次数 < k,那它一定不会出现在合法子串中。比如: s: aaabbaa,k: 3,b 只出现 2 次,它肯定不会出现在合法子串中,分别递归其左右,左右返回值中较大的即为所求(分治)。
充分剪枝
1、当子串的长度小于 k 即可结束递归
2、如果当前递归的子串的两端,即 start 和 end 上的字符,在当前子串中出现的次数小于 k,则合法子串肯定不包含,移动指针,直到指向不小于k的字符。有些状态直接不考察,也是在剪枝。
class Solution { char[]arr; int k; public int longestSubstring(String s, int k) { int n = s.length(); if(nreturn 0; } arr = s.toCharArray(); this.k=k; return dfs(0, n - 1); } private int dfs(int left, int right) { //剪枝1 if(right-left+1 return 0; } //求出[left,right]中所有字符出现次数 int[]hash=new int[26]; for (int i=left;i<=right;i++){ hash[arr[i]-'a']++; } //剪枝2 while (right-left+1>k && hash[arr[left]-'a'] k && hash[arr[right]-'a'] return 0; } for (int i=left;i<=right;i++){ if(hash[arr[i]-'a'] //arr[i]出现次数 =k,则[left,right]的字符串符合题目要求 return right-left+1; } }
本题不能使用滑动窗口