1.排序
1.1 快排
不稳定的排序方法:
[5,3A, 6, 3B] -> [3B, 3A, 5, 6]
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String string = scanner.nextLine();
String[] split = string.trim().split(" ");
int[] nums = Arrays.stream(split).mapToInt(Integer ::parseInt).toArray();
Main main = new Main();
main.quickSort(nums,0, nums.length - 1);
for(int i = 0; i < nums.length; i++){
System.out.printf(nums[i] + " ");
}
}
// 快排
private void quickSort(int[] nums, int left, int right){
if(left < right){
int index = partition(nums, left, right);
quickSort(nums, left, index - 1);
quickSort(nums, index + 1, right);
}
}
// 分区
private int partition(int[] nums, int left, int right) {
int start = left, pivot = nums[start];
right++;
// 以下的内容必须都得用= 要不然会出先越界的问题
while (left <= right){
left++;
while(left <= right && nums[left] < pivot)left++;
right--;
while (left <= right && nums[right] > pivot) right--;
if(left < right) {
swap(nums, left, right);
}
}
swap(nums, start, right);
return right;
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
1.2 冒泡:稳定的排序算法
private void BubbleSort(int[] nums){
for(int i = 1; i < nums.length; i++){
boolean flag = true;
for(int j = 0; i < nums.length - i; j++){
if(nums[j] > nums[j + 1]){
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
flag = false; // 标记交换
}
}
if(flag)
break;
}
}
1.3选择排序
不稳定的排序算法:
[4a, 2, 3, 4b, 1] -> [1, 2, 3, 4b, 4a]
private void SelectionSort(int[] nums){
// 一共要经历 n-1 轮比较
for(int i = 0; i < nums.length - 1; i++){
int min = i;
for(int j = i + 1; i < nums.length; j++){
if(nums[j] < nums[min]){
min = i;
}
}
if(i != min){
int temp = nums[i];
nums[i] = nums[min];
nums[min] = temp;
}
}
}
1.4插入排序 : 稳定算法
private void sort(int[] nums){
// 一共要经历 n-1 轮比较
for(int i = 1; i < nums.length - 1; i++){
// 记录要插入的数据
int temp = nums[i];
// j 寻找插入位置
int j = i;
while(j > 0 && temp < nums[j - 1]){
nums[j] = nums[j - 1];
j--;
}
// 存在比其小的数,插入
if(j != i){
nums[j] = temp;
}
}
}
1.5 归并排序:稳定的排序算法
public class Main {
public static void mergeSort(int[] arr, int left, int right){
if(left < right){
int mid = left + (right - left) /2;
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
}
private static void merge(int[] arr, int left, int mid, int right) {
int n1 = mid - left + 1; // 左半部分长度
int n2 = right - mid; // 右半部分长度
int[] leftArr = new int[n1];
int[] rightArr = new int[n2];
for(int i = 0; i < n1; i++){
leftArr[i] = arr[left + i];
}
for(int j = 0; j < n2; j++){
rightArr[j] = arr[mid + 1 + j];
}
int i = 0, j = 0, k = left;
while (i < n1 && j < n2){
if(leftArr[i] <= rightArr[j]){ // 维持稳定性,当两个元素相同时,先放左边的
arr[k++] = leftArr[i++];
}else{
arr[k++] = rightArr[j++];
}
}
while(i < n1){
arr[k++] = leftArr[i++];
}
while(j < n2){
arr[k++] = rightArr[j++];
}
}
public static void main(String[] args) {
int[] arr = {5, 3, 8, 6, 2, 7, 4, 1};
System.out.println("排序前: " + java.util.Arrays.toString(arr));
mergeSort(arr, 0, arr.length - 1);
System.out.println("排序后: " + java.util.Arrays.toString(arr));
}
}
2.二叉树
2.1力扣:437. 路径总和 III 第1次:未通过; 第2次:3.15未通过
解法:前缀和 + 树形遍历
class Solution {
// 前缀和 + 树形遍历
public int pathSum(TreeNode root, int targetSum) {
Map<Long, Integer> prefix = new HashMap<>();
// 注意先在 prefix 中放入0,不然当前缀和正好等于 targerSum 时没法被计入 res。注意这里要做 int -> Long 的类型转换
prefix.put(0L, 1);
return dfs(root, prefix, 0L, targetSum);
}
private int dfs(TreeNode root, Map<Long, Integer> prefix, long sum, int targetSum) {
if (root == null){
return 0;
}
// 结果的个数
int res = 0;
// 根节点到当前节点的前缀和
sum += root.val;
res += prefix.getOrDefault(sum - targetSum, 0);
// 进入前缀和
prefix.put(sum, prefix.getOrDefault(sum, 0) + 1);
// 左右子树递归
res += dfs(root.left, prefix, sum, targetSum);
res += dfs(root.right, prefix, sum, targetSum);
return res;
}
}
2.2力扣:124. 二叉树中的最大路径和 第一次:通过
; 第二次:3.15 通过
2.3力扣:450. 删除二叉搜索树中的节点(中等) 第一次:3.15未通过
思路:递归,判断当前的情况,找到对应的节点进行处理;注意找下一个节点的时候可以吧节点的左子树放到 key
下一个节点的位置:
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
if(root == null){
return null;
}
if(root.val == key){
if(root.left == null){
return root.right;
}else if(root.right == null){
return root.left;
}else{
TreeNode cur = root.right;
while(cur.left != null){
cur = cur.left;
}
// 将根左孩子放到root节点下一个节点的左孩子上
cur.left = root.left;
return root.right;
}
}
if(root.val > key){
root.left = deleteNode(root.left, key);
}else{
root.right = deleteNode(root.right, key);
}
return root;
}
}
3.栈
3.1力扣:394. 字符串解码
第一次:未通过 ; 第二次 :3.15未通过
class Solution {
public String decodeString(String s) {
// 记录返回结果当前已经明确的支付串
StringBuilder curString = new StringBuilder();
// 记录数量
Deque<Integer> countStack = new ArrayDeque<>();
Deque<StringBuilder> stringStack = new ArrayDeque<>();
int num = 0;
for(char c : s.toCharArray()){
if(Character.isDigit(c)){
// 累加数字
num = num * 10 + c - '0';
}else if(c == '['){
// 数字进栈保存
countStack.push(num);
// 当前需要编码的字符串入栈
stringStack.push(curString);
// 创建一个新的字符串,用于记录当前字符串
curString = new StringBuilder();
// 重置重复次数
num = 0;
}else if(c == ']'){
// 当前需要编码的字符串
StringBuilder decodedString = curString;
// 上一段的字符串,把这一段的拼接进去
curString = stringStack.pop();
// 当前需要编码的字符串重复的次数
int repeat = countStack.pop();
for(int i = 0; i < repeat; i++){
curString.append(decodedString);
}
} else{
curString.append(c);
}
}
// 符合规定的输入字符串,那么最后一个']'出栈后一定 curString 代表当前所有拼接好的字符串
return curString.toString();
}
}
3.2力扣:84. 柱状图中最大的矩形
第一次:未通过
; 第二次: 3.17通过
思路:单调递增栈,当找到第一个递减的元素就一直出栈计算当前出栈元素的宽度和高度
优化方法:将数组扩展两个位置,前面和后面都加上一个0方便前端不会出栈,以及最后将所有元素都出栈
class Solution {
public int largestRectangleArea(int[] heights) {
Deque<Integer> stack = new LinkedList<>();
int[] newHeight = new int[heights.length + 2];
System.arraycopy(heights,0, newHeight, 1, heights.length);
heights = newHeight;
int maxArea = 0;
stack.push(0);
for(int i = 1; i < heights.length; i++){
while(heights[stack.peek()] > heights[i]){
int height = heights[stack.pop()];
int width = i - stack.peek() - 1;
// stack.peek() + 1 -> i 之间的都大于等于heights[i]
maxArea = Math.max(maxArea, height * width);
}
stack.push(i);
}
return maxArea;
}
}
3.3力扣:496. 下一个更大元素 I : 解法:单调栈
第二次:3.17未通过
class Solution {
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
int n = nums1.length;
Stack<Integer> stack = new Stack<>();
Map<Integer, Integer> map = new HashMap<>();
// 将 nums2 构成递减栈, 最后的元素都是在原数组当中不存在下一个最大元素的值
// 元素在出栈的过程中 map 记录该元素和下一个最大值
for(int num : nums2){
while(!stack.isEmpty() && stack.peek() < num){
map.put(stack.pop(), num);
}
stack.push(num);
}
int[] res = new int[n];
for(int i = 0; i < n; i++){
if(map.containsKey(nums1[i])){
res[i] = map.get(nums1[i]);
}else{
res[i] = -1;
}
}
return res;
}
}
4.滑动窗口
4.1力扣:76. 最小覆盖子串
1次:未通过; 第二次:3.17通过
class Solution {
public String minWindow(String s, String t) {
if (t.length() == 0) return "";
int[] cntT = new int[128];
int[] cntS = new int[128];
int diff = 0;
// Build the map for t and calculate the number of distinct characters
for (char c : t.toCharArray()) {
if(cntT[c] == 0){
diff++;
}
cntT[c]++;
}
int left = 0, ansLeft = -1, ansRight = s.length();
// Sliding window approach
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
cntS[c]++;
if(cntT[c] > 0 && cntS[c] == cntT[c]){
diff--;
}
while(diff == 0){
if(i - left < ansRight - ansLeft){
ansLeft = left;
ansRight = i;
}
char x = s.charAt(left++);
if(cntT[x] > 0 && cntS[x] == cntT[x]){
diff++;
}
cntS[x]--;
}
}
return ansLeft == -1 ? "" : s.substring(ansLeft, ansRight + 1);
}
}
5.哈希
5.1力扣: 49. 字母异位词分组
第一次:未通过; 第二次:3.18未通过
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
for(String str : strs){
char[] chars = str.toCharArray();
Arrays.sort(chars);
String key = String.valueOf(chars);
map.putIfAbsent(key, new ArrayList<>());
map.get(key).add(str);
}
return new ArrayList<>(map.values());
}
}
5.2力扣:41.缺失的第一个正数
1次:未通过 2:通过; 第三次:3.18通过
思路:原地哈希
class Solution {
public int firstMissingPositive(int[] nums) {
for(int i = 0; i < nums.length; i++){
// 原地hash
while(nums[i] >= 1 && nums[i] <= nums.length && nums[nums[i] - 1] != nums[i]){
// 这有一个要注意的点,int temp = nums[nums[i] - 1];这不能变,如果变了会出现数组越界异常
int temp = nums[nums[i] - 1];
nums[nums[i] - 1] = nums[i];
nums[i] = temp;
}
}
for(int i = 0; i < nums.length; i++){
if(nums[i] != i + 1){
return i + 1;
}
}
return nums.length + 1;
}
}
6.贪心& 前缀数和
6.1力扣: 523. 连续的子数组和(中等)
第二次:3.18未通过
思路: 记录前缀数字和sum[],题目要求是sum[j] - sum[i - 1] == k * n
, 即 sum[j]/k - sum[i - 1]/k == n
,因此前缀数组仅需要保存sum[j] % k
即可, 如果存在两个前缀求模相等的话,那么代表前缀可以构成连续子数组和求和为 n * k。
public boolean checkSubarraySum(int[] nums, int k) {
int n = nums.length;
int[] sum = new int[n + 1];
for(int i = 1; i <= n; i++){
sum[i] = sum[i - 1] + nums[i - 1];
}
Set<Integer> set = new HashSet<>();
for(int i = 2; i <= n; i++){
// 添加两个之前的 sum 总和
set.add(sum[i - 2] % k);
// 判断两个之后是否有相同余数
if(set.contains(sum[i] % k)){
return true;
}
}
return false;
}
7.多维动态递归
7.1力扣:120. 三角形最小路径和
3.20第二次通过
7.2力扣:97.交错字符串
3.20第二次未通过
class Solution {
public boolean isInterleave(String s1, String s2, String s3) {
int len1 = s1.length();
int len2 = s2.length();
int len3 = s3.length();
if(len3 != len1 + len2) return false;
boolean[][] dp = new boolean[len1 + 1][len2 + 1];
dp[0][0] = true;
for(int i = 0; i <= len1; i++){
for(int j = 0; j <= len2; j++){
if(i > 0 && s1.charAt(i - 1) == s3.charAt(i + j -1)){
// dp[i][j] 出现则 dp[i - 1][j] 必定出现,如果这个都不可以必定也是不可以
dp[i][j] = dp[i - 1][j];
}
if(j > 0 && s2.charAt(j - 1) == s3.charAt(i + j -1)){
dp[i][j] |= dp[i][j - 1];
}
}
}
return dp[len1][len2];
}
}
7.4力扣:32. 最长有效括号(困难)
第二次:3.20通过
class Solution {
public int longestValidParentheses(String s) {
int n = s.length();
int[] dp = new int[n];
int res = 0;
for(int i = 1; i < n; i++){
if(s.charAt(i) == ')'){
if(s.charAt(i -1) == '('){
dp[i] = 2;
if(i > 2){
dp[i] += dp[i - 2];
}
}else{
// 当前面的也为 ')'时 但是前面的 i - 1 - dp[i - 1] 位置为 '('
if(i - 1 - dp[i - 1] >= 0 && s.charAt(i - 1 - dp[i - 1]) == '('){
dp[i] = dp[i - 1] + 2;
if(i - 2 - dp[i - 1] >= 0){
// 再加上前面的
dp[i] += dp[i - dp[i - 1] - 2];
}
}
}
res = Math.max(res, dp[i]);
}
}
return res;
}
}
8. 动态规划
8.1 01背包问题
8.1.1 力扣:474.一和零(中等)
第二次:3.20未通过
外层遍历物品,内层遍历背包,这个题背包有两个维度:第一个维度 ‘1’ 的个数,第二个维度是 ‘0’ 的个数。
public int findMaxForm(String[] strs, int m, int n) {
int[][] dp = new int[m + 1][n + 1];
for(String str : strs){
// 外层循环遍历物品
int oneNum = 0, zeroNum = 0;
for(char c : str.toCharArray()){
if(c == '1'){
oneNum++;
}else{
zeroNum++;
}
}
// 内层循环两个维度, 遍历 1 和 0 的个数,由于两个维度都无关,先遍历那个维度都可以
for(int i = m; i >= zeroNum; i--){
for(int j = n; j >= oneNum; j--){
dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
}
}
}
return dp[m][n];
}
8.2 完全背包问题
首先完全背包问题和 01 背包问题的区别是:
- 完全背包物品可以使用多次,所以递推公式为:
dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i])
- 01 背包的物品仅可以使用一次,所以递推公式为:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
8.2.1 力扣:322. 零钱兑换 (中等)
第二次:3.18通过
class Solution {
public int coinChange(int[] coins, int amount) {
Arrays.sort(coins);
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1);
dp[0] = 0;
for(int coin : coins){
// 后遍历物品
for(int i = coin; i <= amount; i++){
// 先遍历背包
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
return dp[amount] == amount + 1 ? -1 : dp[amount];
}
}
8.2.2 力扣:518. 零钱兑换 II(中等)
第二次:3.21未通过
由于完全背包是从当前行的左侧和上一行相同位置递推的到所以很容易写为一维dp数组
public class Solution {
public int change(int amount, int[] coins) {
// 每个下标 amount 凑成金额的种类数
int[] dp = new int[amount + 1];
// 初始化
dp[0] = 1;
// 递推
for(int i = 0; i < coins.length; i++) {
// 外层循环遍历物品
for(int j = coins[i]; j <= amount; j++) {
// 内层循环遍历背包
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
}
8.2.3 力扣:377. 组合总和 Ⅳ(中等)
第二次:3.21未完成
解法1* 递归搜索 + 保存递归返回值 = 记忆化搜索
// 爬楼梯有多种爬法的思想
public int combinationSum4(int[] nums, int target) {
int[] memo = new int[target + 1];
Arrays.fill(memo, -1);
return dfs(target, nums, memo);
}
private int dfs(int i, int[] nums, int[] memo) {
if(i == 0){ // 爬完了
return 1;
}
// 记忆化
if(memo[i] != -1){
return memo[i];
}
int res = 0;
for(int x : nums){ // 枚举所有可以爬的台阶数
if(x <= i){
res += dfs(i - x, nums, memo);
}
}
return memo[i] = res; // 记忆化
}
解法二:
在解法1的基础上:忽略了递,只保留归的思路,递归公式依旧是
f[i] = sum(f[i] - nums[j])
public class Solution {
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1];
dp[0] = 1;
for (int i = 1; i <= target; i++) {
// 后遍历背包
for (int j = 0; j < nums.length; j++) {
// 先遍历物品
if(i >= nums[j]) {
dp[i] += dp[i - nums[j]];
}
}
}
return dp[target];
}
}
8.3 普通动态规划
8.3.1 力扣:516. 最长回文子序列(中等)
第二次:3.21未通过
灵神题解
解法1: 记忆化递归
class Solution {
// 方法一:记忆化搜索
public int longestPalindromeSubseq(String s) {
char[] c = s.toCharArray();
int n = s.length();
int[][] memo = new int[n][n];
for(int[] row : memo){
Arrays.fill(row, -1);
}
return dfs(0, n - 1, c, memo);
}
// 求下标 i -> j 之间的最长回文子串的长度
private int dfs(int i, int j, char[] c, int[][] memo) {
if(i > j){ // 空串
return 0;
}
if(i == j){ // 只有一个字母
return 1;
}
if(memo[i][j] != -1){ // 记忆化
return memo[i][j];
}
if(c[i] == c[j]){
// 记忆化
return memo[i][j] = dfs(i + 1, j - 1, c, memo) + 2;
}
return memo[i][j] = Math.max(dfs(i + 1, j, c, memo), dfs(i, j - 1, c, memo));
}
}
方法二: 转换为递推方法
class Solution {
public int longestPalindromeSubseq(String s) {
// 方法二:转换为递推
char[] c = s.toCharArray();
int n = s.length();
int[][] dp = new int[n][n];
for(int i = n - 1; i >= 0; i--){
// 初始化对角线
dp[i][i] = 1;
for(int j = i + 1; j < n; j++){
// 递推公式
dp[i][j] = c[i] == c[j] ?
dp[i + 1][j - 1] + 2 : Math.max(dp[i + 1][j], dp[i][j - 1]);
}
}
return dp[0][n - 1];
}
}
8.3.2 力扣:115. 不同的子序列(困难)
第二次:3.21未通过
灵神解法
解法一: 记忆化递归
class Solution {
// 方法一:记忆化递归
public int numDistinct(String s, String t) {
int n = s.length();
int m = t.length();
int[][] memo = new int[n][m];
for(int[] rows : memo){
Arrays.fill(rows, -1);
}
return dfs(n - 1, m - 1, s.toCharArray(), t.toCharArray(), memo);
}
private int dfs(int i, int j, char[] s, char[] t, int[][] memo) {
if(i < j){ // s串已经没有 t串长了
return 0;
}
if(j < 0){ // t 为空串的情况下
return 1;
}
if(memo[i][j] != -1){
return memo[i][j];
}
// 去掉s的最后一个但是不去掉t的最后一个的结果
int res = dfs(i - 1, j, s, t, memo);
// 如果相等的话,可以加上最后一个
if(s[i] == t[j]){
// 最后一个可以去掉的情况, 如果相等,可以加上去,毕竟加上这一种可能只会多不会少
res += dfs(i - 1, j - 1, s, t, memo);
}
return memo[i][j] = res; // 记忆化保存
}
}
解法2: 转换为递推
class Solution {
public int numDistinct(String s, String t) {
int n = s.length(), m = t.length();
int[][] dp = new int[n + 1][m + 1]; // dp[i][j] 代表s[i-1]的串中出现的t[i-1]串不同的个数
// 初始化
for(int i = 0; i <= n; i++){
dp[i][0] = 1;
}
// 递归公式
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; 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] = dp[i - 1][j];
}
}
}
return dp[n][m];
}
}
8.3.3力扣: 139. 单词拆分(中等)
第二次:3.22未通过
思路:采用动态规划的思路:
- 初始化:
boolean[] dp = new boolean[n + 1]
添加第一个空字符串为true
, - 递推顺序:外层循环遍历背包容量,内层循环遍历物品
- 递推函数:
if(i >= len && dp[i - len] && wordlist.get(j).equals(s.substring(i - len , i)))
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int n = s.length();
boolean[] dp = new boolean[n + 1];
dp[0] = true;
for(int i = 1; i <= n; i++){
// 后遍历背包大小
for(int j = 0; j < wordDict.size(); j++){
// 先遍历物品
int len = wordDict.get(j).length();
// 1.背包容量要够大,2.dp[i - len] == true代表前面是已经匹配好了.3.当前字符串的前len个字符与字典中的单词匹配
if(i >= len && dp[i- len] && wordDict.get(j).equals(s.substring(i - len, i))){
dp[i] = true;
}
}
}
return dp[n];
}
}
8.3.4力扣: 1043. 分隔数组以得到最大和(中等)
第二次:3.22未通过
灵神解法
- 解法一 : 递归 + 记录返回值 = 记忆化搜索
class Solution {
private int[] arr, memo;
private int k;
public int maxSumAfterPartitioning(int[] arr, int k) {
this.arr = arr;
this.k = k;
int n = arr.length;
memo = new int[n];
Arrays.fill(memo, -1); // 表示还没有计算过
return dfs(n - 1);
}
// 递归计算一段区间的和,然后将求和结果返回到栈的下层
private int dfs(int i) {
if(i < 0) return 0;
if(memo[i] != -1) return memo[i]; // 计算过了
int res = 0;
for(int j = i, mx = 0; j > i - k && j >= 0; --j){
mx = Math.max(mx, arr[j]); // 一边枚举,一边计算子数组最大值
// dfs(j - 1) : 代表从0 - j - 1 的子数组的和, 而 (i - j + 1) * mx 是从后向前从[j, i]区间的最大的和
res = Math.max(res, dfs(j - 1) + (i - j + 1) * mx);
}
return memo[i] = res;
}
}
- 解法二:动态规划
class Solution {
public int maxSumAfterPartitioning(int[] arr, int k) {
int n = arr.length;
int[] dp = new int[n + 1];
for(int i = 0; i < n; i++){
for(int j = i, mx = 0; j > i - k && j >= 0; j--){
mx = Math.max(mx, arr[j]); // 一边枚举 j, 一边计算子数组的最大值
// dp[j] 代表之前 0 - j-1 的最大和 然后在加上 j - i的最大和就是最终结果
dp[i + 1] = Math.max(dp[i + 1], dp[j] + (i - j + 1) * mx);
}
}
return dp[n];
}
}
8.3.5力扣: 152. 乘积最大子数组(中等)
第二次:3.20已通过
class Solution {
public int maxProduct(int[] nums) {
int n = nums.length;
int minF = nums[0];
int maxF = nums[0];
int res = nums[0];
for(int i = 1; i < n; i++){
int temp = minF;
minF = Math.min(nums[i], Math.min(minF * nums[i], maxF * nums[i]));
maxF = Math.max(nums[i], Math.max(temp * nums[i], maxF * nums[i]));
res = Math.max(res, maxF);
}
return res;
}
}
8.3.6 美团笔试:跳跃游戏
题目要求:先输入两个值 n 和 k,其中n代表从下标1到n的位置,k是在1和n之间的一个位置,然后再输入字符串s,其中s由字符 ‘R’和’L’ 和 ‘?’ 三个操作符组成, ‘R’ 可以向右跳一个位置,‘L’ 可以向左跳一个位置,‘?’ 可以同时向左右跳,如果当前在边界 1 或者 n,在位置 1 向左跳还是位置 1,位置 n 向右跳还是n。首先从位置 k 开始执行跳,第一行输入两个参数分别为整数 n 和整数 k ,第二行输入要进行跳的操作字符串。最后输出可能跳到的位置,如果最后一次可以跳到该位置,该位置就是1,如果跳不到该位置该位置就是0,输出所有位置的0,1字符串。一下是一个例子:
输入:
5 2
?????
输出:
11111
/**
* @Author: ShuWang Li
* @Date: 2025/3/22 21:24
* @Description: 跳跃游戏
**/
import java.util.Scanner;
public class JumpGame {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(); // 数组长度
int k = sc.nextInt();
String s = sc.next();
char[] chars = s.toCharArray();
int len = s.length();
int[] prev = new int[n + 1];
int[] curr = new int[n + 1];
// 初始化
if (chars[0] == 'R') {
if (k == n) prev[k] = 1;
if (k + 1 <= n) prev[k + 1] = 1;
} else if (chars[0] == 'L') {
if (k == 1) prev[k] = 1;
if (k - 1 >= 1) prev[k - 1] = 1;
} else { // '?'
if (k == n || k == 1) prev[k] = 1;
if (k + 1 <= n) prev[k + 1] = 1;
if (k - 1 >= 1) prev[k - 1] = 1;
}
// 动态规划
for (int i = 1; i < len; i++) {
for (int j = 1; j <= n; j++) {
if (prev[j] == 1) {
if (chars[i] == 'L') {
if (j == 1) curr[j] = 1;
else curr[j - 1] = 1;
} else if (chars[i] == 'R') {
if (j == n) curr[j] = 1;
else curr[j + 1] = 1;
} else { // '?'
if (j == 1) curr[j] = 1;
else curr[j - 1] = 1;
if (j == n) curr[j] = 1;
else curr[j + 1] = 1;
}
}
}
// 滚动数组,将curr赋值给prev,重置curr
System.arraycopy(curr, 1, prev, 1, n);
java.util.Arrays.fill(curr, 0);
}
// 输出结果
StringBuilder result = new StringBuilder();
for (int i = 1; i <= n; i++) {
result.append(prev[i]);
}
System.out.println(result.toString());
sc.close();
}
}
9.数学题:
9.1力扣:50.Pow(x, n)
第二次:3.22未通过
class Solution {
public double myPow(double x, int n) {
if(x == 0.0f){
return 0.0d;
}
long b = n; // 将 n 转化为 long 类型 因为 -2^31没有对应的相反数
double res = 1.0;
// 将指数转换为正数
if(b < 0){
x = 1 / x;
b = -b;
}
while(b > 0){
if((b & 1) == 1){ // 奇数
res *= x;
}
x *= x;
b /= 2;
}
return res;
}
}
9.2力扣 : 149. 直线上最多的点数
第二次:3.22通过
10.二分查找
10.1力扣: 4. 寻找两个正序数组的中位数(困难)
第二次:3.22未通过
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
int left = (m + n + 1) / 2;
int right = (n + m + 2) / 2;
return (findKth(nums1, 0, m - 1, nums2, 0, n - 1, left) + findKth(nums1,0, m - 1, nums2, 0, n - 1, right)) * 0.5;
}
private int findKth(int[] nums1, int start1, int end1, int[] nums2, int start2, int end2, int k) {
int len1 = end1 - start1 + 1;
int len2 = end2 - start2 + 1;
if(len1 > len2){
return findKth(nums2, start2, end2, nums1, start1, end1, k);
}
if(len1 == 0){
return nums2[start2 + k - 1];
}
if(k == 1){
return Math.min(nums1[start1], nums2[start2]);
}
int i = start1 + Math.min(len1, k / 2) - 1;
int j = start2 + Math.min(len2, k / 2) - 1;
if(nums1[i] < nums2[j]){
return findKth(nums1, i + 1, end1, nums2, start2, end2, k - i - 1 + start1);
}else{
return findKth(nums1, start1, end1, nums2, start2, j + 1, k - j - 1 + start2);
}
}
}
11.堆
11.1力扣 295. 数据流的中位数
第二次:3.24通过
11.2 力扣 215. 数组中的第K个最大元素
第二次: 3.24未通过
11.3 力扣 347. 前 K 个高频元素
第二次:3.26未通过
12.链表
12.1力扣 25. K 个一组翻转链表
第二次: 3.26未通过
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode dummy = new ListNode(0, head);
ListNode pre = dummy;
while(head != null){
// 下一段的尾结点
ListNode tail = pre;
for(int i = 0; i < k; i++){
tail = tail.next;
if(tail == null){
return dummy.next;
}
}
ListNode[] rev = reverse(head, tail);
pre.next = rev[0];
pre = rev[1];
head = pre.next;
}
return dummy.next;
}
private ListNode[] reverse(ListNode head, ListNode tail) {
ListNode pre = tail.next;
ListNode p = head;
while(pre != tail){
ListNode next = p.next;
p.next = pre;
pre = p;
p = next;
}
return new ListNode[]{tail, head};
}
}
12.2 力扣 148. 排序链表
第二次: 3.26未通过
class Solution {
public ListNode sortList(ListNode head) {
if(head == null || head.next == null){
return head;
}
ListNode fast = head.next, slow = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
ListNode temp = slow.next;
slow.next = null;
ListNode left = sortList(head);
ListNode right = sortList(temp);
ListNode h = new ListNode(0);
ListNode res = h;
// merge 过程
while(left != null && right != null){
if(left.val < right.val){
h.next = left;
left = left.next;
}else{
h.next = right;
right = right.next;
}
h = h.next;
}
h.next = left != null ? left : right;
return res.next;
}
}
13. 回溯算法
13.1 力扣:93. 复原 IP 地址(中等)
第二次:3.28未通过
经典回溯算法:本题需要考虑的是docCount 点的个数,首先将字符串通过StringBuilder的构造函数传入进入,然后再在 sb 当中添加点,进入dfs首先判断是否已经有三个点了有三个点的话判断最后一段是否符合结果。还有本次算法使用了 StringBuilder 的insert函数在指定位置插入指定字符。还有下一次dfs要跳过两个位置。
public class Solution {
List<String> res = new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
StringBuilder sb = new StringBuilder(s);
dfs(sb, 0, 0);
return res;
}
private void dfs(StringBuilder sb, int start, int docCount) {
// 已经插入三个点,判断最后一段是否合法
if(docCount == 3){
if(check(sb, start, sb.length() - 1)){
res.add(sb.toString());
}
return;
}
for(int i = start; i < sb.length(); i++){
// 如果当前这一段符合规定
if(check(sb, start, i)){
sb.insert(i + 1, '.');
// 向后跳两个位置
dfs(sb, i + 2, docCount + 1);
sb.deleteCharAt(i + 1);
}
}
}
// 判断在 sb 当中当前子串转化为数字是否合法
private boolean check(StringBuilder sb, int start, int end){
if(start > end){
return false;
}
// 最后一段不只一位的情况下,0开头是不合适的
if(sb.charAt(start) == '0' && start != end){
return false;
}
int num = 0;
for(int i = start; i <= end; i++){
num = num * 10 + sb.charAt(i) - '0';
if(num > 255){
return false;
}
}
return true;
}
}
13.2 力扣: 332.重新安排行程(困难)
第二次:3.28未通过
这道题主要是运用了深度优先遍历的思想,由于所有的票全部使用并且使用一次,因此不需要回溯
public class Solution {
public List<String> findItinerary(List<List<String>> tickets) {
// 使用Map来构建邻接表,PriorityQueue 保证按字典顺序排序
Map<String, PriorityQueue<String>> map = new HashMap<>();
for (List<String> ticket : tickets) {
map.putIfAbsent(ticket.get(0), new PriorityQueue<>());
map.get(ticket.get(0)).offer(ticket.get(1));
}
// 结果
List<String> res = new ArrayList<>();
dfs("JFK", map, res);
return res;
}
// 由于题目已经给出至少有一个题解,并且所有机票都用且仅用一次
private void dfs(String airport, Map<String, PriorityQueue<String>> map, List<String> res) {
PriorityQueue<String> destinactions = map.get(airport);
// 机票数目是一定的,因此不用考虑回溯,只需要查找最优解即可
while(destinactions != null && !destinactions.isEmpty()) {
dfs(destinactions.poll(), map, res);
}
// 机场头插,确保路径正确
res.add(0 , airport);
}
}
13 字符串
13.1 力扣:459. 重复的子字符串(简单) :三种写法
第一种解法:将字符串叠加两个在一起,如果新字符串掐头去尾还存在当前子串的情况下,说明该串是由重复的子串构成
class Solution {
public boolean repeatedSubstringPattern(String s) {
String str = s + s;
return str.substring(1, str.length() - 1).contains(s);
}
}
第三种利用前缀数组的思想,计算前缀数组,最后判断前缀数组最后一位是否不为0(表示有前缀)并且可以重复
```java
/**
* 判断字符串是否由重复的子字符串构成。
*
* 该函数通过构造字符串的前缀数组(next数组),利用KMP算法的思想来判断字符串是否由重复的子字符串构成。
* 如果字符串的长度可以被其最长公共前后缀的长度整除,则说明该字符串由重复的子字符串构成。
*
* @param s 待判断的字符串
* @return 如果字符串由重复的子字符串构成,则返回true;否则返回false
*/
public boolean repeatedSubstringPattern(String s) {
int len = s.length();
s = " " + s; // 在字符串前添加一个空格,方便后续处理
char[] chars = s.toCharArray();
// 前缀数组,用于存储每个位置的最长公共前后缀长度
int[] prev = new int[len + 1];
// 构造 next 数组,j 从 0 开始, i 从 2 开始
for(int i = 2, j = 0; i <= len; i++){
// 当字符不匹配时,回退到前一个匹配位置
while(j > 0 && chars[i] != chars[j + 1]){
j = prev[j];
}
// 匹配成功后移
if(chars[i] == chars[j + 1]){
j++;
}
prev[i] = j; // 记录当前位置的最长公共前后缀长度
}
// 如果字符串的最长公共前后缀长度大于0,并且字符串长度可以被其最长公共前后缀的长度整除,则返回true
if(prev[len] > 0 && len % (len - prev[len]) == 0){
return true;
}
return false;
}
13.2 美团笔试:要求字符对称的回文字符串个数
第二次:3.28未通过
/**
* @Author: ShuWang Li
* @Date: 2025/3/22 22:34
* @Description: 有约束条件的回文字符串
**/
public class PalindromeSubstrings {
// 用来检查一个字符是否是有效的回文字符
private static final Set<Character> VALID_CHARACTERS = new HashSet<>();
static {
// 初始化有效字符集合
for (char c : new char[]{'A', 'H', 'I', 'M', '0', 'T', 'U', 'V', 'W', 'X', 'Y'}) {
VALID_CHARACTERS.add(c);
}
}
// 计算回文子串的数量
public static int countValidPalindromeSubstrings(String s) {
int n = s.length();
int count = 0;
// 遍历每个可能的回文中心
for (int center = 0; center < n; center++) {
// 对于奇数长度的回文,中心是一个字符
count += expandAroundCenter(s, center, center);
// 对于偶数长度的回文,中心是两个字符之间的空隙
if (center + 1 < n) {
count += expandAroundCenter(s, center, center + 1);
}
}
return count;
}
// 扩展中心,检查回文子串
private static int expandAroundCenter(String s, int left, int right) {
int count = 0;
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
// 检查子串是否只包含有效字符
if (right - left + 1 > 1 && isValidPalindromeSubstring(s, left, right)) {
count++;
}
left--;
right++;
}
return count;
}
// 检查回文子串是否仅包含有效字符
private static boolean isValidPalindromeSubstring(String s, int left, int right) {
for (int i = left; i <= right; i++) {
if (!VALID_CHARACTERS.contains(s.charAt(i))) {
return false;
}
}
return true;
}
// 主函数
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int result = countValidPalindromeSubstrings(scanner.next());
System.out.println("Valid palindrome substrings count: " + result);
}
}
14. 技巧
14.1力扣: 31. 下一个排列
第二次 :3.28未通过
public void nextPermutation(int[] nums) {
if(nums == null || nums.length <= 1){
return;
}
// step1: 查找第一个正序对
int pos = -1;
for(int i = nums.length - 2; i >= 0; i--){
if(nums[i] < nums[i + 1]){
pos = i;
break;
}
}
if(pos == -1){
reverse(nums, 0, nums.length - 1);
return;
}
// step2: 找到右侧第一个大于 pos 下标值的元素
for(int i = nums.length - 1; i > pos; i--){
if(nums[i] > nums[pos]){
int temp = nums[pos];
nums[pos] = nums[i];
nums[i] = temp;
break;
}
}
// step3: 倒序余下数组
reverse(nums, pos + 1, nums.length - 1);
}
private void reverse(int[] nums, int start, int end){
while(start < end){
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
start++;
end--;
}
}
15.贪心
15.1 力扣:376. 摆动序列(中等)
第二次:3.29未通过
思路:使用贪心的想法,记录波峰和波谷的个数,
- 将右侧记录为1个波谷,左侧前面默认preDiff为0
- 为了避免出现升->平->升的情况,在每次产生变动的时候再更新
preDiff == curDiff
public class Solution {
public int wiggleMaxLength(int[] nums) {
if(nums.length <= 1) return nums.length;
int curDiff = 0; // 当前这一对的差值
int preDiff = 0; // 前一对的差值
int res = 1; // 默认最右边有一个峰值
for(int i = 0; i < nums.length - 1; i++) {
curDiff = nums[i + 1] - nums[i];
// 只判断平坡最后一段的情况
if(preDiff >= 0 && curDiff < 0 || preDiff <= 0 && curDiff > 0) {
res++;
preDiff = curDiff; // 只在发生摆动时才更新 prediff
}
}
return res;
}
}
15.2 力扣:45. 跳跃游戏 II(中等)
第二次:3,28未通过
思路:两个变量nextDistance
和curDistance
可以跳跃到的最远距离;当curDistance == i
的时候更新跳跃的次数以及下一跳可以跳至的距离,并且判断是否可以跳到最后
public class Solution {
public int jump(int[] nums) {
int nextDistance = 0;
int res = 0;
int curDistance = 0;
// i < nums.length - 1 的前提下一定是可以到达
for(int i = 0; i < nums.length - 1; i++) {
// 记录在当前这一跳的范围内,下一跳可以到达的最远距离
nextDistance = Math.max(nextDistance, i + nums[i]);
// 当前已经到最远距离
if(curDistance == i){
res++;
// 当前这一跳可以到达的最远距离
curDistance = nextDistance;
if(nextDistance >= nums.length - 1){
break;
}
}
}
return res;
}
}
15.3 力扣:134. 加油站(中等)
第二次:3.28未通过
解法一: 从全局进行贪心选择:
- 情况一:如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的
- 情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。
- 情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能把这个负数填平,能把这个负数填平的节点就是出发节点。
解法二: 局部贪心 -> 局部最优推出全局最优解
- 每个加油站的剩余量rest[i]为
gas[i] - cost[i]
; - i 从 0 开始累加 rest[i] ,和记为 curSum ,一旦 curSum 小于零,说明 [0, i] 区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到 i 这里都会断油,那么起始位置从 i + 1 算起,再从 0 计算curSum。
// 解法1: 全局贪心
public class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int min = Integer.MAX_VALUE; // 从 0 开始油箱当中最少有多少油,记录,可以为负数
int curSum = 0; // 油箱剩余油量
for(int i = 0; i < gas.length; i++){
int rest = gas[i] - cost[i];
curSum += rest;
if(curSum < min){
min = curSum; // 记录在某一站油箱的最小值
}
}
if(curSum < 0){ //情况1: 油量不够跑完全程
return -1;
}
if(min >= 0){ //情况2: 从 0 开始不会出现断油的情况
return 0;
}
int sum = 0;
int i = gas.length - 1;
for(; i >= 0; i--){
sum += gas[i] - cost[i];
if(sum + min >= 0){
break; // 情况3:找到可以弥补最小缺口的地方
}
}
return i;
}
}
// 解法2:贪心选出局部最优解
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int curSum = 0; // 油箱剩余油量
int totalSum = 0;
int start = 0;
for(int i = 0; i < gas.length; i++) {
curSum += gas[i] - cost[i];
totalSum += gas[i] - cost[i];
// 如果当前 curSum < 0 这种情况发生,那么当前从start -> i 这段区间都是不可以做开头的,需要更新 start
if(curSum < 0) {
start = i + 1; // 重新选择开始区间
curSum = 0; // 重置
}
}
if(totalSum < 0) return -1;
return start;
}
}
15.4 力扣:406. 根据身高重建队列(中等)
第二次:3.29未通过
思路:这里有排名和身高两个维度,只能一个维度一个维度的判断,因此:
- 先将数组按照身高从高到底排序,如果身高相同的话,在按后面的排列顺序升序排列
- 将第一个身高维度拍好之后,然后创建一个
LinkedList
然后使用public void add(int index, E element)
这个重载方法,将p[1] 插入到索引index
当中。
public class Solution {
public int[][] reconstructQueue(int[][] people) {
// 1.第一个维度,按照身高从大往小排(相同身高按照第二个维度)
Arrays.sort(people, (a, b) ->{
if(a[0] == b[0]) return a[1] - b[1];
return b[0] - a[0];
});
LinkedList<int[]> queue = new LinkedList<>();
for(int[] p : people) {
// add的重载方法 将指定的数组 p 插入到 p[1] 的位置上
queue.add(p[1], p);
}
return queue.toArray(new int[people.length][]);
}
}
15.5 力扣:738. 单调递增的数字(中等)
第二次:3.29未通过
思路:两个变量 max
记录当前递增出现的最大值,再往后可能就是相等的最大值;index
记录的是 max
记录的最大值的第一个索引,方便找到非严格递增之后将该值变为 max - 1
,然后将后面的索引位置上的数字全部变为 9
class Solution {
public int monotoneIncreasingDigits(int n) {
char[] arr = ("" + n).toCharArray();
int max = -1, index = -1;
for(int i = 0; i < arr.length - 1; i++){
// 记录当前递增最大值第一次出现的索引, 如果一直相等的情况下也是记录第一次出现的位置
if(max < arr[i]){
max = arr[i];
index = i;
}
// 找到递减的数字
if(arr[i] > arr[i + 1]){
arr[index] -= 1;
for(int j = index + 1; j < arr.length; j++){
arr[j] = '9';
}
break;
}
}
String string = new String(arr);
// 这个方法可以实现 001 - > 1
return Integer.parseInt(string);
}
}
15.6 力扣:968. 监控二叉树(中等)
第二次:3.30通过
思路:为每个节点设置三种状态:
- 状态0:节点未被监控到
- 状态1:节点是摄像头(当然代表是已经被监控了)
- 状态2:节点已被监控但是不是摄像头
采用自底向上的思路(后序遍历情况)
int res = 0;
public int minCameraCover(TreeNode root) {
// 后续遍历自底向上
if(dfs(root) == 0) res++;
return res;
}
// 0 代表未被覆盖的情况;1:代表是摄像头 2:代表被覆盖,当节点为空的时候是做被覆盖
private int dfs(TreeNode root) {
// 空节点标记已被监控覆盖
if(root == null){
return 2;
}
int left = dfs(root.left);
int right = dfs(root.right);
// 情况1:左右节点都被覆盖
if(left == 2 && right == 2){
return 0;
}
// 情况2:左右节点至少有一个没被覆盖,则当前节点需要监控,并返回被监控覆盖
if(left == 0 || right == 0){
res++;
return 1;
}
// 情况3:左右节点至少有一个摄像头
if(left == 1 || right == 1){
return 2;
}
return -1;
}
15.7力扣: 1247. 交换字符使得字符串相同(中等)
第二次:3.18通过
第三次:3.30通过
class Solution {
public int minimumSwap(String s1, String s2) {
int xDiff = 0, yDiff = 0;
for(int i = 0; i < s1.length(); i++){
if(s1.charAt(i) == 'x' && s2.charAt(i) == 'y'){
xDiff++;
}else if(s1.charAt(i) == 'y' && s2.charAt(i) == 'x'){
yDiff++;
}
}
// 奇数不可能成立
if((xDiff + yDiff) % 2 == 1){
return -1;
}
return xDiff % 2 == 1 ? (xDiff + yDiff) / 2 + 1 : xDiff / 2 + yDiff / 2;
}
}
15.8力扣: 300. 最长递增子序列(中等)
第一次:3.20已通过
思路:加个例子就比较容易明白,比如序列是78912345,前三个遍历完以后tail是789,这时候遍历到1,就得把1放到合适的位置,于是在tail二分查找1的位置,变成了189(如果序列在此时结束,因为res不变,所以依旧输出3),再遍历到2成为129,然后是123直到12345
这题难理解的核心不在于算法难,而在于在于官方给的例子太拉了,遇不到这个算法真正要解决的问题,即没有我例子中1要代替7的过程
class Solution {
public int lengthOfLIS(int[] nums) {
int[] tails = new int[nums.length];
// 结尾处的长度
int res = 0;
for(int num : nums){
int i = 0, j = res;
// 找到插入的位置
while(i < j){
int mid = (i + j) / 2;
// 相同的话,找左侧第一个相同的位置
if(tails[mid] < num) i = mid + 1;
else j = mid;
}
// 在插入的位置当中,找到第一个大于num的位置,替换掉
tails[i] = num;
if(res == j) res++;
}
return res;
}
}
16.差分数组
16.1 力扣: 1094. 拼车(中等)
第二次:3.30未通过
- [[2,1,5],[3,3,7]], capacity = 4
- [[2,1,5],[3,3,7]], capacity = 5
- a 数组:a[i]表示车站索引 i 当前车上有多少人
- a[0] = 0, a[1] = 2, a[2] = 2, a[3] = 5, a[4] = 5, a[5] = 3, a[6] = 3, a[7] = 0
- 数组:d[i]表示车站索引 i 当前车上上车有多少人(其中上车人数为正数, 下车人数为负数)
- d[0] = 0, d[1] = 2, d[2] = 0, d[3] = 3, d[4] = 0, d[5] = -2, d[6] = 0, d[7] = -3
public class Solution {
public boolean carPooling(int[][] trips, int capacity) {
int[] d = new int[1001];
// 构建差分数组 d[]
for (int[] trip : trips) {
int t = trip[0], from = trip[1], to = trip[2];
d[from] += t;
d[to] -= t;
}
// 原始数组 a[i] --> 当前第 i 号站牌车上有多少乘客 可以用差分数组 d[] 累加得到
int s = 0;
for (int v : d) {
s += v;
if(s > capacity){
return false;
}
}
return true;
}
}
17.BFS遍历
17.1 力扣: 127. 单词接龙(困难)
第二次:3.30未通过
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
// 如果 endWord 不在 wordList 中,则无法转换,直接返回 0
if(!wordList.contains(endWord)){
return 0;
}
Set<String> set = new HashSet<>(wordList);
Deque<String> queue = new ArrayDeque<>();
queue.add(beginWord);
int depth = 1;
while (!queue.isEmpty()){
int size = queue.size();
// 当前这一轮 BFS
for(int i = 0; i < size; i++){
String currentWord = queue.poll();
if (currentWord.equals(endWord)) return depth; // 提前终止条件
char[] chars = currentWord.toCharArray();
// 遍历当前每一个字母
for(int j = 0; j < currentWord.length(); j++){
char original = chars[j];
// 用另外 25 个字母来替换当前这个字母
for(char c = 'a'; c <= 'z'; c++){
if(c == original) continue;
chars[j] = c;
String newWord = new String(chars);
if(set.contains(newWord)){
queue.add(newWord);
set.remove(newWord);
}
}
chars[j] = original; // 恢复字符
}
}
depth++;
}
// 无法到达
return 0;
}
}
17.2 力扣: 529. 扫雷游戏(中等)
第二次:3.30通过
思路:BFS使用队列进行判断
- 第一个是雷
M
直接设置为X
标记为炸了 - 不是雷进行BFS遍历,遍历周围八个角,记录周围地雷个数
- 如果周围地雷个数不为0直接放上数字即可
- 如果周围没雷,直接标记为
B
即为安全,然后对周围八个入队,然后标记 访问即可
class Solution {
int[][] dir = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}, {-1, -1}, {-1, 1}, {1, -1}, {1, 1}};
public char[][] updateBoard(char[][] board, int[] click) {
int x = click[0], y = click[1];
// 挖出地雷
if(board[x][y] == 'M'){
board[x][y] = 'X';
}else{
// 挖的不是地雷
bfs(board, click[0], click[1]);
}
return board;
}
private void bfs(char[][] board, int i, int j) {
Queue<int[]> queue = new LinkedList<>();
boolean[][] visited = new boolean[board.length][board[0].length];
queue.offer(new int[]{i, j});
visited[i][j] = true;
while (!queue.isEmpty()){
int[] pos = queue.poll();
// count记录雷的个数
int count = 0,x = pos[0], y = pos[1];
for(int[] d : dir){
int nx = x + d[0];
int ny = y + d[1];
if(nx >= 0 && nx < board.length && ny >= 0 && ny < board[0].length && board[nx][ny] == 'M'){
count++;
}
}
// 周围有雷,不能把他周围的加入到队列当中
if(count > 0){
board[x][y] = (char) (count + '0');
}else{
// 周围没雷,标记为无雷,并且有机会进入队列
board[x][y] = 'B';
for(int[] d : dir){
int nx = x + d[0];
int ny = y + d[1];
// 周围处于未被访问且在棋盘当中,并且不是雷的化可以进入队列
if(nx >= 0 && nx < board.length && ny >= 0 && ny < board[0].length && board[nx][ny] == 'E' && !visited[nx][ny]){
queue.offer(new int[]{nx, ny});
visited[nx][ny] = true;
}
}
}
}
}
}
17.3 力扣: 1263. 推箱子(困难)
第二次:3.30未通过
class Solution {
public int minPushBox(char[][] grid) {
int m = grid.length;
int n = grid[0].length;
//获取玩家的起始位置、箱子的起始位置和目标位置
int startX = -1, startY = -1, boxX = -1, boxY = -1, tx = -1, ty = -1;
int[][] dirs = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(grid[i][j] == 'S'){
startX = i;
startY = j;
}else if(grid[i][j] == 'B'){
boxX = i;
boxY = j;
}else if(grid[i][j] == 'T'){
tx = i;
ty = j;
}
}
}
Deque<Node> queue = new LinkedList<>();
boolean[][][][] visited = new boolean[m][n][m][n];
visited[startX][startY][boxX][boxY] = true; // 初识箱子和人的状态已经标记
queue.offer(new Node(startX, startY, boxX, boxY, 0));
while(!queue.isEmpty()) {
Node cur = queue.pollFirst(); // 从头出队
// 找到最终结果了
if (cur.bx == tx && cur.by == ty) {
return cur.step;
}
for (int[] dir : dirs) {
int newPx = cur.px + dir[0], newPy = cur.py + dir[1], newBx = cur.bx, newBy = cur.by, newStep = cur.step;
// 如果当前人的位置到了箱子的位置,那么箱子就要移动,步数当然也需要加 1
if (newPx == cur.bx && newPy == cur.by) {
newBx += dir[0];
newBy += dir[1];
newStep++;
}
// 不能越界,不能进墙,不能被访问
if (newPx < 0 || newPx == m || newPy < 0 || newPy == n || newBx < 0 || newBx == m || newBy < 0
|| newBy == n || grid[newPx][newPy] == '#' || grid[newBx][newBy] == '#'
|| visited[newPx][newPy][newBx][newBy])
continue;
Node newNode = new Node(newPx, newPy, newBx, newBy, newStep);
boolean dummy = newStep == cur.step ? queue.offerFirst(newNode) : queue.offerLast(newNode);
visited[newPx][newPy][newBx][newBy] = true;
}
}
return -1;
}
class Node{
int px;
int py;
int bx;
int by;
int step;
public Node(int px, int py, int bx, int by, int step) {
this.px = px;
this.py = py;
this.bx = bx;
this.by = by;
this.step = step;
}
}
}
17.4 力扣: 934. 最短的桥(中等)
第二次:3.30未完成
解法1:
思路:先找到第一个岛屿,然后将第一个发现的岛屿所有的节点都进行记录,并且都将其改为-1
,即为标志已经被访问过了。既然要访问整个岛屿的全部节点,那么肯定要出队入队,因此需要一个集合保存他。然后再把集合当中的元素放到队列当中。
第二步:开始 bfs 遍历步数,每一轮将当前步数层级的全部出队,然后将周围的海域标记为 -1 然后入队,如果找到第一个 1 即为陆地,直接返回当前的步数层级。否则 step++; 如果到最后都没有找到并且所有都已经被标记了,那么返回 0。
class Solution {
public int shortestBridge(int[][] grid) {
int n = grid.length;
int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
List<int[]> island = new ArrayList<>();
// 岛屿陆地坐标
Queue<int[]> queue = new ArrayDeque<>();
//
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
// 如果当前是岛屿
if(grid[i][j] == 1){
// 入队
queue.offer(new int[]{i, j});
// 标记已经访问
grid[i][j] = -1;
// 将整个岛保存到集合中,方便下次再回溯
while (!queue.isEmpty()){
// 岛屿出队
int[] cur = queue.poll();
int x = cur[0], y = cur[1];
// 岛屿保存到集合中,方便下次再回溯
island.add(cur);
for(int[] dir: dirs){
int nx = x + dir[0], ny = y + dir[1];
if(nx >= 0 && nx < n && ny >= 0 && ny < n && grid[nx][ny] == 1){
queue.offer(new int[]{nx, ny});
grid[nx][ny] = -1;
}
}
}
// 遍历集合,回溯岛屿
for(int[] cell : island){
queue.offer(cell);
}
// 步数
int step = 0;
while(!queue.isEmpty()){
int size = queue.size();
// 遍历这个岛中的每一个节点
for(int k = 0; k < size; k++){
int[] cell = queue.poll();
// 当前岛中的一个节点
int x = cell[0], y = cell[1];
for(int[] dir : dirs){
// 四周节点
int nx = x + dir[0], ny = y + dir[1];
// 在整个地图内部
if(nx >= 0 && nx < n && ny >= 0 && ny < n){
// 属于海域
if(grid[nx][ny] == 0){
// 海域加入队列
queue.offer(new int[]{nx, ny});
// 标记为 -1, 代表已经访问过
grid[nx][ny] = -1;
}else if(grid[nx][ny] == 1){
//找到下一块陆地了
return step;
}
}
}
}
// 遍历完一次的 bfs, 步数加一
step++;
}
}
}
}
return 0;
}
}
解法2:DFS+BFS
思路:在第一次碰到小岛构造初始队列的时候,将BFS构造并且转换为 -1 的同时,并且将出队的入到list当中然后,在全部都遍历一遍之后进行入队。深度优先搜索就没有这个问题。下面是代码:
class Solution {
public int shortestBridge(int[][] grid) {
int n = grid.length;
int[][] dirs = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
if(grid[i][j] == 1){
Queue<int[]> queue = new LinkedList<>();
dfs(i, j, grid, queue);
int step = 0;
while(!queue.isEmpty()){
int size = queue.size();
// 广度优先遍历
for(int k = 0; k < size; k++){
int[] cur = queue.poll();
int x = cur[0], y = cur[1];
// 遍历四周
for(int[] dir : dirs){
int nx = x + dir[0], ny = y + dir[1];
if(nx >= 0 && nx < n && ny >= 0 && ny < n){
if(grid[nx][ny] == 0){
queue.offer(new int[]{nx, ny});
grid[nx][ny] = -1;
}if(grid[nx][ny] == 1){
return step;
}
}
}
}
step++;
}
}
}
}
return 0;
}
// 深度优先搜索用来构造queue
private void dfs(int x, int y, int[][] grid, Queue<int[]> queue) {
if(x < 0 || x >= grid.length || y < 0 || y >= grid[0].length || grid[x][y] != 1){
return;
}
queue.offer(new int[]{x, y});
grid[x][y] = -1;
dfs(x + 1, y, grid, queue);
dfs(x - 1, y, grid, queue);
dfs(x, y + 1, grid, queue);
dfs(x, y - 1, grid, queue);
}
}
17.5 力扣:815. 公交路线(困难)
第二次:3.30未通过
思路:
- 第一步:先构建车站路线图,用
Map<Integer, List<Integer>>
表示, 其中图的节点为车站,公交线路为无向边。 - 第二步: BFS遍历,用
Map<Integer, Integer> dis = new HashMap<>();
记录车站到 source 的距离, 每次出队一个车站,遍历经过车站的所有公交车(遍历过直接清空),然后再对公交车上的车站入队并且标记dis
的距离 + 1。
class Solution {
public int numBusesToDestination(int[][] routes, int source, int target) {
// 记录经过车站 x 的公交路线, key 为车站,value 为经过车站的公交路线
Map<Integer, List<Integer>> stopToBuses = new HashMap<>();
// 构建车站公交线路图 key 是站点, value 为公交车
for(int i = 0; i < routes.length; i++){
// 车站 x
for(int x : routes[i]){
stopToBuses.computeIfAbsent(x, Key -> new ArrayList<>()).add(i);
}
}
if(!stopToBuses.containsKey(source) || !stopToBuses.containsKey(target)){
return source != target ? -1 : 0;
}
// BFS
Map<Integer, Integer> distance = new HashMap<>();
// 记录当前车站 x 的最远距离
distance.put(source, 0);
Deque<Integer> queue = new ArrayDeque<>();
queue.add(source);
// bfs 遍历
while (!queue.isEmpty()){
int x = queue.poll();
int disX = distance.get(x);
// 找到车站
if(x == target){
return disX;
}
// 遍历公交路线
for(int i : stopToBuses.get(x)){
if(routes[i] != null){
// y 是这条路线 i 途径的车站
for(int y : routes[i]){
if(!distance.containsKey(y)){
// x车站上车, y车站下车
distance.put(y, disX + 1);
queue.offer(y);
}
}
}
routes[i] = null; // 清除这条公交线路
}
}
return -1;
}
}
17.6 力扣: 332. 重新安排行程(困难)
思路:题目描述假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次且只能用一次。 并且需要返回字典最小行程顺序:
- 用
Map<String, PriorityQueue<String>
构建图的临接表 - DFS按照图遍历的深度优先进行遍历,按照优先队列的顺序依次遍历
class Solution {
public List<String> findItinerary(List<List<String>> tickets) {
// 使用Map构建邻接矩阵
Map<String, PriorityQueue<String>> map = new HashMap<>();
for (List<String> ticket : tickets) {
String from = ticket.get(0);
String to = ticket.get(1);
map.putIfAbsent(from, new PriorityQueue<>());
map.get(ticket.get(0)).offer(to);
}
List<String> res = new ArrayList<>();
dfs("JFK", map, res);
return res;
}
private void dfs(String airport, Map<String, PriorityQueue<String>> map, List<String> res) {
PriorityQueue<String> destinations = map.get(airport);
// 按字典顺序排序,直到访问完 airport 的所有临接节点
while(destinations != null && !destinations.isEmpty()){
dfs(destinations.poll(), map, res);
}
res.add(0, airport);
}
}
17.6 力扣: 337. 打家劫舍 III(中等)
思路:树形dp
- 每个树节点有两个状态。状态1 : 抢当前节点的最大值,状态二:不抢当前节点的最大值
- 后序遍历,自底向上遍历子树状态
- 终止条件: 当遍历到空节点 返回(0, 0)
- 状态转移方程:
-
选当前节点 = root.val + 所有子节点不选情况的和
-
不选当前节点 = 所有子节点选或者不选的情况
class Solution {
public int rob(TreeNode root) {
int[] res = dfs(root);
return Math.max(res[0], res[1]); // 0: 不抢根节点,1:抢根节点
}
private int[] dfs(TreeNode root) {
if (root == null) {
return new int[]{0, 0};
}
int[] left = dfs(root.left);
int[] right = dfs(root.right);
// 状态转移方程1:当前节点选
int rob = root.val + left[0] + right[0];
// 状态转移方程2:当前节点不选,那么左右子节点可选可不选,选各自的最大值即可
int notRob = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
return new int[]{notRob, rob};
}
}
18.并查集
18.1 力扣: 547. 省份数量(中等)
class Solution {
public int findCircleNum(int[][] isConnected) {
int n = isConnected.length;
// 初始化并查集
UnionFind unionFind = new UnionFind(n);
// 遍历每条边,由于是 无向图,所以只需要遍历一半即可
for(int i = 0; i < n; i++){
for(int j = i + 1; j < n; j++){
if(isConnected[i][j] == 1){
unionFind.union(i, j);
}
}
}
return unionFind.size;
}
// 并查集
class UnionFind{
int[] roots;
int size;
// 初始化并查集
public UnionFind(int n){
roots = new int[n];
size = n;
for(int i = 0; i < n; i++){
roots[i] = i;
}
}
public int find(int x){
if(roots[x] == x){
return x;
}
return roots[x] = find(roots[x]);
}
public void union(int x, int y){
int rootX = find(x);
int rootY = find(y);
if(rootX != rootY){
roots[rootX] = rootY;
size--;
}
}
}
}
18.2 力扣: 684. 冗余连接(中等)
class Solution {
int[] father;
// 初始化并查集
public void init() {
for (int i = 1; i < father.length; i++) {
father[i] = i;
}
}
public int[] findRedundantConnection(int[][] edges) {
father = new int[edges.length + 1];
init();
for(int i = 0; i < edges.length; i++){
if(find(edges[i][0]) == find(edges[i][1])){
return edges[i];
}else{
union(edges[i][0], edges[i][1]);
}
}
return null;
}
private int find(int x) {
if(x == father[x]) return x;
return father[x] = find(father[x]); // 路径压缩
}
private void union(int x, int y) {
int fx = find(x);
int fy = find(y);
if(fx != fy){
father[fx] = fy;
}
}
}
18.2 力扣: 200. 岛屿的数量中等)
解法:并查集
public class Solution {
int[] parent;
private void init() {
for (int i = 0; i < parent.length; i++) {
parent[i] = i;
}
}
private int find(int x) {
return parent[x] == x ? x : find(parent[x]);
}
private void union(int x, int y) {
int xRoot = find(x);
int yRoot = find(y);
if (xRoot != yRoot) {
parent[xRoot] = yRoot;
}
}
public int numIslands(char[][] grid) {
if(grid == null || grid.length == 0){
return 0;
}
int rows = grid.length;
int cols = grid[0].length;
// 构建并初始化并查集
parent = new int[rows * cols];
init();
// 遍历网格,合并相邻的岛屿
for(int i = 0; i < rows; i++){
for(int j = 0; j < cols; j++){
if(grid[i][j] == '1'){
int currentIndex = i * cols + j;
// 检查四个相邻的方向
if(i > 0 && grid[i - 1][j] == '1'){
int upIndex = (i - 1) * cols + j;
union(currentIndex, upIndex);
}
if(j > 0 && grid[i][j - 1] == '1'){
int leftIndex = i * cols + j - 1;
union(currentIndex, leftIndex);
}
}
}
}
// 统计岛屿数量,统计不同根节点的数量
int islandCount = 0;
for(int i = 0; i <rows; i++){
for(int j = 0; j < cols; j++){
if(grid[i][j] == '1' && find(i * cols + j) == i * cols + j){
islandCount++;
}
}
}
return islandCount;
}
}
18.3 力扣: 924. 尽量减少恶意软件的传播(困难)
灵神解法 判断联通块中初始感染者的数量,如果有两个及以上删除了也没用,最后的结果就是联通块当中只有一个节点,且联通块节点最多,如果上述条件均符合,且初始感染者个数不只一个,记录值最小的初始感染者。
class Solution {
private int nodeId, size;
public int minMalwareSpread(int[][] graph, int[] initial) {
int n = graph.length;
boolean[] visited = new boolean[n];
boolean[] isInitial = new boolean[n];
int mn = Integer.MAX_VALUE;
// 标记所有初始感染节点,并找到最小的初始感染节点编号
for(int x : initial){
isInitial[x] = true;
mn = Math.min(mn, x);
}
int ans = -1; // 联通块当中只有一个初始感染者的情况下最小的节点索引值
int maxSize = 0;
// 遍历所有初始感染节点,计算移除每个节点后可以阻止的感染范围
for(int x : initial){
// 当前感染者已经被访问过,跳过
if(visited[x]){
continue;
}
// 记录当前联通块当中,初始感染者的id
nodeId = -1;
size = 0;
// nodeId 记录下第一个非初始感染节点的编号
dfs(x, graph, visited, isInitial);
// 更新最优解:选择能够阻止最大感染范围的节点,如果有多个,选择编号最小的
if(nodeId >= 0 && (size > maxSize || (size == maxSize && nodeId < ans))){
ans = nodeId;
maxSize = size;
}
}
// 如果所有联通块都有至少两个初始感染者,返回第一个最小初始感染者的编号,联通块中只有一个初始感染者的情况
return ans < 0? mn : ans;
}
private void dfs(int x, int[][] graph, boolean[] visited, boolean[] isInitial) {
visited[x] = true;
size++;
// 标记初始感染节点的状态,
// 转态机:当在联通块当中有第一个初始感染节点时状态为 -1 -> x,如果出现第二个初始感染者转态就变为 -2,
// 以后一直出现一直保持-2的状态,那么就不会记录这个联通块内的初始感染者
if(nodeId != -2 && isInitial[x]){
nodeId = nodeId == -1 ? x : -2;
}
// 递归遍历所有相邻节点
for(int y = 0; y < graph[x].length; y++){
if(graph[x][y] == 1 && !visited[y]){
dfs(y, graph, visited, isInitial);
}
}
}
}