[TOC] # 算法总结 | 经典算法 | 解决的问题 | 关键步骤 | 时间复杂度 | 空间复杂度 | 使用的数据结构 | 注意事项 | | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ---------- | ---------------- | ------------------------------------------------------------ | | 递归思想(**入门**,自己调用自己)
**不要跳进递归,而是利用明确的定义来实现算法逻辑。** | 1、斐波那契数列fab(N)
(if(N<1){return N;} return fab(N - 1) + fab(N - 2))
2、爬台阶/跳台阶 | 1、**大问题分解为小问题**(问题规模要变小,或难度要变易)——递推公式;明确当前层应返回什么?
2、**递归终止条件要明确**——没有终止条伯会进入死循环 | 与递归深度有关,
如Fib数为O(2^n).
如对于递归解决反转区间链表的算法,为O(n) | O(n) | 函数自己调用自己 | 处理的技巧是:不要跳进递归,而是利用明确的定义来实现算法逻辑。
1、不要思考整体,而是把目光聚焦局部,只看一个运算符。
说白了,解决递归相关的算法问题,就是一个化整为零的过程,你必须瞄准一个小的突破口,然后把问题拆解,大而化小,利用递归函数来解决。
2、明确递归函数的定义是什么,相信并且利用好函数的定义。

这也是前文经常提到的一个点,因为递归函数要自己调用自己,你必须搞清楚函数到底能干嘛,才能正确进行递归调用。 | | | | | | | | | ## 递归思想 (九阳神功入门,自己调用自己)) ### 解决的问题 1、斐波那契数列fab(N) (if(N<1){return N;} return fab(N - 1) + fab(N - 2)) 2、爬台阶/跳台阶 ### 关键步骤 1、**大问题分解为小问题(**问题规模要变小,或难度要变易)——递推公式;明确当前层应返回什么? 2、**递归终止条件要明确**——没有终止条伯会进入死循环 ### 时间复杂度 与递归深度有关, 如Fib数为O(2^n). 如对于递归解决反转区间链表的算法,为O(n) ### 空间复杂度 O(n) ### 使用的数据结构 函数自己调用自己(只是传入的参数规模变小) ### 注意事项 处理的技巧是:不要跳进递归,而是利用明确的定义来实现算法逻辑。
1、不要思考整体,而是把目光聚焦局部,只看一个运算符。
说白了,解决递归相关的算法问题,就是一个化整为零的过程,你必须瞄准一个小的突破口,然后把问题拆解,大而化小,利用递归函数来解决。
2、明确递归函数的定义是什么,相信并且利用好函数的定义。

这也是前文经常提到的一个点,因为递归函数要自己调用自己,你必须搞清楚函数到底能干嘛,才能正确进行递归调用。 ## 分治策略(九阳神功**入门**) ### 解决的问题 1、快速排序(基准数+分区)/归并排序(分别有序,两个指针,合并)、傅立叶变换 2、反转二叉树(根下翻转——DFS)、路径和 ### 关键步骤 1、大问题分解为小问题(问题规模要变小,或难度要变易或解决方案很明确——**具有最优子结构(可以分解为规模较小的相同问题)**); 2、利用子问题的解可以合并为该问题的最终解。 ```java // 如下合并排序示例,是递归调用的。将数组不断分成两半,直至每个子数组仅有一个元素。然后再将它们合并成一个有序数组。 class Solution { public void sort(int[] nums, int lo, int hi) { if (lo < hi) { int mid = lo + (hi - lo) / 2; // 分 // 对数组的两部分分别排序 sort(nums, lo, mid); sort(nums, mid + 1, hi); // 治 // 合并两个排好序的子数组 merge(nums, lo, mid, hi); } } // 合并升序 private void merge(int[] nums, int lo, int mid, int hi) { int[] tmp = new int[hi - lo + 1]; int i = lo; int j = mid + 1; int k = 0; while (i <= mid && j <= hi) { if (nums[i] <= nums[j]) { tmp[k++] = nums[i++]; } else { tmp[k++] = nums[j++]; } } while (i <= mid) { tmp[k++] = nums[i++]; } while (j <= hi) { tmp[k++] = nums[j++]; } for (int l = 0; l < tmp.length; l++) { nums[lo + l] = tmp[l]; } } } ``` ### 时间复杂度 O(lgN) ### 空间复杂度 O(n) ### 使用的数据结构 无固定,视问题而定。 主要是思维 ### 注意事项 1、不要思考整体,而是把目光聚焦局部,只看一个运算符。 说白了,解决递归相关的算法问题,就是一个化整为零的过程,你必须瞄准一个小的突破口,然后把问题拆解,大而化小,利用递归函数来解决。 2、明确递归函数的定义是什么,相信并且利用好函数的定义。 这也是前文经常提到的一个点,因为递归函数要自己调用自己,你必须搞清楚函数到底能干嘛,才能正确进行递归调用。 分治分治,分而治之,这一步就是把原问题进行了「分」,我们现在要开始「治」了。 1 + 2 * 3 可以有两种加括号的方式,分别是: (1) + (2 * 3) = 7 (1 + 2) * (3) = 9 或者我们可以写成这种形式: 1 + 2 * 3 = [9, 7] 而 4 * 5 当然只有一种加括号方式,就是 4 * 5 = [20]。再回头看下题目给出的函数签名: 这就是我们之前说的第二个关键点,明确函数的定义,相信并且利用这个函数定义。 ```java List diffWaysToCompute("(1 + 2 * 3) - (4 * 5)") { List res = new LinkedList<>(); /****** 分 ******/ List left = diffWaysToCompute("1 + 2 * 3"); List right = diffWaysToCompute("4 * 5"); /****** 治 ******/ for (int a : left) for (int b : right) res.add(a - b); return res; } ``` // 回顾九阳神功第二式——分治思想。 * 开始拿到题目: * 给你一个由数字和运算符组成的字符串 expression ,按不同优先级组合数字和运算符,计算并返回所有可能组合的结果。你可以 按任意顺序 返回答案。 * 符号范围为+、-、*,表达式中数字为【0,99】,表达式长度在20个字符长度类 * 输入一个表达式字符串,要返回不同可能组合的结果值。怎么看,都应该是穷举遍历类算法--不过,不管怎样,都要递归。 * --再按我们正常计算表达式的逻辑,其实就是要计算某一个符号的左边值与右边值,按算术计算后的结果值。 ```java class Solution2 { private HashMap> memo = new HashMap(); // 分治和递归,考虑加括号的位置-只考虑每个计算,不要考虑整体,瞄准一个小口突破 List diffWaysToCompute(String input) { List resultList = new ArrayList<>(); int strLen = input.length(); for (int i = 0; i < strLen; i++) { char curChar = input.charAt(i); if (isAdd(curChar) || isMinus(curChar) || isMulti(curChar)) { // 分 // 以运算符分成运算符的两边,分别计算 List left = diffWaysToCompute(input.substring(0, i)); List right = diffWaysToCompute(input.substring(i + 1, strLen)); // 治 // 根据子问题的结果合成原问题的结果 for (Integer leftEle : left) { for (Integer rightEle : right) { if (isMulti(curChar)) { int result1 = leftEle * rightEle; resultList.add(result1); } else if (isAdd(curChar)) { int result2 = leftEle + rightEle; resultList.add(result2); } else if (isMinus(curChar)) { int result3 = leftEle - rightEle; resultList.add(result3); } } } } } // 经过递归之后,如果仍没有结果 if (resultList.isEmpty()) { resultList.add(Integer.parseInt(input)); } return resultList; } public static boolean isMulti(char c) { if (c == '*') { return true; } return false; } public static boolean isAdd(char c) { if (c == '+') { return true; } return false; } public static boolean isMinus(char c) { if (c == '-') { return true; } return false; } } ``` ​ ## 单调栈&单调队列(九阳神功第1式) ### 解决的问题 (1)栈内(队列内)的存在单调顺序 (2)元素入栈之前会将破坏单调性的元素进行处理(因此,入栈前一般有一个while类循环) 单调栈解决:元素左边第一个比当前观察元素小(单调递增栈) 元素左边第一个比当前观察元素大(单调递减栈),例如接雨水、每日温度、柱状图中最大面积 单调队队解决:求区间【0,i】的最值问题,单调队列能做,单调栈不行。 ——通常应用在一维数组上 20230805回顾温习—重新默写单调栈解决行星碰撞问题,为什么用单调栈—严格来说,行星碰撞问题不是单调栈,但如果把碰撞情况作为破坏单调情况,则就可以理解单调栈。为什么用单调栈。其实就是利用栈的后进先出特性同时用栈是作为辅助记忆空间,刚好栈的后进先出特性相当于当前元素去与原来的以遍历的元素中从后往前去一一比较操作 ### 关键步骤 ```java // 给你一个数组 temperatures,这个数组存放的是近几天的天气气温,你返回一个等长的数组,计算:对于每一天,你还要至少等多少天才能等到一个更暖和的气温;如果等不到那一天,填 0。 public int[] dailyTemperatures(int[] temperatures) { int[] res = new int[temperatures.length]; Stack descendStack = new Stack<>(); // 单调递减栈 for (int i = 0; i < temperatures.length; i++) { // 栈非空时往里面加入比当前栈顶索引指向的元素大的元素--破坏了栈的单调性时 while (!descendStack.isEmpty() && temperatures[i] >= temperatures[descendStack.peek()]) { // todo // 移除栈顶元素 descendStack.pop(); } // 得到索引间距 res[i] = descendStack.isEmpty() ? 0 : (descendStack.peek() - i); // while结束,把更大的元素索引入栈 descendStack.push(i); } return res; } ``` // 单调队列且队首和队尾都可以进行出队操作(因为队尾、队首都要操作——因此需要使用deque数据结构),只有队尾能进入。 单调递增队列:从队尾往前扫描,比当前元素大的出队列,直至碰到比之小的或队列为空,则当前元素入队列(跟单调递减栈的操作一样)。 ### 时间复杂度 O(n) ### 空间复杂度 O(n) ### 使用的数据结构 单调栈常用: 直接用栈Stack<> = new Stack(); queue = new ArrayDequeue()或 stack = new LinkedList()或dequeue 单调队列常用:dequeue ### 注意事项 暂无 ## 并查集