目录
第4章 递归和动态规划
1 斐波那契系列问题的递归和动态规划(将★★★★)
2 矩阵的最小路径和
3 换钱的最少货币数
4 换钱的方法数
5 最长递增子序列(校 ★★★☆)
6 汉诺塔问题(校 ★★★☆)
7 最长公共子序列问题
8 最长公共子串问题(校★★★☆)
9 最小编辑代价(校 ★★★☆)
10 字符串的交错组成(校★★★☆)
11 龙与地下城游戏问题
12 数字字符串转换为字母组合的种数
13 表达式得到期望结果的组成种数(校★★★☆)
14 排成一条线的纸牌博弈问题
15 跳跃游戏(士 ★☆☆☆)
16 数组中的最长连续序列
17 N皇后问题(校 ★★★☆)
第5章 字符串问题
1 判断两个字符串是否互为变形词(士 ★☆☆☆)
2 字符串中数字子串的求和(士 ★☆☆☆)
3 去掉字符串中连续出现&个0的子串(士 ★☆☆☆)
4 判断两个字符串是否互为旋转词(士 ★☆☆☆)
5 将整数字符串转成整数值
6 替换字符串中连续出现的指定字符串(士 ★☆☆☆)
7 字符串的统计字符串(士 ★☆☆☆)
8 判断字符数组中是否所有的字符都只出现过一次
9 在有序但含有空的数组中査找字符串
10 字符串的调整与替换(士 ★☆☆☆)
11 翻转字符串(士 ★☆☆☆)
12 数组中两个字符串的最小距离
13 添加最少字符使字符串整体都是回文字符串(校★★★☆)
14 括号字符串的有效性和最长有效长度(原问题士 ★☆☆☆)
15 公式字符串求值(校 ★★★☆)
16 0左边必有1的二进制字符串数量(校★★★☆)
17 拼接所有字符串产生字典顺序最小的大写字符串(校★★★☆)
18 找到字符串的最长无重复字符子串
19 找到被指的新类型字符(士 ★☆☆☆)
20 最小包含子串的长度(校★★★☆)
21 回文最少分割数(尉★★★☆)
22 字符串匹配问题(校★★★☆)
23 字典树(前缀树)的实现
第6章 大数据和空间限制
1 认识布隆过滤器(尉 303
2 只用2GB内存在20亿个整数中找到出现次数最多的数(士 ★☆☆☆)
3 40亿个非负整数中找到没出现的数
4 找到100亿个URL中重复的URL以及搜索词汇的top K问题(士 ★☆☆☆)
5 40亿个非负整数中找到出现两次的数和所有数的中位数
6 一致性哈希算法的基本原理
第4章 递归和动态规划
暴力递归:
- 把问题转化为规模缩小了的同类问题的子问题
- 有明确的不需要继续进行递归的条件(base case)
- 有当得到了子问题的结果之后的决策过程
- 不记录每一个子问题的解、
动态规划:
- 从暴力递归中来
- 将每一个子问题的解记录下来,避免重复计算
- 把暴力递归的过程,抽象成了状态表达
- 并且存在化简状态表达,使其更加简洁的可能
递归与顺序计算的区别:求n!
在顺序计算中是知道了具体怎么算,1*2*3...这样,因此可以写出代码:
public static long getFactorial2(int n) { long result = 1L; for (int i = 1; i <= n; i++) { result *= i; } return result; }
对于递归而言则是我不知道怎么算,但是知道怎么试,求n!只需要求出n*(n-1)!这样不断递归下去
public static long getFactorial1(int n) { if (n == 1) { return 1L; } return (long) n * getFactorial1(n - 1); }
从上面可以看出直接计算与递归是正反方向去考虑的
依据上面的思路递归方法其时间复杂度为O(N)
public static int cowNumber(int n) { if (n < 1) { return 0; } if (n == 1 || n == 2 || n == 3) { return n; } return cowNumber(n - 1) + cowNumber(n - 3); }
递归版本即从(0,0)位置开始不断向下搜索
public static int minPath(int[][] matrix) { return process(matrix, 0, 0); } //从i、j出发到达最右下角位置的路径和 public static int process(int[][] matrix, int i, int j) { //当已经到达最右下角了便返回最右下角的数 if (i == matrix.length - 1 && j == matrix[0].length-1){ return matrix[i][j]; } //当紧邻着右下角的上方时,最短路径只能向下 if (i == matrix.length - 1){ return matrix[i][j] + process(matrix,i,j+1); } //当紧邻着右下角的左方时,最短路径只能向右 if (j == matrix[0].length - 1){ return matrix[i][j] + process(matrix,i+1,j); } //向右走 int right = process(matrix,i,j+1); //向下走 int down = process(matrix,i+1,j); return matrix[i][j] + Math.min(right,down); }
上面给出了递归版本代码但是其复杂度很高,中间有大量重复计算的过程
因为有重复计算有一种思路即把重复状态记录下来,这样就可以保证只计算一次,这就是记忆化搜索的思路。这里直接分析如何改成动态规划。
可以改成动态规划的条件:在递归过程中有重复状态,并且重复状态和最后的计算结果没有关系
对于上面的这种情况有一个专业术语描述叫做无后效性问题,但对于汉诺塔问题,其前面的移动顺序会对后面的移动顺序产生影响,这样就不可能改成动态规划
对于上面的代码:当i j确定了,返回值就确定了。i与j确定了一个表,那么是否可以构造出一个辅助表来计算
上面这个思路也是暴力递归改动态规划的普遍思路:先写出暴力递归,分析可变参数,根据可变参数的数目画出n维表,分析依赖值和最终结果位置
public static int minPath2(int[][] m) { if (m == null || m.length == 0 || m[0] == null || m[0].length == 0) { return 0; } int row = m.length; int col = m[0].length; int[][] dp = new int[row][col]; dp[0][0] = m[0][0]; //先填充行 for (int i = 1; i < row; i++) { dp[i][0] = dp[i - 1][0] + m[i][0]; } //再填充列 for (int j = 1; j < col; j++) { dp[0][j] = dp[0][j - 1] + m[0][j]; } //一般位置的填充 for (int i = 1; i < row; i++) { for (int j = 1; j < col; j++) { dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + m[i][j]; } } return dp[row - 1][col - 1]; }
先考虑如何进行暴力计算
public static int coins1(int[] arr, int aim) { if (arr == null || arr.length == 0 || aim < 0) { return 0; } return process1(arr, 0, aim); } //暴力递归 index:可以任意自由使用index及其之后所有的钱 //aim:目标钱数 public static int process1(int[] arr, int index, int aim) { int res = 0; if (index == arr.length) { //若一直到最后还有剩余说明方法是无效的,返回0 res = aim == 0 ? 1 : 0; } else { for (int i = 0; arr[index] * i <= aim; i++) { res += process1(arr, index + 1, aim - arr[index] * i); } } return res; }
因为暴力递归中会有大量的重复计算,在递归的过程中记录中间的计算结果便是记忆化搜索,再次升级便是动态规划,下面直接考虑动态规划
例子如下:以(4,2)位置示例,依次寻找(4,3)(2,3)(0,3)进行累加
public static int coins4(int[] arr, int aim) { if (arr == null || arr.length == 0 || aim < 0) { return 0; } int[][] dp = new int[arr.length][aim + 1]; for (int i = 0; i < arr.length; i++) { dp[i][0] = 1; } for (int j = 1; arr[0] * j <= aim; j++) { dp[0][arr[0] * j] = 1; } for (int i = 1; i < arr.length; i++) { for (int j = 1; j <= aim; j++) { dp[i][j] = dp[i - 1][j]; //若减完后没越界便加上 dp[i][j] += j - arr[i] >= 0 ? dp[i][j - arr[i]] : 0; } } return dp[arr.length - 1][aim]; }
在有了上面累加的之后还可以考虑进行优化,要求(4,2)只需要把(2,2)和(4,3)进行累加即可,因为(2,2)已经考虑前面的情况
忽略汉诺塔中的左中右概念,假设有三根柱子分别表示:from、to、help,我们的目的就是把n个圆盘从from柱子上移动到to柱子,同时借用help柱子。
采用递归的思路就是不断的把圆盘一个一个从from移动到to上,把上面的n-1个看出一个整体,最底下1个是另一个整体
//N:总共的圆盘数; public static void func(int N, String from, String help, String to) { if (N == 1) { System.out.println("move " + 1 + " from " + from + " to " + to); } else { //把上面N-1个圆盘从from移动到help上 func( N-1, from, help,to); //单独移动第N个圆盘从from圆盘到to圆盘 System.out.println("Move "+N+" from " + from + " to " + to); //再把N-1个圆盘从help圆盘移动到to圆盘 func(N-1, help, to, from); } }
玩家1不存在必胜策略如[1,100,1],考虑暴力法,那么需要分两种情况:拿左边和右边,然后不断递归寻找一个是自己取值大对手取值小的策略
public static int win1(int[] arr) { if (arr == null || arr.length == 0) { return 0; } return Math.max(f(arr, 0, arr.length - 1), s(arr, 0, arr.length - 1)); } //考虑自己的情况分成两步自己先拿,对手再拿,自己在拿,那么第二次拿时可以认为自己是后手 //这样一次循环后的值才是一轮中自己的期望取值 public static int f(int[] arr, int i, int j) { if (i == j) { return arr[i]; } return Math.max(arr[i] + s(arr, i + 1, j), arr[j] + s(arr, i, j - 1)); } //当对手拿完后自己成为了后拿的人,对手必然给自己留下一个最差的情况,因此要取小值 public static int s(int[] arr, int i, int j) { //当相等时只有一张牌,后拿的人为0 if (i == j) { return 0; } return Math.min(f(arr, i + 1, j), f(arr, i, j - 1)); }
动态规划分析如下:
public static int win2(int[] arr) { if (arr == null || arr.length == 0) { return 0; } int[][] f = new int[arr.length][arr.length]; int[][] s = new int[arr.length][arr.length]; for (int j = 0; j < arr.length; j++) { f[j][j] = arr[j]; for (int i = j - 1; i >= 0; i--) { f[i][j] = Math.max(arr[i] + s[i + 1][j], arr[j] + s[i][j - 1]); s[i][j] = Math.min(f[i + 1][j], f[i][j - 1]); } } return Math.max(f[0][arr.length - 1], s[0][arr.length - 1]); }
第5章 字符串问题
这道题目和LeetCode8 字符串转换整数类似,但从复杂度上而言并没有LeetCode上的复杂
public static int convert(String str){ if (str == null || str.equals("")){ return 0; //不能转换的情况 } char[] chars = str.toCharArray(); if (!isValid(chars)){ return 0; //不能有效转换直接返回0 } //是否以负号开头 boolean posi = chars[0] =='-' ? false : true; int minq = Integer.MIN_VALUE /10; int minr = Integer.MIN_VALUE % 10; int res = 0; int cur = 0; for (int i = posi ? 0: 1; i < chars.length; i++) { cur = '0'-chars[i];//因为负数的范围大,因此使用负数保存数据 if ((res < minq) || (res == minq && cur < minr)) { return 0; //数据超出范围 } res = res *10+cur; } if (posi && res == Integer.MIN_VALUE) { return 0; //当为正数,并且res为int的最小值时,换成正数必然超过int范围,因此返回0 } return posi ? -res : res; } //判断是否有效 private static boolean isValid(char[] chars) { if (chars[0] !='-' && (chars[0] < '0' || chars[0] > '9')){ return false;//不是负号开头,并且不是0到9开头的 } if (chars[0] == '-' && (chars.length==1 || chars[1] == '0')){ return false;//是负号开头的,但是长度为1或负号后面有0 } if (chars[0] =='0' && chars.length >1){ return false;//以0开头并且长度大于1 } for (int i = 1; i < chars.length; i++) { if (chars[i]<'0' || chars[i] > '9'){//遇到非数字的就返回失败 return false; } } return true; }
14 括号字符串的有效性和最长有效长度(原问题士 ★☆☆☆)
17 拼接所有字符串产生字典顺序最小的大写字符串(校★★★☆)
字典序:当长度相等时,值小的在前面:abc在bcd前面;长度不等时将短的补齐后再比(补的时候按照值最小的补):abc与b将b补成b00,然后比,abc仍在前面。
给出一个贪心策略:将这些字符串两两比较,从小到大排序后然后拼接
public static String lowestString(String[] strs) { if (strs == null || strs.length == 0) { return ""; } //依据自己定义的方法进行排序 Arrays.sort(strs, new MyComparator()); //把排序后的结果合并 String res = ""; for (int i = 0; i < strs.length; i++) { res += strs[i]; } } //自己定义比较方法 private static class MyComparator implements Comparator<String> { @Override //排序时比较的应该是拼接后的大小 public int compare(String a, String b) { return (a + b).compareTo(b + a); } }
为什么这样比价后一定是正确?这里给出一部分证明如下图所示:
在上面的证明结束后还要证明这样排序后整体也是最小,从上面可以看出贪心的证明是非常麻烦的,对于贪心策略就是一种最优化策略,平时注意记录多种贪心策略然后写出来后利用产生的对数器方法进行验证,去证明是非常麻烦的一件事。不要去考虑贪心为什么正确,直接记住这个最优方式即可。
第6章 大数据和空间限制
2 只用2GB内存在20亿个整数中找到出现次数最多的数(士 ★☆☆☆)
4 找到100亿个URL中重复的URL以及搜索词汇的top K问题(士 ★☆☆☆)
0