<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>哈希表 on 奇伏禾</title>
        <link>/tags/%E5%93%88%E5%B8%8C%E8%A1%A8/</link>
        <description>Recent content in 哈希表 on 奇伏禾</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-cn</language>
        <lastBuildDate>Sat, 18 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="/tags/%E5%93%88%E5%B8%8C%E8%A1%A8/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>哈希表模块 — LeetCode 热门 100 题精讲（Java 向）</title>
            <link>/p/hash-algorithm-module/</link>
            <pubDate>Sat, 18 Jul 2026 00:00:00 +0000</pubDate>
            <guid>/p/hash-algorithm-module/</guid>
            <description>&lt;h2 id=&#34;引言哈希--算法世界的第一把加速器&#34;&gt;引言：哈希 —— 算法世界的第一把加速器&#xA;&lt;/h2&gt;&lt;p&gt;在刷题初期，我们常被暴力解法的 &lt;code&gt;O(n²)&lt;/code&gt; 甚至 &lt;code&gt;O(n³)&lt;/code&gt; 折磨。而哈希表，往往是打破僵局的第一把钥匙。它不依靠复杂的逻辑推理，而是凭借 &lt;strong&gt;&amp;ldquo;记住已经见过的东西&amp;rdquo;&lt;/strong&gt; 这种朴素思想，让查找从线性变为常数。&lt;/p&gt;&#xA;&lt;p&gt;Java 中的 &lt;code&gt;HashMap&lt;/code&gt; 和 &lt;code&gt;HashSet&lt;/code&gt; 是哈希表的两种基本形态。前者存储键值对，后者只存键（本质上是值为常量的 &lt;code&gt;HashMap&lt;/code&gt;）。它们平均时间复杂度均为 &lt;code&gt;O(1)&lt;/code&gt;，代价是额外的内存开销。这正是 &lt;strong&gt;&amp;ldquo;空间换时间&amp;rdquo;&lt;/strong&gt; 的典型体现。&lt;/p&gt;&#xA;&lt;p&gt;LeetCode 热门 100 题中，有三道题是哈希表的&amp;quot;代言人&amp;quot;：第 1 题、第 49 题、第 128 题。它们分别展示了哈希在 &lt;strong&gt;配对查找&lt;/strong&gt;、&lt;strong&gt;归类聚合&lt;/strong&gt;、&lt;strong&gt;连续性判断&lt;/strong&gt; 三个不同维度的应用。下面逐一拆解。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;1-两数之和leetcode-1&#34;&gt;1. 两数之和（LeetCode 1）&#xA;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;题目&lt;/strong&gt;：在无序数组中，找到两个数，使它们的和等于目标值，返回下标。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;暴力法&lt;/strong&gt;：双重循环枚举所有组合，&lt;code&gt;O(n²)&lt;/code&gt;。当 n 很大时必然超时。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;哈希优化思路&lt;/strong&gt;：遍历数组时，对于当前数 &lt;code&gt;nums[i]&lt;/code&gt;，我们只需要知道 &lt;strong&gt;之前是否出现过 &lt;code&gt;target - nums[i]&lt;/code&gt;&lt;/strong&gt;。如果出现过，就找到了答案。如果没有，就把当前数存入哈希表，供后续元素查找。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;具体步骤&lt;/strong&gt;：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;初始化一个空 &lt;code&gt;HashMap&amp;lt;Integer, Integer&amp;gt;&lt;/code&gt;，键为数值，值为下标。&lt;/li&gt;&#xA;&lt;li&gt;从左到右遍历数组，对每个元素 &lt;code&gt;x&lt;/code&gt;：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;计算 &lt;code&gt;complement = target - x&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;查询哈希表是否包含 &lt;code&gt;complement&lt;/code&gt;。若包含，直接返回 &lt;code&gt;{map.get(complement), i}&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;若不包含，将 &lt;code&gt;(x, i)&lt;/code&gt; 存入哈希表。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;strong&gt;为什么高效？&lt;/strong&gt; 因为哈希表的查找和插入都是 &lt;code&gt;O(1)&lt;/code&gt;，所以整体复杂度降为 &lt;code&gt;O(n)&lt;/code&gt;。这个思路的本质是 &lt;strong&gt;&amp;ldquo;用空间记录历史，避免未来重复查找&amp;rdquo;&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;易错点&lt;/strong&gt;：不能先把所有元素存入哈希表再查找，因为这样可能匹配到同一个元素两次（例如 &lt;code&gt;[3,3]&lt;/code&gt; target=6，如果先存再找，会返回 &lt;code&gt;(0,0)&lt;/code&gt;）。边遍历边存，天然避免了这个问题。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;2-字母异位词分组leetcode-49&#34;&gt;2. 字母异位词分组（LeetCode 49）&#xA;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;题目&lt;/strong&gt;：给定字符串数组，将字母异位词（由相同字母不同排列组成）分到同一组。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;暴力法&lt;/strong&gt;：两两比较每个字符串是否是异位词，复杂度 &lt;code&gt;O(n² * L log L)&lt;/code&gt;，不可接受。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;哈希优化思路&lt;/strong&gt;：异位词的本质是 &lt;strong&gt;字母构成完全相同&lt;/strong&gt;。那么，我们可以找到一个 &lt;strong&gt;归一化标识&lt;/strong&gt;，使得所有异位词的标识相同。最直接的方法就是 &lt;strong&gt;排序&lt;/strong&gt;：对字符串的字符数组排序，排序后的结果就是唯一标识。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;具体步骤&lt;/strong&gt;：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;创建一个 &lt;code&gt;HashMap&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt;&lt;/code&gt;，键为排序后的字符串，值为异位词列表。&lt;/li&gt;&#xA;&lt;li&gt;遍历所有字符串 &lt;code&gt;s&lt;/code&gt;：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;将 &lt;code&gt;s&lt;/code&gt; 转换为字符数组，排序，再转回字符串 &lt;code&gt;key&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;若哈希表中不存在 &lt;code&gt;key&lt;/code&gt;，新建一个 &lt;code&gt;ArrayList&lt;/code&gt; 放入。&lt;/li&gt;&#xA;&lt;li&gt;将 &lt;code&gt;s&lt;/code&gt; 添加到该 &lt;code&gt;key&lt;/code&gt; 对应的列表中。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;最后返回哈希表的所有 &lt;code&gt;value&lt;/code&gt; 组成的列表。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;strong&gt;为什么高效？&lt;/strong&gt; 每个字符串只需一次排序（&lt;code&gt;O(L log L)&lt;/code&gt;）和一次哈希操作，总体时间复杂度为 &lt;code&gt;O(n * L log L)&lt;/code&gt;，远优于两两比较。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;进一步优化&lt;/strong&gt;：如果字符串长度很长，排序开销变大，可以用 &lt;strong&gt;字母计数&lt;/strong&gt; 作为键：统计每个字符出现次数，拼接成 &lt;code&gt;&amp;quot;a3b1c2&amp;quot;&lt;/code&gt; 这种形式。但排序在 Java 中更简洁，且实际题量下足够。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;关键点&lt;/strong&gt;：哈希表在这里充当了 &lt;strong&gt;&amp;ldquo;分类器&amp;rdquo;&lt;/strong&gt;，将具有相同特征（排序后相等）的元素聚合到一起，避免了嵌套循环的碰撞判断。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;3-最长连续序列leetcode-128&#34;&gt;3. 最长连续序列（LeetCode 128）&#xA;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;题目&lt;/strong&gt;：给定未排序的整数数组，找出最长的连续数字序列（如 &lt;code&gt;[100,4,200,1,3,2]&lt;/code&gt; 中最长连续序列为 &lt;code&gt;[1,2,3,4]&lt;/code&gt;，长度为 4）。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;暴力法&lt;/strong&gt;：排序后遍历，复杂度 &lt;code&gt;O(n log n)&lt;/code&gt;，但题目要求 &lt;code&gt;O(n)&lt;/code&gt;，所以排序不可取。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;哈希优化思路&lt;/strong&gt;：我们只需要 &lt;strong&gt;从每个连续序列的起点开始&lt;/strong&gt; 计数。如何判断一个数是否是起点？—— 如果 &lt;code&gt;num - 1&lt;/code&gt; 不在数组中，则 &lt;code&gt;num&lt;/code&gt; 就是起点。这样，我们只需对每个起点向后查找 &lt;code&gt;num+1, num+2...&lt;/code&gt;，直到断掉。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;具体步骤&lt;/strong&gt;：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;将所有数存入 &lt;code&gt;HashSet&amp;lt;Integer&amp;gt;&lt;/code&gt;，去重并支持 &lt;code&gt;O(1)&lt;/code&gt; 查找。&lt;/li&gt;&#xA;&lt;li&gt;遍历集合中的每个数 &lt;code&gt;num&lt;/code&gt;：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;如果 &lt;code&gt;set.contains(num - 1)&lt;/code&gt;，说明 &lt;code&gt;num&lt;/code&gt; 不是起点，直接跳过（因为它的序列会被起点统计）。&lt;/li&gt;&#xA;&lt;li&gt;否则，从 &lt;code&gt;num&lt;/code&gt; 开始，用 &lt;code&gt;while (set.contains(current))&lt;/code&gt; 递增计数，记录长度。&lt;/li&gt;&#xA;&lt;li&gt;更新全局最大长度。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;strong&gt;为什么高效？&lt;/strong&gt; 每个数最多被访问两次：一次作为起点被扫描序列，一次被跳过（作为非起点）。所以总复杂度 &lt;code&gt;O(n)&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;易错点&lt;/strong&gt;：必须遍历 &lt;code&gt;HashSet&lt;/code&gt; 而不是原数组，避免重复处理相同数字。另外，在 while 循环中，每次 &lt;code&gt;current++&lt;/code&gt; 时，不必从集合中删除当前数，因为后面不会再遇到（即使遇到也会被跳过或已处理），但删除可以略微加速，不是必须。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;三题对比与启发&#34;&gt;三题对比与启发&#xA;&lt;/h2&gt;&lt;table&gt;&#xA;&#x9;&lt;thead&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;th&gt;题目&lt;/th&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;th&gt;哈希结构&lt;/th&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;th&gt;核心思想&lt;/th&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;th&gt;时间复杂度&lt;/th&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&lt;/thead&gt;&#xA;&#x9;&lt;tbody&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;两数之和&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;HashMap&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;查找互补数，历史记录&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;O(n)&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;字母异位词分组&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;HashMap&amp;lt;String, List&amp;gt;&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;归一化标识，归类聚合&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;O(n * L log L)&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;最长连续序列&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;HashSet&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;判断起点，只从起点延伸&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;O(n)&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;&lt;strong&gt;共同本质&lt;/strong&gt;：都是利用哈希表的 &lt;strong&gt;&amp;ldquo;存在性查询&amp;rdquo;&lt;/strong&gt; 能力，将原本需要嵌套循环的匹配问题，转化为单次遍历中的即时判断。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;不同侧重&lt;/strong&gt;：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;两数之和：查询的是 &lt;strong&gt;&amp;ldquo;补数是否存在&amp;rdquo;&lt;/strong&gt;，属于一对一匹配。&lt;/li&gt;&#xA;&lt;li&gt;字母异位词分组：查询的是 &lt;strong&gt;&amp;ldquo;是否存在相同的归一化键&amp;rdquo;&lt;/strong&gt;，属于一对多归类。&lt;/li&gt;&#xA;&lt;li&gt;最长连续序列：查询的是 &lt;strong&gt;&amp;ldquo;下一个数是否存在&amp;rdquo;&lt;/strong&gt;，属于链式延伸。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;总结哈希模块的核心心法&#34;&gt;总结：哈希模块的核心心法&#xA;&lt;/h2&gt;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;&lt;strong&gt;哈希表是一种&amp;quot;记忆&amp;quot;工具。它让我们在遍历过程中，能够瞬间回想起之前见过的任何元素，从而避免重复扫描。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;应用哈希表的通用步骤：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;识别问题中是否存在 &lt;strong&gt;&amp;ldquo;查找&amp;rdquo;&lt;/strong&gt; 或 &lt;strong&gt;&amp;ldquo;判断存在&amp;rdquo;&lt;/strong&gt; 的需求。&lt;/li&gt;&#xA;&lt;li&gt;确定 &lt;strong&gt;键&lt;/strong&gt; 是什么（原始值、变换后的值、组合值）。&lt;/li&gt;&#xA;&lt;li&gt;确定 &lt;strong&gt;值&lt;/strong&gt; 是什么（下标、计数、列表、布尔标志）。&lt;/li&gt;&#xA;&lt;li&gt;遍历数据，边处理边维护哈希表，利用实时信息决策。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;掌握这三道经典题，你就掌握了哈希表在算法竞赛中的绝大多数应用场景。后续遇到类似&amp;quot;求和&amp;quot;、&amp;ldquo;分组&amp;rdquo;、&amp;ldquo;连续&amp;quot;等关键词，第一反应就应该想到哈希表。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;扩展思考&#34;&gt;扩展思考&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;如果两数之和要求返回所有不重复的组合，哈希表如何处理？—— 可以排序后使用双指针，或者哈希表记录频次。&lt;/li&gt;&#xA;&lt;li&gt;如果字母异位词分组要求按出现顺序输出，如何保证顺序？—— 使用 &lt;code&gt;LinkedHashMap&lt;/code&gt; 或单独维护顺序列表。&lt;/li&gt;&#xA;&lt;li&gt;如果最长连续序列要求返回具体序列而不是长度，如何修改？—— 在 while 循环中收集元素即可。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;哈希表并非万能，当键值规模极大或哈希冲突严重时，可能退化为 &lt;code&gt;O(n)&lt;/code&gt; 甚至更差。但在面试和竞赛中，它始终是解决问题的首选利器之一。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;</description>
        </item></channel>
</rss>
