知识点
知识点
分治算法详解
分治算法是什么:
分治算法是是一种算法思想,通过将原问题分解成小规模的子问题,然后根据子问题的结果构造出原问题的答案。
这有点类似动态规划,所以说运用分治算法也需要满足一些条件:你的原问题结果应该可以通过合并子问题结果来计算
阶乘相关的算法题目
阶乘后的零
阶乘的时间复杂度最高,因此不可能直接计算然后得出。
首先,两个数相乘结果末尾有 0,一定是因为两个数中有因子 2 和 5,因为 10 = 2 x 5。
也就是说,问题转化为:n!最多可以分解出多少个因子 2 和 5?
同时,因为每个偶数都能分解出因子 2,因子 2 肯定比因子 5 多得多,所以n!最多可以分解出多少个因子 2 和 5只和因子 5相关。
所以问题又可以转化为:n!最多可以分解出多少个因子 5,难点在于像 25,50,125 这样的数,可以提供不止一个因子 5,怎么才能不漏掉。
例如125!有多少个因子5:首先,125 / 5 = 25,这一步就是计算有多少个像 5,15,20,25 这些 5 的倍数,它们一定可以提供一个因子 5;我们再计算出125!中有 125 / 25 = 5 个 25 的倍数,它们每人可以额外再提供一个因子 5;我们还得再计算出125!中有 125 / 125 = 1 个 125 的倍数,它还可以额外再提供一个因子 5;最后,没有更高的倍数了。125!最多可以分解出 20 + 5 + 1 = 26 个因子 5,也就是说阶乘结果的末尾有 26 个 0
接雨水问题
首先,暴力解法:
对于这种问题,我们不要想整体,而应该去想局部,具体来说,仅仅对于位置i,能装下多少水,位置i能达到的水柱高度和其左边的最高柱子、右边的最高柱子有关,我们分别称这两个柱子高度为l_max和r_max;位置 i 最大的水柱高度就是min(l_max, r_max),因此位置i能够装的水为:
// height[0..i]代表i左边的所有柱子,height[i..j]代表i右边的所有zhuz val[i] = min(max(height[0..i]), max(height[i..j])) - height[i];
根据上述思路,写出暴力解法:
public int trap(int[] height) { int res = 0; // 遍历所有柱子 for (int i = 0; i < height.length; i++) { int lmax = 0, rmax = 0; // 寻找左右最高的柱子 for (int m = i; m >= 0; m--) { lmax = Math.max(lmax, height[m]); } for (int n = i; n < height.length; n++) { rmax = Math.max(rmax, height[n]); } // 将i位置能够装的水加入结果中 res = res + (Math.min(lmax, rmax) - height[i]); } // 返回结果 return res; }
暴力解法,时间复杂度为O(N^2),空间复杂度为O(1)
暴力解法的优化:
暴力解法中,每个位置i都要计算r_max和l_max。如果我们提前计算出每个i的l_max和r_max,就不用每次都计算了。
开辟两个长度为i的数组lmax和rmax,记录在i位置的左边最高的柱子高度和右边最高的柱子高度,然后套用暴力解法的思路即可:
public int trap(int[] height) { if (height.length == 0) return 0; int res = 0; // 定义两个数组 int[] lmaxArr = new int[height.length]; int[] rmaxArr = new int[height.length]; // 填充值 lmaxArr[0] = height[0]; for (int i = 1; i < height.length; i++) { lmaxArr[i] = Math.max(height[i], lmaxArr[i - 1]); } rmaxArr[height.length - 1] = height[height.length - 1]; for (int i = height.length - 2; i >= 0; i--) { rmaxArr[i] = Math.max(height[i], rmaxArr[i + 1]); } // 遍历所有柱子 for (int i = 0; i < height.length; i++) { int lmax = lmaxArr[i], rmax = rmaxArr[i]; // 将i位置能够装的水加入结果中 res = res + (Math.min(lmax, rmax) - height[i]); } // 返回结果 return res; }
优化后的暴力解法,时间复杂度为O(N),空间复杂度O(N)
双指针解法:
该解法思路和上述的一样,也是拆解到每个i然后计算,但在实现手法上非常巧妙,我们不需要用备忘录提前计算了,而是用双指针边走边算,节省下空间复杂度
LeetCode算法题
LeetCode算法题
241.【为运算表达式设计优先级】
解题思路:
首先,题目的意思:给你输入一个算式,你可以给它随意加括号,请你穷举出所有可能的加括号方式,并计算出对应的结果
本题的关键是:1、不要首先考虑整个字符串,而是把目光聚焦局部,只看一个运算符;2、明确递归函数的定义
第一步,首先考虑如何穷举出所有加括号的形式。穷举出所有加括号的形式思考起来很难,因为括号之间可以嵌套,但是嵌套可以通过递归来解决,因此我们只需要思考,如果不让括号嵌套(即只加一层括号),有几种加括号的方式。对于某个算式,只加一层括号的所有方式,其实就是按照运算符进行分割,给每个运算符的左右两部分算式加括号。根据递归的思路,括号嵌套的所有情况也可以列出来了。
第二步,上述第一步过程就是分治算法中拆解问题的部分,接下来就是治的过程。第二个关键点,明确函数的定义,函数定义应该就是计算加括号形式产生的不同结果。
注意,该函数的base case。递归函数必须有个 base case 用来结束递归,其实这段代码就是我们分治算法的 base case,代表着你「分」到什么时候可以开始「治」,我们是按照运算符进行「分」的,一直这么分下去,当算式中不存在运算符的时候就可以结束了。算式没有运算符,则直接返回该数字即可。
至此,利用分治算法的思想,就可以解决该问题了:
public List<Integer> diffWaysToCompute(String expression) { // 结果的集合 List<Integer> res = new LinkedList<>(); // 遍历算式,按照运算符切割算式 for (int i = 0; i < expression.length(); i++) { char c = expression.charAt(i); if (c == '+' || c == '-' || c == '*') { // 分,即将切割后的左右算式,利用递归继续切割,切割就等于加括号 List<Integer> left = diffWaysToCompute(expression.substring(0, i)); List<Integer> right = diffWaysToCompute(expression.substring(i + 1)); // 治。将左右两个算式的结果进行结合,合成原问题的结果 for (int a: left) { for (int b: right) { if (c == '+') { res.add(a + b); } else if (c == '-') { res.add(a - b); } else if (c == '*') { res.add(a * b); } } } } } // base case。如果算式是个数字,则res为空,直接将该数字返回 if (res.size() == 0) { res.add(Integer.parseInt(expression)); } // 返回结果 return res; }
172.【阶乘后的零】
解题思路:
如上述的知识点所述,最终代码为:
public int trailingZeroes(int n) { // 记录有多少个0 int res = 0; long divisor = 5; while (divisor <= n) { res += n / divisor; divisor *= 5; } return res; }
42.【接雨水】
解题思路:
使用双指针解法或优化后的暴力解法来做。
102.【二叉树的层序遍历】
解题思路:
简单的BFS即可解决
103.【二叉树的锯齿形层序遍历】
解题思路:
使用BFS进行二叉树遍历,使用双端队列维护当前层节点值输出的顺序(从后面插入就是从左往右遍历,从前面插入就是从右往左遍历)