目录
简要
该文章对leetcode常用算法解法和套路进行总结。
数组
1、考虑是否可用双指针
双指针从左到右:求链表是否有环
双指针中间向两边:回文子串
双指针两边向中间:求容器乘水的最大值
2、考虑是否先进行排序 nums.sort();
如:三数之和
3、指针是否越界要格外注意
如:三数之和,回文子串
4、使用数组时经常要使用到排序
a.数组排序
Integer[] nums = new Integer[2];
Arrays.sort(nums);
Arrays.sort(nums, new Comparator<Integer>() { public int compare(Integer o1, Integer o2) { return 0; } });
b.集合排序
Collections.sort(list,new Comparator<Integer>(){ public int compare(Integer o1, Integer o2){ return o1-o2; } });
c.集合和数组相互转换
Integer[] nums = new Integer[2]; List<Integer> list = Arrays.asList(nums);
Integer[] arr = list.toArray(new Integer[list.size()]);
d、char数组转字符串
char[] chars = {'1','2','3'}; String str = new String(chars);//123
数学相关
1、i % 10 等于i的最后一个数字。可以用来获取整数的最后一个数字。
如:123%10 = 3;12%10=2;1%10=1;0%10=0;-1%10=-1;-12%10=-2;2%10=2;
2、i/10 可以用来去掉i的最后一位。while(i != 0){} i等于0时跳出循环。
如:123/10=12;12/10=1;1/10=0;
3、求最大或最小时,经常要用到Math.min(a,b)或Math.max(a,b);
双指针
1、可以都向左遍历。如:循环链表
2、可以中间向两边。如: 回文子串
3、可以一左一右往中间。如:求容器乘水的最大值
4、同频率向一个方向,类似滑动窗口。如:删除倒数第n个链表
链表
1、可能要创建新链表
2、一般要建立头指针
3、简单且重复操作的可能可以用递归(这点在树的算法题中也适用)
86. 分隔链表 (创建新链表、并拼接)
82. 删除排序链表中的重复元素 II (链表指针判断,删除节点)
字符串
1、常用方法
如:String s = abcdefg;
int len = s.length();//求长度
String ss = s.substring(i,j);//字符串截取前闭后开 [) ;i=1 j=3 得到:bc
char[] charArray = s.toCharArray();//字符串转数组,得到{a,b,c,d,e,f,g}
char charA = s.charAt(i);//得到第i个字符,i=1 得到 b
Character.isLetterOrDigit('a');//判断字符是否是字母或数字
Character.toLowerCase(ch);//字母转小写
2、遍历字符串子串的方法
abcdef
字母从前到后,a~f,b~f,c~f,d~f,e~f,f
字母个数遍历,a,b,c,d,e,f; ab,bc,cd...;abc,bcd,cde...;abcd,bcde,cdef;.........
3、StringBuilder
sb.deleteCharAt(0); abc 变成 bc
ab.append('a'); 可以添加Character类型数据
sb.reverse();反转字符串
二分查找
动态规划
1、找出状态转移方程式,把问题转换为子问题集。
2、一般这类问题大多都是求最大值或者最小值。
3、先求出开始的前几个值,再列出状态转移方程式,可以用一维或者二维数组把每个数据存起来。
4、二维数组可以看成一个矩阵
int[][] grid;
int row = grid.length;//
int column = grid[0].length;
int[][] dp = new int[row][column];
常见问题:62. 不同路径 、 64. 最小路径和 (解法:先填充第一行和第一列的值)
背包问题
a. 完全背包问题
零钱兑换: 凑成总数target需要的最少零钱数
- int[] dp = new int[amount+1]; //最少零钱数
- dp[i] = 最少零钱数; i表示要凑的零钱金额。dp[i] 初始化时为target + 1;
- 状态转移方程式:拿走一个银币后的最小硬币数 + 1 ,1表示自己当前这个硬币,或者抽不齐;
for(int coin : coins) {
dp[i] = Math.min(dp[i - coin] + 1,dp[i]);
}
/**
完全背包问题
动态规划解法:整体思路是从dp[i]里抽取出硬币,然后找到dp[i]和dp[i-coins[j]]的关系
dp[i] = Math.min(dp[i-coins[j]] + 1, dp[i]);
1、dp[i] 表示:总金额为i,凑成后的最少硬币个数
2、如何理解状态转移方程式 dp[i] = Math.min(dp[i-coins[i]],dp[i])
a:依次拿掉一个面值的硬币,此时面额的最少硬币数 + 1 就是dp[i]的值;
b:由于i > i-coins[i]恒成立,所以dp[i-coins[i]] 肯定被提前计算过了
c:如果没有符合条件的情况,就设置一个不可能的数值,如:amount+1;
例如:输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
*/
public int dpMethod(int[] coins, int amount){
if(amount == 0 || coins == null){
return 0;
}
int[] dp = new int[amount+1];
dp[0] =0;
//1、初始化时,先设置成最大的值。(MAX = 通过面值为1的硬币进行组合)
for(int i=1;i<=amount;i++){
//amount+1后初始化的值,不可能存在,因为最大的数量就是amount,即全部为面值为1的硬币
dp[i] = amount + 1;
}
//i表示金钱的面额
for(int i=1;i<=amount;i++){
//2、遍历每个硬币,并从中拿出面额
int coinLen = coins.length;
for(int j=0;j<coinLen;j++){
//有符合条件的硬币数,则:当前金额-其中一个面值金额
if(coins[j]<=i){
dp[i] = Math.min(dp[i-coins[j]] + 1, dp[i]);
}
}
}
//dp[amount] == amount+1表示该面额下,不能通过coins[j]组合出符合条件的场景,因为还是初始化时的大小
return dp[amount] == amount+1 ? -1 : dp[amount];
}
b.0-1背包问题
注意:因为元素不能重复使用,所以一般会二维数组表示,即i可表示可选择的元素,j为目标值。
分割等和子集:一个数组,能否分成两个和相等的数组;
1、dp[i][j]表示:数组下标在[0,i]范围内。如i=2表示 num[0]、num[1]、num[2]这个区间内的值的和使得相加后的容量等于j;j=需要凑齐的总和的值。
2、dp[i][j] 为true表示可以填充满,为fase表示不能填充满。
3、状态转移方程式:
boolean[][] dp = new boolean[nums.length][target+1];
a:上一行为true,即上一个数字就能凑成功了。所以本行肯定为true
b:上一行为false,要看把自己这个数字添加进去能不能凑成目标数(目标数为j)
dp[i][j] = dp[i-1][j] || dp[i-1][j-num[i]]
public boolean canPartition1(int[] nums) {
if(nums == null || nums.length <= 1){
return false;
}
int sum = 0;
for(int num : nums){
sum += num;
}
//基数肯定不行
if(sum % 2 == 1){
return false;
}
int target = sum / 2;
boolean[][] dp = new boolean[nums.length][target+1];
//设置第一列
for(int i=0;i<nums.length;i++){
dp[i][0] = true;
}
//设置第一行
for(int j=0;j<=target;j++){
if(nums[0] == j){
dp[0][j] = true;
}else{
dp[0][j] = false;
}
}
//i为数组角标,j为要凑成的数字
for(int i=1;i<nums.length;i++) {
for(int j=1;j<=target;j++) {
//上一行为true,即上一个数字就能凑成功了。所以本行肯定为true
if(dp[i-1][j]){
dp[i][j] = true;
}else{
//上一行为false,要看把自己这个数字添加进去能不能凑成目标数(目标数为j)
if(nums[i] <= j){
dp[i][j] = dp[i-1][j-nums[i]];
}
}
}
}
return dp[nums.length - 1][target];
}
回溯算法
路走不通就回到原来的岔路口重新选择一条新的路再走。
1、先画所有组合的树形结构
2、利用递归
3、递归后把递归前的操作进行撤销(回溯)
典型例题:46. 全排列 、47. 全排列 II 、39. 组合总和
public void dfs(int[] nums,int dept,boolean[] used,
List<Integer> list, List<List<Integer>> result){
//2、当遍历到最后一层后,添加数据到结果列表里
if(dept == nums.length) {
result.add(new ArrayList(list));
}
for(int i=0;i<nums.length;i++) {
if(!used[i]) {
list.add(nums[i]);
used[i] = true;
//1、深度遍历添加下一个数字
dfs(nums,dept+1,used,list,result);
//3、遍历完开始回溯,把变量替换成之前的状态
used[i] = false;
list.remove(list.size() - 1);
}
}
}
用过的不能返回去 39. 组合总和 40. 组合总和 II
public List<List<Integer>> dfs(int[] candidates,int begin, int target,
List<Integer> list, List<List<Integer>> result ){
if(target == 0){
result.add(new ArrayList(list));
return result;
}
//begin表示开始搜索的起点。由于之前用过begin,所以后续i变化时,begin前的角标都不能使用
for(int i=begin;i<candidates.length;i++){
if(target<candidates[i]) {
break;
}
list.add(candidates[i]);
//由于可以元素可以重复添加,所以这里还是begin还是等于i。表示当前元素自己可以重复选择
dfs(candidates,i,target-candidates[i],list,result);
list.remove(list.size() -1);
}
return result;
}
加大点难度:
二维数组
1、利用边界范围,或者需要注意边界范围
建立四个坐标:
int left = 0;
int right = nums[0].length -1 ;
int top = 0;
int down = nums.length -1;
2、建立一维或二维数组,进行辅助判断
1、79. 单词搜索
- int[][] stepArr = {{0,1},{0,-1},{1,0},{-1,0}}; //利用这个二维数组进行移动
- boolean[][] steped = new boolean[m][n];//通过这个二维数组,判断是否为走过的步数
- public boolean isValidStep(int x, int y){} ;//判断边界值
- 回溯
public boolean isValidStep(int x, int y){
if(x>=0 && x<m && y>=0 && y<n) {
return true;
}
return false;
}
2、73. 矩阵置零
通过两个一维数组,判断哪行、哪列是0?
boolean[] row = new boolean[x]; //第几行为0
boolean[] col = new boolean[y]; //第几列为0
int[][] row = new int[9][10];//初始化时,记录每一行没有重复值,即:初始值为0
int[][] col = new int[9][10];
int[][] box = new int[9][10];
不可能两次为1
row[i][val] = 1;
col[j][val] = 1;
box[(i/3) * 3 + j/3][val] = 1;