70 KiB
[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)
空间复杂度
与具体逻辑有关
使用的数据结构
函数自己调用自己(只是传入的参数规模变小)
注意事项
处理的技巧是:不要跳进递归,而是利用明确的定义来实现算法逻辑。
1、不要思考整体,而是把目光聚焦局部,只看一个运算符。
说白了,解决递归相关的算法问题,就是一个化整为零的过程,你必须瞄准一个小的突破口,然后把问题拆解,大而化小,利用递归函数来解决。
2、明确递归函数的定义是什么,相信并且利用好函数的定义。
这也是前文经常提到的一个点,因为递归函数要自己调用自己,你必须搞清楚函数到底能干嘛,才能正确进行递归调用。
** 有时会用到缓存来避免重复计算**
分治策略(九阳神功入门)
解决的问题
1、快速排序(基准数+分区)/归并排序(分别有序,两个指针,合并)、傅立叶变换 2、反转二叉树(根下翻转——DFS)、路径和
关键步骤
1、大问题分解为小问题(问题规模要变小,或难度要变易或解决方案很明确——具有最优子结构(可以分解为规模较小的相同问题)); 2、利用子问题的解可以合并为该问题的最终解。
// 如下合并排序示例,是递归调用的。将数组不断分成两半,直至每个子数组仅有一个元素。然后再将它们合并成一个有序数组。
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]。再回头看下题目给出的函数签名: 这就是我们之前说的第二个关键点,明确函数的定义,相信并且利用这个函数定义。
List<Integer> diffWaysToCompute("(1 + 2 * 3) - (4 * 5)") {
List<Integer> res = new LinkedList<>();
/****** 分 ******/
List<Integer> left = diffWaysToCompute("1 + 2 * 3");
List<Integer> right = diffWaysToCompute("4 * 5");
/****** 治 ******/
for (int a : left)
for (int b : right)
res.add(a - b);
return res;
}
// 回顾九阳神功第二式——分治思想。
- 开始拿到题目:
- 给你一个由数字和运算符组成的字符串 expression ,按不同优先级组合数字和运算符,计算并返回所有可能组合的结果。你可以 按任意顺序 返回答案。
- 符号范围为+、-、*,表达式中数字为【0,99】,表达式长度在20个字符长度类
- 输入一个表达式字符串,要返回不同可能组合的结果值。怎么看,都应该是穷举遍历类算法--不过,不管怎样,都要递归。
- --再按我们正常计算表达式的逻辑,其实就是要计算某一个符号的左边值与右边值,按算术计算后的结果值。
class Solution2 {
private HashMap<String, List<Integer>> memo = new HashMap();
// 分治和递归,考虑加括号的位置-只考虑每个计算,不要考虑整体,瞄准一个小口突破
List<Integer> diffWaysToCompute(String input) {
List<Integer> 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<Integer> left = diffWaysToCompute(input.substring(0, i));
List<Integer> 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回顾温习—重新默写单调栈解决行星碰撞问题,为什么用单调栈—严格来说,行星碰撞问题不是单调栈,但如果把碰撞情况作为破坏单调情况,则就可以理解单调栈。为什么用单调栈。其实就是利用栈的后进先出特性同时用栈是作为辅助记忆空间,刚好栈的后进先出特性相当于当前元素去与原来的以遍历的元素中从后往前去一一比较操作
关键步骤
// 给你一个数组 temperatures,这个数组存放的是近几天的天气气温,你返回一个等长的数组,计算:对于每一天,你还要至少等多少天才能等到一个更暖和的气温;如果等不到那一天,填 0。
public int[] dailyTemperatures(int[] temperatures) {
int[] res = new int[temperatures.length];
Stack<Integer> 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
注意事项
暂无
并查集(九阳神功第2式)
解决的问题
(1)维护无向图连通性; (2)判断是否产生环--Kruskal算法(最小生成树加边法) 朋友圈、冗余边、句子相似性、岛屿周长/最大面积/人工岛
关键的步骤
用集合中的元素来代表该集合(优化:按秩合并、路径压缩) if (parent[x] == x) //是根时 return x; find(parent, parent[x])
时间复杂度
O(lgN)
空间复杂度
O(n)
使用的数据结构
数组
注意事项
暂无
滑动窗口(九阳神功第3式)*********
解决的问题
找出满足XX条件的最YY的区间(子数组/子串) 滑动窗口算法可以用以解决数组/字符串的子元素问题,它可以将嵌套的循环问题转换为单循环问题,从而降低时间复杂度
关键步骤
解决一些查找满足一定条件的连续区间的性质(长度、最值等) ——由于区间是连续的,当区间变化,通过旧有计算结果、剪枝优化
String slideWindow(String Str) {
异常及边界判断;
左指针初始为0;
for(int right = 0; right < Str.length; right++){
进行窗口内的一系列更新;
/*** debug 输出的位置 ***/
printf("window: [%d, %d)\n", left, right);
while(判断左窗口是否需要收缩) {
左指针向前滑动缩小窗口;
进行窗口内数据的更新
}
}
}
时间复杂度
O(n)
空间复杂度
O(1)
使用的数据结构
临时变量、两个指针/索引
注意事项
暂无
前缀和(九阳神功第4式)
解决的问题
和为K的子数组(如果不连续——不能用,不连续的话,预处理没有用); ——通过预处理,降低查询时间 典型题目:和为K的子数组、和被K整除的子数组、连续的子数组之和
关键步骤
prefixSum[0]=0;
for(int i = 0; i < num.length; i++) {
prefixSum[i + 1] = prefixSum[i] + num[i];
}
优化点:map<前缀和, 次数>
时间复杂度
O(n)
空间复杂度
O(n)
使用的数据结构
数组
注意事项
暂无
差分(九阳神功第5式)
解决的问题
Bi = Ai - Ai-1,即相邻两数之差。差分数组中,每个点上记录变化值。(因有增有减,通过求和判断 是否有超过指定容量的情况发生,超过则不满足要求) 典型问题:拼车、航班、买股票最佳时机
关键步骤
Bi = Ai - Ai-1
时间复杂度
O(n)
空间复杂度
O(n)
使用的数据结构
数组
注意事项
暂无
拓扑排序(九阳神功第6式)——专业级
解决的问题
算法:(1)从DAG图中找出一个没有前驱(即入度为0)的顶点并输出;(2)从图中删除该顶点和所有以它为起点的边;(3)重复(1)和(2)步骤,直到DAG图为空,或者没有入度为0的顶点为止。 典型问题:序列重建、课程表、火星词典
关键步骤
时间复杂度
O(n)
空间复杂度
O(n)
使用的数据结构
有向无环图(DAG,Directed Acyclic Graph)的所有顶点的线性序列。 每个顶点在DAG中出现且仅出现一次;若图中有A到B的路径,那么表示对应序列中A在B前。
注意事项
暂无
字符串(九阳神功第7式)
解决的问题
典型问题:最长回文子串、(1)字符串的子串(2)字符串的子序列、复原IP地址、字符串相乘、中缀转后缀、字符串最短路径、计算器、日志敏感过滤
关键步骤
[i, j)=Str.substring(i,j);
右边往中间走(回文串):Str.charAt(i) = Str.charAt(len - i - 1)
时间复杂度
空间复杂度
使用的数据结构
字符串。解决字符串问题的有时候也会用滑动窗口、栈、DFS\BFS、Hash、二分查找
注意事项
暂无
二分查找(九阳神功第8式)*************
解决的问题
前提:数据已排好序(如果未排好序,则给它排好序) 特点:(1)能随机访问--所以一般是数组;(2)中间能推断两边 典型问题:搜索二维矩阵、寻找两个有序数组的中位数、搜索旋转排序数组 如何判断使用二分: (1)序列是有序的且数据量巨大,需要对处理时间复杂度优化; (2)给出一个要达到的目标值,求出某个自变量能满足目标的最小/最大值,该自变量和目标之间存在单调关系(单调增或单调减)。 (3)寻找一个数、寻找左侧边界、寻找右侧边界
关键步骤
// low,mid,high;根据目标与mid的比对,重新确定查找区间
public int searchInsert(int[] arr, int target) {
int left = 0;
int right = arr.length - 1;
while(left <= right) {
int mid = left + ((right - left)>> 1);
if(arr[mid] == target) {
做相关处理;
}else if(arr[mid] < target){
left = mid + 1;
}else {
right = mid - 1;
}
}
return 相关值;
}
时间复杂度
O(lgN)
空间复杂度
O(n)
使用的数据结构
数组
注意事项
暂无
BFS广搜(第9式)
解决的问题
层序遍历、最短路径——优化:多源BFS(只要把多个源点同时放入初始队列——已知目标顶点的情况下,可以分别从起点和目标顶点(终点)执行广度优先遍历,直到遍历的部分有交集,这是双向广度优先遍历的思想)
典型问题: 单词拆分、被围区域、迷宫、22年3月4日的地铁最优换乘线路(站点最少、同样少时换乘最少)
关键步骤
void bfs(TreeNode root){
if(root == null) return null;
Queue(TreeNode) queue = new ArrayDeQueue();
queue.add(root);
while(!queue.isEmpty()) {
// Java中队列 用 offer/poll进队列/出队列;
// 堆栈用push/pop
TreeNode curNode = queue.poll();
if (curNode.left != null) {
queue.offer(curNode.left);
}
if (curNode.right != null) {
queue.offer(curNode.right);
}
}
}
// 实际复杂的树如果真的需要一层遍历完后,则如下:
private void bfs(int coreCount, BinTreeNode root) {
if (root == null) {
return;
}
Queue<BinTreeNode> treeQueue = new ArrayDeque<>();
treeQueue.offer(root);
while (!treeQueue.isEmpty()) {
List<BinTreeNode> curLayerNodeLst = new ArrayList<>();
while (!treeQueue.isEmpty()) {
BinTreeNode curNode = treeQueue.poll();
curLayerNodeLst.add(curNode);
}
if (curLayerNodeLst.isEmpty()) {
break;
}
// 当前层的操作符个数统计
int tmpOperatorCnt = 0;
for (BinTreeNode node : curLayerNodeLst) {
if (node.nodeType == BinTreeNode.OPERATOR) {
tmpOperatorCnt++;
}
if (node.left != null) {
treeQueue.add(node.left);
}
if (node.right != null) {
treeQueue.add(node.right);
}
}
resultCnt += tmpOperatorCnt / coreCount;
if (tmpOperatorCnt % coreCount != 0) {
resultCnt += 1;
}
}
}
时间复杂度
O(lgN)
空间复杂度
O(n)
使用的数据结构
Queue、 HashSet
注意事项
需要注意剪枝,避免形成环(基于图这一数据结构),都有visited记录已访问过的顶点,避免无穷搜索下去! 遍历二维数组——如果你把二维矩阵中的每一个位置看做一个节点,这个节点的上下左右四个位置就是相邻节点,那么整个矩阵就可以抽象成一幅网状的「图」结构。——二维矩阵本质上是一幅「图」,所以遍历的过程中需要一个visited布尔数组防止走回头路
DFS深搜&回溯(第10式)
解决的问题
遍历或搜索树和图。——可以产生目标图的拓扑排序--无权最长路径问题。 难点在于模型转换——虽然用对应类的逻辑概念来表示也可,实际上用的较多的是(1)邻接矩阵; (2)邻接表 遍历二维数组(岛屿数量问题https://leetcode.cn/problems/number-of-islands/):
对于回溯——在于是否选择某一点,选择和不选择结果可能不一样。 回溯算法通常用于解决组合问题、排列问题、子集问题、棋盘问题、迷宫问题等。回溯算法的核心思想是穷举所有可能的解,并在搜索过程中剪枝,以避免不必要的搜索。
**回溯算法的时间复杂度和空间复杂度都与搜索树的大小相关。**在最坏情况下,搜索树的大小与解的数量成指数关系,因此回溯算法的时间复杂度和空间复杂度都是指数级别的。但是,在实际应用中,通过剪枝等优化措施,可以大大减少搜索树的大小,从而提高算法的效率。O(N!) 关键点:
(1)都是基于递归——因此都需要注意结束条件,注意每层调用应该返回的值。
(2)回溯步骤基本是看结束条件,做出选择,递归调用,撤销选择;DFS则是判断结束条件,按先序遍历/中序遍历/后序遍历策略递归调用。
关键步骤
void dfs(TreeNode root) {
// 判断边界
if (root == null) {
return;
}
dfs(root.left);
dfs(root.right);
}
或
void dfs(parent, depth, parentChildrenMap, result) {
List<> children = parentChildrenMap.get(parent_depth);
if (children == null) {
result.add(result);
return;
}
for (child: children) {
dfs(child, depth + 1, parentChildrenMap, result);
}
// 如果需要父节点
result.add(parent);
}
// 回溯
/**
* 输入一组字符串,要求输出满足下面条件的字符串拼接结果,求其长度的最大值
* 条件: 拼接结果中所有字母唯一,不存在重复----去重
* 例1--“un”、"iq"、"ue",有3!=6 种结果,最长的无重复字付的拼接结果为“ique”或者"ueiq"(不用第一个字符串), 因此输出结果应该为4
* 例2--“赵克明hfasjefusidfhjd”, 输出应该为0
*
* @author h00805266
* @since 2023-05-20
*/
public class DynamicPlan {
public static void main(String[] args) {
Solution solution = new Solution();
String[] test1 = new String[] {"un", "iq", "ue"};
int result1 = solution.getMaxUniqStringLen(test1);
System.out.println(result1);
String[] test2 = new String[] {"hfasjefusidfhjd"};
int result2 = solution.getMaxUniqStringLen(test2);
System.out.println(result2);
}
}
class Solution {
private int maxLength = 0;
// 算法复杂度为 O(n * 2^n)
public int getMaxUniqStringLen(String[] strs) {
maxLength = 0;
if (strs == null || strs.length == 0) {
return 0;
}
// StringBuffer线程安全,但速度慢。 StringBuilder非线程安全,但速度快。记录拼接后的字符串
StringBuilder sb = new StringBuilder();
// 记录每个字符是否出现过
boolean[] used = new boolean[26];
// 采用回溯算法
backtrack(strs, sb, used);
return maxLength;
}
/**
* 回溯法--DFS中退出前,回退之前选择
*
* @param strs 字符串数组
* @param sb 拼接结果
* @param used 字符出现记录
*/
private void backtrack(String[] strs, StringBuilder sb, boolean[] used) {
if (sb.length() > maxLength) {
System.out.println("temporary appended string: " + sb.toString());
maxLength = sb.length();
}
for (String str : strs) {
boolean canAppend = true;
// 先看该字符串中是否有出现过的字符
for (char c : str.toCharArray()) {
if (used[c - 'a']) {
// 已经出现过
canAppend = false;
// 该字符串不能被拼接,本字符串的处理退出
break;
}
// 根据例2,单字符串中也不允许出现重复字符
used[c - 'a'] = true;
}
if (canAppend) {
// 记录新字符串中字符出现过
for (char c : str.toCharArray()) {
used[c - 'a'] = true;
}
sb.append(str);
backtrack(strs, sb, used);
// 为什么回溯--因为有可能前面做的选择影响后面的
sb.delete(sb.length() - str.length(), sb.length());
for (char c : str.toCharArray()) {
used[c - 'a'] = false;
}
} // if
}
}
}
时间复杂度
O(lgN)
空间复杂度
O(n)
使用的数据结构
树节点的数据结构可以用语言本身提供的map/dict,也可以自己写一个类/结构体。如果C语言要用树这种数据结构,可以定义类似下面的结构体:
typedef struct tagTreeNode {
char name[LINE_BUF_LEN]; // 目录名
struct tagTreeNode * children[100];
int childCount;
}TreeNode;
在建立树的过程中,关键是要找到当前层次的父节点,将当前节点添加到父节点下面,从而构造出整棵树。
注意事项
需要注意剪枝,避免形成环(基于图这一数据结构),都有visited记录已访问过的顶点,避免无穷搜索下去! 遍历二维数组——如果你把二维矩阵中的每一个位置看做一个节点,这个节点的上下左右四个位置就是相邻节点,那么整个矩阵就可以抽象成一幅网状的「图」结构。——二维矩阵本质上是一幅「图」,所以遍历的过程中需要一个visited布尔数组防止走回头路
动态规划(第11式)
解决的问题
最优子优解结构——从子问题的最优解推导出原问题的最优解 (变化的量就是状态) 01背包思路——对每一个状态i,枚举下一个状态j,如果拼接后的字符串中所有字母唯一,则更新状态dp[next]的值。最终,遍历所有状态,求出最大值即为所求。 解决一个背包最多能装多少价值的问题,其中每个物品只有一个,且不能分割。这种问题通常被称为0/1背包问题。
关键步骤
public class Knapsack {
/**
* 01背包算法
* @param W 背包容量
* @param wt 物品重量数组
* @param val 物品价值数组
* @param n 物品数量
* @return 背包能装下的最大价值
*/
public static int knapSack(int W, int[] wt, int[] val, int n) {
int[][] dp = new int[n + 1][W + 1]; // 创建二维数组,用于存储子问题的最优解
// 初始化第一行和第一列为0,表示背包容量为0或没有物品可选时,最大价值为0
for (int i = 0; i <= n; i++) {
dp[i][0] = 0;
}
for (int j = 0; j <= W; j++) {
dp[0][j] = 0;
}
// 填充dp数组,选择每一件物品下每一种容量情况下的
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= W; j++) {
if (wt[i - 1] <= j) { // 当前物品重量小于等于背包容量
dp[i][j] = Math.max(val[i - 1] + dp[i - 1][j - wt[i - 1]], dp[i - 1][j]);
// 取当前物品和不取当前物品的最大值
} else { // 当前物品重量大于背包容量
dp[i][j] = dp[i - 1][j]; // 不取当前物品
}
}
}
return dp[n][W]; // 返回背包能装下的最大价值
}
public static void main(String[] args) {
int[] val = new int[]{60, 100, 120}; // 物品价值数组
int[] wt = new int[]{10, 20, 30}; // 物品重量数组
int W = 50; // 背包容量
int n = val.length; // 物品数量
System.out.println(knapSack(W, wt, val, n)); // 输出背包能装下的最大价值
}
}
// 上面为01背包的模板,其中,`w`数组表示每个物品的重量,`v`数组表示每个物品的价值,`C`表示背包的容量。`dp[i][j]`表示前`i`个物品放入容量为`j`的背包中所能获得的最大价值。
// 虽然这个算法结果错误,但对于演示动态规划步骤是一个参考
class Solution2 {
// 动态规划----01背包思路,对每一个状态i,枚举下一个状态j,如果拼接后的字符串中所有字母唯一,则更新状态dp[next]的值。最终,遍历所有状态,求出最大值即为所求。
public int getMaxUniqStringLen(String[] strs) {
if (strs == null || strs.length == 0) {
return 0;
}
boolean[] used = new boolean[26];
// 2^n, 状态为i时的最大长度
int[] dp = new int[1 << strs.length];
for (int i = 0; i < strs.length; i++) {
if (!checkAndSetUsed(strs[i], used)) {
dp[1 << i] = strs[i].length();
}
}
// 枚举每一种状态
for (int i = 0; i < (1 << strs.length); i++) {
if (dp[i] == 0) {
continue;
}
for (int j = 0; j < strs.length; j++) {
if ((i & (1 << j)) != 0) {
continue;
}
// 下一个状态
int next = i | (1 << j);
boolean flag = true;
for (int k = 0; k < strs[j].length(); k++) {
if ((i & (1 << (strs[j].charAt(k) - 'a'))) != 0) {
flag = false;
break;
}
}
if (flag) {
dp[next] = Math.max(dp[next], dp[i] + strs[j].length());
}
}
}
int ans = 0;
for (int i = 0; i < (1 << strs.length); i++) {
ans = Math.max(ans, dp[i]);
}
return ans;
}
/**
* 检查字符串中是否出现过重复字符并且记录字符已出现
*
* @param str
* @return
*/
private boolean checkAndSetUsed(String str, boolean[] used) {
if (str == null || str.length() == 0) {
return false;
}
for (char c : str.toCharArray()) {
if (used[c - 'a']) {
return true;
}
used[c - 'a'] = true;
return false;
}
return false;
}
}
时间复杂度
时间复杂度为O(N*V),其中N为物品的数量,V为背包的容量
空间复杂度
空间复杂度为O(V),因为只需要一个一维数组来存储状态
使用的数据结构
数组
注意事项
暂无
贪心算法(第12式)*********
解决的问题
主要用来解决需要“找到要做某事的最小次数”或“找到某些情况下适合的最大物品数量” 典型问题:最大硬币问题、最多区间、字典最小点覆盖、0/1背包、最少加油次数(https://mp.weixin.qq.com/s/k-z_oewAqMYc3vpmOm4gEQ)
关键步骤
下面我们介绍两种方法巧解这道题,分别是数学图像解法和贪心解法。 主要是每一步都选择最优方案,最终获得全局方案。 贪心思路的本质,如果找不到重复计算,那就通过问题中一些隐藏较深的规律,来减少冗余计算。
时间复杂度
空间复杂度
使用的数据结构
无固定,视问题而定。主要是思维。
注意事项
暂无
字典树(Trie)& 哈希树(冲突存在)
解决的问题
典型问题:检索字符串数据集中的键、自动补全、IP路由(最长路径匹配)、前序排序
关键步骤
键的插入、查找
时间复杂度
O(m)或O(mlogN)
空间复杂度
O(n)
使用的数据结构
科目一认证题纲中考察的基础数据结构中字符串
注意事项
虽然本题也可以用BFS完成,但是更适合用DFS来解;BFS适合查找最短路径等场景。
员工1/2/4的解法,最坏情况下,时间复杂度是O(N3);员工3的Java解法时间复杂度是O(N2);员工5的Go解法时间复杂度是O(N)。
在日常工作中,如果数据量不大,我们也可以采用一些更容易理解,或者工作量更小、占用内存少的实现方案。
寻找最近公共祖先可以参考leetcode 236. 二叉树的最近公共祖先 https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/
-
树的构建
按层序恢复二叉树,参考【案例开放010】1106上机编程“服务器组的均匀拆分”学习交流 http://3ms.huawei.com/km/groups/3803117/blogs/details/9328645?l=zh-cn
按路径分隔符提供树的层次关系,参考【案例开放047】0813上机编程“文件树”学习交流 http://3ms.huawei.com/km/groups/3803117/blogs/details/10851537?l=zh-cn
-
树的深度计算和遍历
高效计算节点的最大深度,参考【案例开放053】0924上机编程“二叉树最大深度”学习交流 http://3ms.huawei.com/km/groups/3803117/blogs/details/11060743
字典序问题
解决的问题
字符串比较:Java String类实现了compareTo接口,可用于两个字符串的比较(基于字典序),用法为str1.compareTo(str2)——返回一个整数值,按字典序str1等于str2字符串时返回0,str1大于str2时返回值大于0,小于时返回值小于0。
字符串序列排序:sort 函数可用于字符串序列的排序,默认按字典序升序排序。基于sort函数有多种方式可以实现降序排序,同时存储字符串可用List或Array:
关键步骤
时间复杂度
空间复杂度
使用的数据结构
注意事项
图的遍历
解决的问题
https://labuladong.github.io/algo/di-yi-zhan-da78c/shou-ba-sh-03a72/tu-lun-ji--d55b2/
关键步骤
/ 图和树有稍微的差别——树一定最终到叶子节点,图则可能成环
// 图的遍历
// 记录被访问过的节点
boolean[] visited;
// 记录从起点到当前节点的路径
boolean[] onPath;
// 图的遍历框架
void traverse(Graph graph, int s) {
if (visited[s]) return;
// 经过s节点,记录为已遍历
visited[s] = true;
// 用了回溯?——区别: 回溯中 做选择 和 撤销选择 在 for循环里面, 而对于onPath的数组的操作在for循环外
// 做选择: 标记S节点在路径上
onPath[s] = true;
for (int neighbor: graph.neighbors(s)) {
traverse(graph, int neighbor);
}
// 撤销选择: s节点离开路径
onPath[s] = false;
}
时间复杂度
空间复杂度
使用的数据结构
邻接表: List[] graph; // 这样每个graph[x]就是x的邻接点,这个是没有权重的。 如果加上权重,List<int[]> [] graph;// graph[x]即是x的所有邻接点列表,例如,graph[x].get(0)[0]可以是第一个邻接点的值,,graph[x].get(0)[1]可以是第一个邻接点的权重。 邻接矩阵: matrix[][], matrix[x][y]可以为x到y的权重。
注意事项
无
重要算法模板
重要算法 | 重要程序 | 算法模板 | 算法示例 |
---|
滑动窗口(https://labuladong.github.io/algo/di-ling-zh-bfe1b/wo-xie-le--f02cd/)
重要程序
5星
算法模板
// 注意:java 代码由 chatGPT🤖 根据我的 cpp 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 cpp 代码对比查看。
/* 滑动窗口算法框架 */
void slidingWindow(String s) {
// 用合适的数据结构记录窗口中的数据
Map<Character, Integer> window = new HashMap<Character, Integer>();
int left = 0, right = 0;
while (right < s.length()) {
// c 是将移入窗口的字符
char c = s.charAt(right);
window.put(c, window.getOrDefault(c, 0) + 1);
// 增大窗口
right++;
// 进行窗口内数据的一系列更新
...
/*** debug 输出的位置 ***/
// 注意在最终的解法代码中不要 print
// 因为 IO 操作很耗时,可能导致超时
System.out.printf("window: [%d, %d)\n", left, right);
/********************/
// 判断左侧窗口是否要收缩
while (left < right && window needs shrink) {
// d 是将移出窗口的字符
char d = s.charAt(left);
window.put(d, window.get(d) - 1);
// 缩小窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
}
算法示例代码
/**
* https://leetcode.cn/problems/longest-substring-without-repeating-characters/description/
*
* @author h00805266
* @since 2023-03-10
*/
public class LengthOfLongestSubstring {
public static void main(String[] args) {
Solution solution = new Solution();
String s1 = "abcabcbb";
int result1 = solution.lengthOfLongestSubstring(s1);
System.out.println(result1);
String s2 = "bbbbb";
int result2 = solution.lengthOfLongestSubstring(s2);
System.out.println(result2);
String s3 = "pwwkew";
int result3 = solution.lengthOfLongestSubstring(s3);
System.out.println(result3);
}
}
class Solution {
public int lengthOfLongestSubstring(String s) {
if (s == null || s.length() == 0) {
return 0;
}
// 初始左指针
int left = 0;
int right = 0;
// 临时变量,记录临时最大长度
int maxSubArrLen = Integer.MIN_VALUE;
// 记录窗口中各个字符的出现次数
Map<Character, Integer> charCntMap = new HashMap<>();
while (right < s.length()) {
charCntMap.put(s.charAt(right), charCntMap.getOrDefault(s.charAt(right), 0) + 1);
// 开始出现重复,则开始收缩窗口的起点
while (charCntMap.get(s.charAt(right)) >= 2) {
// 更新窗口各字符的记录
charCntMap.put(s.charAt(left), charCntMap.get(s.charAt(left)) - 1);
// 窗口起点往后移
left++;
}
maxSubArrLen = Math.max(maxSubArrLen, right - left + 1);
right++;
}
return maxSubArrLen == Integer.MIN_VALUE ? 0 : maxSubArrLen;
}
}
二分查找(https://labuladong.github.io/algo/di-ling-zh-bfe1b/wo-xie-le--3c789/)
重要程度
5星
算法模板
int binarySearch(int[] nums, int target) {
int left = 0, right = ...;
while(...) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
...
} else if (nums[mid] < target) {
left = ...
} else if (nums[mid] > target) {
right = ...
}
}
return ...;
}
// 其中 ... 标记的部分,就是可能出现细节问题的地方; 计算 mid 时需要防止溢出,代码中 left + (right - left) / 2 就和 (left + right) / 2 的结果相同,但是有效防止了 left 和 right 太大,直接相加导致溢出的情况。
算法代码示例
// 寻找一个数,基本的查找
int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 注意
while(left <= right) {
int mid = left + (right - left) / 2;
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1; // 注意
else if (nums[mid] > target)
right = mid - 1; // 注意
}
return -1;
}
// 前者 [left, right] 两端都闭的区间。这个区间其实就是每次进行搜索的区间。搜索区间为空的时候应该终止
比如说给你有序数组 nums = [1,2,2,2,3],target 为 2,此算法返回的索引是 2,没错。但是如果我想得到 target 的左侧边界,即索引 1,或者我想得到 target 的右侧边界,即索引 3,这样的话此算法是无法处理的。
这样的需求很常见,你也许会说,找到一个 target,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的复杂度了
// 寻找左侧边界
int left_bound(int[] nums, int target) {
int left = 0;
int right = nums.length; // 注意
while (left < right) { // 注意
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
right = mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid; // 注意
}
}
return left;
}
// 寻找右边界
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
left = mid + 1; // 注意
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid;
}
}
return left - 1; // 注意
}
并查集(https://labuladong.github.io/algo/di-yi-zhan-da78c/shou-ba-sh-03a72/bing-cha-j-323f3/)
重要程度
4星
算法模板
class UF {
// 连通分量个数
private int count;
// 存储每个节点的父节点
private int[] parent;
// n 为图中节点的个数
public UF(int n) {
this.count = n;
parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
// 将节点 p 和节点 q 连通
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ)
return;
parent[rootQ] = rootP;
// 两个连通分量合并成一个连通分量
count--;
}
// 判断节点 p 和节点 q 是否连通
public boolean connected(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
return rootP == rootQ;
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
// 返回图中的连通分量个数
public int count() {
return count;
}
}
/*
1、自反性:节点 p 和 p 是连通的。
2、对称性:如果节点 p 和 q 连通,那么 q 和 p 也连通。
3、传递性:如果节点 p 和 q 连通,q 和 r 连通,那么 p 和 r 也连通。
*/
代码示例
前缀和&Hash
重要程度
4星
算法模板
prefixSum[0] = 0
for (int i = 0; i < arr.length(); i++) {
prefixSum[i+1] = prefixSum[i] + arr[i];
}
代码示例
贪心
重要程度
4星
算法模板
代码示例
单调栈
重要程度
3星
算法模板
// 抽象思考:把数组的元素想象成并列站立的人,元素大小想象成人的身高。这些人面对你站成一列,如何求元素「2」的下一个更大元素呢?很简单,如果能够看到元素「2」,那么他后面可见的第一个人就是「2」的下一个更大元素,因为比「2」小的元素身高不够,都被「2」挡住了,第一个露出来的就是答案。
int[] nextGreaterElement(int[] nums) {
int n = nums.length;
// 存放答案的数组
int[] res = new int[n];
Stack<Integer> s = new Stack<>();
// 倒着往栈里放
for (int i = n - 1; i >= 0; i--) {
// 判定个子高矮
while (!s.isEmpty() && s.peek() <= nums[i]) {
// 矮个起开,反正也被挡着了。。。
s.pop();
}
// nums[i] 身后的更大元素
res[i] = s.isEmpty() ? -1 : s.peek();
s.push(nums[i]);
}
return res;
}
行星碰撞的问题解决——采用栈的思想倒是比较难想到。
代码示例
差分及其变体(https://labuladong.github.io/algo/di-er-zhan-a01c6/tan-xin-le-9bedf/sao-miao-x-2e810/)
重要程度
算法模板
// 扫描过程中的计数器
int count = 0;
// 双指针技巧
int res = 0, i = 0, j = 0;
while (i < n && j < n) {
if (begin[i] < end[j]) {
// 扫描到一个红点
count++;
i++;
} else {
// 扫描到一个绿点
count--;
j++;
}
// 记录扫描过程中的最大值
res = Math.max(res, count);
}
代码示例
Leetcode Patterns 做题思想
算法模板
- If input array is sorted then 遇到有序数组用二分或双指针
Binary search Two pointers
- If input array is not sorted then 遇到无序数组用双指针或前缀和
Two pointers Prefix sumConvert to sorted Array
- If asked for all permutations/subsets then 求排列或子集用回溯
Backtracking
- If given a tree/graph then 遇到树就用深度优先搜索或广度优先搜索
DFS BFS
- If given a linked list then 遇到链表用双指针
Two pointers
- If recursion is banned then 无法递归就用栈
Stack
- If must solve in-place then 要原地返回结果就交换数据或用一个指针存储多个数据
Swap corresponding values Store one or more different values in the same pointer - Bits
- If asked for maximum/minimum subarray/subset/options then 求最大或最小子数组、子集就用动态规划
Dynamic programming
- If asked for top/least K items then 求 top/least K 就用堆
Heap
- If asked for common strings then
Map Trie Else
Map/Set for O(1) time & O(n) space Sort input for O(nlogn) time and O(1) space
常用技巧
二维数组与一维数组相互交换
A【M行,N列】,A[i][j]
M*N个数的一维数组,B【i * N + j】
一维数组的排序和相等
Arrays.sort()、Arrays.equal()
字符串
统计每个字符出现的次数
timesMap.put(str.charAt(i), timesMap.getOrDefault(str.charAt(i), 0) + 1)
java中,判断某个字符是字母,是否是大写或小写,是否是数字,是否是十进制、八进制、二进制
// 在Java中,可以使用Character类中的一些静态方法来判断一个字符的类型。
// 1. 判断是否是字母
// 可以使用Character类中的isLetter(char c)方法来判断一个字符是否是字母。例如:
char c = 'A';
if (Character.isLetter(c)) {
System.out.println(c + "是字母");
} else {
System.out.println(c + "不是字母");
}
// 2. 判断是否是大写字母
// 可以使用Character类中的isUpperCase(char c)方法来判断一个字符是否是大写字母。例如:
char c = 'A';
if (Character.isUpperCase(c)) {
System.out.println(c + "是大写字母");
} else {
System.out.println(c + "不是大写字母");
}
// 3. 判断是否是小写字母
// 可以使用Character类中的isLowerCase(char c)方法来判断一个字符是否是小写字母。例如:
char c = 'a';
if (Character.isLowerCase(c)) {
System.out.println(c + "是小写字母");
} else {
System.out.println(c + "不是小写字母");
}
// 4. 判断是否是数字
// 可以使用Character类中的isDigit(char c)方法来判断一个字符是否是数字。例如:
char c = '1';
if (Character.isDigit(c)) {
System.out.println(c + "是数字");
} else {
System.out.println(c + "不是数字");
}
//5. 判断是否是十进制数字
// 可以使用Character类中的isDigit(char c)方法来判断一个字符是否是十进制数字。例如:
char c = '1';
if (Character.isDigit(c)) {
System.out.println(c + "是十进制数字");
} else {
System.out.println(c + "不是十进制数字");
}
//6. 判断是否是八进制数字
//可以使用Character类中的isOctalDigit(char c)方法来判断一个字符是否是八进制数字--至少JDK8中实际没有看到此方法。例如:
char c = '7';
if (Character.isOctalDigit(c)) {
System.out.println(c + "是八进制数字");
} else {
System.out.println(c + "不是八进制数字");
}
// 7. 判断是否是二进制数字
//可以使用Character类中的isDigit(char c)方法来判断一个字符是否是二进制数字。例如:
char c = '1';
if (c == '0' || c == '1') {
System.out.println(c + "是二进制数字");
} else {
System.out.println(c + "不是二进制数字");
}
无重复的最长子串
滑动窗口/双指针,[i,j)。如果找到相同的字符,则i右移,否则j右移。当找到相同字符时,取j-i的最大值
最长回文字串
扩展中心法:分别做当前i的奇对称和偶对称的最大值,然后得到起、终位置, start = i - (len - 1)/2 end = i + len /2
树和图
中序遍历二叉树;递归思想:只要树不为空,按照左中右的顺序递归遍历。 迭代思想:使用stack模拟递归过程而已。左子树不为空时则入栈,否则出栈,取right为当前栈,直到node和stack都为空结束。
推荐回溯的套路--有时要加一些减枝技巧
需要一个结束条件,需要递归查询。为提高性能需要设置“剪枝”条件。找到和删除tmpList中的最后一个元素(保证递归完tmpList被清空)
典型题目的多种解法
二进制位和位移算法(集合中的每个元素都可选或不选)
class Solution {
public List<List<Integer>> subSets(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
// 循环遍历所有可能的子集,i表示当前子集的二进制表示(0——无元素,1---表示第num.length -1的元素出现,2---表示num.length
// -2的元素出现),从0到2的nums.length次方-1循环——掩码取数
for (int i = 0; i < (1 << nums.length); i++) {
// 每个子集,创建一个空的整数列表sub,用于存储当前子集中包含的元素
List<Integer> sub = new ArrayList<>();
// 遍历数组中元素,当当前元素应该在子集中时,则将其添加到sub中
for (int j = 0; j < nums.length; j++) {
// 如果当前i(即子集的二进制表示的值)右移j位表示是第j个位置位于j是1,表示第j个应该出现
if (((i >> j) & 1) == 1) {
sub.add(nums[j]);
}
}
res.add(sub);
}
return res;
}
}
求输入数组nums[]的各幂子集
思路
从空集开始,每次遍历整数数组中的一个元素,将该元素添加到当前结果集中的每个子集中,形成新的子集,并将新的子集添加到结果集中。最终得到的结果集包含了原数组的所有子集
代码示例
class Solution {
public List<List<Integer>> subSets(int[] nums) {
// 先记录结果集
List<List<Integer>> res = new ArrayList<>();
// 首先有空集--相当于种子,而集合中所有元素都要往里面加
res.add(new ArrayList<>());
// 集合中所有元素都要往里面加
for (int num : nums) {
// 当前结果集的大小
int resultSize = res.size();
// 遍历当前结果集的每个子集
for (int i = 0; i < resultSize; i++) {
// 对于第i个子集
List<Integer> newSub = new ArrayList<>(res.get(i));
// 将新元素num加入到该子集形成新
newSub.add(num);
// 将新子集放到结果集中
res.add(newSub);
}
}
return res;
}
}
遍历回溯树
遍历回溯树的方法——回溯方法。使用一个链表track记录当前子集,使用一个列表res记录所有子集。在回溯过程中,每次将当前子集加入结果列表,然后递归生成下一个子集,最后回溯,将当前元素从当前子集中移除。
代码示例
class Solution {
// 定义一个公共方法,接收一个整数数组,返回一个包含所有子集的列表
public List<List<Integer>> subSets(int[] nums) {
// 定义一个结果列表
List<List<Integer>> res = new ArrayList<>();
// 定义一个链表,用于记录当前子集
LinkedList<Integer> track = new LinkedList<>();
// 将空集加入结果列表
res.add(new LinkedList<Integer>());
// 回溯生成所有子集
backTrack(nums, 0, track, res);
// 返回结果列表
return res;
}
// 定义一个回溯方法,用于生成所有子集
void backTrack(int[] nums, int start, LinkedList<Integer> track, List<List<Integer>> res) {
// 如果当前子集的长度等于原数组的长度,说明已经生成了一个子集,直接返回
if (track.size() == nums.length) {
return;
}
// 遍历原数组,从start位置开始
for (int i = start; i < nums.length; i++) {
// 将当前元素加入当前子集
track.addLast(nums[i]);
// 将当前子集加入结果列表
res.add(new LinkedList(track));
// 递归生成下一个子集
backTrack(nums, i + 1, track, res);
// 回溯,将当前元素从当前子集中移除
track.removeLast();
}
}
}
二叉树的最近公共祖先
// 百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
public TreeNode nearestCommonAncestor(TreeNode root, TreeNode p, TreeNode q){
// 因为是从根出发
return findCommonAncesstor(root, p, q);
}
// DFS方式
private TreeNode findCommonAncesstor(root, p, q){
if (root == null) {
return null;
}
if (root == p || root == q) {
// 如果在往下深度走的过程中,刚好是到达某一点,说明这一点就是它们的公共祖先
return root;
}
// 找左边--说实话,从这里开始往后,还真是忘了咋写了
TreeNode left = findCommonAncesstor(root.left, p, q);
// 找右边
TreeNode right = findCommonAncesstor(root.right, p, q);
// 这里前提是因为p和q一定在树中,所以两边不为空,则说明此时的root即为它们的共同祖先,又因为这是递归返回,所以返回的是最靠近两个的祖先
if (left != null && right != null) {
return root;
}
return left != null ? left : right;
}
DFS遍历二维数组
// 因为dfs函数遍历到值为0的位置会直接返回,所以只要把经过的位置都设置为0,就可以起到不走回头路的作用。--这就是FloodFill算法
class Solution {
public int numOfIslands(char[][] grid) {
int res = 0;
int m = grid.length;
int n = grid[0].length;
// 遍历每个节点
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 如果是岛屿,则将与该岛屿相连的岛屿都淹没,直至海水,从而标志找到了一块岛屿
if (grid[i][j] == '1') {
res++;
dfs(grid, i, j);
}
}
}
return res;
}
private void dfs(char[][] grid, int i, int j) {
int m = grid.length;
int n = grid[0].length;
// 边界条件1-- 到达边界了
if (i < 0 || j < 0 || i >= m || j >= n) {
return;
}
// 边界条件2--已经是海水了
if (grid[i][j] == '0') {
return;
}
// 不是海水的,则淹没,将(i,j)变成海水
grid[i][j] = '0';
// 继续深度
// --往矩阵该点的下邻节点
dfs(grid, i + 1, j);
// --往矩阵该点的上邻节点
dfs(grid, i - 1, j);
// --往矩阵该点的左邻节点
dfs(grid, i, j - 1);
// --往矩阵该点的右邻节点
dfs(grid, i, j + 1);
}
个人不熟悉点
java中最大堆、最小堆(可以通过优先队列(PriorityQueue)来实现)
以下为AI生成内容,仅供参考:
最小堆和最大堆是常用的数据结构,Java中可以通过优先队列(PriorityQueue)来实现。
最小堆的实现:
```java
import java.util.PriorityQueue;
public class MinHeap {
public static void main(String[] args) {
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.add(3);
minHeap.add(1);
minHeap.add(4);
minHeap.add(2);
minHeap.add(5);
while (!minHeap.isEmpty()) {
System.out.print(minHeap.poll() + " ");
}
}
}
```
// 输出结果为:1 2 3 4 5
最大堆的实现:
```java
import java.util.Collections;
import java.util.PriorityQueue;
public class MaxHeap {
public static void main(String[] args) {
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
maxHeap.add(3);
maxHeap.add(1);
maxHeap.add(4);
maxHeap.add(2);
maxHeap.add(5);
while (!maxHeap.isEmpty()) {
System.out.print(maxHeap.poll() + " ");
}
}
}
```
// 输出结果为:5 4 3 2 1
// 需要注意的是,最大堆需要在创建PriorityQueue对象时传入Collections.reverseOrder()参数,表示使用逆序比较器来实现最大堆。
树类、给定数组-1表示父节点,数组a[i]表示节点i的父节点类题目
1、直接方法,根据路径a[i]不断往上遍历while(a[i] != - 1) {i = a[i];***,pathList.add(i)可记录到根节点的路径}; 2、构造树——个人这块在构造树时容易出错,要做是否有父节点、子节点的判断,易出错。
java集合类框架
(• 最重要:HashMap和ArrayList) 注:通常总的计算量超过100万就不适合打点标记法,关于数据规模的估算可以参考【案例开放023】上机编程题之性能学习交流http://3ms.huawei.com/km/groups/3803117/blogs/details/9892189?l=zh-cn
采用ForEach的写法
前提是需要在forEach写法下拿值时不为空,因为ForEach情况下实际是有个先拿列表的动作,只有列表不为空,才能拿到列表每个项
// 这个关键判空没有做导致异常退出
if (nodeParentsMap.get(keyNodeId) != null) {
for (Integer parentNodeId : nodeParentsMap.get(keyNodeId)) {
resultLst.add(parentNodeId);
dfsForParentSelfCheck(parentNodeId, resultLst, keyNodeSet, nodeParentsMap);
}
}
区间相关问题(区间覆盖、区间交集、区间调度)
(1)排序——意义是啥?不排序时直接两层遍历O(n^2),排序之后有覆盖、交集、不相交三种情况:常见的排序方法就是按照区间起点排序,**或者先按照起点升序排序,若起点相同,则按照终点降序排序。**当然,如果你非要按照终点排序,无非对称操作,本质都是一样的。——排序的意义:后续处理不用再考虑线段谁前谁后;时间复杂度变为排序+一层遍历 (2)**画图。勤动手,**两个区间的相对位置有几种?不同的相对位置怎么处理? 例如:集装箱调度问题?区间覆盖、区间合并、区间交集、会议室预订
class Solution {
// 删除覆盖的区间,输入: 二维数组列表,代表区间的【起点,终点】,输出:剩下的区间数
public int removeCoveredIntervals(int[][] intvs) {
// 先按照起点升序,再按照终点降序--这样可以拿到最长的合并区间
Arrays.sort(intvs, (a, b) -> {
if (a[0] == b[0]) {
return b[1] - a[1];
}
return a[0] - b[0];
});
// 排好序后,记录合并区间的起点与终点,初始时为第1个,
int left = intvs[0][0];
int right = intvs[0][1];
int coverCount = 0;
// 一次遍历数组(去掉第1个记录)
for (int i = 1; i < intvs.length; i++) {
int[] intv = intvs[i];
// 找到覆盖区间
if (left <= intv[0] && right >= intv[1]) {
coverCount++;
}
// 找到交集区间--则合并交集,因为排好序了,这里就不用再操心考虑两个区间谁前谁后了,因为这里是按顺序遍历的
if (right >= intv[0] && right <= intv[1]) {
right = intv[1];
}
// 找到完全不相交的区间——更新更新覆盖区域的起点、终点,因为已排序,所以不用担心对前面的影响
if (right < intv[0]) {
left = intv[0];
right = intv[1];
}
}
return intvs.length - coverCount;
}
}
递归-->DFS/回溯->分治思想
1、递归终止条件; 2、当前调度层应该返回什么?做什么操作? 例如解决求表达式的多种计算结果\数组排列\字符串合并求无重复字符的合并字符串最大长度--输入是什么、输出是什么
贪心思想
例如解决加油站问题、解决产品质检点灯问题--输入是什么、输出是什么
数组转Set
Set<int[]> keyNodeSet = new HashSet(Arrays.asList(keyNodes));
// 不要是如下
Set<Integer> keyNodeSet = new HashSet<>(Arrays.asList(keyNodes));
// 上述得到的keyNodeSet的size是1,没法用keyNodeSet的contains()做相应判断
数组转list
Arrays.asList( )---适配器模式:模板方法,public static List asList(T ...a);Integer[] arr = {1,2,3};可以List lst = Arrays.asList(arr);也可以List lst = asList(1, 2, 3); 注意该方法的返回值是java.util.Arrays类中一个私有静态内部类java.util.Arrays.ArrayList, 它并非java.util.ArrayList类。 java.util.Arrays.ArrayList类具有set(),get(),contains()等方法, 但是不支持添加add()或删除remove()方法,调用这些方法会报错。
也就是说,此种方法残缺:重新得到的 list 不能 add( ) 或者 remove( );
此时选用java.util.ArrayList类构造器构造List,如:
先List list = Arrays.asList; 再List newList = new ArrayList<>(list);
List转数组
list.toArray(T[] a);
输出指定类型的数组,输出的数组类型与括号中参数类型一致;
必须是包装类(String、Integer、Character等),不能是基本数据类型了(string、int、char); 要转为基本类型,可以使用流来操作,list.stream().mapToInt(Integer::intValue).toArray();
List转Set
Set set = new HashSet(new ArrayList());
数组输出问题
不需要手写代码输出数组—Arrays.toString(数组),这样便于节约时间,差异看数组输出结果!
数组初始化值填充
int[] result = new int[input.length];
Arrays.fill(result, -1);
集合Collection里List、Set、Map删除元素的方法总结
对于Map,可以使用remove(key)方法来删除指定的键值对。如果该键存在,则会将其对应的值返回并从Map中删除该键值对;如果该键不存在,则返回null。需要注意的是,如果在遍历Map时删除元素,可能会导致ConcurrentModificationException异常。
对于List,可以使用remove(index)方法来删除指定位置的元素。如果该位置存在元素,则会将其返回并从List中删除;如果该位置不存在元素,则会抛出IndexOutOfBoundsException异常。需要注意的是,如果在遍历List时删除元素,可能会导致ConcurrentModificationException异常。
在删除元素时,需要考虑到可能会影响到其他元素的位置和索引,因此需要谨慎操作。同时,如果在多线程环境下操作,需要考虑到线程安全问题。 (1)List删除时有两个重载方法:E remove(int index); //返回删除的元素 boolean remove(Object o); //返回bool值 而重载的优先级顺序是——先匹配参数个数->参数类型的最佳匹配--直接所属类->向上转型直至没有父类(从包装类到父类到接口)->可变参数列表 在Java中,Map和List都可以单独删除某个元素。
对于list的循环删除,推荐采用迭代器方法,同时注意是使用迭代器的remove方法。
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
Integer x = it.next();
if (x >= baseNum) {
it.remove();
//list.remove(x); //java.util.ConcurrentModificationException
}
}
另外,LinkedList有removeLast()可以来删除链表中的最后一个元素的。该方法会将链表的最后一个节点从链表中移除,并返回该节点的值。如果链表为空,则该方法会抛出NoSuchElementException异常。 (2)Map的删除: map(循环删除)也有类似情况,因此此处举一例,map可以安全删除的例子
代码同上,只需要调用iterator.remove();即可,不要用map.remove(key); https://cloud.tencent.com/developer/article/1497457
java中由一个字符串构造成一个含该字符串的列表有哪些方法,请举例说明
小鲁班(p_xiaoluban) 2023-10-09 20:10
以下为AI生成内容,仅供参考,不允许未经评估直接将生成内容植入工作输出件中。
在Java中,可以使用以下方法将一个字符串构造成一个包含该字符串的列表:
1. 使用Arrays.asList()方法:
```java
String str = "Hello";
List<String> list = Arrays.asList(str);
```
2. 使用Collections.singletonList()方法:
```java
String str = "Hello";
List<String> list = Collections.singletonList(str);
```
3. 使用ArrayList的构造函数:
```java
String str = "Hello";
List<String> list = new ArrayList<>(Arrays.asList(str));
```
4. 使用Stream的collect()方法:
```java
String str = "Hello";
List<String> list = Stream.of(str).collect(Collectors.toList());
```
这些方法都可以将一个字符串构造成一个只包含该字符串的列表。
Map的putIfAbsent、computeIfAbsent、computeIfPresent、compute
put方法——如果是添加成功,返回null, 如果已经存在此key了 会用新值覆盖旧值 然后把oldValue返回出来 putIfAbsent——如果给定的key不存在(或者key对应的value为null),就把value放进去,并返回null。如果存在,则返回当前值(不会把value放进去); computeIfAbsent——如果给定的key不存在(或者key对应的value为null),就去计算mappingFunction的值(计算结果决定了返回值有两种情况如下,我们姑且叫计算的结果为newValue):若key已经存在,就不会执行mappingFunction。返回oldValue。 newValue == null,不会替换旧值。返回null newValue != null,替换旧值。返回新值
模拟类的题目
1、常见方法--先观察数据,估算复杂度(心法),以示例代入尝试寻找解决方法(相当于由具体来总结抽象出算法思路) Hash是常用的提升效率的手段