刷完leedcode 100题

开始时间:2025.3.14

字母异位词分组P49

方法:分组字母异位词(Group Anagrams)

该方法使用 哈希表(HashMap) 进行分组,将每个单词按照字母排序后作为键,值是属于该组的单词列表。


代码注释


   public static List<List<String>> groupAnagrams(String[] strs) {
        // 使用 HashMap 存储字母排序后的字符串作为 key,对应的异位词列表作为 value
        Map<String, List<String>> map = new HashMap<>();
        
        // 遍历字符串数组
        for (String str : strs) {
            // 将字符串转换为字符数组,并进行排序
            char[] strArr = str.toCharArray();
            Arrays.sort(strArr);
            // 排序后的字符串作为 key
            String sortedStr = new String(strArr);

            // 获取当前 key 对应的异位词列表
            List<String> strList = map.get(sortedStr);
            
            if (strList != null) {
                // 如果 key 已存在,直接加入对应列表
                strList.add(str);
            } else {
                // 如果 key 不存在,新建一个列表并加入 map
                ArrayList<String> newList = new ArrayList<>();
                newList.add(str);
                map.put(sortedStr, newList);
            }
        }

        // 提取 HashMap 中所有的值(即分组结果)
        List<List<String>> res = new ArrayList<>();
        for (Map.Entry<String, List<String>> entry : map.entrySet()) {
            res.add(entry.getValue());
        }
        
        return res;
    }

    public static void main(String[] args) {
        String[] strs = {"eat", "tea", "tan", "ate", "nat", "bat"};
        System.out.println(groupAnagrams(strs));
    }
}

思路分析

  1. 使用哈希表(Map<String, List<String>>)存储分组

    • key:将字符串排序后得到的字符串
    • value:所有属于该 key 的字母异位词列表
  2. 遍历 strs 数组,对每个字符串进行处理

    • 将字符串转换为字符数组,并使用 Arrays.sort() 排序
    • 排序后的字符串作为 key
    • 检查 map 是否已有该 key
      • 若存在,则直接添加到对应 List<String>
      • 若不存在,则新建 ArrayList<String> 并存入 map
  3. 返回结果

    • 遍历 map,提取所有 List<String> 作为最终结果

时间复杂度分析

假设输入数组长度为 n,每个字符串的平均长度为 m

  • 排序字符串Arrays.sort(strArr) 需要 O(m log m)
  • 遍历字符串数组:遍历 n 个字符串,每个字符串的处理复杂度是 O(m log m)
  • 哈希表操作
    • put()get() 的均摊时间复杂度为 O(1)
    • 存储 n 个字符串,整体 O(n)

总的时间复杂度:
O(n * m log m)


空间复杂度分析

  • 哈希表存储 n 个字符串O(n)
  • 每个字符串存储 m 个字符,共 O(n * m)
  • 最终返回的 List<List<String>> 也占用 O(n * m)

总的空间复杂度:
O(n * m)


示例运行

输入

String[] strs = {"eat", "tea", "tan", "ate", "nat", "bat"};

输出

[[eat, tea, ate], [tan, nat], [bat]]

总结
核心思路:利用哈希表存储排序后的字符串来分组
时间复杂度O(n * m log m)(排序占主导)
空间复杂度O(n * m)(哈希表存储所有字符串)

这种方法适用于较短字符串的分组问题,在 m 较大时可以使用计数数组法优化排序部分,降低到 O(n * m) 🚀

方法二

/**
 * 分组字母异位词(Anagrams)。
 * 
 * @param strs 输入字符串数组
 * @return 按字母异位词分组后的列表
 * 
 * 思路:
 * 1. 创建一个 `HashMap<String, List<String>>`,用于存储排序后的字符串作为 key,对应的字母异位词列表作为 value。
 * 2. 遍历输入字符串数组:
 *    - 将字符串转换为字符数组并排序,使得异位词具有相同的 key。
 *    - 使用 `getOrDefault(key, new ArrayList<>())` 获取当前 key 对应的列表,如果 key 不存在,则返回一个新的 `ArrayList<>`。
 *    - 将当前字符串添加到该列表中,并更新 `map`。
 * 3. 最后,将 `map` 中的所有 `value`(即字母异位词分组)转换为 `ArrayList<List<String>>` 并返回。
 * 
 * 时间复杂度:
 * - 对每个字符串排序的时间复杂度为 O(k log k)(其中 k 为字符串长度)。
 * - 遍历所有字符串的时间复杂度为 O(n)(其中 n 为字符串个数)。
 * - 整体时间复杂度为 O(n * k log k)。
 * 
 * 空间复杂度:
 * - 主要取决于 `map` 的存储,最坏情况下 O(n * k)。
 */
public static List<List<String>> groupAnagrams3(String[] strs) {
    // 创建一个 HashMap 存储排序后的字符串作为 key,对应的字母异位词列表作为 value
    Map<String, List<String>> map = new HashMap<>();
    
    // 遍历所有字符串
    for (String str : strs) {
        // 将字符串转换为字符数组,并进行排序
        char[] charArray = str.toCharArray();
        Arrays.sort(charArray);
        String key = new String(charArray); // 排序后的字符串作为 key

        // 获取或创建存储相同 key 的字符串列表
        List<String> list = map.getOrDefault(key, new ArrayList<>());
        list.add(str); // 添加当前字符串
        map.put(key, list); // 更新 HashMap
    }
    
    // 返回所有字母异位词分组
    return new ArrayList<>(map.values());
}


最长连续序列P128

解题思路:
在数组中可能存在多个连续的序列,我们需要找到每个序列的起点进行扩展。使用 Set 存储数组元素,以便 O(1) 时间复杂度进行查找,避免重复计算。对于每个数 num,如果 num - 1 存在,则说明 num 不是某个连续序列的起点,直接跳过。否则,从 num 开始向后查找,计算最长的连续序列长度。此外,我们也可以选择最大的数作为起点,进行递减查找,思路相同。

import java.util.HashSet;
import java.util.Set;

public class Solution {
    /**
     * 计算数组中最长的连续序列长度
     * @param nums 整数数组
     * @return 最长连续序列的长度
     */
    public static int longestConsecutive(int[] nums) {
        int max_len = 0; // 记录最长连续序列的长度
        Set<Integer> set = new HashSet<>(); // 使用 HashSet 存储数组中的所有元素,以便快速查找
        
        // 将所有数字存入 HashSet,以便 O(1) 时间复杂度内查找
        for (int num : nums) {
            set.add(num);
        }

        // 遍历数组,寻找最长连续序列
        for (int num : nums) {
            int cur_len = 1; // 记录当前连续序列的长度

            // 如果 num-1 存在,说明 num 不是序列的起点,跳过该数
            if (set.contains(num - 1)) {
                continue;
            }

            // num 作为连续序列的起点,向后寻找连续的数
            while (set.contains(num + 1)) {
                num++;      // 继续寻找下一个连续的数
                cur_len++;  // 增加当前连续序列的长度
            }

            // 更新最长连续序列的长度
            max_len = Math.max(max_len, cur_len);
        }

        return max_len;
    }
}

时间复杂度分析

  • 构建 Set:我们首先遍历 nums 并将其存入 Set,该操作的时间复杂度为 O(n)
  • 遍历 nums 并查找最长序列:对于每个 num,如果 num - 1 存在,则跳过;否则,我们在 Set 中查找 num + 1, num + 2, ... 直到找不到为止。
    • 每个数字最多被访问两次(一次插入 Set,一次作为起点查找),因此整体复杂度仍然是 O(n)

最终时间复杂度: O(n)


空间复杂度分析

  • 使用 Set 存储所有元素,占用 O(n) 额外空间。
  • 仅使用常数级别的额外变量(如 max_lencur_lennum 等),占用 O(1) 额外空间。

最终空间复杂度: O(n)

遇到的问题: 第二次循环使用的是原数组进行遍历,造成重复的元素循环便利,造成代码超时,应该使用set进行遍历

三数之和 P15

方法一双指针

下面是 三数之和 (Three Sum)双指针解法,包括详细注释和时间、空间复杂度分析。


代码(双指针法)

import java.util.*;

public class ThreeSumOptimized {
    /**
     * 计算数组中所有不重复的三元组,使得它们的和为 0
     * @param nums 输入整数数组
     * @return 所有满足条件的三元组
     */
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(nums);  // 先排序 O(n log n),便于去重和双指针查找

        int n = nums.length;
        for (int i = 0; i < n - 2; i++) { // 固定第一个数 O(n)
            // **去重**: 如果当前数与前一个数相同,跳过,避免重复答案
            if (i > 0 && nums[i] == nums[i - 1]) continue;

            int left = i + 1, right = n - 1; // 双指针初始化
            while (left < right) { // O(n)
                int sum = nums[i] + nums[left] + nums[right];

                if (sum == 0) {
                    // 找到一个三元组
                    res.add(Arrays.asList(nums[i], nums[left], nums[right]));

                    // **去重**:跳过重复的 left 和 right 值
                    while (left < right && nums[left] == nums[left + 1]) left++;
                    while (left < right && nums[right] == nums[right - 1]) right--;

                    // 移动指针继续寻找其他解
                    left++;
                    right--;
                } else if (sum < 0) {
                    left++;  // 需要更大的值
                } else {
                    right--; // 需要更小的值
                }
            }
        }
        return res;
    }

    public static void main(String[] args) {
        ThreeSumOptimized solution = new ThreeSumOptimized();
        int[] nums = {-1, 0, 1, 2, -1, -4};
        System.out.println(solution.threeSum(nums)); 
        // 输出: [[-1, -1, 2], [-1, 0, 1]]
    }
}

时间 & 空间复杂度分析

时间复杂度:O(n²)

  1. 排序: Arrays.sort(nums); 需要 O(n log n)
  2. 遍历 nums[i]: 外层 for 循环需要 O(n)
  3. 双指针查找: 每次 while (left < right) 运行 O(n),整体 O(n²)
  4. 总时间复杂度: O(n log n) + O(n²) ≈ O(n²)(排序的时间复杂度相比 O(n²) 可忽略)。

空间复杂度:O(1)

  1. 排序所需空间: 由于 Arrays.sort(nums)原地排序,不需要额外空间(O(1))。
  2. 返回结果 res: 最坏情况下可能存储所有三元组(O(n))。
  3. 总空间复杂度: 主要由 res 结果集占用,最坏情况下 O(n),但如果只考虑额外空间消耗,则是 O(1)

2025.3.18

方法二回溯递归(超时)

   // 结果集,存储所有满足条件的三元组
    private static List<List<Integer>> res;

    /**
     * 主方法:求解三数之和
     * @param nums 输入的整数数组
     * @return 结果集,包含所有不重复的三元组
     */
    public static List<List<Integer>> threeSum(int[] nums) {
        res = new ArrayList<>();
        Arrays.sort(nums);  // **排序 O(n log n),保证去重有效**
        backTracking(nums, new ArrayList<>(), 0, 0);
        return res;
    }

    /**
     * 回溯算法:寻找所有满足条件的三元组
     * @param nums 排序后的数组
     * @param list 当前路径,存储已选择的数
     * @param i 当前递归的起始索引
     * @param sum 当前路径中元素的和
     */
    public static void backTracking(int[] nums, List<Integer> list, int i, int sum) {
        // 终止条件:如果已经选了 3 个数
        if (list.size() == 3) {
            if (sum == 0) { // 只有和为 0 才加入结果集
                res.add(new ArrayList<>(list));
            }
            return; // 终止递归
        }

        // 遍历数组,尝试加入新的数
        for (int j = i; j < nums.length; j++) {
            // **去重**: 只在同一层中跳过重复的元素
            if (j > i && nums[j] == nums[j - 1]) continue;

            // 选择当前元素
            list.add(nums[j]);
            sum += nums[j];

            // 递归探索下一层
            backTracking(nums, list, j + 1, sum);

            // 回溯(撤销选择)
            sum -= nums[j];
            list.remove(list.size() - 1);
        }
    }

时间 & 空间复杂度分析

时间复杂度:O(n³)

  1. 排序: Arrays.sort(nums); 需要 O(n log n)
  2. 回溯搜索:
    • 第一层 for 循环有 O(n) 次迭代。
    • 第二层递归每次执行 O(n) 次,形成 O(n²)
    • 第三层递归也会执行 O(n) 次,形成 O(n³) 复杂度。
    • 但是由于 sum > 0 || sum < 0 会剪枝,实际复杂度稍低,但仍接近 O(n³)
  3. 总复杂度: O(n log n) + O(n³) ≈ O(n³)(排序的影响可以忽略)。

空间复杂度:O(n)

  1. 递归调用栈: 在最坏情况下,递归深度为 3,但在大 n 情况下,递归栈最多可能达到 O(n)
  2. list 临时存储: list 的最大长度为 3,但递归过程中需要额外的存储,最坏情况是 O(n)
  3. res 结果存储: 最坏情况下可能存储 O(n²) 个解。
  4. 总复杂度: O(n) + O(n²) = O(n²)

为什么这个方法会超时?

回溯 (O(n³)) 太慢了,在大数据量情况下容易超时
没有剪枝优化,所有情况都暴力枚举
三数之和问题可以用 双指针优化为 O(n²)


推荐优化:使用双指针

如果你希望提高效率,建议使用 双指针法(时间复杂度 O(n²)),如下所示:

import java.util.*;

public class ThreeSumOptimized {
    public static List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(nums);  // O(n log n)

        int n = nums.length;
        for (int i = 0; i < n - 2; i++) { // O(n)
            if (i > 0 && nums[i] == nums[i - 1]) continue; // 去重

            int left = i + 1, right = n - 1;
            while (left < right) { // O(n)
                int sum = nums[i] + nums[left] + nums[right];

                if (sum == 0) {
                    res.add(Arrays.asList(nums[i], nums[left], nums[right]));

                    while (left < right && nums[left] == nums[left + 1]) left++; // 去重
                    while (left < right && nums[right] == nums[right - 1]) right--; // 去重

                    left++;
                    right--;
                } else if (sum < 0) {
                    left++;
                } else {
                    right--;
                }
            }
        }
        return res;
    }
}

总结

方法时间复杂度空间复杂度适用场景备注
回溯法O(n³)O(n²)小规模数据易超时
双指针法O(n²)O(1)大规模数据推荐!

接雨水P42

代码1

// 方法1:使用单调栈计算能存储的雨水量
    public static int trap(int[] height) {
        int sum = 0;
        Stack<Integer> stack = new Stack<>(); // 维护一个单调递减栈
        
        for (int i = 0; i < height.length; i++) {
            // 当当前高度大于栈顶高度时,说明形成了一个洼地,可以存水
            while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
                Integer idx = stack.pop(); // 取出洼地的底部索引
                
                if (stack.isEmpty()) { // 如果栈为空,说明没有左边界,无法存水
                    break;
                }
                
                Integer j = stack.peek(); // 取左边界的索引
                int h = Math.min(height[i], height[j]) - height[idx]; // 计算洼地的有效高度
                sum += h * (i - j - 1); // 计算当前洼地的存水量,并累加
            }
            
            stack.push(i); // 将当前索引入栈
        }
        return sum;
    }
  

代码2

 // 方法2:使用动态规划计算能存储的雨水量
    public static int trap(int[] height) {
        int n = height.length;
        if (n == 0) {
            return 0;
        }

        // 预计算左侧最大高度数组
        int[] leftMax = new int[n];
        leftMax[0] = height[0];
        for (int i = 1; i < n; ++i) {
            leftMax[i] = Math.max(leftMax[i - 1], height[i]);
        }
        System.out.println("Left Max: " + Arrays.toString(leftMax));

        // 预计算右侧最大高度数组
        int[] rightMax = new int[n];
        rightMax[n - 1] = height[n - 1];
        for (int i = n - 2; i >= 0; --i) {
            rightMax[i] = Math.max(rightMax[i + 1], height[i]);
        }
        System.out.println("Right Max: " + Arrays.toString(rightMax));

        // 计算每个柱子上可以存储的雨水量
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            ans += Math.min(leftMax[i], rightMax[i]) - height[i];
        }
        return ans;

第一个方法 trap(基于单调栈)

这个方法使用 单调递减栈 来计算能存储的雨水量。
工作原理:

  1. 遍历数组中的每个元素(即高度 height[i]),并尝试找到当前元素 height[i] 能够“填满”的区域。
  2. height[i] 大于栈顶元素(意味着有可能形成洼地存水):
    • 弹出栈顶元素 idx,它表示低洼区域的底部。
    • 如果弹出后栈为空,说明没有左边界,无法形成洼地,继续遍历。
    • 取栈顶 j 作为左边界,计算能存的水:高度 = 两边最矮的墙高度 - 洼地的高度宽度 = i - j - 1
    • 把计算出的雨水量加到 sum 里。
  3. 最后返回 sum,即总存水量。

总结:
它是按列计算雨水,每次计算当前柱子 height[i] 作为右边界时,看看之前的洼地能存多少水,并在过程中一点一点地累加。


第二个方法 trap2(基于动态规划)

这个方法采用 前缀最大值 + 后缀最大值 来计算能存的雨水量。
工作原理:

  1. 计算左侧最大值数组 leftMax[]
    • leftMax[i] 表示从左到 i 位置的最高柱子
  2. 计算右侧最大值数组 rightMax[]
    • rightMax[i] 表示从右到 i 位置的最高柱子
  3. 计算 i 位置的存水量
    • water[i] = min(leftMax[i], rightMax[i]) - height[i]
    • 这表示,在 i 位置能存的水取决于左右两侧的最矮墙壁,减去当前高度 height[i]
  4. 累加所有位置的水量,返回结果。

总结:
它是按行计算雨水,即每个 i 位置可以存多少水是独立计算的,然后总和得到结果。


对比

方法计算方式额外空间适用场景
单调栈 (trap)动态处理每个位置的存水情况O(n)(栈的存储)适用于实时处理数据,空间稍优
动态规划 (trap2)预计算左最大 & 右最大,然后遍历一次求雨水量O(n)(两个辅助数组)适用于一次性计算,时间稳定

核心区别:

  • trap(单调栈)是 动态寻找洼地,边遍历边处理存水问题,每个 i 只遍历一次栈结构(更接近实际流体的填充过程)。
  • trap(动态规划)是 提前计算出左右最高墙,再遍历一次统一计算每个位置的存水量(更直观但需要额外存储空间)。

两者在时间复杂度上都是 O(n),但 trap2 额外用了 O(n) 的空间来存 leftMax[]rightMax[],而 trap 只用了一个栈(通常比数组更节省空间)。

你更喜欢哪种方法呢?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值