This commit is contained in:
zhangsan 2025-09-28 20:10:38 +08:00
parent 8d4440e0dc
commit 425feff3c7
5 changed files with 326 additions and 5 deletions

View File

@ -0,0 +1,87 @@
package All;
import java.util.ArrayList;
import java.util.List;
/**
* 题目 241. 为运算表达式设计优先级
* 描述给你一个由数字和运算符组成的字符串 expression 按不同优先级组合数字和运算符计算并返回所有可能组合的结果你可以 按任意顺序 返回答案
* 生成的测试用例满足其对应输出值符合 32 位整数范围不同结果的数量不超过 104
示例 1
输入expression = "2-1-1"
输出[0,2]
解释
((2-1)-1) = 0
(2-(1-1)) = 2
* 链接https://leetcode.cn/problems/different-ways-to-add-parentheses/
*/
//不会
public class DiffWaysToCompute {
/*
核心思想
表达式由数字和运算符组成
我们要尝试给不同的运算符添加括号改变计算顺序得到不同结果
遇到运算符就把它当分隔点
左边子串 所有可能结果
右边子串 所有可能结果
然后把左右结果组合在一起
递归过程
如果当前子串全是数字 直接转成整数作为结果返回
否则枚举子串中的运算符 + - *
分别递归处理左右部分
把左右子结果两两组合用当前运算符计算放进结果集
结束条件
子串没有运算符说明是纯数字直接返回该数字
*/
char[] cs; // 存储输入字符串的字符数组便于处理
public List<Integer> diffWaysToCompute(String s) {
cs = s.toCharArray();
// 从整个表达式 [0, n-1] 开始递归
return dfs(0, cs.length - 1);
}
/**
* 递归函数计算表达式 s[l..r] 的所有可能结果
*/
List<Integer> dfs(int l, int r) {
List<Integer> ans = new ArrayList<>();
// 枚举区间内的每一个字符
for (int i = l; i <= r; i++) {
// 如果是数字跳过我们只在运算符处切分
if (cs[i] >= '0' && cs[i] <= '9') continue;
// 遇到运算符 cs[i]把表达式分为左右两部分
List<Integer> left = dfs(l, i - 1);
List<Integer> right = dfs(i + 1, r);
// 枚举左右子表达式的结果两两组合
for (int a : left) {
for (int b : right) {
int cur;
if (cs[i] == '+') cur = a + b;
else if (cs[i] == '-') cur = a - b;
else cur = a * b; // cs[i] == '*'
ans.add(cur);
}
}
}
// 如果 ans 为空说明整个区间没有运算符是一个纯数字
if (ans.isEmpty()) {
int cur = 0;
for (int i = l; i <= r; i++) {
cur = cur * 10 + (cs[i] - '0'); // 将字符转成数字
}
ans.add(cur);
}
return ans;
}
}

View File

@ -0,0 +1,100 @@
package All;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 题目 390. 消除游戏
* 描述列表 arr 由在范围 [1, n] 中的所有整数组成并按严格递增排序请你对 arr 应用下述算法
* 从左到右删除第一个数字然后每隔一个数字删除一个直到到达列表末尾
* 重复上面的步骤但这次是从右到左也就是删除最右侧的数字然后剩下的数字每隔一个删除一个
* 不断重复这两步从左到右和从右到左交替进行直到只剩下一个数字
* 给你整数 n 返回 arr 最后剩下的数字
示例 1
输入n = 9
输出6
解释
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]
arr = [2, 4, 6, 8]
arr = [2, 6]
arr = [6]
* 链接https://leetcode.cn/problems/elimination-game/
*/
//不会
public class LastRemaining {
/*
我们关心的其实不是整个数组而是
当前这一轮的 首项 a1 是多少
当前剩下多少个数 cnt
当前每两个数之间的间隔 step
每一轮删除操作之后
元素个数减半 cnt = cnt / 2
间隔翻倍 step = step * 2
首项 a1 根据正向 / 反向和剩余个数的奇偶性决定是否要往后挪
*/
public int lastRemaining(int n) {
int a1 = 1; // 当前序列的首项第一轮就是1
int k = 0; // 当前轮数从0开始偶数轮从左往右奇数轮从右往左
int cnt = n; // 当前序列里剩余的元素个数
int step = 1; // 相邻两个数的间隔第一轮是1之后每轮翻倍
while (cnt > 1) {
if (k % 2 == 0) { // 偶数轮从左往右删除
// 左到右时首项必然被删掉所以新的首项要往后移一步
a1 = a1 + step;
} else { // 奇数轮从右往左删除
// 如果剩余的个数是奇数那么首项会被删掉需要往后移一步
// 如果是偶数那么首项保留不变
a1 = (cnt % 2 == 0) ? a1 : a1 + step;
}
k++; // 进入下一轮
cnt = cnt >> 1; // 等价于 cnt = cnt / 2每一轮都会删掉一半右移一位
step = step << 1; // 等价于 step = step * 2间隔翻倍
}
return a1;
}
//容易想但时间复杂度高
public int lastRemaining2(int n) {
// 初始化数组 [1, 2, ..., n]
List<Integer> arr = new ArrayList<>();
for (int i = 1; i <= n; i++) {
arr.add(i);
}
boolean leftToRight = true; // true 表示从左到右false 表示从右到左
// 模拟过程直到只剩下一个数
while (arr.size() > 1) {
//只收集未被删除的元素
List<Integer> next = new ArrayList<>();
if (leftToRight) { // 从左到右
for (int i = 1; i < arr.size(); i += 2) {
next.add(arr.get(i));
}
} else { // 从右到左
for (int i = arr.size() - 2; i >= 0; i -= 2) {
next.add(arr.get(i));
}
// 注意反向删除后结果是反序的要反转一下
Collections.reverse(next);
}
arr = next; // 更新为新的一轮
leftToRight = !leftToRight; // 改变方向
}
return arr.get(0); // 最后剩下的唯一元素
}
public static void main(String[] args) {
LastRemaining solution = new LastRemaining();
System.out.println(solution.lastRemaining(9)); // 输出 6
}
}

View File

@ -0,0 +1,65 @@
package All;
import java.util.HashMap;
/**
* 题目 395. 至少有 K 个重复字符的最长子串
* 描述给你一个字符串 s 和一个整数 k 请你找出 s 中的最长子串 要求该子串中的每一字符出现次数都不少于 k 返回这一子串的长度
* 如果不存在这样的子字符串则返回 0
示例 1
输入s = "ababbc", k = 2
输出5
解释最长子串为 "ababb" 其中 'a' 重复了 2 'b' 重复了 3
* 链接https://leetcode.cn/problems/longest-substring-with-at-least-k-repeating-characters/
*/
//不会
public class LongestSubstring {
/*
整体思路
我们要找的子串必须满足每个字符的出现次数 k
如果整个字符串中有某些字符出现次数 < k那么这些字符一定不可能出现在结果子串中
所以我们可以把这些字符当作分隔符把字符串分割成若干子串递归处理
递归过程
如果当前子串长度小于 k直接返回 0不可能满足条件
统计每个字符出现次数
找到那些出现次数 < k 的字符把它们作为分隔符切开子串递归求解每一部分
如果所有字符的出现次数都 k那么整个子串就是合法解直接返回长度
结束条件
子串长度 < k 返回 0
子串完全合法 返回子串长度
否则递归分治返回最大值
*/
public int longestSubstring(String s, int k) {
// 如果字符串长度小于 k不可能有满足条件的子串
if (s.length() < k) return 0;
// 统计每个字符的出现次数
HashMap<Character, Integer> counter = new HashMap<>();
for (int i = 0; i < s.length(); i++) {
counter.put(s.charAt(i), counter.getOrDefault(s.charAt(i), 0) + 1);
}
// 遍历所有字符找到出现次数 < k 的字符
for (char c : counter.keySet()) {
if (counter.get(c) < k) {
// 这个字符不能出现在任何合法子串中
// 因此我们用它作为分隔符切分字符串
int res = 0;
for (String t : s.split(String.valueOf(c))) {
// 递归处理切分后的子串
res = Math.max(res, longestSubstring(t, k));
}
return res; // 返回最长的那个子串结果
}
}
// 如果所有字符出现次数都 >= k
// 那么整个字符串就是一个合法的解
return s.length();
}
}

View File

@ -0,0 +1,66 @@
package All;
/**
* 题目 312. 戳气球
* 描述 n 个气球编号为0 n - 1每个气球上都标有一个数字这些数字存在数组 nums
* 现在要求你戳破所有的气球戳破第 i 个气球你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币 这里的 i - 1 i + 1 代表和 i 相邻的两个气球的序号如果 i - 1或 i + 1 超出了数组的边界那么就当它是一个数字为 1 的气球
* 求所能获得硬币的最大数量
示例 1
输入nums = [3,1,5,8]
输出167
解释
nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167
* 链接https://leetcode.cn/problems/burst-balloons/
*/
//不会
public class MaxCoins {
/*
nums = [3,1,5,8]加虚拟气球 [1,3,1,5,8,1]
我们看区间 (0, 5)
如果最后戳 i=3 (=5)此时左右邻居是 val[0]=1 val[5]=1
收益 = 1*5*1 + (0,3)区间最大值 + (3,5)区间最大值
如果最后戳 i=2 (=1)邻居是 val[0]=1 val[5]=1
收益 = 1*1*1 + (0,2)区间最大值 + (2,5)区间最大值
注意我们根本不用管它前面戳没戳过谁因为只要保证它是最后一个它的邻居就固定成了边界
*/
int[][] memo;
int[] nums;
public int maxCoins(int[] arr) {
int n = arr.length;
// 构造新数组在两端加 1
nums = new int[n + 2];
nums[0] = nums[n + 1] = 1;
for (int i = 0; i < n; i++) nums[i + 1] = arr[i];
memo = new int[n + 2][n + 2];
return dfs(0, n + 1);
}
// 戳破 (l, r) 开区间内的所有气球
//dfs(l, r) 表示 开区间 (l, r) 内的最大值
int dfs(int l, int r) {
if (l + 1 >= r) return 0; // 区间内没有气球了
if (memo[l][r] > 0) return memo[l][r];
int res = 0;
// 枚举最后一个戳破的气球 i 如果 i 是区间 (l, r) 里最后一个被戳的气球那么它的邻居 一定是 l r因为 (l, r) 之间其它气球都已经戳光了
for (int i = l + 1; i < r; i++) {
int coins = nums[l] * nums[i] * nums[r]; // 戳破 i 的得分
coins += dfs(l, i) + dfs(i, r); // 左右子区间结果
res = Math.max(res, coins);
}
memo[l][r] = res;
return res;
}
public static void main(String[] args) {
MaxCoins solver = new MaxCoins();
System.out.println(solver.maxCoins(new int[]{3,1,5,8})); // 输出 167
}
}

View File

@ -33,9 +33,12 @@ public class BuildTree {
return helper(0,cnt-1,0,cnt-1,preorder,inorder);
}
private Map<Integer, Integer> indexMap;
public TreeNode myBuildTree(int[] preorder, int[] inorder, int preorder_left, int preorder_right, int inorder_left) {
public TreeNode myBuildTree(int[] preorder, int preorder_left, int preorder_right, int inorder_left) {
if (preorder_left > preorder_right) {
return null;
}
@ -49,20 +52,20 @@ public class BuildTree {
int size_left_subtree = inorder_root - inorder_left;
// 递归地构造左子树并连接到根节点
// 先序遍历中 左边界+1 开始的 size_left_subtree个元素就对应了中序遍历中 左边界 开始到 根节点定位-1的元素
root.left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left);
root.left = myBuildTree(preorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left);
// 递归地构造右子树并连接到根节点
// 先序遍历中 左边界+1+左子树节点数目 开始到 右边界的元素就对应了中序遍历中 根节点定位+1 右边界的元素
root.right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1);
root.right = myBuildTree(preorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1);
return root;
}
public TreeNode buildTree(int[] preorder, int[] inorder) {
int n = preorder.length;
// 构造哈希映射帮助我们快速定位根节点
indexMap = new HashMap<Integer, Integer>();
indexMap = new HashMap<>();
for (int i = 0; i < n; i++) {
indexMap.put(inorder[i], i);
}
return myBuildTree(preorder, inorder, 0, n - 1, 0);
return myBuildTree(preorder, 0, n - 1, 0);
}
}