一次吃透LeetCode哈希表经典题:附完整思路与代码解析
哈希表核心知识点整理1. 哈希表是什么本质定义一种存储数据的容器核心是通过「哈希函数」将数据映射到特定的存储位置实现快速访问。核心原理输入数据如 int 型数字 5 → 哈希函数 → 映射为数组下标 → 存入对应位置2. 哈希表有什么用核心优势是极致高效的查找性能时间复杂度 平均查找时间O(1)常数级单次访问就能定位数据空间复杂度O(n)需要额外的空间存储映射关系对比其他查找方式查找方式时间复杂度前提条件哈希表查找O(1)无无需有序二分查找O(logN)数组必须有序线性遍历查找O(n)无3. 什么时候用哈希表核心场景频繁查找某个元素的时候比如统计数据出现的次数如字符串中字符频率、数组中数字出现次数判断元素是否存在如两数之和、数组去重快速映射键值对如字典/映射表场景4. 怎么用哈希表两种基础实现方式方式1通用哈希表键值对结构形式index, n[index]即「键key→ 值value」的映射用途适合数据范围不固定、需要自定义映射关系的场景如 Python 的 dict、Java 的 HashMap方式2用数组模拟简易哈希表适用场景数据范围很小且已知比如统计字符串中的字符范围a-z/A-Z共 26/52 个整数范围有限如 1~1000、-1000~1000实现步骤1 定义数组作为存储容器长度覆盖数据范围如 [-1000,1000] 可定义长度为 2001 的数组通过偏移量 x 1000 将负数转为非负下标2 数据存入count[index] 1统计次数或直接赋值3 数据查找直接访问 count[index]O(1) 时间获取结果补充关键细节与避坑点1 哈希冲突不同数据经过哈希函数计算后映射到同一个位置的情况。实际应用中需要解决如链地址法、开放地址法。2 简易哈希表的局限性仅适用于数据范围小的场景数据范围过大时会造成空间浪费如存储 1~10^9 的数据数组长度需要 10^9内存无法承受。3 偏移量的使用处理负数数据时需要通过偏移量将数据转为非负下标避免数组越界。题目1两数之和LeetCode 11. 题目描述给定一个整数数组 nums 和一个整数目标值 target请你在该数组中找出和为目标值 target 的那两个整数并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是数组中同一个元素在答案里不能重复出现。你可以按任意顺序返回答案。• 示例 1输入nums [2,7,11,15], target 9输出[0,1]解释因为 nums[0] nums[1] 9返回 [0, 1]。• 示例 3输入nums [3,3], target 6输出[0,1]2. 暴力解法思路拆解1 先固定其中一个数遍历数组依次把每个元素nums[i]作为第一个数。2 依次与该数之前的数相加在nums[i]之前的元素下标j i中逐个判断nums[i] nums[j]是否等于目标值target。如果相等直接返回[j, i]如果不相等继续遍历下一个数。对应代码C示例class Solution { public: vectorint twoSum(vectorint nums, int target) { int n nums.size(); // 固定一个数nums[i] for (int i 0; i n; i) { // 与该数之前的数相加 for (int j 0; j i; j) { if (nums[i] nums[j] target) { return {j, i}; } } } return {}; // 题目保证有解此行仅为语法兼容 } };复杂度分析时间复杂度O(n^2)两层嵌套循环最坏情况需要遍历所有元素组合。空间复杂度O(1)仅用常数额外空间。3. 哈希表解法详解核心算法思路基础逻辑把「数组元素值」和「它的下标」绑定存入哈希表然后遍历数组时直接在哈希表中查找 target - nums[i]如果存在就说明找到了两个数返回它们的下标。优化技巧关键不需要先把所有元素都放进哈希表再遍历一次会有重复元素处理问题而是边遍历边存哈希表1. 对当前元素 nums[i]计算目标补数 x target - nums[i]2. 检查哈希表中是否已经存在 x如果存在说明 x 是之前遍历过的元素直接返回 {hash[x], i}hash[x] 是 x 的下标i 是当前元素的下标如果不存在把当前元素 nums[i] 和它的下标 i 存入哈希表继续遍历复杂度分析时间复杂度O(N)遍历数组一次哈希表的查询和插入都是 O(1) 平均时间空间复杂度O(N)最坏情况下哈希表需要存储所有元素本质是用空间换时间比暴力解法的 O(N^2) 时间复杂度更优class Solution { public: vectorint twoSum(vectorint nums, int target) { unordered_mapint, int hash; // nums[i], i 键值对元素值 - 下标 for(int i 0; i nums.size(); i) { int x target - nums[i]; // 计算当前元素的补数 if(hash.count(x)) // 检查补数是否已经存在于哈希表中 return {hash[x], i}; // 存在则直接返回结果 hash[nums[i]] i; // 不存在则将当前元素存入哈希表 } // 照顾编译器虽然题目保证有解但语法上需要return return {-1, -1}; } };4. 关键知识点拆解1 哈希表unordered_map的核心用法定义unordered_mapint, int hash;第一个模板参数 int键的类型这里存数组元素的值第二个模板参数 int值的类型这里存数组元素的下标常用操作hash.count(x)检查哈希表中是否存在键为 x 的元素存在返回 1不存在返回 0时间复杂度 O(1)hash[x] i将键为 x、值为 i 的键值对存入哈希表hash[x]访问键为 x 对应的值如果不存在会自动插入默认值所以查询时优先用 count2 边遍历边存的核心优势避免了二次遍历直接一次遍历完成查询和存储完美处理重复元素的情况比如 nums [3,3]target 6遍历第一个 3 - 补数是 3哈希表中不存在存入 hash[3] 0遍历第二个 3 - 补数是 3哈希表中存在 hash[3] 0直接返回 [0,1]不会出现同一个元素被重复使用的情况因为当前元素还没存入哈希表查询时只会匹配之前遍历过的元素3 易错点与细节题目保证有解所以循环结束前一定会 return最后的 return {-1, -1} 只是为了满足C的语法检查实际不会执行返回的下标顺序可以是 {hash[x], i}也可以是 {i, hash[x]}题目不要求顺序哈希表的键是数组元素的值值是数组下标不要搞反如果键存下标、值存元素值就无法通过值快速查询下标了解法时间复杂度空间复杂度优点缺点暴力双循环O(n^2)O(1)实现简单无需额外空间时间效率低大数据量会超时哈希表边存边查O(n)O(n)时间最优大数据量也能高效运行需要额外的哈希表空间题目2判定是否互为字符重排LeetCodeLeLeetCode 面试题 01.021. 题目描述给定两个字符串 s1 和 s2请编写一个程序确定其中一个字符串的字符重新排列后能否变成另一个字符串。• 示例 1输入s1 abc, s2 bca输出true• 示例 2输入s1 abc, s2 bad输出false2. 两个哈希表解法两个哈希表的实现逻辑是1 先判断两个字符串长度是否相等不等直接返回false2 分别用两个哈希表统计s1和s2中每个字符的出现次数3 遍历其中一个哈希表检查两个表中每个字符的频次是否完全一致#include string #include unordered_map using namespace std; class Solution { public: bool CheckPermutation(string s1, string s2) { // 1. 长度不等直接返回false if (s1.size() ! s2.size()) { return false; } // 2. 定义两个哈希表分别统计两个字符串的字符频次 unordered_mapchar, int map1, map2; // 统计s1的字符频次 for (char ch : s1) { map1[ch]; } // 统计s2的字符频次 for (char ch : s2) { map2[ch]; } // 3. 遍历map1对比两个哈希表的频次 for (auto pair : map1) { char ch pair.first; int count pair.second; // 如果map2中不存在该字符或频次不相等直接返回false if (map2.find(ch) map2.end() || map2[ch] ! count) { return false; } } // 所有字符频次都匹配返回true return true; } };为什么要先判断长度长度不等时两个字符串的总字符数都不一样频次统计肯定不可能匹配提前判断可以避免无效计算。哈希表的统计逻辑map1[ch]当字符ch第一次出现时unordered_map会自动创建键值对默认值为0执行后变为1后续重复出现时直接对已存在的键值对做自增。 两个哈希表分别统计完全独立互不干扰。对比频次的过程遍历map1的每个键值对检查map2中是否存在该键且值相等。因为两个字符串长度相等只要map1的所有键都在map2中且频次相等map2中就不会有多余的键否则总字符数会不一致所以不需要再遍历map2。时间复杂度On空间复杂度On3. 优化一个哈希表解法1 前置判断长度校验如果两个字符串的长度不相等直接返回 false。逻辑很简单字符重排不会改变字符串的长度长度不同的字符串绝对不可能互为重排。2 核心逻辑字符频次统计两个字符串互为字符重排的充要条件是每个字符出现的次数完全相同。我们可以用一个「计数数组」本质是哈希表的简化实现来统计频次遍历第一个字符串 s1统计每个字符出现的次数存入计数数组。遍历第二个字符串 s2每遇到一个字符就将计数数组中对应的次数减 1。如果减 1 后次数小于 0说明 s2 中出现了 s1 没有的字符或出现次数超过了 s1直接返回 false。遍历完成后没有提前返回说明两个字符串的字符频次完全匹配返回 true。class Solution { public: bool CheckPermutation(string s1, string s2) { // 1. 长度不相等直接返回false if(s1.size() ! s2.size()) return false; // 2. 初始化计数数组对应26个小写字母 int hash[26] { 0 }; // 3. 统计s1中每个字符的出现次数 for(auto ch : s1) hash[ch - a]; // ch - a 将字符映射到0~25的索引 // 4. 遍历s2对计数数组做减法校验 for(auto ch : s2) { hash[ch - a]--; // 如果出现负数说明s2有多余的字符直接返回false if(hash[ch - a] 0) return false; } // 5. 所有字符频次匹配返回true return true; } };4. 关键知识点拆解1 为什么用 int hash[26] 而不是 unordered_map这是一种数组哈希的优化写法因为题目默认是小写英文字母字符范围固定在 a~z共 26 个。用数组计数的优势时间效率更高数组的访问是 O(1)比 unordered_map 少了哈希计算的开销。空间更省固定大小为 26不需要额外的哈希表结构开销。逻辑更简单直接通过 ch - a 就能映射到索引不需要处理哈希冲突。2 为什么 hash[ch - a] 能统计字符次数对于字符 aa - a 0对应数组索引 0对于字符 bb - a 1对应数组索引 1以此类推z 对应索引 25刚好覆盖 26 个字母。遍历 s1 时每遇到一个字符就给对应索引的计数加 1最终数组里的值就是每个字符出现的次数。3 为什么 hash[ch - a] 0 就能直接返回 false举个例子s1 abcs2 abb遍历 s1 后hash[0] 1a的次数、hash[1] 1b的次数、hash[2] 1c的次数其余为0。遍历 s2第一个 ahash[0]-- → hash[0] 0正常。第一个 bhash[1]-- → hash[1] 0正常。第二个 bhash[1]-- → hash[1] -1此时 hash[1] 0说明 s2 中 b 的次数比 s1 多直接返回 false。这个提前终止的技巧能避免遍历完整个字符串优化了最坏情况下的执行效率。4 复杂度分析时间复杂度O(N)其中 N 是字符串的长度两次遍历字符串的时间都是 O(N)。空间复杂度O(1)计数数组的大小固定为 26和输入字符串的长度无关属于常数级空间。题目3存在重复元素LeetCode 2171. 题目描述给你一个整数数组 nums。如果任一值在数组中出现至少两次返回 true如果数组中每个元素互不相同返回 false。• 示例 1输入nums [1,2,3,1]输出true• 示例 2输入nums [1,2,3,4]输出false2. 核心算法思路哈希集合1 核心逻辑题目只需要判断是否存在重复元素不需要统计次数因此可以用 unordered_set哈希集合来实现哈希集合的特点不允许存储重复元素且插入、查询的时间复杂度都是 O(1) 平均情况。遍历数组时边检查边插入对当前元素 x检查哈希集合中是否已经存在 x如果存在说明出现了重复元素直接返回 true如果不存在将 x 插入哈希集合继续遍历遍历结束后都没有返回说明数组中没有重复元素返回 false2 复杂度分析时间复杂度O(N)其中 N 是数组长度每个元素只被遍历一次哈希集合的操作是 O(1) 平均时间。空间复杂度O(N)最坏情况下数组中没有重复元素哈希集合需要存储所有 N 个元素。class Solution { public: bool containsDuplicate(vectorint nums) { unordered_setint hash; // 哈希集合存储已遍历过的元素 for(auto x : nums) // 遍历数组中的每个元素 { // 检查当前元素是否已经在集合中 if(hash.count(x)) return true; // 存在重复直接返回true else hash.insert(x); // 不存在则插入集合 } // 遍历结束仍未发现重复返回false return false; } };3. 关键知识点拆解1 为什么用 unordered_set 而不是 unordered_mapunordered_set 是集合只存储元素本身不需要额外的键值对结构空间更省逻辑也更贴合题目需求。用 unordered_map 也能实现比如 map[num]如果 count 1 就返回 true但会多存一个无用的“次数”值属于冗余操作。2 核心操作解析hash.count(x)检查集合中是否存在元素 x存在返回 1不存在返回 0时间复杂度 O(1)。hash.insert(x)将元素 x 插入集合如果集合中已经存在 x插入操作不会改变集合但也不会报错只是返回一个 pair 告知插入是否成功。边检查边插入的优势一旦发现重复立刻终止遍历不需要遍历完整个数组优化了最好情况下的执行效率。题目4存在重复元素 II LeetCode 2191. 题目描述给你一个整数数组 nums 和一个整数 k判断数组中是否存在两个不同的索引 i 和 j满足nums[i] nums[j]abs(i - j) k如果存在返回 true否则返回 false。• 示例 1输入nums [1,2,3,1], k 3输出true解释nums[0] 和 nums[3] 都是 1且 abs(0-3) 3 k• 示例 2输入nums [1,0,1,1], k 1输出true解释nums[2] 和 nums[3] 都是 1且 abs(2-3) 1 k2. 核心算法思路哈希表贪心1 核心逻辑题目需要同时满足两个条件元素值相同 下标差不超过k。我们用哈希表来解决这个问题哈希表的键key存储数组中的元素值 nums[i]哈希表的值value存储该元素上一次出现的下标 lastIndex2 贪心优化关键遍历数组时遇到相同元素时只需要记录它上一次出现的下标而不需要保存所有历史下标如果当前元素 nums[i] 已经在哈希表中计算当前下标 i 和上一次出现的下标 hash[nums[i]] 的差 i - hash[nums[i]]如果差 k直接返回 true如果差 k更新哈希表中该元素的下标为当前 i因为后续再出现相同元素时当前下标会比之前的下标更近更有可能满足条件如果当前元素不在哈希表中直接将 nums[i] 和 i 存入哈希表3 遍历结束未返回说明不存在满足条件的元素返回 falseclass Solution { public: bool containsNearbyDuplicate(vectorint nums, int k) { unordered_mapint, int hash; // key: 数组元素值, value: 元素上一次出现的下标 for(int i 0; i nums.size(); i) { // 如果当前元素已经存在于哈希表中 if(hash.count(nums[i])) { // 检查下标差是否k if(i - hash[nums[i]] k) return true; } // 更新当前元素的下标无论是否存在都更新为当前下标 hash[nums[i]] i; } // 遍历结束仍未找到满足条件的元素 return false; } };3. 关键知识点拆解1 为什么只需要记录「上一次出现的下标」这是贪心思想的体现下标是从小到大遍历的当前下标 i 是最新的后续再出现相同元素时与当前下标 i 的差一定比与之前所有下标的差更小。举个例子nums [1, 2, 1, 1], k1i01 不在哈希表存入 hash[1]0i21 存在差为 2-02 1更新 hash[1]2i31 存在差为 3-21 1返回 true如果不更新下标i3 时会和 0 比较差为 3 1会错误返回 false2 复杂度分析时间复杂度O(N)其中 N 是数组长度每个元素只被遍历一次哈希表的操作是 O(1) 平均时间空间复杂度O(N)最坏情况下数组中没有重复元素哈希表需要存储所有 N 个元素3 易错点提醒不要在差 k 时不更新下标否则后续的相同元素会和旧下标比较导致错误判断下标差用 i - hash[nums[i]] 即可因为 i 是递增的i 永远比 hash[nums[i]] 大不需要用 abs()4. 拓展滑动窗口解法这道题也可以用滑动窗口结合哈希集合实现空间复杂度更优class Solution { public: bool containsNearbyDuplicate(vectorint nums, int k) { unordered_setint window; for (int i 0; i nums.size(); i) { // 窗口大小超过k移除窗口最左边的元素 if (i k) window.erase(nums[i - k - 1]); // 检查当前元素是否在窗口中 if (window.count(nums[i])) return true; window.insert(nums[i]); } return false; } };时间复杂度O(N)空间复杂度O(k)哈希集合中最多存储 k1 个元素题目5字母异位词分组LeetCode 491. 题目描述给你一个字符串数组请你将字母异位词组合在一起。可以按任意顺序返回结果列表。字母异位词是由重新排列源单词的字母得到的一个新单词所有源单词中的字母通常恰好只用一次。•示例 1:输入:strs [eat, tea, tan, ate, nat, bat]输出:[[bat],[nat,tan],[ate,eat,tea]]解释在 strs 中没有字符串可以通过重新排列来形成bat。字符串nat和tan是字母异位词因为它们可以重新排列以形成彼此。字符串ateeat和tea是字母异位词因为它们可以重新排列以形成彼此。•示例 2:输入:strs []输出:[[]]•示例 3:输入:strs [a]输出:[[a]]•提示1 strs.length 1040 strs[i].length 100strs[i]仅包含小写字母2. 核心算法思路哈希表 排序1 核心逻辑字母异位词的关键特性互为字母异位词的两个字符串排序后的结果一定完全相同。例如 eat、tea、ate 排序后都是 aettan、nat 排序后都是 ant。我们可以利用这个特性把排序后的字符串作为哈希表的 key把原字符串存入对应的 value字符串数组中实现分组。2 具体步骤遍历字符串数组对每个字符串生成它的「排序后副本」作为分组键。存入哈希表将原字符串存入以「排序后副本」为键的哈希表对应的列表中。提取结果遍历哈希表将所有键对应的列表取出作为最终结果。class Solution { public: vectorvectorstring groupAnagrams(vectorstring strs) { // 哈希表key为排序后的字符串value为对应的异位词列表 unordered_mapstring, vectorstring hash; // 1. 遍历所有字符串按排序后的key分组 for(auto s : strs) { string tmp s; // 复制原字符串避免修改原数据 sort(tmp.begin(), tmp.end());// 对副本进行排序得到分组key hash[tmp].push_back(s); // 将原字符串存入对应分组 } // 2. 从哈希表中提取所有分组转为结果格式 vectorvectorstring ret; for(auto [key, group] : hash) // C17结构化绑定遍历哈希表 { ret.push_back(group); } return ret; } };3. 关键知识点拆解1 为什么用「排序后的字符串」作为哈希表的 key这是一种「哈希映射」的技巧利用排序的结果将所有字母异位词映射到同一个 key 上实现自动分组。对于任意两个字母异位词它们排序后的结果必然相同反之排序后结果相同的两个字符串一定是字母异位词假设字符集相同。2 容器嵌套的理解这道题的哈希表是 unordered_mapstring, vectorstring属于容器嵌套外层容器unordered_map实现 key 到 value 的映射。内层容器vectorstring存储同一组的所有字母异位词。这种结构的优势是自动帮我们完成分组不需要手动维护多个列表。3 复杂度分析时间复杂度O(N * Klog K)其中 N 是字符串数组的长度K 是字符串的平均长度。每个字符串排序的时间复杂度是 O(K * K)遍历所有字符串的时间是 O(N)因此总时间复杂度为 O(N * Klog K)。空间复杂度O(N * K)哈希表需要存储所有字符串的副本总空间为所有字符串的长度之和。4. 拓展优化计数哈希避免排序如果字符串的字符集固定如仅小写字母可以用「字符计数」的方式生成 key避免排序的开销class Solution { public: vectorvectorstring groupAnagrams(vectorstring strs) { // 用字符计数数组作为key这里用string表示计数比如a1b2... unordered_mapstring, vectorstring hash; for (auto s : strs) { int count[26] {0}; for (char c : s) count[c - a]; // 将计数数组转为字符串作为key string key; for (int i 0; i 26; i) { key to_string(count[i]) #; // 用#分隔避免歧义 } hash[key].push_back(s); } vectorvectorstring ret; for (auto p : hash) ret.push_back(p.second); return ret; } };时间复杂度O(N * K)无需排序仅需两次遍历字符串。空间复杂度O(N * K)和排序法相同。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2576168.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!