Skip to content

哈希

更新: 8/26/2025 字数: 0 字 时长: 0 分钟

两数之和

java
/**
 * <p>采用暴力枚举法,通过双重循环遍历所有可能的数对。</p>
 *
 * <h3>优化关键点:</h3>
 * <ul>
 *   <li><strong>无优化:</strong> 这是最直接但效率最低的解法。</li>
 *   <li><strong>双重循环:</strong> 通过嵌套循环遍历所有组合,确保 {@code j} 始终大于 {@code i} 以避免重复和自身相加。</li>
 * </ul>
 *
 * <h3>性能分析:</h3>
 * <ul>
 *   <li><strong>时间复杂度:</strong> {@code O(n^2)} - 外层循环 {@code n} 次,内层循环平均 {@code n/2} 次。</li>
 *   <li><strong>空间复杂度:</strong> {@code O(1)} - 除了存储输入和输出,没有使用额外的辅助空间。</li>
 * </ul>
 *
 * @param nums   输入整数数组。
 * @param target 目标和。
 * @return 包含两个数索引的数组,如果不存在则返回空数组。
 */
public int[] twoSum(int[] nums, int target) {
    int n = nums.length; // 获取数组长度
    // 外层循环:遍历数组中的每一个元素作为第一个加数 (nums[i])
    // i 从 0 遍历到倒数第二个元素,因为 j 会从 i+1 开始
    for (int i = 0; i < n - 1; i++) {
        // 内层循环:遍历 i 之后的每一个元素作为第二个加数 (nums[j])
        // 确保 j > i,避免重复组合和自身相加
        for (int j = i + 1; j < n; j++) {
            // 检查当前两个元素的和是否等于目标值 target
            if (nums[i] + nums[j] == target) {
                // 如果找到,立即返回这两个元素的索引
                return new int[]{i, j};
            }
        }
    }
    // 如果遍历完所有组合都没有找到,返回一个空数组(表示无解)
    return new int[0];
}
java
/**
 * <p>采用暴力枚举法,略微改变内层循环的遍历方向,但本质仍是 O(n^2)。</p>
 *
 * <h3>优化关键点:</h3>
 * <ul>
 *   <li><strong>时间复杂度上无本质优化:</strong> 仍然是 {@code O(n^2)}。</li>
 *   <li><strong>思维上的调整:</strong> 先确定当前数 {@code currentNum} 和补数 {@code complement},然后尝试在之前遍历过的元素中查找 {@code complement}。这为哈希表解法奠定了思路基础。</li>
 * </ul>
 *
 * <h3>性能分析:</h3>
 * <ul>
 *   <li><strong>时间复杂度:</strong> {@code O(n^2)} - 外层循环 {@code n} 次,内层循环平均 {@code n/2} 次。</li>
 *   <li><strong>空间复杂度:</strong> {@code O(1)} - 除了存储输入和输出,没有使用额外的辅助空间。</li>
 * </ul>
 *
 * @param nums   输入整数数组。
 * @param target 目标和。
 * @return 包含两个数索引的数组,如果不存在则返回空数组。
 */
public int[] twoSum(int[] nums, int target) {
    // 外层循环:遍历数组中的每一个元素
    for (int i = 0; i < nums.length; i++) {
        // 当前元素的值
        int currentNum = nums[i];
        // 计算需要查找的“补数”
        int complement = target - currentNum;

        // 内层循环:遍历当前元素 (i) 之前的所有元素 (j)
        // 查找是否存在一个元素 nums[j] 等于 complement
        for (int j = 0; j < i; j++) { // 注意 j 的范围是 0 到 i-1
            // 如果找到了补数
            if (nums[j] == complement) {
                // 返回补数的索引 j 和当前元素的索引 i
                return new int[]{j, i};
            }
        }
    }
    // 如果遍历完所有组合都没有找到,返回一个空数组
    return new int[0];
}
java
/**
 * <p>采用哈希表({@code HashMap})进行优化,将时间复杂度从 {@code O(n^2)} 降低到 {@code O(n)}。</p>
 * <p>通过<strong>空间换时间</strong>的方式,实现单次遍历即可找到结果。</p>
 *
 * <h3>优化关键点:</h3>
 * <ul>
 *   <li><strong>空间换时间:</strong> 使用 {@code HashMap} 存储已遍历的数字及其索引,避免重复计算。</li>
 *   <li><strong>O(1) 查找:</strong> {@code HashMap} 的 {@code containsKey()} 和 {@code get()} 操作平均时间复杂度为 {@code O(1)}。</li>
 *   <li><strong>单次遍历:</strong> 整个算法只需要遍历数组一次。在遍历过程中,每访问一个元素:
 *     <ol>
 *       <li>计算其补数 {@code complement = target - currentNum}。</li>
 *       <li>在 {@code HashMap} 中查找 {@code complement} 是否存在。</li>
 *       <li><strong>若存在:</strong> 说明已找到匹配项,立即返回补数的索引({@code map.get(complement)})和当前元素的索引({@code i})。</li>
 *       <li><strong>若不存在:</strong> 将当前元素 {@code currentNum} 及其索引 {@code i} 存入 {@code HashMap},供后续元素查找。</li>
 *     </ol>
 *   </li>
 * </ul>
 *
 * <h3>性能分析:</h3>
 * <ul>
 *   <li><strong>时间复杂度:</strong> {@code O(n)} - 数组单次遍历,{@code HashMap} 操作平均为 {@code O(1)}。</li>
 *   <li><strong>空间复杂度:</strong> {@code O(n)} - 最坏情况下,{@code HashMap} 可能存储数组中的所有 {@code n} 个元素。</li>
 * </ul>
 *
 * @param nums   输入整数数组。
 * @param target 目标和。
 * @return 包含两个数索引的数组,如果不存在则返回空数组。
 */
public int[] twoSum(int[] nums, int target) {
    // 创建一个 HashMap,用于存储“数字 -> 索引”的映射
    // key 是数组中的数字,value 是该数字在数组中的索引
    HashMap<Integer, Integer> map = new HashMap<>();

    // 遍历数组中的每一个元素
    for (int i = 0; i < nums.length; i++) {
        // 当前遍历到的数字
        int currentNum = nums[i];
        // 计算当前数字的“补数”
        int complement = target - currentNum;

        // 检查 HashMap 中是否已经存在这个“补数”
        if (map.containsKey(complement)) {
            // 如果存在,说明我们找到了两个数:
            // 1. 之前存入 HashMap 的 complement 对应的数字,其索引为 map.get(complement)
            // 2. 当前遍历到的 currentNum,其索引为 i
            return new int[]{map.get(complement), i};
        }

        // 如果 HashMap 中不存在 complement,
        // 那么将当前数字 currentNum 及其索引 i 存入 HashMap
        // 供后续的数字查找使用。
        map.put(currentNum, i);
    }

    // 如果遍历完整个数组都没有找到,返回一个空数组(表示无解)
    return new int[0];
}

字母异位词分组

java
/**
 * <h3>算法思路:</h3>
 * <p>核心思想是如果两个字符串是字母异位词,那么它们排序后的字符串是完全相同的。</p>
 * <ol>
 *   <li>创建一个 {@code HashMap},其键(Key)是排序后的字符串,值(Value)是一个存储原始字符串的列表。</li>
 *   <li>遍历输入字符串数组 {@code strs} 中的每个字符串 {@code s}。</li>
 *   <li>将 {@code s} 转换为字符数组,并进行排序。</li>
 *   <li>将排序后的字符数组转换回字符串 {@code sortedStr},作为哈希表的键。</li>
 *   <li>使用 {@code sortedStr} 去哈希表中查找。
 *     <ul>
 *       <li>如果 {@code sortedStr} 已经存在:将原始字符串 {@code s} 添加到其对应的列表中。</li>
 *       <li>如果 {@code sortedStr} 不存在:创建一个新的列表,添加 {@code s},然后将 {@code sortedStr} 和新列表作为键值对存入哈希表。</li>
 *     </ul>
 *   </li>
 *   <li>最后,返回哈希表中所有值(即所有列表)的集合。</li>
 * </ol>
 *
 * <h3>优化关键点:</h3>
 * <ul>
 *   <li><strong>哈希表的使用:</strong> 通过将排序后的字符串作为哈希表的键,实现了 {@code O(1)} 平均时间复杂度的查找和插入,高效地将异位词归类。</li>
 *   <li><strong>排序作为标准化:</strong> 排序字符串是识别异位词的标准且简单的方法。</li>
 * </ul>
 *
 * <h3>性能分析:</h3>
 * <ul>
 *   <li><strong>时间复杂度:</strong> {@code O(N * K log K)}
 *     <ul>
 *       <li>{@code N} 是输入数组 {@code strs} 的长度。</li>
 *       <li>{@code K} 是字符串的最大长度。</li>
 *       <li>对于每个字符串:
 *         <ul>
 *           <li>转换为字符数组:{@code O(K)}</li>
 *           <li>字符数组排序:{@code O(K log K)}</li>
 *           <li>构建哈希表键和查找/插入:平均 {@code O(K)} (取决于字符串哈希和比较,最坏 {@code O(K)} 如果键冲突严重,但在实际应用中通常接近 {@code O(1)})。</li>
 *         </ul>
 *       </li>
 *       <li>因此,总时间复杂度为 {@code N} 次 {@code K log K} 操作,即 {@code O(N * K log K)}。</li>
 *     </ul>
 *   </li>
 *   <li><strong>空间复杂度:</strong> {@code O(N * K)}
 *     <ul>
 *       <li>在最坏情况下,所有字符串都是唯一的(没有异位词),哈希表将存储 {@code N} 个键值对。</li>
 *       <li>每个键是长度为 {@code K} 的字符串,每个值是一个列表(平均存储一个字符串)。</li>
 *       <li>因此,总空间复杂度为 {@code O(N * K)}。</li>
 *     </ul>
 *   </li>
 * </ul>
 *
 * @param strs 输入的字符串数组。
 * @return 一个列表的列表,其中每个内部列表包含一组字母异位词。
 */
public List<List<String>> groupAnagrams(String[] strs) {
    // 创建一个 HashMap,用于存储排序后的字符串作为键,以及对应的原始字符串列表作为值
    // 例如:{"aet": ["eat", "tea", "ate"], "ant": ["tan", "nat"], "abt": ["bat"]}
    HashMap<String, List<String>> map = new HashMap<>();

    // 遍历输入字符串数组
    for (String s : strs) {
        // 将当前字符串转换为字符数组
        char[] charArray = s.toCharArray();
        // 对字符数组进行排序
        Arrays.sort(charArray);
        // 将排序后的字符数组转换回字符串,作为哈希表的键
        String sortedStr = new String(charArray);

        // 如果哈希表中不存在这个排序后的字符串作为键
        if (!map.containsKey(sortedStr)) {
            // 则创建一个新的列表,并将其作为值与 sortedStr 关联
            map.put(sortedStr, new ArrayList<>());
        }
        // 将原始字符串 s 添加到对应 sortedStr 的列表中
        map.get(sortedStr).add(s);
    }

    // 最后,返回哈希表中所有值(即所有分组的列表)的集合
    // new ArrayList<>(map.values()) 将 Map 的所有值收集到一个新的 List 中
    return new ArrayList<>(map.values());
}

最长连续序列

java
/**
 * <h3>算法思路:</h3>
 * <p>为了达到 O(n) 的时间复杂度,我们采用哈希集合来快速判断数字是否存在,并优化序列的查找方式。</p>
 * <ol>
 *   <li><strong>去重与快速查找:</strong> 将所有输入数字存入一个 {@code HashSet}。这允许我们在 {@code O(1)} 平均时间复杂度内检查一个数字是否存在。</li>
 *   <li><strong>寻找序列起点:</strong> 遍历 {@code HashSet} 中的每个数字 {@code num}。我们只对那些可能是<strong>连续序列起点</strong>的数字感兴趣。
 *     <ul>
 *       <li>如果 {@code num - 1} 存在于 {@code HashSet} 中,说明 {@code num} 不是一个新序列的起点(它属于以 {@code num - 1} 开头的某个序列),我们直接跳过它,因为它会在其真正的起点处被处理。</li>
 *       <li>如果 {@code num - 1} 不存在于 {@code HashSet} 中,那么 {@code num} 就是一个潜在的最长连续序列的起点。</li>
 *     </ul>
 *   </li>
 *   <li><strong>扩展序列:</strong> 当找到一个序列起点 {@code num} 时,我们开始向右(即递增方向)查找连续的数字。
 *     <ul>
 *       <li>初始化当前序列长度 {@code currentLength = 1}。</li>
 *       <li>从 {@code currentNum = num + 1} 开始,不断检查 {@code currentNum} 是否存在于 {@code HashSet} 中。</li>
 *       <li>如果存在,则 {@code currentLength} 加一,{@code currentNum} 也加一,继续查找。</li>
 *       <li>如果不存在,说明当前序列到此结束。</li>
 *     </ul>
 *   </li>
 *   <li><strong>更新最长长度:</strong> 每次找到一个序列的长度后,更新全局最大长度 {@code maxLength}。</li>
 * </ol>
 *
 * <h3>优化关键点:</h3>
 * <ul>
 *   <li><strong>哈希集合:</strong> 提供 {@code O(1)} 平均时间复杂度的查找能力,避免了排序或嵌套循环带来的高复杂度。</li>
 *   <li><strong>跳过非起点元素:</strong> 关键优化在于,只从序列的起点开始扩展。如果一个数字 {@code x} 的前驱 {@code x-1} 存在,那么 {@code x} 肯定不是一个新序列的起点,因此跳过对它的处理,避免重复计算。每个数字只会被作为某个序列的“起点”进行一次处理(或者被跳过)。</li>
 * </ul>
 *
 * <h3>性能分析:</h3>
 * <ul>
 *   <li><strong>时间复杂度:</strong> {@code O(n)}
 *     <ul>
 *       <li>将所有数字存入 {@code HashSet}:{@code O(n)}。</li>
 *       <li>外层循环遍历 {@code HashSet} 中的所有 {@code n} 个数字。</li>
 *       <li>内层循环(扩展序列):虽然是嵌套的,但每个数字(例如 {@code num}, {@code num+1}, {@code num+2}, ...)只会被 {@code contains} 操作访问<strong>常数次</strong>。具体来说,每个数字最多被作为“某个序列的起点”检查一次,被作为“某个序列的后续元素”检查一次。因此,所有内层循环的总操作次数加起来是线性的。</li>
 *       <li>综合起来,总时间复杂度为 {@code O(n)}。</li>
 *     </ul>
 *   </li>
 *   <li><strong>空间复杂度:</strong> {@code O(n)}
 *     <ul>
 *       <li>{@code HashSet} 在最坏情况下需要存储所有 {@code n} 个不重复的数字。</li>
 *     </ul>
 *   </li>
 * </ul>
 *
 * @param nums 未排序的整数数组。
 * @return 最长数字连续序列的长度。
 */
public int longestConsecutive(int[] nums) {
    // 如果数组为空,直接返回 0
    if (nums == null || nums.length == 0) {
        return 0;
    }

    // 1. 将所有数字存入哈希集合,以便 O(1) 查找
    Set<Integer> numSet = new HashSet<>();
    for (int num : nums) {
        numSet.add(num);
    }

    int maxLength = 0; // 记录找到的最长连续序列的长度

    // 2. 遍历哈希集合中的每个数字
    for (int num : numSet) {
        // 3. 判断当前数字是否是一个连续序列的起点
        // 如果 num - 1 存在于集合中,说明 num 不是起点,它只是某个更长序列的中间部分,跳过它
        if (!numSet.contains(num - 1)) {
            // 如果 num - 1 不存在,则 num 是一个新序列的起点
            int currentNum = num; // 当前正在检查的数字
            int currentLength = 1; // 当前序列的长度

            // 4. 从起点开始,向右扩展序列
            while (numSet.contains(currentNum + 1)) {
                currentNum++;     // 继续检查下一个连续数字
                currentLength++;  // 序列长度增加
            }

            // 5. 更新最长序列长度
            maxLength = Math.max(maxLength, currentLength);
        }
    }

    return maxLength;
}

贡献者

The avatar of contributor named as LI SIR LI SIR

页面历史