参考资料:https://programmercarl.com/
https://codetop.cc/home
回溯算法
组合
电话号码的字母组合(17)- 13
// https://leetcode.cn/problems/letter-combinations-of-a-phone-number/
// 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回
// 给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母
// 输入:digits = "23"
// 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
// 输入:digits = "2"
// 输出:["a","b","c"]
class Solution {
// 设置全局列表存储最后的结果
List<String> result = new ArrayList<>();
// 初始对应所有的数字
String[] numString = {"abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
public List<String> letterCombinations(String digits) {
if (digits == null || digits.length() == 0) {
return result;
}
// 迭代处理
backTracking(digits, 0);
return result;
}
// 存储回溯过程中的字符
StringBuilder path = new StringBuilder();
public void backTracking(String digits, int index) {
// 终止条件:字母组合长度 与 输入字符长度相等
if (path.length() == digits.length()) {
result.add(path.toString());
return;
}
// 数字到字母映射的对应位置(字符 - 字符)
int pos = digits.charAt(index) - '2';
// 遍历当前数字对应的字符集(比如数字2对应的字符集为abc)
for (int i = 0; i < numString[pos].length(); i++) {
path.append(numString[pos].charAt(i)); // 把单个字母添加到路径
backTracking(digits, index + 1); // 进入下一个数字
path.deleteCharAt(path.length()-1); // 去除路径中最后添加的字母
}
}
}
组合总和(39)- 62
// https://leetcode.cn/problems/combination-sum/
// 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合
// candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的
// 对于给定的输入,保证和为 target 的不同组合数少于 150 个
// 输入:candidates = [2,3,6,7], target = 7
// 输出:[[2,2,3],[7]]
// 解释:
// 2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
// 7 也是一个候选, 7 = 7 。
// 仅有这两种组合。
class Solution {
List<List<Integer>> result = new ArrayList<List<Integer>>();
List<Integer> path = new ArrayList<Integer>();
// 存储计算过程中的值
int sum = 0;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
backtrack(candidates, target, 0);
return result;
}
public void backtrack(int[] candidates, int target, int start){
if(sum == target){
// 此处不能直接使用path
result.add(new ArrayList<>(path));
return;
}
// 剪枝操作:若sum + candidates[i] > target, 则不进入循环
for(int i = start; i < candidates.length && sum + candidates[i] <= target; i++){
sum = sum + candidates[i];
path.add(candidates[i]);
// 因为candidates中的元素可以无限制重复被选取,所以此处递归传入i,而不是start+1
backtrack(candidates, target, i);
// 去除路径中最后添加的数字
path.remove(path.size() - 1);
// 减去最后添加的数字(此时可能刚好满足条件,或者和超过target)
sum = sum - candidates[i];
}
}
}
组合总和 II(40)- 34
// https://leetcode.cn/problems/combination-sum-ii/
// 给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合
// candidates 中的每个数字在每个组合中只能使用 一次
// 注意:解集不能包含重复的组合
// 输入: candidates = [10,1,2,7,6,1,5], target = 8,
// 输出:
// [
// [1,1,6],
// [1,2,5],
// [1,7],
// [2,6]
// ]
class Solution {
List<List<Integer>> result = new ArrayList<List<Integer>>();
List<Integer> path = new ArrayList<Integer>();
int sum = 0;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
process(candidates, target, 0);
return result;
}
public void process(int[] candidates, int target, int start){
if(sum == target){
result.add(new ArrayList<>(path));
return;
}
for(int i = start; i < candidates.length && sum + candidates[i] <= target; i++){
// 去重,若candidates中存在重复元素,只需要取其中一个元素进行递归操作即可
// 后续相同的元素就不需要再进行求和操作,因为肯定与前一个相同元素求出的结果一致,此时就重复了
if(i > start && candidates[i] == candidates[i-1]) continue;
sum = sum + candidates[i];
path.add(candidates[i]);
process(candidates, target, i + 1);
// 回溯
path.remove(path.size() - 1);
sum = sum - candidates[i];
}
}
}
组合(77)- 11
// https://leetcode.cn/problems/combinations/description/
// 给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合
// 你可以按 任何顺序 返回答案
// 输入:n = 4, k = 2
// 输出:
// [
// [2,4],
// [3,4],
// [2,3],
// [1,2],
// [1,3],
// [1,4],
// ]
class Solution {
// 存放符合条件结果的集合
List<List<Integer>> result = new ArrayList<List<Integer>>();
public List<List<Integer>> combine(int n, int k) {
backtrack(n, k, 1);
return result;
}
// 用来存放符合条件结果
LinkedList<Integer> path = new LinkedList<Integer>();
// startIndex记录下一层递归,搜索的起始位置
private void backtrack(int n, int k, int startIndex){
// 终止条件
if(path.size() == k){
result.add(new ArrayList<>(path)); // 存放结果
return;
}
// 控制树的横向遍历
// n-(k-path.size())+1:表示起始位置最多能到达的位置
for(int i = startIndex; i <= n-(k-path.size())+1; i++){
// 处理节点
path.add(i);
// 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
backtrack(n, k, i + 1);
// 回溯,撤销处理的节点
path.removeLast();
}
}
}
组合总和 III(216)- 34
// https://leetcode.cn/problems/combination-sum-iii/
// 找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
// 只使用数字1到9
// 每个数字 最多使用一次
// 返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回
// 输入: k = 3, n = 9
// 输出: [[1,2,6], [1,3,5], [2,3,4]]
// 解释:
// 1 + 2 + 6 = 9
// 1 + 3 + 5 = 9
// 2 + 3 + 4 = 9
// 没有其他符合的组合了
class Solution {
List<List<Integer>> result = new ArrayList<List<Integer>>();
List<Integer> path = new ArrayList<Integer>();
public List<List<Integer>> combinationSum3(int k, int n) {
process(k, n, 1);
return result;
}
public void process(int num, int target, int start){
// 终止条件
if(path.size() == num || target <= 0){
if(path.size() == num && target == 0){
result.add(new ArrayList<>(path));
}
return;
}
for(int i = start; i < 10; i++){
path.add(i);
process(num, target-i, i+1);
path.remove(path.size() - 1);
}
}
}
组合总和 Ⅳ(377)- 3
// https://leetcode.cn/problems/combination-sum-iv/description/
// 给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
// 题目数据保证答案符合 32 位整数范围
// 输入:nums = [1,2,3], target = 4
// 输出:7
// 解释:
// 所有可能的组合为:
// (1, 1, 1, 1)
// (1, 1, 2)
// (1, 2, 1)
// (1, 3)
// (2, 1, 1)
// (2, 2)
// (3, 1)
// 请注意,顺序不同的序列被视作不同的组合
class Solution {
public int combinationSum4(int[] nums, int target) {
//【1】dp[i]表示凑成目标正整数为i的排列个数为dp[i]
int[] dp = new int[target + 1];
//【2】dp[0]要初始化为1,这样递归其他dp[i]的时候才会有数值基础
dp[0] = 1;
// 如果求组合数就是外层for循环遍历物品,内层for循环遍历背包
// 如果求排列数就是外层for循环遍历背包,内层for循环遍历物品
//【3】先遍历背包容量,再遍历物品
for (int i = 0; i <= target; i++) {
for (int j = 0; j < nums.length; j++) {
//【4】只有当背包容量大于等于物品i的容量时,才需要考虑装 或 不装物品i
if (i >= nums[j]) {
//【5】dp[i]表示不装物品j后凑成目标i的排列个数,dp[i - nums[j]]表示装物品j后凑成目标i的排列个数
// i - nums[j]表示装填物品j需要的容量
dp[i] = dp[i] + dp[i - nums[j]];
}
}
}
//【6】凑成目标为target的排列个数
return dp[target];
}
List<List<Integer>> result = new ArrayList<List<Integer>>();
List<Integer> path = new ArrayList<Integer>();
int sum = 0;
// 解答错误
public int combinationSum41(int[] nums, int target) {
process(nums, target, 0);
return result.size();
}
public void process(int[] nums, int target, int start){
if(start == nums.length){
return;
}
if(sum == target){
result.add(new ArrayList<>(path));
}
for(int i = start; i < nums.length && sum + nums[i] <= target; i++){
sum = sum + nums[i];
path.add(nums[i]);
process(nums, target, start);
sum = sum - nums[i];
path.remove(path.size() - 1);
}
}
}
分割
复原IP地址(93)
// https://leetcode.cn/problems/restore-ip-addresses/
// 有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。
// 例如:"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。
// 给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。
// 输入:s = "101023"
// 输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
class Solution {
List<String> result = new ArrayList<String>();
Deque<String> path = new ArrayDeque<>(4);
public List<String> restoreIpAddresses(String s) {
process(s, 0, 4);
return result;
}
// start表示当前索引位置,reside表示剩余段数
public void process(String s, int start, int reside){
// 终止条件
if(start == s.length()){
// 当变量到最后一个字符 且 剩余段数为0时,将此时的path添加到结果集中
if(reside == 0){
result.add(String.join(".", path));
}
return;
}
for(int i = start; i < start + 3 && i < s.length(); i++){
// 若字符串剩余长度 大于 剩余分段所需最大长度,表明先前的分段不合理,最后会存在剩余字符
if(s.length() - i > reside * 3){
continue;
}
// 截取的字符串满足条件
if(judgeNumber(s, start, i)){
// 截取部分字符添加到path中
String cur = s.substring(start, i+1);
path.addLast(cur);
// 纵向递归
process(s, i+1, reside-1);
path.removeLast();
}
}
}
// 判断截取的字符串是否满足要求
public boolean judgeNumber(String s, int left, int right){
int len = right - left + 1;
// 当前为0开头 且 长度大于1的数字需要剪枝
if(s.charAt(left) == '0' && len > 1){
return false;
}
// 将当前截取的字符串转化成数字
int res = Integer.parseInt(s.substring(left, right+1));
// 判断截取到的数字是否符合
return res >= 0 && res <= 255;
}
}
分割回文串(131)- 9
// https://leetcode.cn/problems/palindrome-partitioning/
// 给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案
// 回文串 是正着读和反着读都一样的字符串
// 输入:s = "aab"
// 输出:[["a","a","b"],["aa","b"]]
class Solution {
List<List<String>> result = new ArrayList<List<String>>();
List<String> path = new ArrayList<String>();
// 分割后所有子串相加后的总长度
// int lenSum = 0;
public List<List<String>> partition(String s) {
process(s, 0);
return result;
}
public void process(String s, int start){
// 终止条件:分割后所有子串相加后的总长度 等于 原字符串长度
// if (lenSum == s.length()){
// 终止条件:起始位置 大于 s长度,表明找到一组分割方案
if (start >= s.length()){
result.add(new ArrayList<>(path));
return;
}
// 横向遍历字符串
for (int i = start; i < s.length(); i++) {
// 截取字符串判断是否回文串
String str = s.substring(start, i+1);
if (!isPalindrome(str)){
continue;
}
// 若是回文串,加入path
path.add(str);
// 被截取字符串的长度
// int len = i + 1 - start;
// lenSum += len;
// 纵向遍历:递归
process(s, i+1);
// lenSum -= len;
path.remove(path.size()-1);
}
}
// 判断回文串
public boolean isPalindrome(String s){
for(int i = 0; i < s.length() / 2; i++){
if(s.charAt(i) != s.charAt(s.length() - 1 - i)){
return false;
}
}
return true;
}
}
22222 - 分割回文串 II(132)
子集
子集(78)- 72
// https://leetcode.cn/problems/subsets/
// 给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)
// 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集
// 输入:nums = [1,2,3]
// 输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
class Solution {
List<List<Integer>> result = new ArrayList<List<Integer>>();
List<Integer> path = new ArrayList<Integer>();
public List<List<Integer>> subsets(int[] nums) {
process(nums, 0);
return result;
}
public void process(int[] nums, int start){
// 每更新一次list,都加入结果集 首次进来加的是空集(把所有节点都记录下来,就是要求的子集集合)
result.add(new ArrayList<>(path));
// 到数组末尾结束当前递归
if(start == nums.length){
return;
}
for(int i = start; i < nums.length; i++){
path.add(nums[i]);
process(nums, i+1);
path.remove(path.size() - 1);
}
}
public List<List<Integer>> subsets1(int[] nums) {
// 首先将空集加入解集中
result.add(new ArrayList<Integer>());
for(int i = 0; i < nums.length; i++){
// 当前子集数
int size = result.size();
for(int j = 0; j < size; j++){
// 拷贝所有子集
List<Integer> newList = new ArrayList<>(result.get(j));
// 向拷贝的子集中加入当前数形成新的子集
newList.add(nums[i]);
// 向lists中加入新子集
result.add(newList);
}
}
return result;
}
}
子集 II(90)- 4
// https://leetcode.cn/problems/subsets-ii/
// 给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
// 解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列
// 输入:nums = [1,2,2]
// 输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
class Solution {
List<List<Integer>> result = new ArrayList<List<Integer>>();
List<Integer> path = new ArrayList<Integer>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
process(nums, 0);
return result;
}
public void process(int[] nums, int start){
result.add(new ArrayList<>(path));
// 终止条件,可以不添加
if(start >= nums.length){
return;
}
// i必须从start开始进行剪枝,否则运行会超过内存限制
for(int i = start; i < nums.length; i++){
// 去重,此处就体现了首先对nums排序的重要性
if(i > start && nums[i] == nums[i-1]) continue;
path.add(nums[i]);
process(nums, i+1);
path.remove(path.size()-1);
}
}
}
排列
全排列(46)- 178
// https://leetcode.cn/problems/permutations/
// 给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案
// 输入:nums = [1,2,3]
// 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
class Solution {
List<List<Integer>> result = new ArrayList<List<Integer>>();
List<Integer> path = new ArrayList<Integer>();
public List<List<Integer>> permute(int[] nums) {
process(nums);
return result;
}
public void process(int[] nums){
// 此处不要加终止条件,根据纵向递归判断终止条件
if(path.size() == nums.length){
result.add(new ArrayList<>(path));
}
// 纵向递归:有终止条件
for(int i = 0; i < nums.length; i++){
// path中已存在,则跳过
if(path.contains(nums[i])) continue;
path.add(nums[i]);
process(nums);
path.remove(path.size()-1);
}
}
}
0 - 全排列II(47)- 33
// https://leetcode.cn/problems/permutations-ii/
// 给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列
//
// 输入:nums = [1,1,2]
// 输出:
// [[1,1,2], [1,2,1], [2,1,1]]
class Solution {
List<List<Integer>> result = new ArrayList<List<Integer>>();
List<Integer> path = new ArrayList<Integer>();
public List<List<Integer>> permuteUnique(int[] nums) {
// 方式一:将nums排序,后续判断nums[i] == nums[i-1]
Arrays.sort(nums);
boolean[] used = new boolean[nums.length];
process(nums, used);
return result;
// 方式二:对最终的结果进行排序
// return result.stream().distinct().collect(Collectors.toList());
}
public void process(int[] nums, boolean[] used){
if(path.size() == nums.length){
result.add(new ArrayList<>(path));
}
for(int i = 0; i < nums.length; i++){
// 当前元素被使用过 或者 (当前元素与上一元素相同 同时 上一元素未被使用过),则跳过此次循环
// !used[i-1]也可以使用成used[i-1],不过效率要降低
if(used[i] || (i > 0 && nums[i] == nums[i-1] && !used[i-1])) continue;
path.add(nums[i]);
used[i] = true;
process(nums, used);
used[i] = false;
path.remove(path.size()-1);
}
}
}
棋盘问题
解数独(37)- 17
// https://leetcode.cn/problems/sudoku-solver/description/
// 编写一个程序,通过填充空格来解决数独问题。
// 数独的解法需遵循如下规则:
// 数字 1-9 在每一行只能出现一次。
// 数字 1-9 在每一列只能出现一次。
// 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
// 数独部分空格内已填入了数字,空白格用 '.' 表示
class Solution {
public void solveSudoku(char[][] board) {
dfs(board);
}
public boolean dfs(char[][] board){
//【1】遍历二维棋盘
for(int i = 0; i < board.length; i++){
for(int j = 0; j < board[0].length; j++){
//【2】跳过原始数字
if(board[i][j] != '.') continue;
//【3】在位置(i, j)放入数字k,检查棋盘是否合法
for(char k = '1'; k <= '9'; k++){
//【3.1】若合法,则将数字k放入当前位置,并递归遍历
if(isValidSudoku(board, i, j, k) ){
board[i][j] = k;
//【3.2】递归遍历,此时当前位置及其前面空位都已经放入数字,在遍历时会跳过,所以此处不需要移动当前位置i、j
// 找到一组合适的数字,表示从当前位置放入k开始,后续所有空位都能放入合适数字
if(dfs(board)){
return true;
}
//【3.3】回溯操作,未找到一组合适的数字,要撤销当前位置的k,重新赋值
board[i][j] = '.';
}
}
//【4】9个数字均尝试一遍放入当前位置,都未找到一组合适的数字,返回false,相当于回溯操作,结束当层递归,上一个空位置重新赋值
return false;
}
}
//【5】遍历完没有返回false,说明找到了合适棋盘位置了
return true;
}
// 在位置(row, col)放入数字value,检查棋盘是否合法
public boolean isValidSudoku(char[][] board, int row, int col, char value){
// 同行是否重复
for (int i = 0; i < 9; i++){
if (board[row][i] == value){
return false;
}
}
// 同列是否重复
for (int j = 0; j < 9; j++){
if (board[j][col] == value){
return false;
}
}
// 9宫格里是否重复
int startRow = (row / 3) * 3;
int startCol = (col / 3) * 3;
for (int i = startRow; i < startRow + 3; i++){
for (int j = startCol; j < startCol + 3; j++){
if (board[i][j] == value){
return false;
}
}
}
return true;
}
}
N皇后(51)- 15
// https://leetcode.cn/problems/n-queens/description/
// 按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
// n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
// 给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
// 每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
// 输入:n = 4
// 输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
// 解释:如上图所示,4 皇后问题存在两个不同的解法。
class Solution {
List<List<String>> result = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
//【1】初始化n*n的棋盘
char[][] chessboard = new char[n][n];
for (char[] c : chessboard) {
Arrays.fill(c, '.');
}
backTrack(chessboard, n, 0);
return result;
}
public void backTrack(char[][] chessboard, int n, int rowStart) {
//【2】递归终止条件:遍历到末尾行,将解法(path)加入结果集
if (rowStart == n) {
result.add(ArrayToList(chessboard));
return;
}
//【3】遍历每列
for (int col = 0; col < n; col++) {
//【4】判断当前位置放入皇后是否合法
if (isValid (chessboard, rowStart, col, n)) {
chessboard[rowStart][col] = 'Q'; // 放置皇后
backTrack(chessboard, n, rowStart + 1); // 向下一行递归遍历
chessboard[rowStart][col] = '.'; // 回溯,撤销皇后
}
}
}
//【5】字符数组 转换 为字符串集合
public List ArrayToList(char[][] chessboard) {
List<String> list = new ArrayList<>();
for (char[] c : chessboard) {
list.add(String.copyValueOf(c));
}
return list;
}
//【6】在单层搜索的过程中,每一层递归,只会选for循环(也就是同一行)里的一个元素,所以不用检查行进行去重
public boolean isValid(char[][] chessboard, int row, int col, int n) {
// 检查列是否有皇后
for (int i = 0; i < row; i++) { // 相当于剪枝
if (chessboard[i][col] == 'Q') {
return false;
}
}
// 检查45度对角线是否有皇后
for (int i = row-1, j = col-1; i >= 0 && j >= 0; i--, j--) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
// 检查135度对角线是否有皇后
for (int i = row-1, j = col+1; i >= 0 && j <= n-1; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
}
其他
22222 - 重新安排行程(332)
递增子序列(491)- 2
// https://leetcode.cn/problems/non-decreasing-subsequences/description/
// 给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
// 数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backtracking(nums, 0);
return result;
}
public void backtracking (int[] nums, int start) {
//【1】终止条件,只要满足path长度大于等于2,就加入结果集
if (path.size() >= 2) {
result.add(new ArrayList<>(path));
}
//【2】使用used数组来进行去重操作,题目说数值范围[-100, 100]
// 数值范围元素均加100,转化为used数组的下标,[-100+100, 100+100] -> [0, 200]
int[] used = new int[201];
//【3】横向遍历数组
for (int i = start; i < nums.length; i++) {
//【4】剪枝操作:
// 当前元素 小于 path中最后一个元素,表明当前元素针对path不是一个递增的数
// used[nums[i] + 100] == 1表示当前元素已经被使用过了
if (!path.isEmpty() && nums[i] < path.get(path.size() - 1) ||
(used[nums[i] + 100] == 1)) continue;
//【5】记录当前元素在本层已使用,本层后面就不能再使用
used[nums[i] + 100] = 1;
path.add(nums[i]);
//【6】纵向递归遍历
backtracking(nums, i + 1);
path.remove(path.size() - 1); // 回溯
}
}
}
单词搜索(79)- 36
// https://leetcode.cn/problems/word-search/description/?favorite=2cktkvj
// 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
// 单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
// 输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
// 输出:true
class Solution {
public boolean exist(char[][] board, String word) {
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[i].length; j++) {
// 若board数组当前位置元素 与 word的首个字符相等,才需要进行下一步判断
if (board[i][j] == word.charAt(0)) {
// board数组从当前位置出发、通过上下左右搜索,能否搜索到word从下标0开始的整个字符串
// 若能够搜索到word,则直接返回true,结束循环;若根据当前位置未搜索到word,则继续循环
if (dfs(board, i, j, word, 0)) {
return true;
}
}
}
}
return false;
}
// 返回值表示board数组从当前位置出发、通过上下左右搜索,能否搜索到word从当前下标元素至末尾的子字符串
public boolean dfs(char[][] board, int i, int j, String word, int index) {
//【1】递归终止条件1:word中所有单词均被搜索到
if (index == word.length()) {
return true;
}
//【2】递归终止条件2:board数组越界
if (i < 0 || j < 0 || i >= board.length || j >= board[i].length) {
return false;
}
//【3】递归终止条件3:word当前索引元素 与 board数组中当前位置元素不相等
if (word.charAt(index) != board[i][j]) {
return false;
}
char temp = board[i][j];
//【4】将当前位置元素置为'0',防止在递归过程中被重复使用
board[i][j] = '0';
//【5】从当前位置上下左右递归遍历,搜索word下一索引元素
boolean res = dfs(board, i-1, j, word, index+1) || dfs(board, i+1, j, word, index+1) ||
dfs(board, i, j-1, word, index+1) || dfs(board, i, j+1, word, index+1);
//【6】在递归完成后需要恢复board[i][j]的值,不然从其他节点出发经过该节点时,值为'0'而造成错误判断
board[i][j] = temp;
return res;
}
}
双指针法
移除元素(27)- 4
// https://leetcode.cn/problems/remove-element/
// 给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度
// 不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组
// 元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素
// 输入:nums = [0,1,2,2,3,0,4,2], val = 2
// 输出:5, nums = [0,1,4,0,3]
// 解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素
class Solution {
public int removeElement(int[] nums, int val) {
int left = 0;
for(int rigth = 0; rigth < nums.length; rigth++){
if(nums[rigth] != val){
nums[left] = nums[rigth];
left++;
}
}
return left;
}
}
反转字符串(344)- 17
// https://leetcode.cn/problems/reverse-string/
// 编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出
// 不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题
// 输入:s = ["h","e","l","l","o"]
// 输出:["o","l","l","e","h"]
// 输入:s = ["H","a","n","n","a","h"]
// 输出:["h","a","n","n","a","H"]
class Solution {
public void reverseString(char[] s) {
int left = 0;
int right = s.length - 1;
while(left < right){
char temp = s[left];
s[left] = s[right];
s[right] = temp;
left++;
right--;
}
}
}
2 - 反转字符串 II(541)- 2
// https://leetcode.cn/problems/reverse-string-ii/description/
// 给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。
// 如果剩余字符少于 k 个,则将剩余字符全部反转。
// 如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样
// 输入:s = "abcdefg", k = 2
// 输出:"bacdfeg"
class Solution {
public String reverseStr(String s, int k) {
int len =s.length();
// 将字符串变为字符数组,方便当个字符遍历
char[] chars= s.toCharArray();
for(int left = 0; lef t< len; left += 2*k){
// 左指针是为了定义反转位置的开始,右指针定义反转位置的结束。
// 特别备注下,这边的Math.min()起到的作用是,字符小于k时,全部反转的小姑。
// 当k>len的时候,就取len。
// 例如;“a” 4。这种情况就rigth不能取4,应该取1。
reverse(chars, left, Math.min(left+k, len)-1);
}
// 返回结果
return new String(chars);
}
public void reverse(char[] chars, int left, int rigth){
// 只要左指针小于右指针就反转
// 这边是为了兼容处理 k > len情况下,将所有字符都反转
while(left < rigth){
char temp = chars[left];
chars[left] =chars[rigth];
chars[rigth] = temp;
// 左指针向右移动
left++;
// 右指针向左移动
rigth--;
}
}
}
替换空格(剑指 Offer 05)- 4
// https://leetcode.cn/problems/ti-huan-kong-ge-lcof/
// 请实现一个函数,把字符串 s 中的每个空格替换成"%20"
// 输入:s = "We are happy."
// 输出:"We%20are%20happy."
class Solution {
public String replaceSpace(String s) {
StringBuilder string = new StringBuilder();
for (int i = 0; i < s.length(); i++){
char c = s.charAt(i);
if (c == ' '){
string.append("%20");
} else {
string.append(c);
}
}
return string.toString();
// 直接调用API
// return s.replace(" ", "%20");
}
}
反转字符串中的单词 III(557)- 17
// https://leetcode.cn/problems/reverse-words-in-a-string-iii/
// 给定一个字符串 s ,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序
// 输入:s = "Let's take LeetCode contest"
// 输出:"s'teL ekat edoCteeL tsetnoc"
class Solution {
public String reverseWords(String s) {
StringBuffer result = new StringBuffer();
int i = 0;
while (i < s.length()) {
int start = i;
while (i < s.length() && s.charAt(i) != ' ') {
i++;
}
for (int p = start; p < i; p++) {
result.append(s.charAt(start + i - 1 - p));
}
while (i < s.length() && s.charAt(i) == ' ') {
i++;
result.append(' ');
}
}
return result.toString();
}
public String reverseWords1(String s) {
String[] split = s.split(" ");
for (int i = 0; i < split.length; i++) {
split[i] = new StringBuffer(split[i]).reverse().toString();
}
return String.join(" ", split);
}
}
反转链表(206)- 529
// https://leetcode.cn/problems/reverse-linked-list/
// 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表
// 输入:head = [1,2,3,4,5]
// 输出:[5,4,3,2,1]
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while(cur != null){
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
2 - 反转链表 II(92)- 151
// https://leetcode.cn/problems/reverse-linked-list-ii/
// 给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表
// 输入:head = [1,2,3,4,5], left = 2, right = 4
// 输出:[1,4,3,2,5]
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
// 重新设置头节点
ListNode dummyNode = new ListNode(-1, head);
ListNode pre = dummyNode;
// pre最终指向left上一个节点
for (int i = 0; i < left - 1; i++) {
pre = pre.next;
}
// cur指向left节点
ListNode cur = pre.next;
ListNode next;
for (int i = 0; i < right - left; i++) {
// 下移指针
next = cur.next;
cur.next = next.next;
next.next = pre.next;
pre.next = next;
}
return dummyNode.next;
}
}
删除链表的倒数第 N 个结点(19)- 109
// https://leetcode.cn/problems/remove-nth-node-from-end-of-list/
// 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点
// 输入:head = [1,2,3,4,5], n = 2
// 输出:[1,2,3,5]
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
// 重新定义头节点
ListNode dummy = new ListNode(0, head);
// 快指针
ListNode first = head;
// 慢指针
ListNode second = dummy;
for (int i = 0; i < n; ++i) {
first = first.next;
}
// 当快指针到链表尾部时,慢指针到达被删除节点的上一节点
while (first != null) {
first = first.next;
second = second.next;
}
// 将慢指针对应的下一个节点重新指定
second.next = second.next.next;
ListNode ans = dummy.next;
return ans;
}
public ListNode removeNthFromEnd1(ListNode head, int n) {
ListNode dummy = new ListNode(0, head);
int length = getLength(head);
ListNode cur = dummy;
for (int i = 1; i < length - n + 1; ++i) {
cur = cur.next;
}
cur.next = cur.next.next;
ListNode ans = dummy.next;
return ans;
}
// 获取链表长度
public int getLength(ListNode head) {
int length = 0;
while (head != null) {
++length;
head = head.next;
}
return length;
}
}
相交链表(160)- 168
// https://leetcode.cn/problems/intersection-of-two-linked-lists/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null || headB == null){
return null;
}
ListNode tempA = headA;
ListNode tempB = headB;
while(tempA != tempB){
tempA = tempA == null ? headB : tempA.next;
tempB = tempB == null ? headA : tempB.next;
}
return tempA;
}
}
环形链表 II(142)- 144
// https://leetcode.cn/problems/linked-list-cycle-ii/
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode temp = head;
Set<ListNode> set = new HashSet<>();
while(temp != null){
if(set.contains(temp)){
return temp;
}else {
set.add(temp);
}
temp = temp.next;
}
return null;
}
public ListNode detectCycle1(ListNode head) {
if (head == null) {
return null;
}
ListNode slow = head, fast = head;
while (fast != null) {
slow = slow.next;
if (fast.next != null) {
fast = fast.next.next;
} else {
return null;
}
if (fast == slow) {
fast = head;
while (fast != slow) {
fast = fast.next;
slow = slow.next;
}
return fast;
}
}
return null;
}
}
三数之和(15)- 265
// https://leetcode.cn/problems/3sum/
// 给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0
// 请你返回所有和为 0 且不重复的三元组
// 输入:nums = [-1,0,1,2,-1,-4]
// 输出:[[-1,-1,2],[-1,0,1]]
// 解释:
// nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
// nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
// nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
// 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
// 注意,输出的顺序和三元组的顺序并不重要
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);
for (int i = 0; i < nums.length; i++) {
if (nums[i] > 0) {
return result;
}
// 去重处理,题意要求答案中不可以包含重复的三元组
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.length - 1;
while (right > left) {
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0) {
right--;
} else if (sum < 0) {
left++;
} else {
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 去重处理
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
right--;
left++;
}
}
}
return result;
}
}
四数之和(18)- 16
// https://leetcode.cn/problems/4sum/
// 给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复)
// 0 <= a, b, c, d < n
// a、b、c 和 d 互不相同
// nums[a] + nums[b] + nums[c] + nums[d] == target
// 你可以按 任意顺序 返回答案
// 输入:nums = [1,0,-1,0,-2,2], target = 0
// 输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);
for(int i = 0; i < nums.length; i++){
// 剪枝操作,否则最后一个用例跑不过
if (nums[i] > 0 && nums[i] > target) return result;
// 排除与上一次遍历相同的元素
if(i > 0 && nums[i] == nums[i-1]) continue;
for(int j = i+1; j < nums.length; j++){
// 剪枝操作,此处只能使用break,不能使用return
if (nums[i] + nums[j] > target && nums[i] + nums[j] >= 0) break;
// 排除与上一次遍历相同的元素
if(j > i+1 && nums[j] == nums[j-1]) continue;
int left = j + 1;
int right = nums.length - 1;
while(left < right){
int sum = nums[i] + nums[j] + nums[left] + nums[right];
if(sum > target) right--;
if(sum < target) left++;
if(sum == target){
result.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
while(right > left && nums[left] == nums[left+1]) left++;
while(right > left && nums[right] == nums[right-1]) right--;
left++;
right--;
}
}
}
}
return result;
}
}
贪心算法
贪心算法一般分为如下四步:
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
简单题目
分发饼干(455)- 1
```java // https://leetcode.cn/problems/assign-cookies/description/ // 假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。 // 对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。 // 你的目标是尽可能满足越多数量的孩子,并输出这个最大数值 // 输入: g = [1,2,3], s = [1,1] // 输出: 1 // 解释: // 你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。 // 虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。 // 所以你应该输出1
// 输入: g = [1,2], s = [1,2,3] // 输出: 2 // 解释: // 你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。 // 你拥有的饼干数量和尺寸都足以让所有孩子满足。 // 所以你应该输出2
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int result = 0;
int gstart = 0;
int sstart = 0;
while(gstart < g.length && sstart < s.length){
// 若当前饼干尺寸 大于 当前孩子胃口,将该饼干分配给该孩子,同时移动两个数组指针
if(s[sstart] >= g[gstart]){
result++;
gstart++;
sstart++;
}else{
sstart++;
}
}
return result;
}
}
<a name="dYsCy"></a>
### K次取反后最大化的数组和(1005)- 1
```java
// https://leetcode.cn/problems/maximize-sum-of-array-after-k-negations/description/
// 给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:
// 选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。
// 重复这个过程恰好 k 次。可以多次选择同一个下标 i 。
// 以这种方式修改数组后,返回数组 可能的最大和
// 输入:nums = [4,2,3], k = 1
// 输出:5
// 解释:选择下标 1 ,nums 变为 [4,-2,3]
// 输入:nums = [3,-1,0,2], k = 3
// 输出:6
// 解释:选择下标 (1, 2, 2) ,nums 变为 [3,1,0,2]
class Solution {
public int largestSumAfterKNegations(int[] nums, int k) {
Arrays.sort(nums);
int start = 0;
// 替换当前数组中所有负数为正数
while (k > 0 && start < nums.length && nums[start] < 0){
nums[start] = -nums[start];
start++;
k--;
}
// 重新排序
Arrays.sort(nums);
// k可能还未使用完,替换第一个最小元素
while (k > 0){
nums[0] = -nums[0];
k--;
}
int result = 0;
for(int num : nums){
result = result + num;
}
return result;
}
public int largestSumAfterKNegations1(int[] nums, int k) {
Arrays.sort(nums);
int start = 0;
while (k > 0){
// 当k > nums.length时,遍历一遍数组,将指针重新指向索引0,再重头开始遍历
if(start >= nums.length){
start = 0;
}
// 只要是负数,就从后向前遍历并赋值
if (nums[start] < 0){
nums[start] = -nums[start];
start++;
k--;
// 若遇到正数,表明整个数组只有正数
}else {
// 重新排序,此时第一个元素最小,将第一个元素遍历k的剩余次数
Arrays.sort(nums);
while (k > 0){
nums[0] = -nums[0];
k--;
}
}
}
int result = 0;
for(int num : nums){
result = result + num;
}
return result;
}
}
柠檬水找零(860)- 1
// https://leetcode.cn/problems/lemonade-change/description/
// 在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
// 每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
// 注意,一开始你手头没有任何零钱。
// 给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false
// 输入:bills = [5,5,5,10,20]
// 输出:true
// 解释:
// 前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
// 第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
// 第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
// 由于所有客户都得到了正确的找零,所以我们输出 true
// 输入:bills = [5,5,10,10,20]
// 输出:false
// 解释:
// 前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。
// 对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。
// 对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。
// 由于不是每位顾客都得到了正确的找零,所以答案是 false
class Solution {
public boolean lemonadeChange(int[] bills) {
int five = 0, ten = 0;
for (int bill : bills) {
if (bill == 5) {
five++;
} else if (bill == 10) {
if (five == 0) {
return false;
}
five--;
ten++;
} else {
// 给账单20找零优先消耗美元10,因为美元10只能给账单20找零,而美元5可以给账单10和账单20找零,美元5更万能
if (five > 0 && ten > 0) {
five--;
ten--;
} else if (five >= 3) {
five -= 3;
} else {
return false;
}
}
}
return true;
}
public boolean lemonadeChange1(int[] bills) {
if(bills[0] != 5) return false;
// temp为临时数组,保存剩余零钱
List<Integer> temp = new ArrayList<>();
temp.add(5);
for(int i = 1; i < bills.length; i++){
if(bills[i] == 5){
temp.add(5);
}else if(bills[i] == 10){
if (temp.contains(5)){
temp.add(10);
temp.remove((Object)5); // 找零
}else {
return false;
}
}else if(bills[i] == 15){
// 若有10元零钱,则先添加15元后、删除10元
// 若没有10元零钱,则判断是否有两个5元。若有两个5元,添加15元后,删除10元返回false,不影响结果
if (temp.contains(10) || (temp.remove((Object)5) && temp.remove((Object)5))){
temp.add(15);
temp.remove((Object)10);
}else {
return false;
}
}else if(bills[i] == 20){
// 若有15元零钱,则先添加20元后、删除15元
// 若没有15元零钱,若有10元零钱 + 5元零钱,则先添加20元后、删除10元、5元
// 若前面两个均不满足,则判断是否有三个5元
if (temp.contains(15) || (temp.contains(10) && temp.contains(5))
|| (temp.remove((Object)5) && temp.remove((Object)5) && temp.remove((Object)5))){
temp.add(20);
temp.remove((Object)15); // 即使没有15元,而是满足其他两个条件,这一步删除操作也不影响结果
if(temp.contains(10) && temp.contains(5)) {
temp.remove((Object)10);
temp.remove((Object)5);
}
}else {
return false;
}
}
}
return true;
}
}
中等题目
序列问题
摆动序列(376)- 3
// https://leetcode.cn/problems/wiggle-subsequence/description/
// 如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
// 例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
// 相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
// 子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
// 给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度
// 输入:nums = [1,7,4,9,2,5]
// 输出:6
// 解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3)
// 输入:nums = [1,17,5,10,13,15,10,5,16,8]
// 输出:7
// 解释:这个序列包含几个长度为 7 摆动序列。
// 其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8)
class Solution {
public int wiggleMaxLength(int[] nums) {
int result = 1;
// flag表示上一个差值为正数(true) 或 负数(false)
boolean flag = false;
int left = 1;
while(left < nums.length){
//【1】当前元素 与 上一位元素构成的差值为正数
if(nums[left] > nums[left-1]){
// 若上一个差值为负数,则当前差值有效,result加1,同时标记flag为正数
if(!flag){
result++;
flag = true;
}
//【2】当前元素 与 上一位元素构成的差值为负数
}else if(nums[left] < nums[left-1]){
// 若上一个差值为正数 或者是第一个差值,则当前差值有效,result加1,同时标记flag为负数
if(flag || result == 1){
result++;
flag = false;
}
}
//【3】当前元素 与 上一位元素相等,则两者构成的差值无效
left++;
}
return result;
}
}
单调递增的数字(738)- 3
// https://leetcode.cn/problems/monotone-increasing-digits/description/
// 当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。
// 给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增
// 输入: n = 10
// 输出: 9
// 输入: n = 1234
// 输出: 1234
// 输入: n = 332
// 输出: 299
class Solution {
public int monotoneIncreasingDigits(int N) {
if (N < 10) return N;
char[] chars = String.valueOf(N).toCharArray();
//【1】从左往右遍历各位数字,找到第一个开始下降的数字索引[i]
int i = 0;
while (i+1 < chars.length && chars[i] <= chars[i+1]) {
i++;
}
//【2】整个数字符合要求,直接返回
if (i == chars.length-1) return N;
//【3】需要再往前再看下,是否还有等于当前chars[i]的数字,找到最前面那个
while (i-1 >= 0 && chars[i-1] == chars[i]) {
i--;
}
//【4】将此时的chars[i]减1,并将[i+1 ...]各位数字全部置为9
chars[i] = (char) (chars[i]- 1);
i++;
while (i < chars.length) {
chars[i] = '9';
i++;
}
return Integer.parseInt(String.valueOf(chars));
}
// 解答错误:n = 100,预期99,实际90
public int monotoneIncreasingDigits1(int n) {
StringBuilder result = new StringBuilder();
String str = n + "";
char[] chars = str.toCharArray();
int right = str.length() - 1;
while(right > 0){
if(chars[right] < chars[right-1]){
result.append("9");
chars[right-1] = (char)(chars[right-1] - '0' - 1 + '0');
}else if(chars[right] > chars[right-1]){
result.append(chars[right]);
}
while(chars[right] == chars[right-1]){
}
right--;
}
result.append(chars[0]);
return Integer.parseInt(result.reverse().toString());
}
}
贪心解决股票问题
买卖股票的最佳时机II(可以买卖多次)(122)- 48
// https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/description/
class Solution {
public int maxProfit1(int[] prices) {
// 第一步:确定dp数组以及下标的含义
// dp[i][0]表示第i天未持有股票时的最大利润
// dp[i][1]表示第i天持有股票时的最大利润
int[][] dp = new int[prices.length][2];
// 第二步:确定递推公式
// 若第i天未持有股票dp[i][0]:max(第i-1天未持有股票dp[i-1][0],第i-1天持有股票且i天卖出股票dp[i-1][1]+prices[i])
// 若第i天持有股票dp[i][1]:max(第i-1天未持有股票且第i天买入股票dp[i-1][0]-prices[i],第i-1天持有股票dp[i-1][1])
// 第三步:初始化dp数组
dp[0][0] = 0;
dp[0][1] = -prices[0];
// 第四步:确认遍历顺序
for(int i = 1; i < prices.length; i++){
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
}
return dp[prices.length-1][0];
}
public int maxProfit(int[] prices) {
int result = 0;
for(int i = 1; i < prices.length; i++){
// 因为可以同一天可以买入和卖出股票,所以既可以prices[i+1] - prices[i],又可以prices[i] - prices[i-1]
result = result + Math.max(prices[i] - prices[i-1], 0);
}
return result;
}
}
买卖股票的最佳时机含手续费(买卖多次,每次有手续费)(714)- 3
// https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/
// 给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。
// 你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
// 返回获得利润的最大值。
// 注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
// 输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
// 输出:8
// 解释:能够达到的最大利润:
// 在此处买入 prices[0] = 1
// 在此处卖出 prices[3] = 8
// 在此处买入 prices[4] = 4
// 在此处卖出 prices[5] = 9
// 总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8
class Solution {
// 卖出时支付手续费
public int maxProfit(int[] prices, int fee) {
int length = prices.length;
//【1】dp[i][j]表示第i天 持股或不持股 所获得利润的最大值
// 0 - 持股(买入)、1 - 不持股(售出)
int[][] dp = new int[length][2];
//【2】初始化dp数组
dp[0][0] = -prices[0];
for (int i = 1; i < length; i++) {
//【3】确定递推公式
//【3.1】第i天持股:第i-1天已经持股,或者第i-1天不持股、同时第i天买入股票
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] - prices[i]);
//【3.2】第i天不持股:第i-1天已经不持股,或者第i-1天持股、同时第i天卖出股票、需要支付手续费
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i] - fee);
}
return Math.max(dp[length - 1][0], dp[length - 1][1]);
}
// 买入时支付手续费
public int maxProfit1(int[] prices, int fee) {
int len = prices.length;
// 0 : 持股(买入)
// 1 : 不持股(售出)
// dp 定义第i天持股/不持股 所得最多现金
int[][] dp = new int[len][2];
// 考虑买入的时候就支付手续费
dp[0][0] = -prices[0] - fee;
for (int i = 1; i < len; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i] - fee);
dp[i][1] = Math.max(dp[i - 1][0] + prices[i], dp[i - 1][1]);
}
return Math.max(dp[len - 1][0], dp[len - 1][1]);
}
}
两个维度权衡问题
分发糖果(135)- 29
// https://leetcode.cn/problems/candy/description/
// n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
// 你需要按照以下要求,给这些孩子分发糖果:
// 每个孩子至少分配到 1 个糖果。
// 相邻两个孩子评分更高的孩子会获得更多的糖果。
// 请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目
// 输入:ratings = [1,0,2]
// 输出:5
// 解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果
// 输入:ratings = [1,2,2]
// 输出:4
// 解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。
// 第三个孩子只得到 1 颗糖果,这满足题面中的两个条件
class Solution {
public int candy(int[] ratings) {
int length = ratings.length;
int[] res = new int[length];
res[0] = 1;
//【1】从左往右遍历
for(int i = 1; i < length; i++){
//【1.1】若当前元素 > 其左边元素,则当前元素 = 左边元素 + 1
if (ratings[i] > ratings[i-1]){
res[i] = res[i-1] + 1;
}else {
//【1.2】若当前元素 <= 其左边元素,则当前元素只需要满足最低要求即可
res[i] = 1;
}
}
int result = res[length - 1];
//【2】从右往左遍历
for(int i = length - 2; i >= 0 ; i--){
//【2.1】若当前元素 > 右边元素,则当前元素 = 右边元素 + 1,此时符合当前元素 比 右边元素大
// 同时,步骤【1】中已经实现当前元素 比 左边元素大。所以当前元素取两者之间的最大值,即可满足当前元素比左右相邻元素大
if (ratings[i] > ratings[i+1]){
res[i] = Math.max(res[i], res[i+1] + 1);
}else {
//【2.2】若当前元素 <= 右边元素,此时只需要满足当前元素 比 左边元素大即可(步骤【1】已实现)
res[i] = res[i];
}
//【3】添加到结果集
result = result + res[i];
}
return result;
}
}
22222 - 根据身高重建队列(406)- 3
有点难度
区间问题
跳跃游戏(55)- 35
// https://leetcode.cn/problems/jump-game/
// 给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。
// 数组中的每个元素代表你在该位置可以跳跃的最大长度。
// 判断你是否能够到达最后一个下标
// 输入:nums = [2,3,1,1,4]
// 输出:true
// 解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标
// 输入:nums = [3,2,1,0,4]
// 输出:false
// 解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标
class Solution {
public boolean canJump(int[] nums) {
// dp[i]表示从0到i-1个元素能否跳到第i个元素
boolean[] dp = new boolean[nums.length];
// 初始化
dp[0] = true;
// 递推公式:
// dp[i]由前面0到i-1位置的dp[j]决定,如果dp[j] == true 并且 j + nums[j] >= i,则可以跳到第i个位置
for(int i = 1; i < nums.length; i++){
for(int j = 0; j < i; j++){
if(dp[j] == true && j + nums[j] >= i){
dp[i] = true;
break;
}
}
}
return dp[nums.length - 1];
}
public boolean canJump1(int[] nums) {
// k为当前能够到达的最大位置
int k = 0;
for(int i = 0; i < nums.length; i++){
// 若遍历元素位置下标 大于 当前能够到达的最大位置下标,则不能到达
// 举例:nums = [3,2,1,0,4],下标i为4时,k为0
if(i > k) return false;
// 能够到达当前位置,看是否更新能够到达的最大位置k
k = Math.max(k, i + nums[i]);
}
// 跳出则表明能够到达最大位置
return true;
}
public boolean canJump2(int[] nums) {
// 初始化当前元素能够跳到的最远距离
int k = 0;
// k是目前最大的终点,所以i <= 最大的终点
for (int i = 0; i <= k; i++) {
// 第i个元素能够跳到的最远距离
int temp = i + nums[i];
// 更新最远距离
k = Math.max(k, temp);
// 如果最远距离已经大于或等于最后一个元素的下标,则说明能跳过去,退出循环
if (k >= nums.length - 1) {
return true;
}
}
// 最远距离k不再改变,且没有到末尾元素
return false;
}
}
跳跃游戏 II(45)- 25
// https://leetcode.cn/problems/jump-game-ii/description/
// 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。
// 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:
// 0 <= j <= nums[i]
// i + j < n
// 返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]
// 输入: nums = [2,3,1,1,4]
// 输出: 2
// 解释: 跳到最后一个位置的最小跳跃数是 2。
// 从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置
// 输入: nums = [2,3,0,1,4]
// 输出: 2
class Solution {
public int jump(int[] nums) {
int result = 0;
// 上一步覆盖的最远距离下标
int preDistance = 0;
// 当前位置覆盖的最远距离下标
int curDistance = 0;
// 注意这里是小于nums.size() - 1,这是关键所在
for(int index = 0; index < nums.length - 1; index++){
// 更新当前位置覆盖的最远距离下标
curDistance = Math.max(curDistance, nums[index] + index);
// 当前位置到达了边界,那么一定要跳了,下一跳的边界下标就是之前统计的最优情况maxPosition,并且步数加1
// 当前位置 到达 上一步覆盖的最远距离下标,此时必须要再往后跳一步才行
if(index == preDistance){
preDistance = curDistance; // 更新上一步覆盖的最远距离下标
result++;
}
}
return result;
}
}
22222 - 用最少数量的箭引爆气球(452)- 7
1 - 无重叠区间(435)- 8
// https://leetcode.cn/problems/non-overlapping-intervals/description/
// 给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。
// 返回 需要移除区间的最小数量,使剩余区间互不重叠
// 输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
// 输出: 1
// 解释: 移除 [1,3] 后,剩下的区间没有重叠
// 输入: intervals = [ [1,2], [1,2], [1,2] ]
// 输出: 2
// 解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠
class Solution {
// 局部最优:优先选右边界小的区间,所以从左向右遍历,留给下一个区间的空间大一些,从而尽量避免交叉
// 全局最优:选取最多的非交叉区间
public int eraseOverlapIntervals(int[][] intervals) {
//【1】思路:按照左边界排序,从左向右遍历,因为右边界越小越好,只要右边界越小,留给下一个区间的空间就越大,所以从左向右遍历,优先选右边界小的
Arrays.sort(intervals, (o1, o2) -> (o1[0] - o2[0]));
//【2】记录非交叉区间的个数(第一个区间就是非交叉区间)
int count = 1;
//【3】从第二个区间开始从左往右遍历
for(int i = 1; i < intervals.length; i++){
//【3.1】相邻区间存在交叉,当前区间的第一个元素 小于 上一个区间的第二个元素
if(intervals[i][0] < intervals[i-1][1]){
// 合并区间,更新当前区间的第二个元素,取小值,从而留给下一个区间的空间大一些
intervals[i][1] = Math.min(intervals[i][1], intervals[i-1][1]);
//【3.2】当前区间与上一个区间不交叉
}else{
count++;
}
}
return intervals.length - count;
}
}
2 - 划分字母区间(763)- 13
// https://leetcode.cn/problems/partition-labels/description/
// 给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
// 注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。
// 返回一个表示每个字符串片段的长度的列表
// 输入:s = "ababcbacadefegdehijhklij"
// 输出:[9,7,8]
// 解释:
// 划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。
// 每个字母最多出现在一个片段中。
// 像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少
// 输入:s = "eccbbbbdec"
// 输出:[10]
class Solution {
public List<Integer> partitionLabels(String s) {
List<Integer> result = new LinkedList<>();
char[] chars = s.toCharArray();
//【1】统计每个字符出现的下标(char-'a'转化为数字)
int[] edge = new int[26];
for (int index = 0; index < chars.length; index++) {
char ch = chars[index];
edge[ch - 'a'] = index;
}
//【2】初始化字符最初出现的位置 与 最后出现的位置
int leftIndex = 0, rightIndex = 0;
for (int i = 0; i < chars.length; i++) {
//【3】找到字符出现的最远边界
int index = edge[chars[i] - 'a'];
rightIndex = Math.max(rightIndex, index);
//【4】
if (i == rightIndex) {
result.add(rightIndex - leftIndex + 1);
leftIndex = rightIndex + 1;
}
}
return result;
}
}
合并区间(56)- 106
// https://leetcode.cn/problems/merge-intervals/
// 以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间
// 输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
// 输出:[[1,6],[8,10],[15,18]]
// 解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6]
// 输入:intervals = [[1,4],[4,5]]
// 输出:[[1,5]]
// 解释:区间 [1,4] 和 [4,5] 可被视为重叠区间
class Solution {
public int[][] merge(int[][] intervals) {
List<int[]> result = new ArrayList<>();
//【1】将数组中的每个子数组按照第一个元素大小进行排序
Arrays.sort(intervals, (o1, o2) -> (o1[0] - o2[0]));
for(int[] interval : intervals){
//【2】当前集合为空 或者 当前遍历数组的第一个元素 大于 集合中最后一个数组的第二个元素,表明此时数组不需要合并,加上该新数组即可
if(result.size() == 0 || interval[0] > result.get(result.size()-1)[1]){
result.add(interval);
}else{
//【3】当前遍历数组的第一个元素 不大于 集合中最后一个数组的第二个元素,表明此时两个数组会有重叠,需要合并这两个数组。第二个元素取较大值
result.get(result.size()-1)[1] = Math.max(interval[1], result.get(result.size()-1)[1]);
}
}
// 集合转数组
return result.toArray(new int[result.size()][]);
}
}
// 时间复杂度:O(nlogn)
// 空间复杂度:O(nlogn)
最大子数组和(53)- 237
// https://leetcode.cn/problems/maximum-subarray/
// 给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
// 子数组 是数组中的一个连续部分
// 输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
// 输出:6
// 解释:连续子数组 [4,-1,2,1] 的和最大,为 6
// 输入:nums = [1]
// 输出:1
class Solution {
public int maxSubArray(int[] nums) {
int len = nums.length;
// 第一步:定义状态:dp[i]表示第i个元素为尾结点构成的子数组的和
int[] dp = new int[len];
// 第二步:初始化
dp[0] = nums[0];
int maxSum = dp[0];
// 第四步:确定遍历顺序
for(int i = 1; i < len; i++){
// 第三步:状态转移
// dp[i-1] > 0,则dp[i] = dp[i-1] + nums[i]。此处不能判断dp[i-1] + nums[i] > 0,需考虑dp[i-1]为负数场景
// dp[i-1] <= 0,则dp[i] = nums[i];
if(dp[i-1] > 0){
dp[i] = dp[i-1] + nums[i];
}else {
dp[i] = nums[i];
}
maxSum = Math.max(maxSum, dp[i]);
}
// 第五步:确定最大值
return maxSum;
}
public int maxSubArray1(int[] nums) {
int result = nums[0]; // 最终求出的连续子数组的最大和
int sum = 0; // 当前元素前面的连续子数组的最大和
for(int num: nums) {
if(sum > 0) {
sum += num; // 说明sum对结果有增益效果,则sum保留并加上当前遍历数字
} else {
sum = num; // 说明sum对结果无增益效果,需要舍弃,则sum直接更新为当前遍历数字
}
result = Math.max(result, sum);
}
return result;
}
}
2 - 加油站(134)- 18
// https://leetcode.cn/problems/gas-station/description/
// 在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
// 你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
// 给定两个整数数组 gas 和 cost ,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的
// 输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
// 输出: 3
// 解释:
// 从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
// 开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
// 开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
// 开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
// 开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
// 开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
// 因此,3 可为起始索引
// 输入: gas = [2,3,4], cost = [3,4,3]
// 输出: -1
// 解释:
// 你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。
// 我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油
// 开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油
// 开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油
// 你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。
// 因此,无论怎样,你都不可能绕环路行驶一周
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int sum = 0; // 行驶一圈的剩余总油量
int min = 0; // 从0开始,行驶一圈过程中的最低剩余油量
for (int i = 0; i < gas.length; i++) {
sum += (gas[i] - cost[i]);
min = Math.min(sum, min);
}
//【1】情况一:如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的
if (sum < 0) return -1;
//【2】情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点
if (min >= 0) return 0;
//【3】情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能把这个负数填平,能把这个负数填平的节点就是出发节点
for (int i = gas.length - 1; i > 0; i--) {
min += (gas[i] - cost[i]);
if (min >= 0) return i;
}
return -1;
}
public int canCompleteCircuit2(int[] gas, int[] cost) {
int curSum = 0;
int totalSum = 0;
int index = 0;
// 局部最优:当前累加rest[j]的和curSum一旦小于0,起始位置至少要是j+1,因为从j开始一定不行。
// 全局最优:找到可以跑一圈的起始位置
for (int i = 0; i < gas.length; i++) {
//【1】位置[0, i]区间累加的剩余油量
curSum += gas[i] - cost[i];
//【2】行驶一圈最终的剩余油量
totalSum += gas[i] - cost[i];
//【3】说明[0, i]区间都不能作为起始位置,起始位置从i+1算起,再从0计算curSum
if (curSum < 0) {
index = (i + 1) % gas.length;
curSum = 0;
}
}
//【4】若最终剩余油量 小于0,表示无论如何也无法行驶一圈
if(totalSum < 0) return -1;
return index;
}
// 超出时间限制:时间复杂度:O(n^2)、空间复杂度:O(1)
public int canCompleteCircuit1(int[] gas, int[] cost) {
//【1】遍历每个加油站
for(int index = 0; index < gas.length; index++){
//【2】初始化从当前加油站出发 到达 下一个加油站的剩余油量
int rest = gas[index] - cost[index];
//【3】模拟以index为起点行驶一圈(index+1赋值给next表示先行驶一步)
int next = (index + 1) % gas.length;
// 终止条件为剩余油量小于等于0,或者index已经行驶一圈后回到原位
while(rest > 0 && next != index){
rest = rest + (gas[next] - cost[next]); // 每行驶一步就更新剩余油量
next = (next + 1) % gas.length; // 相当于next++,取模的目的就是为了行驶一圈
}
//【4】如果以index为起点行驶一圈,且剩余油量>=0,返回该起始位置
if(rest >= 0 && next == index) return index;
}
return -1;
}
}
1 - 监控二叉树(968)- 4
// https://leetcode.cn/problems/binary-tree-cameras/solutions/
// 给定一个二叉树,我们在树的节点上安装摄像头。
// 节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
// 计算监控树的所有节点所需的最小摄像头数量
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int result = 0;
public int minCameraCover(TreeNode root) {
// 若根节点是无覆盖状态,此时需要在根节点放一个摄像头
if (minCame(root) == 0) {
result++;
}
return result;
}
// 节点的状态值:0--表示无覆盖、1--表示有摄像头、2--表示有覆盖
// 后序遍历,根据左右节点的情况, 来判断父节点的状态
public int minCame(TreeNode root) {
//【1】空节点默认为有覆盖状态,避免在叶子节点上放摄像头
if (root == null) {
return 2;
}
int left = minCame(root.left);
int right = minCame(root.right);
//【2】若左右节点都是有覆盖状态, 那么当前父节点就是无覆盖状态
// 左右节点的状态为(2, 2)
if (left == 2 && right == 2) {
return 0;
//【3】若左右节点至少存在一个是无覆盖状态, 那么当前父节点就应该放一个摄像头来覆盖其左右节点
// 左右节点的状态为(0, 0) (0, 1) (0, 2) (1, 0) (2, 0)
} else if (left == 0 || right == 0) {
result++; // 当前父节点放置摄像头
return 1; // 返回1表示当前父节点是有摄像头状态
//【4】若左右节点至少存在一个摄像头,那么当前父节点就是处于有覆盖状态
// 左右节点的状态为(1, 1) (1, 2) (2, 1)
} else {
return 2;
}
}
}
动态规划
- dp数组的定义和下标
- 递推公式
- dp数组如何初始化
- 遍历顺序
- 01:先遍历背包,后遍历物品
- 排列:背包在外,物品在内(322)
- 组合:物品在外,背包在内(518)
- 打印dp数组
基础题目
斐波那契数列(509)- 33
// https://leetcode.cn/problems/fei-bo-na-qi-shu-lie-lcof/
// 斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是
// F(0) = 0,F(1) = 1
// F(n) = F(n - 1) + F(n - 2),其中 n > 1
// 输入:n = 3
// 输出:2
// 解释:F(3) = F(2) + F(1) = 1 + 1 = 2
class Solution {
public int fib(int n) {
if(n < 2){
return n;
}
int[] dp = new int[n+1];
dp[0] = 0;
dp[1] = 1;
for(int i = 2; i <= n; i++){
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
}
// 剑指 Offer 10- I. 斐波那契数列
// https://leetcode.cn/problems/fei-bo-na-qi-shu-lie-lcof/description/
class Solution {
public int fib(int n) {
final int MOD = 1000000007;
if(n < 2){
return n;
}
int[] dp = new int[n+1];
dp[0] = 0;
dp[1] = 1;
for(int i = 2; i <= n; i++){
dp[i] = dp[i-1] + dp[i-2];
dp[i] = dp[i] % MOD;
}
return dp[n];
}
}
爬楼梯(70)- 105
// https://leetcode.cn/problems/climbing-stairs/
// 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
// 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
// 输入:n = 3
// 输出:3
// 解释:有三种方法可以爬到楼顶。
// 1. 1 阶 + 1 阶 + 1 阶
// 2. 1 阶 + 2 阶
// 3. 2 阶 + 1 阶
class Solution {
public int climbStairs(int n) {
if(n < 3) return n;
// dp[i]表示i阶梯盘到楼顶的方法数
int[] dp = new int[n+1];
dp[1] = 1;
dp[2] = 2;
for(int i = 3; i <= n; i++){
// i阶梯方法数 = i-1阶梯爬一步到达i阶梯 + i-2阶梯爬两步到达i阶梯
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
}
1 - 使用最小花费爬楼梯(746)- 1
// https://leetcode.cn/problems/min-cost-climbing-stairs/
// 给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
// 你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
// 请你计算并返回达到楼梯顶部的最低花费
// 输入:cost = [1,100,1,1,1,100,1,1,100,1]
// 输出:6
// 解释:你将从下标为 0 的台阶开始。
// - 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
// - 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
// - 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
// - 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
// - 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
// - 支付 1 ,向上爬一个台阶,到达楼梯顶部。
// 总花费为 6 。
class Solution {
public int minCostClimbingStairs(int[] cost) {
// 最后一个元素dp[cost.length],即到达楼顶的总花费(构建出的楼顶节点)
int[] dp = new int[cost.length + 1];
dp[0] = 0;
dp[1] = 0;
// dp[i]表示到达第i个台阶的最低花费
for(int i = 2; i <= cost.length; i++){
// i可以从i-1节点(需要加上i-1节点的花费) 或者 从i-2节点(需要加上i-2节点的花费)到达
dp[i] = Math.min(dp[i-1] + cost[i - 1], dp[i-2] + cost[i - 2]) ;
}
return dp[cost.length];
}
}
不同路径(62)- 51
// https://leetcode.cn/problems/unique-paths/
// 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
// 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
// 问总共有多少条不同的路径?
// 输入:m = 3, n = 2
// 输出:3
// 解释:
// 从左上角开始,总共有 3 条路径可以到达右下角。
// 1. 向右 -> 向下 -> 向下
// 2. 向下 -> 向下 -> 向右
// 3. 向下 -> 向右 -> 向下
class Solution {
// m表示行数、n表示列数
public int uniquePaths(int m, int n) {
// dp(i,j)表示从左上角走到(i,j)的路径数量
int[][] dp = new int[m][n];
// 第一行:由于在边界,所以都只能为1
for (int i = 0; i < m; ++i) {
dp[i][0] = 1;
}
// 第一列:由于在边界,所以都只能为1
for (int j = 0; j < n; ++j) {
dp[0][j] = 1;
}
// 其他位置:既可以从上边来(dp[i][j-1]),也可以从左边来(dp[i-1][j])
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
dp[i][j] = dp[i][j-1] + dp[i-1][j];
}
}
return dp[m - 1][n - 1];
}
public int uniquePaths1(int m, int n) {
int[] cur = new int[n];
Arrays.fill(cur,1);
for (int i = 1; i < m;i++){
for (int j = 1; j < n; j++){
cur[j] += cur[j-1] ;
}
}
return cur[n-1];
}
}
1 - 不同路径II(63)- 17
// https://leetcode.cn/problems/unique-paths-ii/
// 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
// 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
// 现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
// 网格中的障碍物和空位置分别用 1 和 0 来表示。
// 输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
// 输出:2
// 解释:3x3 网格的正中间有一个障碍物。
// 从左上角到右下角一共有 2 条不同的路径:
// 1. 向右 -> 向右 -> 向下 -> 向下
// 2. 向下 -> 向下 -> 向右 -> 向右
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int[][] dp = new int[m][n];
// 初始化第一列,只要碰到1,后面都无法到达
for (int i = 0; i < m; i++) {
if (obstacleGrid[i][0] == 1){
break;
}
dp[i][0] = 1;
}
// 初始化第一行,只要碰到1,后面都无法到达
for (int i = 0; i < n; i++) {
if (obstacleGrid[0][i] == 1){
break;
}
dp[0][i] = 1;
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
//自动跳过路障
if (obstacleGrid[i][j] != 1){
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
}
return dp[m-1][n-1];
}
}
0 - 整数拆分(343)- 14
// https://leetcode.cn/problems/integer-break/
// 给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化
// 返回 你可以获得的最大乘积
// 输入: n = 10
// 输出: 36
// 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36
class Solution {
public int integerBreak(int n) {
// dp[i]为正整数i拆分结果的最大乘积
int[] dp = new int[n+1];
dp[2] = 1;
for(int i = 3; i <= n; i++) {
for (int j = 1; j < i; j++) {
// j*(i-j)代表把i拆分为j和i-j两个数相乘
// j*dp[i-j]代表把i拆分成j和继续把(i-j)这个数拆分,取(i-j)拆分结果中的最大乘积与j相乘
// 因为dp[i]在内层循环中不断更新,所以需要将它放入公式取最大值
dp[i] = Math.max(dp[i], Math.max(j * (i - j), j * dp[i - j]));
}
}
return dp[n];
}
public int integerBreak1(int n) {
if (n <= 3) {
return n - 1;
}
int quotient = n / 3;
int remainder = n % 3;
if (remainder == 0) {
return (int) Math.pow(3, quotient);
} else if (remainder == 1) {
return (int) Math.pow(3, quotient - 1) * 4;
} else {
return (int) Math.pow(3, quotient) * 2;
}
}
}
n = 2 = 1+1; r = 1*1 = 1 1*1=1
n = 3 = 1+2; r = 1*2 = 2 2*1=2
n = 4 = 2+2; r = 2*2 = 4 2*2=4
n = 5 = 2+3; r = 2*3 = 6 3*2=6
n = 6 = 3+3; r = 3*3 = 9 3*3=9
n = 7 = 3+4; r = 3*4 = 12 3*2*2=12
n = 8 = 4+4; r = 4*4 = 16 3*3*2=18
n = 9 = 4+5; r = 4*5 = 20 3*3*3=27
n = 10 = 5+5; r = 5*5 = 25 3*3*4=36
n = 11 = 5+6; r = 5*6 = 30 3*3*3*2=54
n = 12 = 6+6; r = 6*6 = 36 3*3*3*3=81 2*2*4*4=64 3*3*2*4=72
n = 13 = 6+7; r = 6*7 = 42 3*3*3*4=108
2 - 不同的二叉搜索树(96)
// https://leetcode.cn/problems/unique-binary-search-trees/
// 给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数
// 输入:n = 3
// 输出:5
class Solution {
public int numTrees(int n) {
// dp[i]表示i个不同的数组成的二叉搜索数的个数
// 假设 i = 5
// 当根节点等于 1 时,其余数字都比1大,只能在右边 dp[i] += dp[4](相当于dp[i] += dp[0] * dp[4])
// 当根节点等于 2 时,左边有一个1比2小,右边有三个比2大的数字 dp[i] += dp[1] * dp[3]
// 当根节点等于 3 时,左边有两个数比3小,右边有两个数比3大的数字 dp[i] += dp[2] * dp[2]
// ...
// 当根节点等于 5 时,左边有4个数字比5小,只能放在5的左边,dp[i] += dp[4](相当于dp[i] += dp[4] * dp[0])
// 1.状态定义:dp[i]表示当有i个节点时,一共可以组成的二叉搜索树数目
int[] dp = new int[n+1];
// 2. 状态转移:
// 当前节点数为3时,左子树节点数与右子树节点数的可能情况有:0-2、1-1、2-0
// dp[3] = dp[0]*dp[2] + dp[1]*dp[1] + dp[2]*dp[0]
// 可以理解为前面一项dp[0]是左子树情况数,后面一项dp[2]是右子树情况数,两者相乘,最后再相加即可
// 即:dp[i] = ∑dp[j]*dp[i-1-j],其中j∈[0, i-1]
// 3.初始化:dp[0] = 1,dp[1] = dp[0]*dp[0] = 1
dp[0] = 1;
dp[1] = 1;
// 4.遍历顺序:正序
for(int i = 2; i <= n; i++) {
// j表示根节点
for(int j = 1; j <= i; j++) {
// 对于第i个节点,需要考虑1作为根节点直到i作为根节点的情况,所以需要累加
// 一共i个节点,对于根节点j时,左子树的节点个数为j-1,右子树的节点个数为i-j
int leftNum = dp[j-1];
int rightNum = dp[i-j];
dp[i] += leftNum * rightNum;
}
}
// 5.返回形式:返回dp[n]
return dp[n];
}
}
背包问题
01背包
0 - 分割等和子集(416)- 11
// https://leetcode.cn/problems/partition-equal-subset-sum/description/
// 给你一个 只包含正整数 的 非空 数组 nums 。
// 请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等
// 输入:nums = [1,5,11,5]
// 输出:true
// 解释:数组可以分割成 [1, 5, 5] 和 [11]
// 输入:nums = [1,2,3,5]
// 输出:false
// 解释:数组不能分割成两个元素和相等的子集
class Solution {
public boolean canPartition(int[] nums) {
int length = nums.length;
//【1】计算数组总和
int sum = 0;
for(int num : nums) sum += num;
// 剪枝操作:总和为奇数,无法平分
if(sum % 2 != 0) return false;
//【2】计算目标值(背包容量、同样也是背包价值)
int target = sum / 2;
//【3】dp[i]表示容量为i的背包能够装下的物品最大价值
int[] dp = new int[target+1];
//【4】nums[i]既表示物品i的重量,又表示物品i的价值
for(int i = 0; i < length; i++){ // 先遍历物品
for(int j = target; j >= nums[i]; j--){ // 再遍历背包(容量)
// dp[j]表示不装物品i所能装下的最大价值
// dp[j - nums[i]]表示背包容量还能装下物品i的容量时,背包最大价值
// dp[j - nums[i]] + nums[i]表示装下物品i,背包最大价值
dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
//【5】判断背包容量为最大值target时,背包最大价值是否为target
return dp[target] == target;
}
}
1 - 目标和(494)- 15
// https://leetcode.cn/problems/target-sum/description/
// 给你一个整数数组 nums 和一个整数 target 。
// 向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
// 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
// 返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
// 输入:nums = [1,1,1,1,1], target = 3
// 输出:5
// 解释:一共有 5 种方法让最终目标和为 3 。
// -1 + 1 + 1 + 1 + 1 = 3
// +1 - 1 + 1 + 1 + 1 = 3
// +1 + 1 - 1 + 1 + 1 = 3
// +1 + 1 + 1 - 1 + 1 = 3
// +1 + 1 + 1 + 1 - 1 = 3
class Solution {
int result = 0;
public int findTargetSumWays2(int[] nums, int target) {
dfs(nums, target, 0, 0);
return result;
}
// sum表示当前表达式的结果
public void dfs(int[] nums, int target, int start, int sum) {
//【1】终止条件:遍历到末尾
if (start == nums.length) {
//【1.1】若表达式的结果等于目标数target,则该表达式即为符合要求的表达式
if (sum == target) {
result++;
}
//【2】数组nums的每个元素都可以添加符号+或-,因此每个元素有2种添加符号的方法,n个数共有2^n种添加符号的方法,对应2^n种不同的表达式
} else {
//【2.1】当前元素添加减号(-)
dfs(nums, target, start + 1, sum + nums[start]);
//【2.2】当前元素添加加号(+)
dfs(nums, target, start + 1, sum - nums[start]);
}
}
public int findTargetSumWays(int[] nums, int target) {
//【1】计算数组元素最大总和sum
int sum = 0;
for (int i = 0; i < nums.length; i++) sum += nums[i];
// 剪枝操作
if ((target + sum) % 2 != 0) return 0;
if (sum < Math.abs(target)) return 0;
//【2】计算加法的总和
// 加法的总和 + 减法的总和 = sum
// 加法的总和 - 减法的总和 = target
int size = (target + sum) / 2;
if(size < 0) size = -size;
//【3】dp[i]表示填满容量为i的背包 的方法数
int[] dp = new int[size + 1];
dp[0] = 1;
for (int i = 0; i < nums.length; i++) { // 先遍历物品
for (int j = size; j >= nums[i]; j--) { // 再遍历背包(容量)
// 【4】递推公式:
// dp[j]表示不装物品i,填满容量为j的背包,有dp[j]种方法
// dp[j-nums[i]]表示装物品i,要想填满容量为j的背包,还需要的容量为j-nums[i],所以只需要考虑填满容量为j-nums[i]的方法数即可
dp[j] = dp[j] + dp[j - nums[i]];
}
}
return dp[size];
}
public int findTargetSumWays1(int[] nums, int target) {
int sum = 0;
for (int i = 0; i < nums.length; i++){
sum += nums[i];
}
// 最终目标绝对值 大于 sum绝对值,表明无论如何也构造不出来表达式
if (Math.abs(target) > Math.abs(sum)) return 0;
int len = nums.length;
// 因为要包含负数所以要两倍,又要加上0这个中间的那个情况
int range = sum * 2 + 1;
// 这个数组是从总和为-sum开始的
int[][] dp = new int[len][range];
// 加上sum纯粹是因为下标界限问题,赋第二维的值的时候都要加上sum
// 初始化 第一个数只能分别组成+-nums[i]的一种情况
dp[0][sum + nums[0]] += 1;
dp[0][sum - nums[0]] += 1;
for (int i = 1; i < len; i++) {
for (int j = -sum; j <= sum; j++) {
if((j+nums[i]) > sum) {//+不成立 加上当前数大于了sum 只能减去当前的数
dp[i][j+sum] = dp[i-1][j-nums[i]+sum]+0;
}else if((j-nums[i]) < -sum) {//-不成立 减去当前数小于-sum 只能加上当前的数
dp[i][j+sum] = dp[i-1][j+nums[i]+sum]+0;
}else {//+-都可以
dp[i][j+sum] = dp[i-1][j+nums[i]+sum]+dp[i-1][j-nums[i]+sum];
}
}
}
return dp[len - 1][sum + target];
}
}
1 - 一和零(474)- 2
// https://leetcode.cn/problems/ones-and-zeroes/description/
// 给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
// 请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
// 如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集
// 输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
// 输出:4
// 解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
// 其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
//【1】dp[i][j]表示最多有i个0 和 j个1的strs的最大子集大小为dp[i][j]
int[][] dp = new int[m+1][n+1];
int zeroNum, oneNum;
//【2】先遍历物品(strs里的字符串)
for(String str : strs){
zeroNum = 0;
oneNum = 0;
//【3】统计每个字符串中0 和 1的个数
for(char ch : str.toCharArray()){
if (ch == '0') {
zeroNum++;
} else {
oneNum++;
}
}
//【4】遍历0号背包容量 且 从后向前遍历
for(int i = m; i >= zeroNum; i--){
//【5】遍历1号背包容量 且 从后向前遍历
for(int j = n; j >= oneNum; j--){
//【6】确定递推公式:dp[i][j]可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1
// 01背包的递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
// 字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])
// dp[i][j]表示不选当前字符串;dp[i - zeroNum][j - oneNum] + 1表示选当前字符,需要剔除当前字符串的0、1
dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
}
}
}
return dp[m][n];
}
}
1 - 最后一块石头的重量II(1049)- 2
// https://leetcode.cn/problems/last-stone-weight-ii/description/
// 有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
// 每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
// 如果 x == y,那么两块石头都会被完全粉碎;
// 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
// 最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0
// 输入:stones = [2,7,4,1,8,1]
// 输出:1
// 解释:
// 组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
// 组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
// 组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
// 组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值
class Solution {
// 思路:将stones分成两堆石头,然后两堆石头进行相撞
public int lastStoneWeightII(int[] stones) {
//【1】计算数组总和
int sum = 0;
for(int stone : stones){
sum += stone;
}
//【2】计算背包容量
int target = sum / 2;
//【3】dp[j]表示容量为j的背包,最多可以背最大重量为dp[j]
int[] dp = new int[target+1];
for(int i = 0; i < stones.length; i++){
for(int j = target; j >= stones[i]; j--){
//【4】dp[j]表示不装物品i,dp[j-stones[i]]+stones[i]表示装物品i
// 石头的重量是stones[i],石头的价值也是stones[i]
dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
//【5】dp[target]表示容量为target的背包所能背的最大重量
// 那么分成两堆石头,一堆石头的总重量是dp[target],另一堆就是sum - dp[target]
// 在计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的
// 那么相撞之后剩下的最小石头重量就是 (sum - dp[target]) - dp[target]
return sum - 2 * dp[target];
}
}
完全背包
0 - 零钱兑换(322)- 79
// https://leetcode.cn/problems/coin-change/description/
// 给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
// 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
// 你可以认为每种硬币的数量是无限的
// 输入:coins = [1, 2, 5], amount = 11
// 输出:3
// 解释:11 = 5 + 5 + 1
class Solution {
public int coinChange(int[] coins, int amount) {
//【1】凑足总额为j所需钱币的最少个数为dp[j]
int[] dp = new int[amount+1];
//【2】初始化dp数组。dp[j]必须初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖
for(int j = 1; j < dp.length; j++){
dp[j] = Integer.MAX_VALUE;
}
dp[0] = 0; // 当金额为0时需要的硬币数目为0
//【3】先遍历物品,再遍历背包容量
for(int i = 0; i < coins.length; i++){
// 正序遍历:完全背包每个硬币可以选择多次
for(int j = coins[i]; j <= amount; j++){
// 如果dp[j - coins[i]]是初始值则跳过
if(dp[j - coins[i]] != Integer.MAX_VALUE){
// 选择硬币数目最小的情况。dp[j]表示不装物品i,dp[j - coins[i]] + 1表示装物品i
// j - coins[i]表示装物品i需要的容量
dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
}
}
}
return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
}
}
0 - 零钱兑换II(518)- 34
// https://leetcode.cn/problems/coin-change-ii/description/
// 给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
// 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
// 假设每一种面额的硬币有无限个。
// 题目数据保证结果符合 32 位带符号整数
// 输入:amount = 5, coins = [1, 2, 5]
// 输出:4
// 解释:有四种方式可以凑成总金额:
// 5=5
// 5=2+2+1
// 5=2+1+1+1
// 5=1+1+1+1+1
class Solution {
public int change(int amount, int[] coins) {
//【1】dp[j]表示凑成总金额j的货币组合数为dp[j]
int[] dp = new int[amount+1];
// 初始化dp数组,表示金额为0时只有一种情况,也就是什么都不装
dp[0] = 1;
for(int i = 0; i < coins.length; i++){ // 先遍历物品
for(int j = coins[i]; j <= amount; j++){ // 再遍历背包容量
//【3】递推公式:
// dp[j]表示不装当前物品i,dp[j - coins[i]]表示装当前物品i,[j - coins[i]表示装填物品i需要的容量大小
dp[j] = dp[j] + dp[j - coins[i]];
}
}
//【4】凑成总金额amount的货币组合数为dp[amount]
return dp[amount];
}
}
0 - 组合总和IV(377)- 3
// https://leetcode.cn/problems/combination-sum-iv/description/
// 给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
// 题目数据保证答案符合 32 位整数范围
// 输入:nums = [1,2,3], target = 4
// 输出:7
// 解释:
// 所有可能的组合为:
// (1, 1, 1, 1)
// (1, 1, 2)
// (1, 2, 1)
// (1, 3)
// (2, 1, 1)
// (2, 2)
// (3, 1)
// 请注意,顺序不同的序列被视作不同的组合
class Solution {
public int combinationSum4(int[] nums, int target) {
//【1】dp[i]表示凑成目标正整数为i的排列个数为dp[i]
int[] dp = new int[target + 1];
//【2】dp[0]要初始化为1,这样递归其他dp[i]的时候才会有数值基础
dp[0] = 1;
// 如果求组合数就是外层for循环遍历物品,内层for循环遍历背包
// 如果求排列数就是外层for循环遍历背包,内层for循环遍历物品
//【3】先遍历背包容量,再遍历物品
for (int i = 0; i <= target; i++) {
for (int j = 0; j < nums.length; j++) {
//【4】只有当背包容量大于等于物品i的容量时,才需要考虑装 或 不装物品i
if (i >= nums[j]) {
//【5】dp[i]表示不装物品j后凑成目标i的排列个数,dp[i - nums[j]]表示装物品j后凑成目标i的排列个数
// i - nums[j]表示装填物品j需要的容量
dp[i] = dp[i] + dp[i - nums[j]];
}
}
}
//【6】凑成目标为target的排列个数
return dp[target];
}
List<List<Integer>> result = new ArrayList<List<Integer>>();
List<Integer> path = new ArrayList<Integer>();
int sum = 0;
// 解答错误
public int combinationSum41(int[] nums, int target) {
process(nums, target, 0);
return result.size();
}
public void process(int[] nums, int target, int start){
if(start == nums.length){
return;
}
if(sum == target){
result.add(new ArrayList<>(path));
}
for(int i = start; i < nums.length && sum + nums[i] <= target; i++){
sum = sum + nums[i];
path.add(nums[i]);
process(nums, target, start);
sum = sum - nums[i];
path.remove(path.size() - 1);
}
}
}
2 - 完全平方数(279)- 17
// https://leetcode.cn/problems/perfect-squares/description/
// 给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
// 完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。
// 例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是
// 输入:n = 12
// 输出:3
// 解释:12 = 4 + 4 + 4
class Solution {
public int numSquares(int n) {
//【1】dp[i]表示和为j的完全平方数的最少数量
int[] dp = new int[n + 1];
//【2】初始化
dp[0] = 0; // 当和为0时,组合的个数为0
for (int j = 1; j <= n; j++) {
dp[j] = Integer.MAX_VALUE;
}
//【3】先遍历物品,再遍历背包容量
for (int i = 1; i*i <= n; i++) {
for (int j = i*i; j <= n; j++) {
if (dp[j - i*i] != Integer.MAX_VALUE) {
//【4】dp[j]表示不装物品i,dp[j - i*i] + 1表示装物品i,j - i*i表示装填物品i需要的容量
dp[j] = Math.min(dp[j], dp[j - i*i] + 1);
}
}
}
//【5】dp[n]表示和为n的完全平方数的最少数量
return dp[n];
}
}
单词拆分(139)- 46
// https://leetcode.cn/problems/word-break/description/
// 输入: s = "applepenapple", wordDict = ["apple", "pen"]
// 输出: true
// 解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
// 注意,你可以重复使用字典中的单词。
class Solution {
// 时间复杂度为O(n^2)、空间复杂度为 O(n)
public boolean wordBreak(String s, List<String> wordDict) {
// Set<String> wordDictSet = new HashSet(wordDict);
int len = s.length();
// dp[i]:表示字符串的前i个字符(不包含i)是否可以由wordDict中的单词组成
boolean[] dp = new boolean[len+1];
// 确定递推公式:
// 若j < i,dp[j] = true,同时[j, i]区间的子串在wordDict数组中,则dp[i] = true
// dp[j] = false,则dp[i] = false
// 初始化dp数组
// dp[0] = true表示空串且合法
// 下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词
dp[0] = true;
for(int i = 1; i <= len; i++){
for(int j = 0; j < i; j++){
if(dp[j] && wordDict.contains(s.substring(j, i))){ // substring表示左闭右开
dp[i] = true;
break;
}
}
}
return dp[len];
}
// 参考解题:https://leetcode.cn/problems/word-break/solutions/1466726/by-lfool-jjq9/
private boolean result = false;
private Set<String> wordSet = new HashSet<>();
public boolean wordBreak1(String s, List<String> wordDict) {
for (String word : wordDict) {
wordSet.add(word);
}
backtrack(s, 0);
return result;
}
private void backtrack(String s, int start) {
// 终止条件
if (result) return;
if (start == s.length()) {
result = true;
return ;
}
for (int i = start; i < s.length(); i++) {
String subStr = s.substring(start, i + 1);
if (!wordSet.contains(subStr)) continue;
backtrack(s, i + 1);
}
}
}
打家劫舍
打家劫舍(198)- 49
// https://leetcode.cn/problems/house-robber/
// 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警
// 给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额
// 输入:[2,7,9,3,1]
// 输出:12
// 解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
// 偷窃到的最高金额 = 2 + 9 + 1 = 12
class Solution {
public int rob1(int[] nums) {
int len = nums.length;
// dp[i]表示第i个房屋可盗窃的最大值
int[] dp = new int[len+1];
dp[0] = 0;
// 第一个房屋可盗窃的最大值
dp[1] = nums[0];
for(int i = 2; i <= len; i++) {
// 当前位置i房屋可盗窃的最大值
// 要么是i-1房屋可盗窃的最大值,要么是i-2房屋可盗窃的最大值加上当前房屋的值,二者之间取最大值
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i-1]);
}
return dp[len];
}
public int rob(int[] nums) {
// pre[0]、pre[1]表示上一个房子不偷和偷的状态
int[] pre = new int[2];
// cur[0]、cur[1]表示当前房子不偷和偷的状态
int[] cur = new int[2];
// 初始化第一个房子的状态
pre[0] = 0;
pre[1] = nums[0];
for(int i = 1; i < nums.length; i++) {
// 当前房子不偷时获取到的金额:上一个房子偷 或者 上一个房子不偷 取最大值
cur[0] = Math.max(pre[0], pre[1]);
// 当前房子偷时获取到的金额:上一房子不偷 + 当前房子的现金
cur[1] = pre[0] + nums[i];
pre[0] = cur[0];
pre[1] = cur[1];
}
return Math.max(pre[0], pre[1]);
}
}
1 - 打家劫舍II(213)- 23
// https://leetcode.cn/problems/house-robber-ii/
// 你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警
// 给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额
// 输入:nums = [1,2,3,1]
// 输出:4
// 解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
// 偷窃到的最高金额 = 1 + 3 = 4
class Solution {
public int rob(int[] nums) {
int len = nums.length;
if(len == 1) return nums[0];
// dp不抢第一个房间、dp1不抢最后一个房间
int[] dp = new int[len+1];
int[] dp1 = new int[len+1];
// 第二个房间的最高金额:只能取第二个房间的现金,因为不能取第一个房间
dp[2] = nums[1];
dp1[1] = nums[0];
dp1[2] = Math.max(nums[0], nums[1]);
for(int i = 2; i <= len; i++){
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i-1]);
dp1[i] = Math.max(dp1[i-1], dp1[i-2] + nums[i-1]);
}
// dp1不能抢最后一个房间,所以它的最高金额即为前一个房间所获得的最高金额
return Math.max(dp[len], dp1[len - 1]);
}
}
0 - 打家劫舍III(337)- 12
// https://leetcode.cn/problems/house-robber-iii/
// 小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root
// 除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
// 给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额
// 输入: root = [3,2,3,null,3,null,1]
// 输出: 7
// 解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int rob(TreeNode root) {
// rootStatus表示root节点选中 或 未选中时最大值构成的二维数组
int[] rootStatus = postOrder(root);
return Math.max(rootStatus[0], rootStatus[1]);
}
public int[] postOrder(TreeNode node){
if(node == null){
// 当前节点选中 或 未选中时最大值均为0(因为它为null,且不存在左右子节点)
return new int[]{0, 0};
}
int[] left = postOrder(node.left);
int[] right = postOrder(node.right);
// 场景一:当前节点被选中时最大值为:当前节点值值 + 左子节点未被选中最大值 + 右子节点未被选中最大值
int selected = node.val + left[1] + right[1];
// 场景二:当前节点未被选中时最大值为:(左子节点选中 或 不选情况下最大值) + (右子节点选中 或 不选情况下最大值)
int notSelected = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
// 返回当前节点选中 或 未选时最大值构成的二维数组
return new int[]{selected, notSelected};
}
}
股票问题
买卖股票的最佳时机(只能买卖一次)(121)- 189
// https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/?favorite=2cktkvj
// 给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
// 你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
// 返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0
// 输入:[7,1,5,3,6,4]
// 输出:5
// 解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
// 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票
class Solution {
// 时间复杂度:O(n)
// 空间复杂度:O(1)
public int maxProfit2(int[] prices) {
int minprice = prices[0];
int maxprofit = 0;
for (int i = 0; i < prices.length; i++) {
if (prices[i] < minprice) {
minprice = prices[i];
} else if (prices[i] - minprice > maxprofit) {
maxprofit = prices[i] - minprice;
}
}
return maxprofit;
}
// 时间复杂度:O(n²)
// 空间复杂度:O(1)
// 超出时间限制
public int maxProfit1(int[] prices) {
int maxprofit = 0;
for (int i = 0; i < prices.length - 1; i++) {
for (int j = i+1; j < prices.length; j++) {
maxprofit = Math.max(maxprofit, prices[j] - prices[i]);
}
}
return maxprofit;
}
public int maxProfit(int[] prices) {
int n = prices.length;
if (n < 2) return 0;
// 第一步:确定dp数组以及下标的含义
// dp[i][0]表示第i天持有股票所得现金
// dp[i][1]表示第i天不持有股票所得现金
int[][] dp = new int[n][2];
// 第二步:确定递推公式
// 一、如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来:
// 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
// 第i天买入股票,所得现金就是买入今天的股票后所得现金即:-prices[i]
// 那么dp[i][0]应该选所得现金最大的,所以dp[i][0] = max(dp[i - 1][0], -prices[i]);
// 二、如果第i天不持有股票即dp[i][1], 也可以由两个状态推出来:
// 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
// 第i天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:prices[i] + dp[i - 1][0]
// 同样dp[i][1]取最大的,dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
// 第三步:初始化dp数组
// dp[0][0]表示第0天持有股票,此时的持有股票就一定是买入股票了,因为不可能有前一天推出来,所以dp[0][0] -= prices[0]
// dp[0][1]表示第0天不持有股票,不持有股票那么现金就是0,所以dp[0][1] = 0
dp[0][0] = -prices[0];
// 第四步:确定遍历顺序:dp[i]都是由dp[i-1]推导出来的,那么一定是从前向后遍历
for (int i = 1; i < n; i++) {
dp[i][0] = Math.max(dp[i-1][0], -prices[i]); // 因为只有一次交易机会,持股一定是-price[i]
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i]);
}
// 第五步:确定最终结果
return dp[n-1][1];
}
}
1 - 买卖股票的最佳时机II(可以买卖多次)(122)- 48
// https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/description/
class Solution {
public int maxProfit1(int[] prices) {
// 第一步:确定dp数组以及下标的含义
// dp[i][0]表示第i天未持有股票时的最大利润
// dp[i][1]表示第i天持有股票时的最大利润
int[][] dp = new int[prices.length][2];
// 第二步:确定递推公式
// 若第i天未持有股票dp[i][0]:max(第i-1天未持有股票dp[i-1][0],第i-1天持有股票且i天卖出股票dp[i-1][1]+prices[i])
// 若第i天持有股票dp[i][1]:max(第i-1天未持有股票且第i天买入股票dp[i-1][0]-prices[i],第i-1天持有股票dp[i-1][1])
// 第三步:初始化dp数组
dp[0][0] = 0;
dp[0][1] = -prices[0];
// 第四步:确认遍历顺序
for(int i = 1; i < prices.length; i++){
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
}
return dp[prices.length-1][0];
}
public int maxProfit(int[] prices) {
int result = 0;
for(int i = 1; i < prices.length; i++){
// 因为可以同一天可以买入和卖出股票,所以既可以prices[i+1] - prices[i],又可以prices[i] - prices[i-1]
result = result + Math.max(prices[i] - prices[i-1], 0);
}
return result;
}
}
买卖股票的最佳时机III(最多买卖两次)(123)- 32
// https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/description/
// 给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
// 设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
// 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)
// 输入:prices = [3,3,5,0,0,3,1,4]
// 输出:6
// 解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
// 随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3
// 输入:prices = [1,2,3,4,5]
// 输出:4
// 解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
// 注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
// 因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票
class Solution {
public int maxProfit(int[] prices) {
//【1】dp[i][j]表示第i天、状态j所能获取的最大利润
// 定义 5 种状态:0: 没有操作, 1: 第一次持有股票, 2: 第一次不持有股票, 3: 第二次持有股票, 4: 第二次不持有股票
int[][] dp = new int[prices.length][5];
//【2】初始化dp
dp[0][1] = -prices[0];
dp[0][2] = 0;
dp[0][3] = -prices[0]; // 初始化第二次买入的状态是确保 最后结果是最多两次买卖的最大利润
dp[0][4] = 0;
for (int i = 1; i < prices.length; i++) {
//【3】确定递推公式
//【3.1】第i天第一次持有股票:可能是前一天已经持有股票(dp[i-1][1]),或者第i天买入股票(-prices[i])
dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
//【3.2】第i天第一次不持有股票:可能是前一天已经不持有股票(dp[i-1][2]),或者第i天卖出股票(dp[i][1] + prices[i])
dp[i][2] = Math.max(dp[i-1][2], dp[i][1] + prices[i]);
//【3.3】第i天第二次持有股票:可能是前一天已经持有股票(dp[i-1][3]),或者第i天买入股票(dp[i][2] - prices[i])
dp[i][3] = Math.max(dp[i-1][3], dp[i][2] - prices[i]);
//【3.4】第i天第二次不持有股票:可能是前一天已经不持有股票(dp[i-1][4]),或者第i天买入股票(dp[i][3] + prices[i])
dp[i][4] = Math.max(dp[i-1][4], dp[i][3] + prices[i]);
}
//【4】dp[len-1][4]表示最后一天不持有股票所能获取的最大利润
return dp[prices.length - 1][4];
}
}
买卖股票的最佳时机IV(最多买卖K次)(188)- 12
// https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/description/
// 给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
// 设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
// 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)
// 输入:k = 2, prices = [2,4,1]
// 输出:2
// 解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2
// 输入:k = 2, prices = [3,2,6,5,0,3]
// 输出:7
// 解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
// 随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3
class Solution {
public int maxProfit(int k, int[] prices) {
if (prices.length == 0) return 0;
//【1】dp[i][j][m]表示第i天、j次交易,不持有股票所能获取的最大利润
// [天数][交易次数][状态]。状态:0表示不持有/卖出, 1表示持有/买入
int[][][] dp = new int[prices.length][k + 1][2];
//【2】dp数组初始化:初始化所有的交易次数是为确保 最后结果是最多k次买卖的最大利润
for (int j = 0; j <= k; j++) {
dp[0][j][1] = -prices[0];
}
for (int i = 1; i < prices.length; i++) {
for (int j = 1; j <= k; j++) {
//【3】确定递推公式:
//【3.1】第i天第j次不持有股票:第i-1天第j次不持有股票,或者第i-1天第j次持有股票、同时第i天卖出股票
dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1] + prices[i]);
//【3.2】第i天第j次持有股票:第i-1天第j次已经持有股票,或者第i-1天第j次不持有股票、同时第i天买入股票
dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i]);
}
}
//【5】dp[prices.length - 1][k][0]表示最后一天完成k笔交易后不持有股票所能获取的最大利润
return dp[prices.length - 1][k][0];
}
}
最佳买卖股票时机含冷冻期(买卖多次,卖出有一天冷冻期)(309)- 2
// https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/
// 给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。
// 设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
// 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
// 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)
// 输入: prices = [1,2,3,0,2]
// 输出: 3
// 解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
class Solution {
public int maxProfit(int[] prices) {
int length = prices.length;
if (prices.length == 0) return 0;
//【1】dp[i][j]表示第i天、状态为j所能获得的最大利润
// 四种状态:0 - 持有股票状态、1 - 保持卖出股票的状态(前一天就是卖出股票状态,一直没操作)
// 2 - 今天卖出股票、3 - 今天为冷冻期状态
int[][] dp = new int[length+1][4];
//【2】初始化dp数组
dp[0][0] = -prices[0]; // 第一天买入股票
dp[0][1] = 0; // 第一天的前一天已经卖出股票
dp[0][2] = 0; // 第一天卖出股票(没持有过、卖出利润也应该是0)
dp[0][3] = 0; // 第一天为冷冻期
for (int i = 1; i < length; i++) {
//【3】确定递推公式
//【3.1】第i天持有股票:第i-1天已经持有股票,或者第i-1天的前一天已经卖出股票、第i天买入股票,或者第i-1天为冷冻期、第i天买入股票
dp[i][0] = Math.max(Math.max(dp[i-1][0], dp[i-1][1] - prices[i]), dp[i-1][3] - prices[i]);
//【3.2】第i天保持卖出股票的状态:第i-1天已经保持卖出股票的状态,或者第i-1天为冷冻期
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][3]);
//【3.3】第i天卖出股票:第i-1天持有股票(并非要求第i-1天买入股票)、同时在第i天卖出股票
dp[i][2] = dp[i-1][0] + prices[i];
//【3.4】第i天为冷冻期:第i-1天卖出股票
dp[i][3] = dp[i-1][2];
}
return Math.max(Math.max(dp[length-1][1], dp[length-1][2]), dp[length-1][3]);
}
}
买卖股票的最佳时机含手续费(买卖多次,每次有手续费)(714)- 3
// https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/
// 给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。
// 你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
// 返回获得利润的最大值。
// 注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
// 输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
// 输出:8
// 解释:能够达到的最大利润:
// 在此处买入 prices[0] = 1
// 在此处卖出 prices[3] = 8
// 在此处买入 prices[4] = 4
// 在此处卖出 prices[5] = 9
// 总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8
class Solution {
// 卖出时支付手续费
public int maxProfit(int[] prices, int fee) {
int length = prices.length;
//【1】dp[i][j]表示第i天 持股或不持股 所获得利润的最大值
// 0 - 持股(买入)、1 - 不持股(售出)
int[][] dp = new int[length][2];
//【2】初始化dp数组
dp[0][0] = -prices[0];
for (int i = 1; i < length; i++) {
//【3】确定递推公式
//【3.1】第i天持股:第i-1天已经持股,或者第i-1天不持股、同时第i天买入股票
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] - prices[i]);
//【3.2】第i天不持股:第i-1天已经不持股,或者第i-1天持股、同时第i天卖出股票、需要支付手续费
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i] - fee);
}
return Math.max(dp[length - 1][0], dp[length - 1][1]);
}
// 买入时支付手续费
public int maxProfit1(int[] prices, int fee) {
int len = prices.length;
// 0 : 持股(买入)
// 1 : 不持股(售出)
// dp 定义第i天持股/不持股 所得最多现金
int[][] dp = new int[len][2];
// 考虑买入的时候就支付手续费
dp[0][0] = -prices[0] - fee;
for (int i = 1; i < len; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i] - fee);
dp[i][1] = Math.max(dp[i - 1][0] + prices[i], dp[i - 1][1]);
}
return Math.max(dp[len - 1][0], dp[len - 1][1]);
}
}
子序列问题
子序列(不连续)
最长递增子序列(300)- 136
// https://leetcode.cn/problems/longest-increasing-subsequence/description/
// 给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
// 子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列
// 输入:nums = [10,9,2,5,3,7,101,18]
// 输出:4
// 解释:最长递增子序列是[2,3,7,101],因此长度为4
class Solution {
public int lengthOfLIS(int[] nums) {
// 第一步:状态定义:dp[i]代表nums以nums[i]结尾的最长子序列长度,nums[i]必须被选取
int[] dp = new int[nums.length];
// 第二步:初始化:dp[i]所有元素置为1,含义是每个元素都至少可以单独成为子序列,此时长度都为1
Arrays.fill(dp, 1);
int maxans = 1;
// 第四步:确定遍历顺序:dp[i]是有0到i-1各个位置的最长升序子序列 推导而来,那么遍历i一定是从前向后遍历
// j其实就是0到i-1,遍历i的循环里外层,遍历j则在内层
for (int i = 1; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
// 第三步:状态转移方程:位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列+1的最大值
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1); // 每轮内存循环中的dp[i]都会被更新
}
}
// 第五步:返回值:返回dp列表最大值,即可得到全局最长上升子序列长度
maxans = Math.max(maxans, dp[i]);
}
return maxans;
}
}
// 时间复杂度O(N2):遍历计算dp列表需O(N),计算每个dp[i],需O(N)
// 空间复杂度O(N):dp列表占用线性大小额外空间
最长递增子序列的个数(673)- 19
// https://leetcode.cn/problems/number-of-longest-increasing-subsequence/description/
// 给定一个未排序的整数数组 nums , 返回最长递增子序列的个数 。
// 注意 这个数列必须是 严格 递增的。
// 输入: [1,3,5,4,7]
// 输出: 2
// 解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]
// 输入: [2,2,2,2,2]
// 输出: 5
// 解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5
class Solution {
// 参考解答:https://leetcode.cn/problems/number-of-longest-increasing-subsequence/solutions/1007341/gong-shui-san-xie-lis-de-fang-an-shu-wen-obuz/
public int findNumberOfLIS(int[] nums) {
//【1】dp[i]表示索引i之前(包括i)最长递增子序列的长度
int[] dp = new int[nums.length];
// count[i]表示以nums[i]为结尾的字符串,最长递增子序列的个数
int[] count = new int[nums.length];
// maxCount记录全局最长上升子序列的最大长度
int maxCount = 0;
for (int i = 0; i < nums.length; i++) {
//【2】初始化
dp[i] = 1;
count[i] = 1;
for (int j = 0; j < i; j++) {
//【3】遍历区间[0, i)的所有数nums[j],若满足nums[j] < nums[i],说明nums[i]可以接在nums[j]后面形成递增子序列
if (nums[i] > nums[j]) {
//【???】说明在[0, i]中找到了两个相同长度的递增子序列
if (dp[j] + 1 == dp[i]) {
// 说明找到了一个新的符合条件的前驱,此时将值继续累加到方案数当中
count[i] = count[i] + count[j];
//【4】在[0, i)范围内,找到一个比dp[i]更长的递增子序列
} else if (dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1; // 更新以i结尾子串的最长递增子序列的长度
count[i] = count[j]; // 更新以i结尾子串的最长递增子序列的个数
}
}
}
maxCount = Math.max(maxCount, dp[i]);
}
int result = 0;
for (int i = 0; i < nums.length; i++) {
// 根据最长递增子序列长度,统计其个事
if (maxCount == dp[i]) {
result += count[i];
}
}
return result;
}
}
1 - 最长公共子序列(1143)- 96
// https://leetcode.cn/problems/longest-common-subsequence/
// 给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
// 一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
// 例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
// 两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列
// 输入:text1 = "abcde", text2 = "ace"
// 输出:3
// 解释:最长公共子序列是 "ace" ,它的长度为 3
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length();
int n = text2.length();
// 第一步:状态定义dp[i][j]:长度为[0, i-1]的字符串text1 与 长度为[0, j-1]的字符串text2的最长公共子序列
int[][] dp = new int[m+1][n+1];
// 第二步:初始化:text1[0, i-1]和空串的最长公共子序列自然是0,所以dp[i][0] = 0。同理dp[0][j]也是0
// 第四步:确定遍历顺序
for(int i = 1; i <= m; i++){
char char1 = text1.charAt(i-1);
for(int j = 1; j <= n; j++){
char char2 = text2.charAt(j-1);
// 第三步:状态转移方程
if(char1 == char2){
// 如果text1[i-1] 与 text2[j-1]相同,那么找到了一个公共元素,所以dp[i][j] = dp[i-1][j-1] + 1
dp[i][j] = dp[i-1][j-1] + 1;
}else {
// 如果text1[i-1] 与 text2[j-1]不相同,那就看看text1[0, i-2] 与 text2[0, j-1]的最长公共子序列 和 text1[0, i-1]与text2[0, j-2]的最长公共子序列,取最大的
dp[i][j] =Math.max(dp[i-1][j], dp[i][j-1]);
}
}
}
// 第五步:确定最大值
return dp[m][n];
}
}
22222 - 不相交的线(1035)
子序列(连续)
最长连续递增序列(674)- 11
// https://leetcode.cn/problems/longest-continuous-increasing-subsequence/description/
// 给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
// 连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列
// 输入:nums = [1,3,5,4,7]
// 输出:3
// 解释:最长连续递增序列是 [1,3,5], 长度为3。
// 尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开
class Solution {
public int findLengthOfLCIS(int[] nums) {
int len = nums.length;
int maxlength = 1;
// 第一步:定义状态:dp[i]表示第i个元素时的连续递增子序列长度
int[] dp = new int[len];
// 第二步:初始化
Arrays.fill(dp, 1);
// 第四步:确定遍历顺序
for(int i = 1; i < len; i++){
// 第三步:状态流转
// nums[i] 大于 nums[i-1],则dp[i] = dp[i-1] + 1
// nums[i] 不大于 nums[i-1],则dp[i] = 1
if(nums[i] > nums[i-1]){
dp[i] = dp[i-1] + 1;
}
maxlength = Math.max(maxlength, dp[i]);
}
// 第五步:确定最大值
return maxlength;
}
public int findLengthOfLCIS1(int[] nums) {
int maxlength = 1;
int premax = 1;
for(int i = 0; i < nums.length - 1; i++){
if(nums[i+1] > nums[i]) premax++;
else premax = 1;
maxlength = Math.max(maxlength, premax);
}
return maxlength;
}
}
最长重复子数组(718)- 58
// https://leetcode.cn/problems/maximum-length-of-repeated-subarray/description/
// 给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度
// 输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
// 输出:3
// 解释:长度最长的公共子数组是 [3,2,1]
class Solution {
public int findLength(int[] A, int[] B) {
int m = A.length;
int n = B.length;
int maxLength = 0;
// 第一步:定义状态:dp[i][j]表示A[i-1],B[j-1]重复子数组
int[][] dp = new int[m+1][n+1];
// 第二步:初始化
// Arrays.fill(dp, 0);
// 第四步:确认遍历顺序
for(int i = 1; i <= m; i++){
for(int j = 1; j <= n; j++){
// 第三步:状态转移
// A[i-1] = B[j-1],dp[i][j] = dp[i-1][j-1] + 1
// A[i-1] != B[j-1],dp[i][j] = 0
if(A[i-1] == B[j-1]){
dp[i][j] = dp[i-1][j-1] + 1;
}
maxLength = Math.max(maxLength, dp[i][j]);
}
}
// 第五步:确认最大值
return maxLength;
}
public int findLength1(int[] A, int[] B) {
int m = A.length;
int n = B.length;
int[] dp = new int[n+1];
int ans = 0;
for(int i = m-1; i >= 0; i--){
int p = 0;
int q = 0;
for(int j = n-1; j >= 0; j--){
p = q;
q = dp[j];
dp[j] = (A[i] == B[j]) ? p + 1 : 0;
ans = Math.max(ans,dp[j]);
}
}
return ans;
}
}
最大子序和(53)- 231
// https://leetcode.cn/problems/maximum-subarray/
// 给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
// 子数组 是数组中的一个连续部分
// 输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
// 输出:6
// 解释:连续子数组 [4,-1,2,1] 的和最大,为 6
class Solution {
public int maxSubArray(int[] nums) {
int len = nums.length;
// 第一步:定义状态:dp[i]表示第i个元素为尾结点构成的子数组的和
int[] dp = new int[len];
// 第二步:初始化
dp[0] = nums[0];
int maxSum = dp[0];
// 第四步:确定遍历顺序
for(int i = 1; i < len; i++){
// 第三步:状态转移
// dp[i-1] > 0,则dp[i] = dp[i-1] + nums[i]。此处不能判断dp[i-1] + nums[i] > 0,需考虑dp[i-1]为负数场景
// dp[i-1] <= 0,则dp[i] = nums[i];
if(dp[i-1] > 0){
dp[i] = dp[i-1] + nums[i];
}else {
dp[i] = nums[i];
}
maxSum = Math.max(maxSum, dp[i]);
}
// 第五步:确定最大值
return maxSum;
}
public int maxSubArray1(int[] nums) {
int ans = nums[0]; // 最终求出的连续子数组的最大和
int sum = 0; // 当前元素前面的连续子数组的最大和
for(int num: nums) {
if(sum > 0) {
sum += num; // 说明sum对结果有增益效果,则sum保留并加上当前遍历数字
} else {
sum = num; // 说明sum对结果无增益效果,需要舍弃,则sum直接更新为当前遍历数字
}
ans = Math.max(ans, sum);
}
return ans;
}
}
编辑距离
判断子序列(392)- 6
// https://leetcode.cn/problems/is-subsequence/description/
// 给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
// 字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)
// 输入:s = "abc", t = "ahbgdc"
// 输出:true
class Solution {
public boolean isSubsequence2(String s, String t) {
int m = s.length();
int n = t.length();
int left = 0; // 每次确认一个s中的元素在t中时,将内层循环初始指针往后移
int num = 0; // 收集遍历过程中s在t中相同元素的个数
for(int i = 0; i < m; i++){
char char1 = s.charAt(i);
for(int j = left; j < n; j++){
char char2 = t.charAt(j);
if(char1 == char2) {
left = j+1;
num++;
break;
}
}
}
// 判断最终收集到的相同元素个数 是否与 字符串s的长度相等
return num == m;
}
// 双指针解法
public boolean isSubsequence1(String s, String t) {
int n = s.length(), m = t.length();
// i指向字符串s,j指向字符串t
int i = 0, j = 0;
while (i < n && j < m) {
if (s.charAt(i) == t.charAt(j)) {
// 后移字符串s的指针
i++;
}
// 后移字符串t的指针
j++;
}
return i == n;
}
// 动态规划解法
public boolean isSubsequence(String s, String t) {
int m = s.length();
int n = t.length();
// 第一步:定义状态:dp[i][j]表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]
// 此处的dp[0][0]无实际意义,默认为0
int[][] dp = new int[m+1][n+1];
// 第二步:确定递推公式
// s[i-1] == t[j-1],表明t中找到了一个字符在s中也出现了。dp[i][j] = dp[i-1][j-1] + 1
// s[i-1] != t[j-1],此时相当于t要删除元素,t如果把当前元素t[j-1]删除
// 那么dp[i][j]的数值就是看s[i-1]与t[j-2]的比较结果了,即:dp[i][j] = dp[i][j-1];
// 第三步:初始化
// 第四步:确认遍历顺序
for(int i = 1; i <= m; i++){
for(int j = 1; j <= n; j++){
if(s.charAt(i-1) == t.charAt(j-1)){
dp[i][j] = dp[i-1][j-1] + 1;
}else{
dp[i][j] = dp[i][j-1];
}
}
}
// 第五步:确认结果
return dp[m][n] == m;
}
}
0 - 不同的子序列(115)- 15
// https://leetcode.cn/problems/distinct-subsequences/description/
class Solution {
public int numDistinct1(String s, String t) {
int m = s.length();
int n = t.length();
if (m < n) {
return 0;
}
// 第一步:定义状态:dp[i][j]表示在s[i:]的子序列中t[j:]出现的个数
int[][] dp = new int[m+1][n+1];
// 第二步:确认递推公式
// 场景一:当j = n时,t[j:]为空字符串,由于空字符串是任何字符串的子序列,因此对任意0 <= i <= m,有dp[i][n]==1
// 场景二:当i = m 且 j < n时,s[i:]为空字符串,t[j:]为非空字符串,由于非空字符串不是空字符串的子序列,因此对任意0 <= j <= n,有dp[m][j]=0
// 场景三:当i < m 且 j < n时:考虑dp[i][j的计算:
// 1、当s[i] = t[j]时:dp[i][j] = dp[i+1][j+1] + dp[i+1][j]
// 1.1 如果s[i]和t[j]匹配,则考虑t[j+1:]作为s[i+1:]的子序列,子序列数为dp[i+1][j+1]
// 1.2 如果s[i]和t[j]不匹配,则考虑t[j:]作为s[i+1:]的子序列,子序列数为dp[i+1][j]
// 2、当s[i] ≠ t[j]时,s[i]和t[j]不匹配,因此只考虑t[j:]作为s[i+1:]的子序列,子序列数为dp[i+1][j]
// 因此当s[i] ≠ t[j],有dp[i][j] = dp[i+1][j]
// 第三步:初始化
for (int i = 0; i <= m; i++) {
dp[i][n] = 1;
}
// 第四步:确认遍历顺序
for (int i = m - 1; i >= 0; i--) {
char sChar = s.charAt(i);
for (int j = n - 1; j >= 0; j--) {
char tChar = t.charAt(j);
if (sChar == tChar) {
dp[i][j] = dp[i + 1][j + 1] + dp[i + 1][j];
} else {
dp[i][j] = dp[i + 1][j];
}
}
}
// 第五步:确认结果
return dp[0][0];
}
public int numDistinct(String s, String t) {
int m = s.length();
int n = t.length();
if (m < n) {
return 0;
}
// 以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]
int[][] dp = new int[ m+1][n+1];
for (int i = 0; i < s.length() + 1; i++) {
dp[i][0] = 1;
}
for (int i = 1; i < s.length() + 1; i++) {
for (int j = 1; j < t.length() + 1; j++) {
// 当s[i-1] 与 t[j-1]相等时,dp[i][j]可以有两部分组成:一部分是用s[i-1]来匹配,那么个数为dp[i-1][j-1];一部分是不用s[i-1]来匹配,个数为dp[i-1][j]
if (s.charAt(i - 1) == t.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
}else{
// dp[i][j]只有一部分组成,不用s[i-1]来匹配,即:dp[i-1][j]
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[s.length()][t.length()];
}
}
// 时间复杂度:O(mn)
// 空间复杂度:O(mn)
0 - 两个字符串的删除操作(583)- 2
// https://leetcode.cn/problems/delete-operation-for-two-strings/description/
// 给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
// 每步 可以删除任意一个字符串中的一个字符
// 输入: word1 = "sea", word2 = "eat"
// 输出: 2
// 解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
// 第一步:确定dp数组以及下标的含义
// dp[i][j]:以i-1结尾的字符串word1,和以j-1结尾的字符串word2,想要达到相等,所需要删除元素的最少次数
int[][] dp = new int[m+1][n+1];
// 第二步:确定递推公式:
// 当word1[i-1] = word2[j-1]时:dp[i][j] = dp[i-1][j-1]
// 当word1[i-1] != word2[j-1]时:dp[i][j] = min({dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + 2})
// 情况一:删word1[i-1],最少操作次数为dp[i-1][j] + 1(在i-2结尾的字符串上,删除下一个元素word1[i-1])
// 情况二:删word2[j - 1],最少操作次数为dp[i][j-1] + 1
// 情况三:同时删word1[i-1]和word2[j-1],操作的最少次数为dp[i-1][j-1] + 2
// 第三步:初始化dp数组
// dp[i][0]:word2为空字符串,以i-1为结尾的字符串word2要删除多少个元素,才能和word1相同呢,很明显dp[i][0] = i
// dp[0][j]:同理,dp[0][j] = j
for (int i = 0; i <= m; i++) dp[i][0] = i;
for (int j = 0; j <= n; j++) dp[0][j] = j;
// 第四步:确定遍历顺序:dp[i][j]都是根据左上方、正上方、正左方推出来的,遍历的时候一定是从上到下,从左到右
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i-1][j-1];
}else{
dp[i][j] = Math.min(dp[i-1][j-1] + 2, Math.min(dp[i-1][j] + 1, dp[i][j-1] + 1));
}
}
}
// 第五步:确认结果
return dp[m][n];
}
}
// 时间复杂度:O(mn)
// 空间复杂度:O(mn)
0 - 编辑距离(72)- 114
https://leetcode.cn/problems/edit-distance/
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
// 第一步:确定dp数组以及下标的含义
// dp[i][j]表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]
int[][] dp = new int[m+1][n+1];
// 第二步:确定递推公式:
// 当word1[i-1] = word2[j-1]时:说明不用任何编辑,dp[i][j] = dp[i-1][j-1]
// 当word1[i-1] != word2[j-1]时:dp[i][j] = min({dp[i-1][j-1], dp[i-1][j], dp[i][j-1]}) + 1
// 操作一:word1删除一个元素,那么就是以下标i-2为结尾的word1 与 j-1为结尾的word2的最近编辑距离 再加上一个操作。即dp[i][j] = dp[i-1][j] + 1;
// 操作二:word2删除一个元素,那么就是以下标i-1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 再加上一个操作。即dp[i][j] = dp[i][j-1] + 1;
// 操作三:替换元素,word1替换word1[i-1],使其与word2[j-1]相同,此时不用增加元素,那么以下标i-2为结尾的word1 与 j-2为结尾的word2的最近编辑距离 加上一个替换元素的操作。即 dp[i][j] = dp[i-1][j-1] + 1;
// 第三步:初始化dp数组
// dp[i][0]:以下标i-1为结尾的字符串word1,和空字符串word2,最近编辑距离为dp[i][0]。dp[i][0]就应该是i,对word1里的元素全部做删除操作,即:dp[i][0] = i
// 同理dp[0][j] = j
for (int i = 1; i <= m; i++) dp[i][0] = i;
for (int j = 1; j <= n; j++) dp[0][j] = j;
// 第四步:确定遍历顺序:dp[i][j]是依赖左方,上方和左上方元素,dp矩阵中一定是从左到右从上到下去遍历
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// 因为dp数组有效位从1开始,所以当前遍历到的字符串的位置为i-1 | j-1
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i-1][j-1];
} else {
dp[i][j] = Math.min(Math.min(dp[i-1][j-1], dp[i][j-1]), dp[i-1][j]) + 1;
}
}
}
// 第五步:举例推导dp数组,确认结果
return dp[m][n];
}
}
回文
1 - 回文子串(647)- 184
// https://leetcode.cn/problems/longest-palindromic-substring/description/
class Solution {
// 暴力解法:时间复杂度:两层for循环O(n²),for循环里边判断是否为回文O(n),所以时间复杂度为O(n³)
// 空间复杂度:O(1),常数个变量
public String longestPalindrome(String s) {
String ans = "";
int max = 0;
int len = s.length();
for (int i = 0; i < len; i++)
for (int j = i + 1; j <= len; j++) {
String test = s.substring(i, j);
if (isPalindromic(test) && test.length() > max) {
ans = s.substring(i, j);
max = Math.max(max, ans.length());
}
}
return ans;
}
public boolean isPalindromic(String s) {
int len = s.length();
for (int i = 0; i < len / 2; i++) {
if (s.charAt(i) != s.charAt(len - i - 1)) {
return false;
}
}
return true;
}
}
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() < 1){
return "";
}
// 初始化最大回文子串的起点和终点
int start = 0;
int end = 0;
// 遍历每个位置,当做中心位
for (int i = 0; i < s.length(); i++) {
// 分别拿到奇数偶数的回文子串长度
int len_odd = expandCenter(s,i,i);
int len_even = expandCenter(s,i,i + 1);
// 对比最大的长度
int len = Math.max(len_odd,len_even);
// 计算对应最大回文子串的起点和终点
if (len > end - start){
start = i - (len - 1)/2;
end = i + len/2;
}
}
// 注意:这里的end+1是因为 java自带的左闭右开的原因
return s.substring(start,end + 1);
}
/**
* @param s 输入的字符串
* @param left 起始的左边界
* @param right 起始的右边界
* @return 回文串的长度
*/
private int expandCenter(String s,int left,int right){
// left = right 的时候,此时回文中心是一个字符,回文串的长度是奇数
// right = left + 1 的时候,此时回文中心是一个空隙,回文串的长度是偶数
// 跳出循环的时候恰好满足 s.charAt(left) != s.charAt(right)
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)){
left--;
right++;
}
// 回文串的长度是right-left+1-2 = right - left - 1
return right - left - 1;
}
}
// class Solution{
// public String longestPalindrome(String s) {
// String res = s.substring(0, 1);
// for(int i = 0; i < s.length(); i++){
// for(int j = i+1; j < s.length(); j++){
// if(j-i+1 > res.length() && isPalindrome(s, i, j)){
// res = s.substring(i, j-i+1);
// }
// }
// }
// return res;
// }
// boolean isPalindrome(String s, int left, int right){
// while(left <= right){
// if(s.charAt(left) != s.charAt(right)){
// return false;
// }
// left++;
// right--;
// }
// return true;
// }
// }
0 - 最长回文子序列(516)- 15
// https://leetcode.cn/problems/longest-palindromic-subsequence/
class Solution {
public int longestPalindromeSubseq(String s) {
int len = s.length();
// 第一步:确定dp数组以及下标的含义
// dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]
int[][] dp = new int[len+1][len+1];
// 第二步:确定递推公式
// 当s[i] = s[j]时:dp[i][j] = dp[i+1][j-1] + 2
// 当s[i] != s[j]时:dp[i][j] = max(dp[i+1][j], dp[i][j-1])
// 加入s[j]的回文子序列长度为dp[i+1][j],此时要删除s[i]
// 加入s[i]的回文子序列长度为dp[i][j-1],此时要删除s[j]
// 第三步:初始化dp数组
// 当i与j相同,那么dp[i][j]一定是等于1的,即:一个字符的回文子序列长度就是1
// 第四步:确定遍历顺序:遍历i的时候一定要从下到上遍历,这样才能保证,下一行的数据是经过计算的
for (int i = len-1; i >= 0; i--) { // 从后往前遍历 保证情况不漏
dp[i][i] = 1; // 初始化
for (int j = i+1; j < len; j++) {
if (s.charAt(i) == s.charAt(j)) {
dp[i][j] = dp[i+1][j-1] + 2;
} else {
dp[i][j] = Math.max(dp[i+1][j], Math.max(dp[i][j], dp[i][j-1]));
}
}
}
// 第五步:举例推导dp数组,确定最终结果
return dp[0][len-1];
}
}
二叉树
二叉树理论基础
二叉树的种类
满二叉树
- 如果该二叉树的所有叶子节点都在最后一层,并且结点总数= 2^n -1,n为层数,则我们称为满二叉树
完全二叉树
- 如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树
二叉搜索(排序)树(BST)
- 对于二叉搜索树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大
平衡二叉搜索树(AVL)
- 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等
顺序存储二叉树
- 从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组,看下面的示意图
二叉树的存储方式
二叉树可以链式存储(使用指针),也可以顺序存储(使用数组)
顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在散落在各个地址的节点串联一起
链式存储:
顺序存储:
如果父节点的数组下标是 i,那么它的左孩子就是 i 2 + 1,右孩子就是 i 2 + 2
二叉树的遍历方式
- 广度优先遍历:一层一层的去遍历(可以借助队列先进先出的特点来实现)
- 层次遍历(迭代法)
- 深度优先遍历:先往深走,遇到叶子节点再往回走(可以借助栈使用非递归的方式来实现)
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
二叉树的递归遍历
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因
递归三要素:
- 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
- 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
确定单层递归的逻辑:确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程 ```java // 前序遍历·递归·LC144_二叉树的前序遍历 class Solution { public List
preorderTraversal(TreeNode root) { List<Integer> result = new ArrayList<Integer>();
preorder(root, result);
return result;
}
public void preorder(TreeNode root, List
result) { if (root == null) {
return;
}
result.add(root.val);
preorder(root.left, result);
preorder(root.right, result);
} }
// 中序遍历·递归·LC94_二叉树的中序遍历
class Solution {
public List
void inorder(TreeNode root, List<Integer> list) {
if (root == null) {
return;
}
inorder(root.left, list);
list.add(root.val); // 注意这一句
inorder(root.right, list);
}
}
// 后序遍历·递归·LC145_二叉树的后序遍历
class Solution {
public List
void postorder(TreeNode root, List<Integer> list) {
if (root == null) {
return;
}
postorder(root.left, list);
postorder(root.right, list);
list.add(root.val); // 注意这一句
}
}
<a name="e1ehB"></a>
### 二叉树的递归遍历
**前序遍历(迭代法):**前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子<br />![](https://cdn.nlark.com/yuque/0/2022/gif/22523384/1669640774407-5442a47d-de8f-46f3-ac3d-b5e22a28f612.gif#averageHue=%23000000&clientId=u79b236b7-c316-4&from=paste&height=295&id=ub928e7fd&originHeight=472&originWidth=530&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=u00ced94b-e277-4813-bfea-be04bffaf5a&title=&width=331)<br />**中序遍历(迭代法):**先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的<br />**后序遍历(迭代法):**<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22523384/1669640877574-53f6b4ee-f6e1-4402-9f8f-eaa3b8e56b98.png#averageHue=%23f6f5f5&clientId=u79b236b7-c316-4&from=paste&height=145&id=ue470a588&originHeight=262&originWidth=1128&originalType=url&ratio=1&rotation=0&showTitle=false&size=30917&status=done&style=none&taskId=u39dfce68-343e-4363-a2fa-5139a8a4d8a&title=&width=625)
```java
// 前序遍历顺序:中-左-右,入栈顺序:中-右-左
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null){
return result;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()){
TreeNode node = stack.pop();
result.add(node.val);
if (node.right != null){
stack.push(node.right);
}
if (node.left != null){
stack.push(node.left);
}
}
return result;
}
}
// 中序遍历顺序: 左-中-右 入栈顺序: 左-右
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null){
return result;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()){
if (cur != null){
stack.push(cur);
cur = cur.left;
}else{
cur = stack.pop();
result.add(cur.val);
cur = cur.right;
}
}
return result;
}
}
// 后序遍历顺序 左-右-中 入栈顺序:中-左-右 出栈顺序:中-右-左, 最后翻转结果
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null){
return result;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()){
TreeNode node = stack.pop();
result.add(node.val);
if (node.left != null){
stack.push(node.left);
}
if (node.right != null){
stack.push(node.right);
}
}
Collections.reverse(result);
return result;
}
}
二叉树的统一迭代法
可以看出我们将访问的节点直接加入到栈中,但如果是处理的节点则后面放入一个空节点, 这样只有空节点弹出的时候,才将下一个节点放进结果集
// 迭代法前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new LinkedList<>();
Stack<TreeNode> st = new Stack<>();
if (root != null) st.push(root);
while (!st.empty()) {
TreeNode node = st.peek();
if (node != null) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈)
if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈)
st.push(node); // 添加中节点
st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.peek(); // 重新取出栈中元素
st.pop();
result.add(node.val); // 加入到结果集
}
}
return result;
}
}
// 迭代法中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new LinkedList<>();
Stack<TreeNode> st = new Stack<>();
if (root != null) st.push(root);
while (!st.empty()) {
TreeNode node = st.peek();
if (node != null) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈)
st.push(node); // 添加中节点
st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈)
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.peek(); // 重新取出栈中元素
st.pop();
result.add(node.val); // 加入到结果集
}
}
return result;
}
}
// 迭代法后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new LinkedList<>();
Stack<TreeNode> st = new Stack<>();
if (root != null) st.push(root);
while (!st.empty()) {
TreeNode node = st.peek();
if (node != null) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
st.push(node); // 添加中节点
st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈)
if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈)
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.peek(); // 重新取出栈中元素
st.pop();
result.add(node.val); // 加入到结果集
}
}
return result;
}
}
二叉树的层序遍历
- 层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树
- 需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而是用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。
- 而这种层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上
// 102.二叉树的层序遍历
class Solution {
public List<List<Integer>> resList = new ArrayList<List<Integer>>();
public List<List<Integer>> levelOrder(TreeNode root) {
//checkFun01(root,0);
checkFun02(root);
return resList;
}
// DFS--递归方式
public void checkFun01(TreeNode node, Integer deep) {
if (node == null) return;
deep++;
if (resList.size() < deep) {
// 当层级增加时,list的Item也增加,利用list的索引值进行层级界定
List<Integer> item = new ArrayList<Integer>();
resList.add(item);
}
resList.get(deep - 1).add(node.val);
checkFun01(node.left, deep);
checkFun01(node.right, deep);
}
// BFS--迭代方式--借助队列
public void checkFun02(TreeNode node) {
if (node == null) return;
Queue<TreeNode> que = new LinkedList<TreeNode>();
que.offer(node);
while (!que.isEmpty()) {
List<Integer> itemList = new ArrayList<Integer>();
int len = que.size();
while (len > 0) {
TreeNode tmpNode = que.poll();
itemList.add(tmpNode.val);
if (tmpNode.left != null) que.offer(tmpNode.left);
if (tmpNode.right != null) que.offer(tmpNode.right);
len--;
}
resList.add(itemList);
}
}
}
二叉树的属性
对称二叉树(101)- 70
// https://leetcode.cn/problems/symmetric-tree/
// 给你一个二叉树的根节点 root , 检查它是否轴对称
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null){
return true;
}
return dfs(root.left, root.right);
}
boolean dfs(TreeNode left, TreeNode right){
if(left == null && right == null){
return true;
}
if(left == null || right == null){
return false;
}
if(left.val != right.val){
return false;
}
return dfs(left.left, right.right) && dfs(left.right, right.left);
}
}
二叉树的最大深度(104)- 72
// https://leetcode.cn/problems/maximum-depth-of-binary-tree/
class Solution {
public int maxDepth(TreeNode root) {
if(root == null){
return 0;
}
int left = maxDepth(root.left);
int right = maxDepth(root.right);
return Math.max(left, right) + 1;
}
}
二叉树的最小深度(111)- 17
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
// https://leetcode.cn/problems/minimum-depth-of-binary-tree/description/
class Solution {
// 第一步:确定递归函数的参数和返回值
public int minDepth(TreeNode root) {
// 第二步:确定终止条件,表示当前节点的高度为0
if(root == null){
return 0;
}
int left = minDepth(root.left);
int right = minDepth(root.right);
// 第三步:确定单层递归的逻辑
// 1、若左子树为空,右子树不为空,最小深度 = 右子树的深度+1,此时不是直接取0,题意要求根节点到最近叶子节点的深度
if(root.left == null) return right + 1;
// 2、若右子树为空,左子树不为空,最小深度 = 左子树的深度+1
if(root.right == null) return left + 1;
// 3、若左右子树都不为空,返回左右子树深度最小值 + 1
return Math.min(left, right) + 1;
}
// 迭代法,层序遍历
public int minDepth1(TreeNode root) {
if (root == null) {
return 0;
}
Deque<TreeNode> deque = new LinkedList<>();
deque.offer(root);
int depth = 0;
while (!deque.isEmpty()) {
int size = deque.size();
depth++;
for (int i = 0; i < size; i++) {
TreeNode poll = deque.poll();
if (poll.left == null && poll.right == null) {
// 是叶子结点,直接返回depth,因为从上往下遍历,所以该值就是最小值
return depth;
}
if (poll.left != null) {
deque.offer(poll.left);
}
if (poll.right != null) {
deque.offer(poll.right);
}
}
}
return depth;
}
}
完全二叉树的节点个数(222)- 13
// https://leetcode.cn/problems/count-complete-tree-nodes/
class Solution {
// 第一步:确定递归函数的参数和返回值
public int countNodes1(TreeNode root) {
// 第二步:确定终止条件,表示当前节点的高度为0
if(root == null){
return 0;
}
// 第三步:确定单层递归的逻辑
int left = countNodes(root.left);
int right = countNodes(root.right);
return left + right + 1;
}
public int countNodes(TreeNode root) {
if(root == null) return 0;
TreeNode node = root;
int leftH = 0; // 左子树高度
int rightH = 0; // 右子树高度
while(node != null){
node = node.left;
leftH++;
}
while(node != null){
node = node.right;
rightH++;
}
// 左子树是满二叉树
if(leftH == rightH){
return (int)Math.pow(2, leftH+1) - 1;
}
return countNodes(root.left) + countNodes(root.right) + 1;
}
}
平衡二叉树(110)- 73
// https://leetcode.cn/problems/ping-heng-er-cha-shu-lcof/
class Solution {
boolean result = true;
public boolean isBalanced(TreeNode root) {
dfs(root);
return result;
}
// 当前节点的深度
public int dfs(TreeNode node){
if(node == null){
return 0;
}
// 当前节点左右子树的深度
int left = dfs(node.left);
int right = dfs(node.right);
if(Math.abs(left - right) > 1){
result = false;
}
return Math.max(left, right) + 1;
}
}
0 - 二叉树的所有路径(257)- 12
// https://leetcode.cn/problems/binary-tree-paths/description/
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
List<String> result = new ArrayList<>();
// 解法一:
public List<String> binaryTreePaths1(TreeNode root) {
String path = root.val + "";
dfs(root, path);
return result;
}
// 第一步:递归函数函数参数以及返回值
private void dfs(TreeNode node, String path) {
// 第二步:确定递归终止条件
if (node.left == null && node.right == null) {
result.add(path);
return;
}
// 第三步:确定单层递归逻辑
if (node.left != null) {
dfs(node.left, path + "->" + node.left.val);
}
if (node.right != null) {
dfs(node.right, path + "->" + node.right.val);
}
}
// 解法二:迭代法
public List<String> binaryTreePaths(TreeNode root) {
List<String> result = new ArrayList<>();
if (root == null)
return result;
Stack<Object> stack = new Stack<>();
// 节点和路径同时入栈
stack.push(root);
stack.push(root.val + "");
while (!stack.isEmpty()) {
// 节点和路径同时出栈
String path = (String) stack.pop();
TreeNode node = (TreeNode) stack.pop();
// 若找到叶子节点
if (node.left == null && node.right == null) {
result.add(path);
}
// 右子节点不为空
if (node.right != null) {
stack.push(node.right);
stack.push(path + "->" + node.right.val);
}
// 左子节点不为空
if (node.left != null) {
stack.push(node.left);
stack.push(path + "->" + node.left.val);
}
}
return result;
}
// 解法三:
public List<String> binaryTreePaths2(TreeNode root) {
dfs1(root);
return result;
}
StringBuilder path0 = new StringBuilder();
private void dfs1(TreeNode node) {
// 终止条件
if (node == null) return;
int len = path0.length();
path0.append(node.val);
// 当前节点的左右节点都为空时,表明该节点为叶子节点
if (node.left == null && node.right == null){
result.add(path0.toString());
}
path0.append("->");
dfs1(node.left);
dfs1(node.right);
// 回溯:删除从索引len到path.length()的元素,包含前len、不包含path.length()
path0.delete(len, path0.length());
}
}
0 - 左叶子之和(404)- 5
// https://leetcode.cn/problems/sum-of-left-leaves/
// 给定二叉树的根节点 root ,返回所有左叶子之和。
// 输入: root = [3,9,20,null,null,15,7]
// 输出: 24
// 解释: 在这个二叉树中,有两个左叶子,分别是 9 和 15,所以返回 24
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
// 递归法
public int sumOfLeftLeaves(TreeNode root) {
if(root == null) return 0;
// 递归查询左子树对应的左叶子节点之和
int left = sumOfLeftLeaves(root.left);
// 递归查询右子树对应的右叶子节点之和
int right = sumOfLeftLeaves(root.right);
int mid = 0;
// 当前节点的左子节点即为左叶子节点
if(root.left != null && root.left.left == null && root.left.right == null){
mid = root.left.val;
}
int sum = left + right + mid;
return sum;
}
// 迭代法
public int sumOfLeftLeaves1(TreeNode root) {
if (root == null) return 0;
Stack<TreeNode> stack = new Stack<>();
stack.add(root);
int result = 0;
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
if (node.left != null && node.left.left == null && node.left.right == null) {
result += node.left.val;
}
if (node.right != null) stack.add(node.right);
if (node.left != null) stack.add(node.left);
}
return result;
}
}
0 - 找树左下角的值(513)
// https://leetcode.cn/problems/find-bottom-left-tree-value/description/
// 给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。
// 假设二叉树中至少有一个节点。
// 输入: [1,2,3,4,null,5,6,null,null,7]
// 输出: 7
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int maxDeep = -1;
int result = 0;
// 深度优先搜索
public int findBottomLeftValue1(TreeNode root) {
dfs(root, 0);
return result;
}
// deep表示当前节点的高度
public void dfs(TreeNode node, int deep){
if (node == null) {
return;
}
if (deep > maxDeep){
maxDeep = deep;
// 先遍历左子树,然后再遍历右子树,所以对同一高度的所有节点,最左节点肯定是最先被遍历到的
result = node.val;
}
// 递归遍历左右子节点
dfs(node.left, deep + 1);
dfs(node.right, deep + 1);
}
// 广度优先搜索
// 在遍历一个节点时,需要先把它的非空右子节点放入队列,然后再把它的非空左子节点放入队列,这样才能保证从右到左遍历每一层的节点。广度优先搜索所遍历的最后一个节点的值就是最底层最左边节点的值
public int findBottomLeftValue(TreeNode root) {
int result = 0;
Queue<TreeNode> queue = new ArrayDeque<TreeNode>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
if (node.right != null) {
queue.offer(node.right);
}
if (node.left != null) {
queue.offer(node.left);
}
result = node.val;
}
return result;
}
}
0 - 路径总和(112)- 62
// https://leetcode.cn/problems/path-sum/description/
// 给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。
// 如果存在,返回 true ;否则,返回 false。
// 输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22
// 输出:true
// 解释:等于目标和的根节点到叶节点路径如上图所示。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public boolean hasPathSum1(TreeNode root, int targetSum) {
if(root == null) return false;
// 若当前节点就是叶子节点,那么我们直接判断sum是否等于val即可
if(root.left == null && root.right == null){
return targetSum == root.val;
}
// 递归遍历左右子树
boolean left = hasPathSum(root.left, targetSum - root.val);
boolean right = hasPathSum(root.right, targetSum - root.val);
return left || right;
}
public boolean hasPathSum(TreeNode root, int sum) {
if (root == null) {
return false;
}
Queue<TreeNode> queNode = new LinkedList<TreeNode>();
Queue<Integer> queVal = new LinkedList<Integer>();
queNode.offer(root);
queVal.offer(root.val);
while (!queNode.isEmpty()) {
TreeNode now = queNode.poll();
int temp = queVal.poll();
// 如果该节点是叶子节点了,同时该节点的路径数值等于sum,那么就返回true
if (now.left == null && now.right == null) {
if (temp == sum) {
return true;
}
continue;
}
// 左节点,压进去一个节点的时候,将该节点的路径数值也记录下来
if (now.left != null) {
queNode.offer(now.left);
queVal.offer(now.left.val + temp);
}
// 右节点,压进去一个节点的时候,将该节点的路径数值也记录下来
if (now.right != null) {
queNode.offer(now.right);
queVal.offer(now.right.val + temp);
}
}
return false;
}
}
1 - 路径总和 II(113)- 67
// https://leetcode.cn/problems/path-sum-ii/description/
// 给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径
// 输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
// 输出:[[5,4,11,2],[5,8,4,5]]
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
List<List<Integer>> result = new ArrayList<List<Integer>>();
List<Integer> path = new ArrayList<Integer>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
if(root == null) return result;
process(root, targetSum);
return result;
}
public void process(TreeNode node, int targetSum){
path.add(node.val);
// 终止条件:遍历到叶子节点
if(node.left == null && node.right == null){
if(targetSum - node.val == 0){
result.add(new ArrayList<>(path));
}
return;
}
if(node.left != null){
process(node.left, targetSum - node.val);
path.remove(path.size() - 1); // 回溯
}
if(node.right != null){
process(node.right, targetSum - node.val);
path.remove(path.size() - 1); // 回溯
}
}
}
1 - 路径总和 III(437)
// https://leetcode.cn/problems/path-sum-iii/
// 给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
// 路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)
// 输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
// 输出:3
// 解释:和等于 8 的路径有 3 条,如图所示
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int result = 0;
public int pathSum(TreeNode root, int targetSum) {
if(root == null) return 0;
dfs(root, targetSum);
// 因为每个节点可以复用,向左右子树递归遍历
pathSum(root.left, targetSum);
pathSum(root.right, targetSum);
return result;
}
public void dfs(TreeNode node, long targetSum){
// 递归终止条件
if(node == null){
return;
}
// 若当前节点的值 与 目标值相等,表明找到了一条路径满足条件
if(node.val == targetSum){
result++;
}
// 向左右子树递归遍历
dfs(node.left, targetSum - node.val);
dfs(node.right, targetSum - node.val);
}
}
二叉树的修改与构造
1 - 翻转二叉树(226)- 56
// https://leetcode.cn/problems/invert-binary-tree/description/
// 给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点
// 输入:root = [4,2,7,1,3,6,9]
// 输出:[4,7,2,9,6,3,1]
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode invertTree1(TreeNode root) {
if(root == null) return null;
// 当前节点左右子树翻转后得到的左右子节点
TreeNode left = invertTree(root.left);
TreeNode right = invertTree(root.right);
root.left = right;
root.right = left;
return root;
}
public TreeNode invertTree(TreeNode root) {
if (root == null) return null;
Deque<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
while (size-- > 0) {
TreeNode node = queue.poll();
swap(node);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
return root;
}
public void swap(TreeNode root) {
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
}
}
22222 - 从中序与后序遍历序列构造二叉树(106)
最大二叉树(654)
1 - 合并二叉树(617)- 11
// https://leetcode.cn/problems/merge-two-binary-trees/description/
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode mergeTrees1(TreeNode root1, TreeNode root2) {
if(root1 == null) return root2;
if(root2 == null) return root1;
TreeNode root = new TreeNode(root1.val + root2.val);
root.left = mergeTrees(root1.left, root2.left);
root.right = mergeTrees(root1.right, root2.right);
return root;
}
// 使用队列迭代
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if (root1 == null) return root2;
if (root2 ==null) return root1;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root1);
queue.offer(root2);
while (!queue.isEmpty()) {
TreeNode node1 = queue.poll();
TreeNode node2 = queue.poll();
// 此时两个节点一定不为空,val相加
node1.val = node1.val + node2.val;
// 如果两棵树左节点都不为空,加入队列
if (node1.left != null && node2.left != null) {
queue.offer(node1.left);
queue.offer(node2.left);
}
// 如果两棵树右节点都不为空,加入队列
if (node1.right != null && node2.right != null) {
queue.offer(node1.right);
queue.offer(node2.right);
}
// 若node1的左节点为空,直接赋值
if (node1.left == null && node2.left != null) {
node1.left = node2.left;
}
// 若node2的左节点为空,直接赋值
if (node1.right == null && node2.right != null) {
node1.right = node2.right;
}
}
return root1;
}
}
求二叉搜索树的属性
0 - 二叉搜索树中的搜索(700)- 4
// https://leetcode.cn/problems/search-in-a-binary-search-tree/description/
// 给定二叉搜索树(BST)的根节点 root 和一个整数值 val
// 你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null
// 输入:root = [4,2,7,1,3], val = 2
// 输出:[2,1,3]
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
// 递归,普通二叉树
public TreeNode searchBST(TreeNode root, int val) {
if (root == null || root.val == val) {
return root;
}
TreeNode left = searchBST(root.left, val);
if (left != null) {
return left;
}
return searchBST(root.right, val);
}
// 递归,利用二叉搜索树特点,优化
public TreeNode searchBST1(TreeNode root, int val) {
if (root == null || root.val == val) {
return root;
}
if (val < root.val) {
return searchBST1(root.left, val);
} else {
return searchBST1(root.right, val);
}
}
// 迭代,普通二叉树
public TreeNode searchBST2(TreeNode root, int val) {
if (root == null || root.val == val) {
return root;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
if (node.val == val) {
return node;
}
if (node.right != null) {
stack.push(node.right);
}
if (node.left != null) {
stack.push(node.left);
}
}
return null;
}
// 迭代,利用二叉搜索树特点,优化,可以不需要栈
public TreeNode searchBST3(TreeNode root, int val) {
while (root != null)
if (val < root.val) root = root.left;
else if (val > root.val) root = root.right;
else return root;
return null;
}
}
1 - 验证二叉搜索树(98)- 68
// https://leetcode.cn/problems/validate-binary-search-tree/
class Solution {
public boolean isValidBST(TreeNode root) {
return isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
public boolean isValidBST(TreeNode node, long lower, long upper) {
if (node == null) {
return true;
}
if (node.val <= lower || node.val >= upper) {
return false;
}
return isValidBST(node.left, lower, node.val) && isValidBST(node.right, node.val, upper);
}
}
22222 - 二叉搜索树的最小绝对差(530)
22222 - 二叉搜索树中的众数(501)
1 - 把二叉搜索树转换为累加树(538)
// https://leetcode.cn/problems/convert-bst-to-greater-tree/
// 给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。
// 提醒一下,二叉搜索树满足下列约束条件:
// 节点的左子树仅包含键 小于 节点键的节点。
// 节点的右子树仅包含键 大于 节点键的节点。
// 左右子树也必须是二叉搜索树。
// 注意:本题和 1038: https://leetcode-cn.com/problems/binary-search-tree-to-greater-sum-tree/ 相同
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int sum = 0;
// 从右向左递归遍历,将节点值依次相加,最先从最右子节点开始递归
public TreeNode convertBST(TreeNode root) {
if(root == null) return root;
convertBST(root.right);
sum += root.val;
root.val = sum;
convertBST(root.left);
return root;
}
}
二叉树公共祖先问题
二叉树的最近公共祖先(236)- 181
// https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/solutions/
// https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/solutions/625063/236-er-cha-shu-de-zui-jin-gong-gong-zu-x-tl5b/
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 1. baseCase:p和q其中有一个正好是root,直接返回root就行
if(root == null || root == p || root == q) return root;
// 2. 然后通过递归,得到左右两棵子树的值
// 在递归函数有返回值的情况下:如果要搜索一条边,递归函数返回值不为空的时候,立刻返回,如果搜索整个树,直接用一个变量left、right接住返回值,
// 这个left、right后序还有逻辑处理的需要,也就是后序遍历中处理中间节点的逻辑(也是回溯)
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 3. 因为在baseCase里给出了第一种情况,因此这里只需要讨论第2、第3种情况
// 3.1 第二种情况:left和right都有值,返回root
// 2.1、当left和right同时不为空:说明p、q分列在root的异侧(分别在左/右子树),返回root
if(left != null && right != null){
return root;
}
// 2.2 当left和right同时为空:说明root的左/右子树中都不包含p、q,返回null
if(left == null && right == null){
return null;
}
// 3.1 当left为空,right不为空:p、q都不在root的左子树中,直接返回right
// 1、p、q其中一个在root右子树中,此时right指向p或q
// 2、p、q两节点都在root的右子树中,此时的right指向最近公共祖先节点
if(left == null){
return right;
}
// 3.2 当left不为空,right为空:p、q都不在root的右子树中,直接返回left
if(right == null){
return left;
}
return null;
}
}
1 - 二叉搜索树的最近公共祖先(235)-6
// https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-search-tree/description/
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
// 递归法
public TreeNode lowestCommonAncestor1(TreeNode root, TreeNode p, TreeNode q) {
if(root == null) return null;
// 两个目标节点同在根节点右侧,递归向右子树遍历
if(root.val < p.val && root.val < q.val){
return lowestCommonAncestor(root.right, p, q);
}
// 两个目标节点同在根节点左侧,递归向左子树遍历
if(root.val > p.val && root.val > q.val){
return lowestCommonAncestor(root.left, p, q);
}
// 两个目标节点分别在根节点左右侧
return root;
}
// 迭代法
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
while(true){
if(root.val < p.val && root.val < q.val){
root = root.right;
}else if(root.val > p.val && root.val > q.val){
root = root.left;
}else{
break;
}
}
return root;
}
}
二叉搜索树的修改与构造
1 - 二叉搜索树中的插入操作(701)-8
// https://leetcode.cn/problems/insert-into-a-binary-search-tree/description/
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
// root节点为空,或者递归遍历过程中左右子节点为null
if(root == null) {
return new TreeNode(val);
}
// 被插入值 大于 当前节点的值,向右递归遍历插入val
if(root.val < val){
root.right = insertIntoBST(root.right, val);
// 被插入值 小于 当前节点的值,向左递归遍历插入val
}else if(root.val > val){
root.left = insertIntoBST(root.left, val);
}
return root;
}
}
1 - 删除二叉搜索树中的节点(450)-31
// https://leetcode.cn/problems/delete-node-in-a-bst/
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
if(root == null){
return null;
}
if(root.val < key){
root.right = deleteNode(root.right, key); // 向右子树递归遍历删除
}else if(root.val > key){
root.left = deleteNode(root.left, key); // 向左子树递归遍历删除
}else{ // 当前节点 = 被删除的节点
if(root.left == null) return root.right; // 被删除节点无左子树,直接返回被删除节点的右子树作为当前节点
else if(root.right == null) return root.left;
else if(root.left != null && root.right != null){ // 被删除节点存在左右子树
TreeNode node = root.right;
while(node.left != null){ // 遍历查找当前节点右子树上的最左节点
node = node.left;
}
// 二叉搜索树特点:当前节点 小于 其右子树上所有节点的值;当前节点 大于 其左子树上所有节点的值
// 所以当前节点被删除后,其左子节点 应成为 其右子树最左节点的左子树
node.left = root.left; // 将当前节点的左子树 成为 当前节点右子树的最左节点 的左子树
root = root.right; // 将当前节点的右子节点 顶替当前节点位置,当前节点即被删除
}
}
return root;
}
}
22222 - 修剪二叉搜索树(669)
0 - 将有序数组转换为二叉搜索树(108)-12
// https://leetcode.cn/problems/convert-sorted-array-to-binary-search-tree/solutions/
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
// 对于整个中序遍历序列,下标范围从 left=0 到 right=nums.length−1
return process(nums, 0, nums.length - 1);
}
public TreeNode process(int[] nums, int left, int right) {
// 当left > right时,平衡二叉搜索树为空
if (left > right) {
return null;
}
// 总是选择中间位置右边的数字作为根节点
// int mid = (left + right + 1) / 2;
// 总是选择中间位置左边的数字作为根节点
int mid = (left + right) / 2;
TreeNode root = new TreeNode(nums[mid]);
// 向左递归遍历,创建的节点 连接到 当前节点的左子节点
root.left = process(nums, left, mid - 1);
// 向右递归遍历,创建的节点 连接到 当前节点的右子节点
root.right = process(nums, mid + 1, right);
return root;
}
}
栈与队列
Stack继承的是Vector类,栈的底层也是一个数组
- 栈(Stack)的方法:
- push( num):入栈
- pop():栈顶元素出栈
- isEmpty():判定栈是否为空
- peek():获取栈顶元素
- search(num):判断元素num是否在栈中,如果在返回1,不在返回-1
- size():返回栈尺寸
LinkedList类实现了Queue接口,因此我们可以把LinkedList当成Queue来用
- 队列(Queue)的方法:
- add:增加一个元索。如果队列已满,则抛出一个IIIegaISlabEepeplian异常
- remove:移除并返回队列头部的元素,如果队列为空,则抛出一个NoSuchElementException异常
- element:返回队列头部的元素,如果队列为空,则抛出一个NoSuchElementException异常
- offer:添加一个元素并返回true,如果队列已满,则返回false
- poll:移除并返问队列头部的元素,如果队列为空,则返回null
- peek:返回队列头部的元素,如果队列为空,则返回null
- put:添加一个元素,如果队列满,则阻塞
- take:移除并返回队列头部的元素,如果队列为空,则阻塞
- contains( Object):是否包含某个对象,包含返回true;否则为false;
- size:返回队列尺寸
- clear:队列清理
用栈实现队列(232)-112
```java // https://leetcode.cn/problems/implement-queue-using-stacks/ // 请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
// 实现 MyQueue 类: // void push(int x) 将元素 x 推到队列的末尾 // int pop() 从队列的开头移除并返回元素 // int peek() 返回队列开头的元素 // boolean empty() 如果队列为空,返回 true ;否则,返回 false // 你只能使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的
class MyQueue {
// 输入栈,用于压入push传入的数据
Stack<Integer> inStack = new Stack<>();
// 输出栈,用于pop和peek操作
Stack<Integer> outStack = new Stack<>();
// 将元素x推到队列的末尾
public void push(int x) {
inStack.push(x);
}
// 从队列的开头移除并返回元素
public int pop() {
if(outStack.isEmpty()){
while(!inStack.isEmpty()){
// 从inStack(后进先出)取出元素,push到outStack(后进先出)。两者刚好使得元素先进先出
int x = inStack.pop();
outStack.push(x);
}
}
return outStack.pop();
}
// 返回队列开头的元素
public int peek() {
if(outStack.isEmpty()){
while(!inStack.isEmpty()){
int x = inStack.pop(); // 输入栈取出元素
outStack.push(x); // 输出栈放入元素
}
}
return outStack.peek();
}
// 如果队列为空,返回true
public boolean empty() {
return outStack.isEmpty() && inStack.isEmpty();
}
}
/**
- Your MyQueue object will be instantiated and called as such:
- MyQueue obj = new MyQueue();
- obj.push(x);
- int param_2 = obj.pop();
- int param_3 = obj.peek();
- boolean param_4 = obj.empty();
*/
```
用队列实现栈(225)-29
```java // https://leetcode.cn/problems/implement-stack-using-queues/ // 请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
// 实现 MyStack 类: // void push(int x) 将元素 x 压入栈顶。 // int pop() 移除并返回栈顶元素。 // int top() 返回栈顶元素。 // boolean empty() 如果栈是空的,返回 true ;否则,返回 false
class MyStack {
Queue<Integer> queue1; // 用于存储栈内的元素
Queue<Integer> queue2; // 作为入栈操作的辅助队列
public MyStack() {
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}
// 先将元素加入辅助队列,然后将正式队列中所有元素取出并放入辅助队列(此时先前元素均在新添加元素后面)
// 然后将辅助队列 赋值给 正式队列,并将辅助队列置为空队列
public void push1(int x) {
if(queue1.isEmpty()) {
queue1.offer(x);
}
else{
queue2.offer(x);
while(!queue1.isEmpty()){
queue2.offer(queue1.poll());
}
// 此时queue1为空
Queue<Integer> temp = queue1;
queue1 = queue2;
queue2 = temp;
}
}
public void push(int x) {
int n = queue1.size();
queue1.offer(x);
// 遍历队列中先前存放元素,放到新添加元素的后面
for(int i = 0; i < n; i++){
queue1.offer(queue1.poll());
}
}
public int pop() {
return queue1.poll();
}
public int top() {
return queue1.peek();
}
public boolean empty() {
return queue1.isEmpty();
}
}
/**
- Your MyStack object will be instantiated and called as such:
- MyStack obj = new MyStack();
- obj.push(x);
- int param_2 = obj.pop();
- int param_3 = obj.top();
- boolean param_4 = obj.empty();
*/
```
有效的括号(20)-188
```java // https://leetcode.cn/problems/valid-parentheses/description/ // 给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串 s ,判断字符串是否有效
class Solution {
public boolean isValid1(String s) {
while(true){
int len = s.length();
s = s.replace("()", "");
s = s.replace("{}", "");
s = s.replace("[]", "");
if(s.length() == len){
return len == 0;
}
}
}
public boolean isValid2(String s) {
Stack<Character> stack = new Stack<>();
stack.push(s.charAt(0));
for (int i = 1; i < s.length(); i++) {
char c = s.charAt(i);
if(c == ')' && !stack.empty()){
Character pop = stack.pop();
if(pop != '('){
return false;
}else {
continue; // 跳过本次循环,不能进入后续的if判断,否则会再次弹出栈顶元素
}
}
if(c == ']' && !stack.empty()){
Character pop = stack.pop();
if(pop != '['){
return false;
}else {
continue;
}
}
if(c == '}' && !stack.empty()){
Character pop = stack.pop();
if(pop != '{'){
return false;
}else {
continue;
}
}
// 如果不是右括号闭合,则将字符加入栈顶
stack.push(s.charAt(i));
}
return stack.empty();
}
public boolean isValid(String s) {
// Deque表示栈
Deque<Character> deque = new LinkedList<>();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
// 碰到左括号,就把相应的右括号入队列
if (ch == '(') {
deque.push(')');
}else if (ch == '{') {
deque.push('}');
}else if (ch == '[') {
deque.push(']');
} else if (deque.isEmpty() || deque.peek() != ch) {
return false; // 如果是右括号,则栈不能为空,此时判断该字符是否和栈顶元素匹配,不匹配直接返回false
}else {
deque.pop(); // 匹配直接弹出栈顶元素
}
}
// 最后判断栈是否为空
return deque.isEmpty();
}
}
<a name="aW641"></a>
## 删除字符串中的所有相邻重复项(1047)-19
```java
// https://leetcode.cn/problems/remove-all-adjacent-duplicates-in-string/description/
// 给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
// 在 S 上反复执行重复项删除操作,直到无法继续删除。
// 在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
// 输入:"abbaca"
// 输出:"ca"
// 解释:
// 例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",
// 其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
class Solution {
public String removeDuplicates(String s) {
Stack<Character> stack = new Stack<>();
stack.push(s.charAt(0));
// 遍历字符串得到每个字符,取出栈顶元素与该字符进行比对,
// 若两者不相同 或 栈为空,则将该字符压入栈中
// 若两者相同,则弹出栈顶元素(达到删除相邻重复项的作用)
for(int i = 1; i < s.length(); i++){
char ch = s.charAt(i);
if(stack.isEmpty() || stack.peek() != ch){
stack.push(ch);
}else{
stack.pop();
}
}
String result = "";
while(!stack.isEmpty()){
// result = result + stack.pop(); // 需要反转字符串
result = stack.pop() + result;
}
// return result.reverse().toString(); // 反转字符串
return result;
}
}
逆波兰表达式求值(150)-8
// https://leetcode.cn/problems/evaluate-reverse-polish-notation/description/
// 根据 逆波兰表示法,求表达式的值。
// 有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
// 注意 两个整数之间的除法只保留整数部分。
// 可以保证给定的逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
// 输入:tokens = ["4","13","5","/","+"]
// 输出:6
// 解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
for(String item : tokens){
// 当遇到运算符时,取出栈顶两个相邻元素进行加减乘除运算,将运算后的值再次压入栈中
if("+".equals(item) || "-".equals(item) || "*".equals(item) || "/".equals(item) ){
int p1 = stack.pop();
int p2 = stack.pop();
if("+".equals(item)) stack.push(p2+p1);
else if("-".equals(item)) stack.push(p2-p1);
else if("*".equals(item)) stack.push(p2*p1);
else if("/".equals(item)) stack.push(p2/p1);
}else{
stack.push(Integer.valueOf(item));
}
}
return stack.peek();
}
}
22222 - 滑动窗口最大值
2 - 前K个高频元素(347)- 25
// https://leetcode.cn/problems/top-k-frequent-elements/
// 给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案
// 输入: nums = [1,1,1,2,2,3], k = 2
// 输出: [1,2]
// 输入: nums = [1], k = 1
// 输出: [1]
// 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的
class Solution {
public int[] topKFrequent1(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
// 按相同元素进行分组放入map中
for(int num : nums){
int value = map.getOrDefault(num, 0);
map.put(num, value + 1);
}
// 出现最多的元素的出现次数
int maxTimes = 0;
// 找出出现次数最多的元素出现的次数
for(Map.Entry<Integer, Integer> entry : map.entrySet()){
if(entry.getValue() > maxTimes){
maxTimes = entry.getValue();
}
}
int[] result = new int[k];
// 按出现次数从大到小添加到结果数组
while(k > 0){
for(Map.Entry<Integer, Integer> entry : map.entrySet()){
if(entry.getValue() == maxTimes){
result[k-1] = entry.getKey();
k--;
}
}
maxTimes--;
}
return result;
}
public int[] topKFrequent(int[] nums, int k) {
HashMap<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
Set<Map.Entry<Integer, Integer>> entries = map.entrySet();
// 根据map的value值,构建于一个大顶堆(o1 - o2: 小顶堆, o2 - o1 : 大顶堆)
PriorityQueue<Map.Entry<Integer, Integer>> queue = new PriorityQueue<>((o1, o2) -> o2.getValue() - o1.getValue());
for (Map.Entry<Integer, Integer> entry : entries) {
queue.offer(entry);
}
int[] result = new int[k];
for (int i = k - 1; i >= 0; i--) {
result[i] = queue.poll().getKey();
}
return result;
}
}