本文带来的是以哈希表为主题的一些经典题目,主要实现是C++和Python。
哈希表是根据关键码的值而直接进行访问的数据结构。数组就是一张哈希表。哈希表中关键码就是数组的索引下表,然后通过下表直接访问数组中的元素。
一般哈希表都是用来快速判断一个元素是否出现集合里。
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O ( log n ) O(\log n) O(logn) | O ( log n ) O(\log n) O(logn) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O ( log n ) O(\log n) O(logn) | O ( log n ) O(\log n) O(logn) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O ( 1 ) O(1) O(1) | O ( 1 ) O(1) O(1) |
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::map | 红黑树 | key有序 | key不可重复 | key不可修改 | O ( log n ) O(\log n) O(logn) | O ( log n ) O(\log n) O(logn) |
std::multimap | 红黑树 | key有序 | key可重复 | key不可修改 | O ( log n ) O(\log n) O(logn) | O ( log n ) O(\log n) O(logn) |
std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可修改 | O ( 1 ) O(1) O(1) | O ( 1 ) O(1) O(1) |
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的。
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
C++:
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = { 0 };
for (char i : s) {
record[i - 'a']++;
}
for (char j : t) {
record[j - 'a']--;
}
for (int k : record) {
if (k != 0) {
return false;
}
}
return true;
}
};
Python:
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
chord = [0] * 26
for s_ in s:
chord[ord(s_) - ord('a')] += 1
for t_ in t:
chord[ord(t_) - ord('a')] -= 1
for i in chord:
if i != 0:
return False
return True
Python里面,
ord()
返回值是对应的十进制整数
defaultdict用法:
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
from collections import defaultdict
s_dict = defaultdict(int)
t_dict = defaultdict(int)
for x in s: s_dict[x] += 1
for y in t: t_dict[y] += 1
return s_dict == t_dict
给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串,判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成。如果可以构成,返回 true ;否则返回 false。
(题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次。)
C++:
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int chord[26] = { 0 };
for (char r : ransomNote) {
chord[r - 'a']++;
}
for (char m : magazine) {
chord[m - 'a']--;
}
for (int i : chord) {
if (i > 0) {
return false;
}
}
return true;
}
};
Python:
class Solution:
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
r_chord, m_chord = [0] * 26, [0] * 26
for s_ in ransomNote:
r_chord[ord(s_) - ord('a')] += 1
for t_ in magazine:
m_chord[ord(t_) - ord('a')] += 1
for i in range(26):
if r_chord[i] > m_chord[i]:
return False
return True
class Solution:
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
from collections import defaultdict
hashmap = defaultdict(int)
for i in magazine:
hashmap[i] += 1
for x in ransomNote:
value = hashmap.get(x)
if value is None or value == 0:
return False
else:
hashmap[x] -= 1
return True
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。字母异位词 是由重新排列源单词的字母得到的一个新单词,所有源单词中的字母通常恰好只用一次。
由于互为字母异位词的两个字符串包含的字母相同,因此对两个字符串分别进行排序之后得到的字符串一定是相同的,故可以将排序之后的字符串作为哈希表的键。
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> map;
for (auto s : strs) {
string key = s;
sort(key.begin(), key.end());
map[key].push_back(s);
}
vector<vector<string>> ans;
for (auto it = map.begin(); it != map.end(); it++) {
ans.push_back(it->second);
}
return ans;
}
};
由于互为字母异位词的两个字符串包含的字母相同,因此两个字符串中的相同字母出现的次数一定是相同的,故可以将每个字母出现的次数使用字符串表示,作为哈希表的键。由于字符串只包含小写字母,因此对于每个字符串,可以使用长度为 26 的数组记录每个字母出现的次数。
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
mp = collections.defaultdict(list)
for st in strs:
counts = [0] * 26
for ch in st:
counts[ord(ch) - ord('a')] += 1
mp[tuple(counts)].append(st)
return list(mp.values())
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。(异位词指字母相同,但排列不同的字符串。)
滑动窗口+双指针+哈希表
C++:
class Solution {
public:
vector<int> findAnagrams1(string s, string p) {
if (p.size() > s.size()) return {};
vector<int> need(26);
vector<int> chord(26);
vector<int> ret;
for (char i : p) {
need[i - 'a']++;
}
int left = 0, right = 0;
while (right < s.size()) {
chord[s[right] - 'a']++;
if (right - left + 1 == p.size()) {
if (chord == need) {
ret.push_back(left);
}
chord[s[left] - 'a']--;
left++;
}
right++;
}
return ret;
}
vector<int> findAnagrams(string s, string p) {
int sLen = s.size(), pLen = p.size();
if (sLen < pLen) return {};
vector<int> chord(26);
for (char i : p) {
chord[i - 'a']++;
}
vector<int> ret;
for (int left = 0, right = 0; right < sLen; right++) {
chord[s[right] - 'a']--;
while (chord[s[right] - 'a'] < 0) { // 如果小于0就收缩左侧边界直到不再小于0
chord[s[left] - 'a']++;
left++;
}
if (right - left + 1 == pLen) {
ret.push_back(left);
}
}
return ret;
}
};
Python:
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
low, fast = 0, 0
ret = []
window, need = [0] * 26, [0] * 26
for i in p:
need[ord(i) - ord('a')] += 1
while fast < len(s):
window[ord(s[fast]) - ord('a')] += 1
if fast - low + 1 == len(p):
if need == window:
ret.append(low)
window[ord(s[low]) - ord('a')] -= 1
low += 1
fast += 1
return ret
给定两个数组,编写一个函数来计算它们的交集。
使用 unordered_set
:
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set;
unordered_set<int> num_set(nums1.begin(), nums1.end());
for (int n : nums2) {
if (num_set.find(n) != num_set.end()) {
result_set.insert(n);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
Python:利用集合set()
set()
函数创建一个无序不重复元素集,可进行关系测试,删除重复数据,还可以计算交集、差集、并集等。x & y
交集x | y
并集class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
num1, num2 = set(nums1), set(nums2)
ret = []
for i in num1:
if i in num2:
ret.append(i)
return list(set(ret))
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
return list(set(nums1) & set(nums2)) # 求交集
给定两个数组,编写一个函数来计算它们的交集。(输出结果中每个元素出现的次数,应与元素在两个数组中出现次数的最小值一致。我们可以不考虑输出结果的顺序。)
class Solution:
def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
from collections import Counter
if len(nums1) > len(nums2):
nums1, nums2 = nums2, nums1
m = Counter()
for num in nums1: m[num] += 1
ret = []
for num in nums2:
if m.get(num, 0) > 0: # 如果dict中没有num,则取i的value为0
ret.append(num)
m[num] -= 1
if m[num] == 0:
m.pop(num)
return ret
class Solution {
public:
vector<int> intersect1(vector<int>& nums1, vector<int>& nums2) {
if (nums1.size() > nums2.size()) {
return intersect(nums2, nums1);
}
unordered_map<int, int> m;
for (int num : nums1) m[num]++;
vector<int> ret;
for (int num : nums2) {
if (m.count(num)) { // count(k):如果k存在返回1,不存在返回0
ret.push_back(num);
m[num]--;
if (m[num] == 0) {
m.erase(num);
}
}
}
return ret;
}
};
如果两个数组是有序的,则可以使用双指针的方法得到两个数组的交集。首先对两个数组进行排序,然后使用两个指针遍历两个数组。
如果是进阶问题一中已排序的数组,则只需 O(n) 的时间复杂度。
class Solution:
def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
nums1 = sorted(nums1)
nums2 = sorted(nums2)
ret = []
left, right = 0, 0
while left < len(nums1) and right < len(nums2):
if nums1[left] < nums2[right]:
left += 1
elif nums1[left] == nums2[right]:
ret.append(nums1[left])
left += 1
right += 1
else:
right += 1
return ret
class Solution {
public:
vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
sort(nums1.begin(), nums1.end());
sort(nums2.begin(), nums2.end());
int n1 = nums1.size(), n2 = nums2.size();
int i = 0, j = 0;
vector<int> ret;
while (i < n1 and j < n2) {
if (nums1[i] == nums2[j]) {
ret.push_back(nums1[1]);
i++;
j++;
}
else if (nums1[i] < nums2[j]) {
i++;
}
else {
j++;
}
}
return ret;
}
};
编写一个算法来判断一个数 n 是不是快乐数。「快乐数」定义为:
如果 n 是快乐数就返回 true ;不是,则返回 false 。
使用哈希法来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。
快乐数必定会收敛到1,1必须是收敛的,因为1的平方还是1,若生成的一组数中有两个重合(非1)的,那么就会构成一种循环
class Solution {
public:
bool isHappy(int n) {
unordered_set<int> set;
while (1) {
int sum = getSum(n);
if (sum == 1) {
return true;
}
// 如果这个sum曾经出现过,说明已经陷入了无限循环了
if (set.count(sum)) {
return false;
}
else {
set.insert(sum);
}
n = sum;
}
}
int getSum(int n) {
int sum = 0;
while (n) {
sum += (n % 10) * (n % 10);
n /= 10;
}
return sum;
}
};
class Solution:
def isHappy(self, n: int) -> bool:
def calculate_happy(num):
sum_ = 0
while num:
sum_ += (num % 10) ** 2
num = num // 10
return sum_
record = set()
while True:
n = calculate_happy(n)
if n == 1:
return True
if n in record:
return False
else:
record.add(n)
class Solution:
def isHappy(self, n: int) -> bool:
num = list(str(n))
sum_ = 0
record = set()
while sum_ != 1:
sum_ = sum([int(i)**2 for i in num])
num = list(str(sum_))
if sum_ in record:
return False
record.add(sum_)
return True
快慢指针:“快指针” 每次走两步,“慢指针” 每次走一步,当二者相等时,即为一个循环周期。此时,判断是不是因为 1 引起的循环,是的话就是快乐数,否则不是快乐数。
class Solution {
public:
bool isHappy(int n) {
int slow = n, fast = n;
do {
slow = getSum(slow);
fast = getSum(getSum(fast));
} while (slow != fast);
return slow == 1;
}
int getSum(int n) {
int sum = 0;
while (n) {
sum += (n % 10) * (n % 10);
n /= 10;
}
return sum;
}
};
利用双指针判断循环!!
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。你可以按任意顺序返回答案。
暴力搜索时间复杂度较高的原因是寻找 target - x 的时间复杂度过高。因此,我们需要一种更优秀的方法,能够快速寻找数组中是否存在目标元素。如果存在,我们需要找出它的索引。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> m;
for (int i = 0; i < nums.size(); i++) {
auto it = m.find(target - nums[i]);
if (it != m.end()) {
return { it->second, i };
}
m[nums[i]] = i;
}
return {};
}
};
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
records = dict()
for idx, val in enumerate(nums):
if target - val not in records:
records[val] = idx
else:
return [records[target - val], idx]
给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l)
,使得 A[i] + B[j] + C[k] + D[l] = 0
。为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500
。所有整数的范围在 -2^28 到 2^28 - 1 之间,最终结果不会超过 2^31 - 1 。
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int, int> m;
for (int a : nums1) {
for (int b : nums2) {
m[a + b]++;
}
}
int count = 0;
for (int c : nums3) {
for (int d : nums4) {
if (m.count(0 - (c + d))) {
count += m[-c - d];
}
}
}
return count;
}
};
class Solution:
def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
hashmap = {}
for n1 in nums1:
for n2 in nums2:
if n1 + n2 in hashmap:
hashmap[n1 + n2] += 1 # 记录和相同的组合的对数
else:
hashmap[n1 + n2] = 1
count = 0
for n3 in nums3:
for n4 in nums4:
key = -n3 - n4
if key in hashmap:
count += hashmap[key]
return count
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
双指针:难点在去重
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end());
vector<vector<int>> ans;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] > 0) {
return ans;
}
if (i > 0 and nums[i] == nums[i - 1]) continue; // 去重
int left = i + 1;
int right = nums.size() - 1;
while (left < right) {
if (nums[i] + nums[left] + nums[right] > 0) right--;
else if (nums[i] + nums[left] + nums[right] < 0) left++;
else {
ans.push_back({ nums[i],nums[left],nums[right] });
while (left < right and nums[right] == nums[right - 1]) right--;
while (left < right and nums[left] == nums[left + 1]) left++;
right--;
left++;
}
}
}
return ans;
}
};
当我们需要枚举数组中的两个元素时,如果我们发现随着第一个元素的递增,第二个元素是递减的,那么就可以使用双指针的方法,将枚举的时间复杂度从 O(N^2)减少至 O(N)。
给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]]
:
你可以按 任意顺序 返回答案。
对于15.三数之和 双指针法就是将原本暴力 O ( n 3 ) O(n^3) O(n3)的解法,降为 O ( n 2 ) O(n^2) O(n2)的解法,四数之和的双指针解法就是将原本暴力 O ( n 4 ) O(n^4) O(n4)的解法,降为 O ( n 3 ) O(n^3) O(n3)的解法。
双指针:
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
vector<vector<int>> ret;
for (int i = 0; i < nums.size(); i++) {
if (i > 0 and nums[i] == nums[i - 1]) continue;
for (int j = i + 1; j < nums.size(); j++) {
if (j > i + 1 and nums[j] == nums[j - 1]) continue;
int left = j + 1, right = nums.size() - 1;
while (left < right) {
if (nums[i] + nums[j] < target - (nums[left] + nums[right])) {
left++;
}
else if (nums[i] + nums[j] > target - (nums[left] + nums[right])) {
right--;
}
else {
ret.push_back({ nums[i], nums[j], nums[left], nums[right] });
while (left < right && nums[left] == nums[left + 1]) left++;
while (left < right && nums[right] == nums[right - 1]) right--;
left++;
right--;
}
}
}
}
return ret;
}
};
哈希表:
class Solution:
def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
hashmap = {}
for n in nums:
if n in hashmap:
hashmap[n] += 1
else:
hashmap[n] = 1
# good thing about using python is you can use set to drop duplicates.
ret = set()
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
for k in range(j + 1, len(nums)):
val = target - nums[i] - nums[j] - nums[k]
if val in hashmap:
count = (nums[i] == val) + (nums[j] == val) + (nums[k] == val)
if hashmap[val] > count:
ret.add(tuple(sorted([nums[i], nums[j], nums[k], val])))
return list(ret)
Todo:
- 数组
- 链表
- 哈希表
- 双指针
- 栈与队列
- 二叉树
- 回溯
- 贪心
- 动态规划
以上题解大多来自【代码随想录】,在此基础上做了一定总结,并附带一些自己的理解。随缘更新,有错误请指出!
END