哈希表通关八题:从两数之和到LRU缓存,手撕高频面试题(Python + C++)

哈希表通关八题:从两数之和到LRU缓存,手撕高频面试题(Python + C++)

哈希表是算法面试中使用频率最高的数据结构之一。本文整理了8道经典题目,每道题都包含:题目描述、解题思路、图解(文本示意)、Python代码、C++代码、时间复杂度与空间复杂度分析。掌握这些,哈希表类题目基本通关。


📌 题目清单

题号 题目 核心考点
1 两数之和 哈希表 O(n)
217 存在重复元素 集合去重
349 两个数组的交集 哈希集合
242 有效的字母异位词 字符计数 / 排序
387 字符串中的第一个唯一字符 哈希计数
49 字母异位词分组 哈希映射(键为排序后字符串)
146 LRU 缓存 哈希表 + 双向链表(高频)
3 无重复字符的最长子串 滑动窗口 + 哈希表

1. 两数之和(LeetCode 1)

题目描述

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 的两个整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案,且同一个元素不能使用两遍。

示例

输入:nums = [2,7,11,15], target = 9

输出:[0,1] 解释:nums[0] + nums[1] = 2 + 7 = 9

解题思路

  • 遍历数组,对于每个元素 nums[i],检查 target - nums[i] 是否已经在哈希表中。
  • 如果在,直接返回当前下标和哈希表中存储的下标。
  • 如果不在,将当前元素的值和下标存入哈希表。

图解

复制代码
nums = [2,7,11,15], target = 9
i=0: num=2, need=7, map{} → 存入 {2:0}
i=1: num=7, need=2, map中有2 → 返回 [0,1]

Python代码

python 复制代码
def twoSum(nums, target):
    seen = {}
    for i, num in enumerate(nums):
        need = target - num
        if need in seen:
            return [seen[need], i]
        seen[num] = i
    return []

C++代码

cpp 复制代码
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> seen;
        for (int i = 0; i < nums.size(); ++i) {
            int need = target - nums[i];
            if (seen.count(need)) {
                return {seen[need], i};
            }
            seen[nums[i]] = i;
        }
        return {};
    }
};

复杂度分析

  • 时间复杂度:O(n),只需遍历一次数组,哈希表查找 O(1)。
  • 空间复杂度:O(n),哈希表最多存储 n 个元素。

2. 存在重复元素(LeetCode 217)

题目描述

给定一个整数数组,判断是否存在重复元素。如果任一值在数组中出现至少两次,返回 true;否则返回 false。

示例

输入:[1,2,3,1] → 输出:true

输入:[1,2,3,4] → 输出:false

解题思路

使用哈希集合,遍历数组时检查当前元素是否已在集合中,若在则返回 true,否则加入集合。

图解

复制代码
nums = [1,2,3,1]
集合: {} → 加入1 → {1} → 加入2 → {1,2} → 加入3 → {1,2,3} → 遇到1,已存在 → 返回 true

Python代码

python 复制代码
def containsDuplicate(nums):
    seen = set()
    for num in nums:
        if num in seen:
            return True
        seen.add(num)
    return False

C++代码

cpp 复制代码
class Solution {
public:
    bool containsDuplicate(vector<int>& nums) {
        unordered_set<int> seen;
        for (int num : nums) {
            if (seen.count(num)) return true;
            seen.insert(num);
        }
        return false;
    }
};

复杂度分析

  • 时间复杂度:O(n),遍历一次,集合查找插入 O(1) 平均。
  • 空间复杂度:O(n),集合最多存储 n 个元素。

3. 两个数组的交集(LeetCode 349)

题目描述

给定两个数组 nums1nums2,返回它们的交集。输出结果中的每个元素一定是唯一的,可以不考虑输出结果的顺序。

示例

输入:nums1 = [1,2,2,1], nums2 = [2,2] → 输出:[2]

解题思路

  • nums1 转换为哈希集合。
  • 遍历 nums2,如果元素在集合中,则加入结果集(自动去重),并从集合中删除以避免重复添加。

图解

复制代码
nums1 = [1,2,2,1] → set1 = {1,2}
nums2 = [2,2] → 遍历: 2在set1中 → 加入结果,删除2 → set1={1}
结果: [2]

Python代码

python 复制代码
def intersection(nums1, nums2):
    set1 = set(nums1)
    result = set()
    for num in nums2:
        if num in set1:
            result.add(num)
    return list(result)

C++代码

cpp 复制代码
class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> set1(nums1.begin(), nums1.end());
        unordered_set<int> res;
        for (int num : nums2) {
            if (set1.count(num)) {
                res.insert(num);
            }
        }
        return vector<int>(res.begin(), res.end());
    }
};

复杂度分析

  • 时间复杂度:O(m + n),其中 m、n 分别为两个数组的长度。
  • 空间复杂度:O(min(m, n)),结果集和哈希集合的大小。

4. 有效的字母异位词(LeetCode 242)

题目描述

给定两个字符串 st,编写一个函数判断 t 是否是 s 的字母异位词(即字符种类和数量相同,顺序不同)。

示例

输入:s = "anagram", t = "nagaram" → 输出:true

输入:s = "rat", t = "car" → 输出:false

解题思路

  • 如果长度不同,直接返回 false。
  • 使用长度为26的数组(或哈希表)统计 s 中每个字符的出现次数。
  • 遍历 t,遇到字符则对应计数减1,如果减后小于0则返回 false。
  • 最后所有计数应为0。

图解

复制代码
s = "anagram", t = "nagaram"
计数数组: a:3, n:1, g:1, r:1, m:1
遍历t:
n → a:3,n:0,g:1,r:1,m:1
a → a:2,n:0,g:1,r:1,m:1
g → a:2,n:0,g:0,r:1,m:1
a → a:1,n:0,g:0,r:1,m:1
r → a:1,n:0,g:0,r:0,m:1
a → a:0,n:0,g:0,r:0,m:1
m → a:0,n:0,g:0,r:0,m:0 → true

Python代码

python 复制代码
def isAnagram(s, t):
    if len(s) != len(t):
        return False
    count = [0] * 26
    for ch in s:
        count[ord(ch) - ord('a')] += 1
    for ch in t:
        idx = ord(ch) - ord('a')
        count[idx] -= 1
        if count[idx] < 0:
            return False
    return True

C++代码

cpp 复制代码
class Solution {
public:
    bool isAnagram(string s, string t) {
        if (s.size() != t.size()) return false;
        vector<int> count(26, 0);
        for (char c : s) count[c - 'a']++;
        for (char c : t) {
            int idx = c - 'a';
            if (--count[idx] < 0) return false;
        }
        return true;
    }
};

复杂度分析

  • 时间复杂度:O(n),n 为字符串长度。
  • 空间复杂度:O(1),固定大小的计数数组。

5. 字符串中的第一个唯一字符(LeetCode 387)

题目描述

给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1。

示例

输入:s = "leetcode" → 输出:0

输入:s = "loveleetcode" → 输出:2

解题思路

  • 第一遍遍历,使用哈希表统计每个字符出现的次数。
  • 第二遍遍历,找到第一个出现次数为1的字符,返回其索引。

图解

复制代码
s = "loveleetcode"
统计: l:1, o:1, v:1, e:3, t:1, c:1, d:1
遍历: 索引0 'l' 次数1 → 返回0

Python代码

python 复制代码
def firstUniqChar(s):
    count = {}
    for ch in s:
        count[ch] = count.get(ch, 0) + 1
    for i, ch in enumerate(s):
        if count[ch] == 1:
            return i
    return -1

C++代码

cpp 复制代码
class Solution {
public:
    int firstUniqChar(string s) {
        unordered_map<char, int> count;
        for (char c : s) count[c]++;
        for (int i = 0; i < s.size(); ++i) {
            if (count[s[i]] == 1) return i;
        }
        return -1;
    }
};

复杂度分析

  • 时间复杂度:O(n),遍历两次字符串。
  • 空间复杂度:O(1) 或 O(Σ),由于字符集有限(26个字母),可视为 O(1)。

6. 字母异位词分组(LeetCode 49)

题目描述

给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同但排列不同的字符串。

示例

输入:strs = ["eat", "tea", "tan", "ate", "nat", "bat"]

输出:[["bat"],["nat","tan"],["ate","eat","tea"]]

解题思路

  • 使用哈希表,键为排序后的字符串,值为该组异位词的列表。
  • 遍历每个字符串,排序后作为键,将原字符串加入对应的列表。

图解

复制代码
strs = ["eat","tea","tan","ate","nat","bat"]
"eat" -> 排序"aet" -> map["aet"] = ["eat"]
"tea" -> 排序"aet" -> map["aet"] = ["eat","tea"]
"tan" -> 排序"ant" -> map["ant"] = ["tan"]
"ate" -> 排序"aet" -> map["aet"] = ["eat","tea","ate"]
"nat" -> 排序"ant" -> map["ant"] = ["tan","nat"]
"bat" -> 排序"abt" -> map["abt"] = ["bat"]
最终输出所有value

Python代码

python 复制代码
def groupAnagrams(strs):
    from collections import defaultdict
    groups = defaultdict(list)
    for s in strs:
        key = ''.join(sorted(s))
        groups[key].append(s)
    return list(groups.values())

C++代码

cpp 复制代码
class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        unordered_map<string, vector<string>> groups;
        for (string& s : strs) {
            string key = s;
            sort(key.begin(), key.end());
            groups[key].push_back(s);
        }
        vector<vector<string>> res;
        for (auto& p : groups) {
            res.push_back(p.second);
        }
        return res;
    }
};

复杂度分析

  • 时间复杂度:O(n × k log k),n 为字符串个数,k 为字符串最大长度;排序每个字符串 O(k log k)。
  • 空间复杂度:O(n × k),存储所有字符串。

7. LRU 缓存(LeetCode 146)

题目描述

设计和实现一个 LRU (最近最少使用) 缓存机制。支持 get(key)put(key, value) 操作,平均时间复杂度 O(1)。

要求

  • get(key):如果 key 存在,返回 value,否则返回 -1,并将该 key 标记为最近使用。
  • put(key, value):如果 key 存在,修改 value 并标记为最近使用;否则插入新键值对,若缓存达到容量,则删除最久未使用的键。

解题思路

  • 哈希表:存储 key 到节点的映射,实现 O(1) 查找。
  • 双向链表:维护访问顺序,最近使用的节点放在头部,最久未使用的放在尾部。
  • get:从哈希表找到节点,将其移动到链表头部。
  • put:若 key 存在,更新值并移动到头部;若不存在,插入新节点到头部,如果超过容量则删除尾部节点并从哈希表中删除。

图解

复制代码
容量=2,操作序列: put(1,1), put(2,2), get(1), put(3,3)
init: 空
put(1,1): head<->1<->tail
put(2,2): head<->2<->1<->tail
get(1): 将1移到头部: head<->1<->2<->tail
put(3,3): 容量满,删除尾部2: head<->3<->1<->tail

Python代码

python 复制代码
class DLinkedNode:
    def __init__(self, key=0, value=0):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None

class LRUCache:
    def __init__(self, capacity: int):
        self.cache = {}
        self.capacity = capacity
        self.size = 0
        self.head = DLinkedNode()
        self.tail = DLinkedNode()
        self.head.next = self.tail
        self.tail.prev = self.head

    def _add_node(self, node):
        node.prev = self.head
        node.next = self.head.next
        self.head.next.prev = node
        self.head.next = node

    def _remove_node(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

    def _move_to_head(self, node):
        self._remove_node(node)
        self._add_node(node)

    def _pop_tail(self):
        node = self.tail.prev
        self._remove_node(node)
        return node

    def get(self, key: int) -> int:
        if key in self.cache:
            node = self.cache[key]
            self._move_to_head(node)
            return node.value
        return -1

    def put(self, key: int, value: int) -> None:
        if key in self.cache:
            node = self.cache[key]
            node.value = value
            self._move_to_head(node)
        else:
            node = DLinkedNode(key, value)
            self.cache[key] = node
            self._add_node(node)
            self.size += 1
            if self.size > self.capacity:
                tail = self._pop_tail()
                del self.cache[tail.key]
                self.size -= 1

C++代码

cpp 复制代码
struct DLinkedNode {
    int key, value;
    DLinkedNode* prev;
    DLinkedNode* next;
    DLinkedNode(): key(0), value(0), prev(nullptr), next(nullptr) {}
    DLinkedNode(int k, int v): key(k), value(v), prev(nullptr), next(nullptr) {}
};

class LRUCache {
private:
    unordered_map<int, DLinkedNode*> cache;
    DLinkedNode* head;
    DLinkedNode* tail;
    int capacity;
    int size;

    void addToHead(DLinkedNode* node) {
        node->prev = head;
        node->next = head->next;
        head->next->prev = node;
        head->next = node;
    }

    void removeNode(DLinkedNode* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }

    void moveToHead(DLinkedNode* node) {
        removeNode(node);
        addToHead(node);
    }

    DLinkedNode* removeTail() {
        DLinkedNode* node = tail->prev;
        removeNode(node);
        return node;
    }

public:
    LRUCache(int capacity) {
        this->capacity = capacity;
        this->size = 0;
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head->next = tail;
        tail->prev = head;
    }
    
    int get(int key) {
        if (!cache.count(key)) return -1;
        DLinkedNode* node = cache[key];
        moveToHead(node);
        return node->value;
    }
    
    void put(int key, int value) {
        if (cache.count(key)) {
            DLinkedNode* node = cache[key];
            node->value = value;
            moveToHead(node);
        } else {
            DLinkedNode* node = new DLinkedNode(key, value);
            cache[key] = node;
            addToHead(node);
            ++size;
            if (size > capacity) {
                DLinkedNode* tailNode = removeTail();
                cache.erase(tailNode->key);
                delete tailNode;
                --size;
            }
        }
    }
};

复杂度分析

  • 时间复杂度:get 和 put 均为 O(1)。
  • 空间复杂度:O(capacity),缓存大小。

8. 无重复字符的最长子串(LeetCode 3)

题目描述

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例

输入:"abcabcbb" → 输出:3("abc")

输入:"bbbbb" → 输出:1

输入:"pwwkew" → 输出:3("wke")

解题思路:滑动窗口 + 哈希表

  • 使用两个指针 leftright 表示窗口边界,哈希表记录字符最近出现的位置。
  • 遍历字符串,right 右移:
    • 如果当前字符已在哈希表中且其索引 >= left,说明窗口内有重复,将 left 移动到重复字符的下一个位置。
    • 更新哈希表中字符的索引为当前 right
    • 计算当前窗口长度 right - left + 1,更新最大长度。

图解

复制代码
s = "abcabcbb"
left=0, right=0: 'a' 未出现 -> map{a:0}, maxLen=1
right=1: 'b' -> map{a:0,b:1}, maxLen=2
right=2: 'c' -> map{a:0,b:1,c:2}, maxLen=3
right=3: 'a' 已存在且索引0>=left -> left=1, 更新a:3, 窗口[1,3]="bca" 长度3
right=4: 'b' 已存在索引1>=left -> left=2, 更新b:4, 窗口[2,4]="cab" 长度3
right=5: 'c' 索引2>=left -> left=3, 更新c:5, 窗口[3,5]="abc" 长度3
right=6: 'b' 索引4>=left -> left=5, 更新b:6, 窗口[5,6]="cb" 长度2
right=7: 'b' 索引6>=left -> left=7, 窗口[7,7]="b" 长度1
最大长度=3

Python代码

python 复制代码
def lengthOfLongestSubstring(s: str) -> int:
    char_index = {}
    left = 0
    max_len = 0
    for right, ch in enumerate(s):
        if ch in char_index and char_index[ch] >= left:
            left = char_index[ch] + 1
        char_index[ch] = right
        max_len = max(max_len, right - left + 1)
    return max_len

C++代码

cpp 复制代码
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        unordered_map<char, int> charIndex;
        int left = 0, maxLen = 0;
        for (int right = 0; right < s.size(); ++right) {
            char c = s[right];
            if (charIndex.count(c) && charIndex[c] >= left) {
                left = charIndex[c] + 1;
            }
            charIndex[c] = right;
            maxLen = max(maxLen, right - left + 1);
        }
        return maxLen;
    }
};

复杂度分析

  • 时间复杂度:O(n),每个字符最多被 left 和 right 指针各遍历一次。
  • 空间复杂度:O(min(n, Σ)),哈希表存储字符,字符集大小有限(如 ASCII 128),可视为 O(1)。

🎯 总结

题目 核心技巧 时间复杂度 空间复杂度
1. 两数之和 哈希表记录补数 O(n) O(n)
217. 存在重复元素 集合去重 O(n) O(n)
349. 两个数组的交集 哈希集合 O(m+n) O(min(m,n))
242. 有效的字母异位词 字符计数数组 O(n) O(1)
387. 第一个唯一字符 两次遍历+计数 O(n) O(1)
49. 字母异位词分组 排序后作为哈希键 O(n×k log k) O(n×k)
146. LRU缓存 哈希表+双向链表 O(1) per op O(capacity)
3. 无重复最长子串 滑动窗口+哈希表 O(n) O(min(n,Σ))

哈希表的核心价值在于 O(1) 的查找和插入,常用于:判重、计数、映射、缓存等场景。熟练掌握这几道题,哈希表相关题目基本都能应对。

相关推荐
Jay-r7 分钟前
手势粒子特效系统 Gesture Particle FX(附源码下载)
python·ai·编程·pygame·百度云·手势控制
额恩6612 分钟前
ResearchPilot 第三阶段总结报告
python·算法
小罗水15 分钟前
第13章 Redis 缓存、幂等锁与任务状态
数据库·redis·缓存
红叶舞25 分钟前
基于线段树的数据结构
数据结构·python·算法
热心市民lcj26 分钟前
Spring Boot 整合 Caffeine 本地缓存实战
spring boot·后端·缓存
创世宇图30 分钟前
Video2Txt:160 行代码实现本地视频转文字
python·faster-whisper·video2txt·large-v3-turbo
落苜蓿蓝1 小时前
Java 循环中对象复用导致属性覆盖?从 JVM 内存模型讲解原因
java·jvm·python
小白说大模型2 小时前
Python 工程化设计模式:AI 项目中的异步流水线与策略模式实战
人工智能·python·设计模式
zx1154502 小时前
【热点 Key 探测与加长缓存 TTL 实战】
缓存
阿童木写作2 小时前
TikTok Shop视频翻译工具实战测评
人工智能·python·音视频