Leetcode Hot 100 题目详解-哈希表

1. 两数之和 - 力扣(LeetCode)

java 复制代码
class Solution {
    // 1. 定义一个名为 twoSum 的公共方法,接收整数数组 nums 和整数目标值 target,返回一个整数数组(即两个索引)。
    public int[] twoSum(int[] nums, int target) {
        // 2. 创建一个 HashMap,名为 hashtable。
        //    键 (Key) 用于存储数组中的元素值 (Integer),值 (Value) 用于存储该元素对应的数组下标 (Integer)。
        Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();
        
        // 3. 使用 for 循环遍历数组 nums,变量 i 表示当前元素的索引。
        for (int i = 0; i < nums.length; ++i) {
            // 4. 检查哈希表中是否存在一个键,其值等于 target - nums[i]。
            //    target - nums[i] 就是与当前元素 nums[i] 配对的目标值(补数)。
            if (hashtable.containsKey(target - nums[i])) {
                // 5. 如果哈希表中存在这个补数,说明找到了匹配的一对。
                //    返回一个新的整数数组,包含两个索引:
                //    第一个索引是哈希表中存储的补数对应的值(即补数在原数组中的下标),
                //    第二个索引是当前元素的下标 i。
                return new int[]{hashtable.get(target - nums[i]), i};
            }
            // 6. 如果哈希表中没有找到匹配的补数,就将当前元素 nums[i] 作为键,其索引 i 作为值,存入哈希表。
            //    这样,当后续遍历到其他元素时,就可以在这个表中查询它。
            hashtable.put(nums[i], i);
        }
        // 7. 如果循环结束仍未找到任何匹配对(根据题目描述,这种情况实际上不会发生,因为题目保证有解),
        //    则返回一个空数组。
        return new int[0];
    }
}

核心思路与注意点

  • 核心思路 :这是一次遍历+哈希表查找的解法。它牺牲了空间复杂度(使用哈希表)来换取时间复杂度(从O(n²)降到O(n)) 。在遍历数组时,对于每个元素nums[i],只需以O(1)的时间复杂度检查其"补数"target - nums[i]是否已经存在于哈希表中。

  • 为什么先查找再插入 :这是此代码最精妙之处。先检查补数是否存在,再将当前元素放入哈希表 ,这样可以保证不会出现自己与自己匹配的情况。例如,如果target = 6, nums = [3],在检查时哈希表为空,不会误判3 + 3 = 6

  • 声明与方法 :代码中的Map<Integer, Integer>是Java的接口,HashMap是其实现类。使用接口类型声明hashtable是良好的编程实践,便于日后更换实现。

  • 返回新数组return new int[]{...} 是Java中创建并返回一个匿名整型数组的简洁写法。

49. 字母异位词分组 - 力扣(LeetCode)

java 复制代码
class Solution {
    // 1. 定义一个名为 groupAnagrams 的公共方法,接收字符串数组 strs,返回一个列表的列表。
    //    每个内部列表包含一组字母异位词。
    public List<List<String>> groupAnagrams(String[] strs) {
        // 2. 创建一个 HashMap,名为 map。
        //    键 (Key) 是排序后的字符串 (String),作为一组字母异位词的唯一标识。
        //    值 (Value) 是一个字符串列表 (List<String>),用于存储所有拥有相同键的原始字符串。
        Map<String, List<String>> map = new HashMap<String, List<String>>();
        
        // 3. 使用增强 for 循环遍历字符串数组 strs 中的每一个字符串 str。
        for (String str : strs) {
            // 4. 将当前字符串 str 转换为字符数组 array。
            //    例如 "tea" -> ['t', 'e', 'a']
            char[] array = str.toCharArray();
            // 5. 对字符数组 array 进行排序。
            //    例如 ['t', 'e', 'a'] -> ['a', 'e', 't']
            Arrays.sort(array);
            // 6. 将排序后的字符数组转换回字符串,作为哈希表的键 key。
            //    例如 ['a', 'e', 't'] -> "aet"。所有字母异位词(如 "tea", "eat")都会得到相同的 key。
            String key = new String(array);
            // 7. 从哈希表 map 中获取键 key 对应的值(字符串列表)。
            //    使用 getOrDefault 方法:如果 key 存在,则返回对应的 List;如果不存在,则返回一个新的空 ArrayList。
            List<String> list = map.getOrDefault(key, new ArrayList<String>());
            // 8. 将当前原始字符串 str 添加到这个列表中。
            //    例如,当 key 为 "aet" 时,将 "tea" 添加到包含 "eat" 的列表中。
            list.add(str);
            // 9. 将更新后的列表放回哈希表 map 中,与键 key 关联。
            //    如果 key 已存在,此操作会更新其对应的值。
            map.put(key, list);
        }
        // 10. 循环结束后,所有字母异位词都已分组。
        //     返回一个新的 ArrayList,其内容为哈希表 map 中所有的值(即所有的字符串列表)。
        //     map.values() 返回一个包含所有 List<String> 的集合视图。
        return new ArrayList<List<String>>(map.values());
    }
}

核心思路与注意点

  • 核心思路 :利用排序后的字符串作为唯一键。互为字母异位词的字符串,在排序后一定完全相同(例如 "eat" 和 "tea" 排序后都是 "aet")。通过这个唯一的键,可以轻松地将所有异位词分组到同一个列表中。

  • 关键方法 getOrDefault :这是代码的精妙之处。它简化了逻辑:如果键存在,就获取已有的列表;如果不存在,就创建一个新列表。这避免了需要先判断 map.containsKey(key) 的繁琐写法。

  • 时间与空间复杂度 :该方法的时间复杂度是 O(n * k log k) ,其中 n 是字符串数量,k 是最大字符串长度(主要来自排序操作)。空间复杂度是 O(n * k),用于存储哈希表和所有字符串。

  • 适用场景 :此方法简洁易懂,适用于所有字符串(不仅限小写字母)。当字符串长度较短或数量适中时,性能表现良好。如果字符串很长,可以改用官方题解中的**"计数"方法**(使用长度为26的数组统计字母频率作为键),能将时间复杂度优化到 O(n * k)。

java 复制代码
class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        // 1. 创建一个 HashMap,用于存储分组结果。
        //    键 (Key) 是字母频率的唯一标识(字符串),值 (Value) 是该组异位词的列表。
        Map<String, List<String>> map = new HashMap<String, List<String>>();
        
        // 2. 遍历输入的每个字符串
        for (String str : strs) {
            // 3. 创建一个长度为26的整型数组,用于统计每个字母的出现次数。
            //    索引0对应'a',索引1对应'b',以此类推。
            int[] counts = new int[26];
            int length = str.length();
            
            // 4. 遍历当前字符串的每个字符,并更新计数。
            //    str.charAt(i) - 'a' 计算出字符对应的索引(例如 'a'->0, 'e'->4)。
            for (int i = 0; i < length; i++) {
                counts[str.charAt(i) - 'a']++;
            }
            
            // 5. 使用 StringBuffer 高效地构建键(字符串)。
            StringBuffer sb = new StringBuffer();
            
            // 6. 遍历26个字母的计数数组
            for (int i = 0; i < 26; i++) {
                // 7. 如果某个字母的出现次数不为0,则将其拼接到键中。
                //    拼接格式为:字母 + 出现次数。例如,出现1次'a'和1次'e'和1次't',键为"a1e1t1"。
                //    这样确保了所有字母异位词(如"eat", "tea")生成相同的键。
                if (counts[i] != 0) {
                    sb.append((char) ('a' + i)); // 将索引转换回字母
                    sb.append(counts[i]);         // 追加出现次数
                }
            }
            
            // 8. 将 StringBuffer 转换为字符串,作为哈希表的键。
            String key = sb.toString();
            
            // 9. 从 map 中获取键 key 对应的列表。
            //    如果 key 不存在,则创建一个新的空 ArrayList。
            List<String> list = map.getOrDefault(key, new ArrayList<String>());
            
            // 10. 将当前原始字符串 str 添加到列表中。
            list.add(str);
            
            // 11. 将更新后的列表放回 map 中。
            map.put(key, list);
        }
        
        // 12. 循环结束后,所有异位词已分组。
        //     返回一个新的 ArrayList,包含 map 中所有的值(即所有分组列表)。
        return new ArrayList<List<String>>(map.values());
    }
}

核心思路与关键点

  • 核心思路 :此方法利用字母出现频率作为唯一键 。因为互为字母异位词的字符串,其中每个字母出现的次数必然完全相同。所以,将每个字母及其出现次数拼接成字符串(如 "a1e1t1")作为键,就能准确地将所有异位词归入同一组。

  • 与排序解法的对比 :排序解法的时间复杂度是 O(n * k log k) (主要来自排序),而计数解法是 O(n * k)性能更优 ,尤其适合长字符串。但计数解法假设字符串只包含小写字母(题目已说明 strs[i] 仅包含小写字母),适用场景更特定。

  • 键的生成细节 :代码中拼接了字母和次数,例如 "a2b1"。这样可以避免歧义,并确保不同字母组合生成的键唯一。例如,"a2""a1b1" 会被正确区分。

  • 为什么用 StringBuffer :在循环中拼接字符串时,StringBuffer(或 StringBuilder)比直接使用 + 操作符效率更高,因为后者会创建多个临时字符串对象。

128. 最长连续序列 - 力扣(LeetCode)

java 复制代码
class Solution {
    public int longestConsecutive(int[] nums) {
        // 1. 创建一个 HashSet,名为 num_set,用于存储数组中的所有数字。
        //    使用 Set 的目的是为了去重,并提供 O(1) 时间复杂度的查找功能。
        Set<Integer> num_set = new HashSet<Integer>();
        // 2. 遍历数组 nums,将所有元素添加到 Set 中。
        for (int num : nums) {
            num_set.add(num);
        }

        // 3. 初始化变量 longestStreak,用于记录目前找到的最长连续序列的长度。
        int longestStreak = 0;

        // 4. 遍历 Set 中的每一个数字 num。
        //    注意:这里遍历的是 Set,而不是原数组,可以自动去重,避免对重复元素进行不必要的处理。
        for (int num : num_set) {
            // 5. 关键判断:检查当前数字 num 是否是某个连续序列的"起点"。
            //    如果 num - 1 存在于 Set 中,说明 num 不是序列的第一个元素,
            //    那么以 num 为起点的序列长度一定不是最长的,因此跳过本次循环。
            if (!num_set.contains(num - 1)) {
                // 6. 当前数字是序列起点,初始化当前序列的数字 currentNum 和长度 currentStreak。
                int currentNum = num;
                int currentStreak = 1;

                // 7. 使用 while 循环,持续检查 currentNum + 1 是否存在于 Set 中。
                //    如果存在,则说明序列可以继续延伸,更新 currentNum 和 currentStreak。
                while (num_set.contains(currentNum + 1)) {
                    currentNum += 1;
                    currentStreak += 1;
                }

                // 8. 当 while 循环结束,说明从起点开始的连续序列已经完整找到。
                //    使用 Math.max 更新全局的最长序列长度 longestStreak。
                longestStreak = Math.max(longestStreak, currentStreak);
            }
        }

        // 9. 返回最终找到的最长连续序列的长度。
        return longestStreak;
    }
}

核心思路与关键点

  • 核心思路 :利用 哈希集合(HashSet) 实现 O(1) 的查找,并通过只从序列起点开始计数 来避免重复计算,从而实现整体 O(n) 的时间复杂度。这是解决"最长连续序列"问题的经典且高效的方法。

  • 为什么从起点开始? 这是整个算法的精髓。通过 if (!num_set.contains(num - 1)) 这个判断,我们保证了只有当 num 是一个连续序列的第一个元素时(即不存在前驱 num-1),才进入内层循环 。这避免了例如 [1,2,3,4] 中,从 234 开始重复计数,使得每个元素在内层循环中最多被访问一次。

  • 时间复杂度分析 :虽然代码中有 for 循环嵌套 while 循环,但总体时间复杂度是 O(n)。因为外层循环遍历每个元素,内层循环总共只会遍历所有序列中的元素一次。

  • 空间复杂度O(n),用于存储哈希集合。

相关推荐
lucas_AI1 小时前
Muse Glimmer 30B:Meta 难得给的真开源,强在哪、虚在哪
人工智能·算法
ai产品老杨1 小时前
国产NPU视觉算法完整流程
算法
过期的秋刀鱼!1 小时前
带替换的采样
人工智能·python·算法·决策树·机器学习
过期的秋刀鱼!1 小时前
使用多个决策树
人工智能·算法·决策树·机器学习·数据挖掘
203号居民2 小时前
LeetCode hot 100 —560. 和为 K 的子数组
java·算法·leetcode
Doraemomo2 小时前
数据结构-双向链表与多文件工程整合之makefile基础
数据结构·链表
退休倒计时3 小时前
【每日一题】LeetCode 20. 有效的括号 TypeScript
算法·leetcode·职场和发展·typescript
文心快码BaiduComate3 小时前
光本位联合文心快码打造面向光电芯片研发的全栈AI Agent Lightmate
算法
一次旅行3 小时前
ViT/CLIP/LLaVA/GPT-4V/VideoLLM多模态架构全解:原理仿真+工业落地选型+完整推理代码
人工智能·算法·架构