文章目录
哈希表
1、理论基础
哈希表定义
哈希表是根据关键码的值而直接进行访问的数据结构。
常见的三种哈希结构
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
- 数组
- set (集合)
- map(映射)
这里数组就没啥可说的了,我们来看一下set。
在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O(logn) | O(logn) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O(logn) | O(logn) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::map | 红黑树 | key有序 | key不可重复 | key不可修改 | O(logn) | O(logn) |
std::multimap | 红黑树 | key有序 | key可重复 | key不可修改 | O(logn) | O(logn) |
std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可修改 | O(1) | O(1) |
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
那么再来看一下map ,在map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。
其他语言例如:java里的HashMap ,TreeMap 都是一样的原理。可以灵活贯通。
虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,但是std::set、std::multiset 依然使用哈希函数来做映射,只不过底层的符号表使用了红黑树来存储数据,所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。
这里在说一下,一些C++的经典书籍上 例如STL源码剖析,说到了hash_set hash_map,这个与unordered_set,unordered_map又有什么关系呢?
实际上功能都是一样一样的, 但是unordered_set在C++11的时候被引入标准库了,而hash_set并没有,所以建议还是使用unordered_set比较好,这就好比一个是官方认证的,hash_set,hash_map 是C++11标准之前民间高手自发造的轮子。
2、数组作为哈希表
leetcode 242. 有效的字母异位词
leetcode 242. 有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
示例 1:
- 输入: s = “anagram”, t = “nagaram”
- 输出: true
示例 2:
- 输入: s = “rat”, t = “car”
- 输出: false。
直接使用数组当作哈希表,数组下标为哈希表的键值key,数组元素为个字符出现的次数value
代码如下:
bool isAnagram(char* s, char* t) {
int hash[26] = {0};
for (int i = 0; i < strlen(s); i++) {
hash[s[i] - 'a']++;
}
for (int i = 0; i < strlen(t); i++) {
hash[t[i] - 'a']--;
}
for (int i = 0; i < 26; i++) {
if (hash[i] != 0) {
return false;
}
}
return true;
}
leetcode 383. 赎金信
leetcode 383. 赎金信
给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串,判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成。如果可以构成,返回 true ;否则返回 false。
(题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次。)
注意:你可以假设两个字符串均只含有小写字母。
- canConstruct(“a”, “b”) -> false
- canConstruct(“aa”, “ab”) -> false
- canConstruct(“aa”, “aab”) -> true
此题因为只有小写字母,所以直接使用数组当做哈希表即可,
C语言代码如下:
bool canConstruct(char * ransomNote, char * magazine) {
/* 定义哈希表 */
int hashtable[26] = {0};
/* 计算magazine的哈希值 */
for (int i = 0; i < strlen(magazine); i++) {
hashtable[magazine[i] - 'a']++;
}
/* 判断ransomNote在magazine哈希表中的值是否符合 */
for (int i = 0; i < strlen(ransomNote); i++) {
/* magazine中的哈希值小于ransomNote中的哈希值,返回false */
if (--hashtable[ransomNote[i] - 'a'] < 0) {
return false;
}
}
return true;
}
3、set作为哈希表
leetcode 349. 两个数组的交集
leetcode 349. 两个数组的交集
题意:给定两个数组,编写一个函数来计算它们的交集。
说明: 输出结果中的每个元素一定是唯一的。 我们可以不考虑输出结果的顺序。
242题由于数组不是很大使用数组当作哈希表可以,而且如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
此时就要使用另一种结构体了,set ,关于set,C++ 给提供了如下三种可用的数据结构:
- std::set
- std::multiset
- std::unordered_set
std::set和std::multiset底层实现都是红黑树,std::unordered_set的底层实现是哈希表, 使用unordered_set 读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set。
C++代码如下:
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set; // 存放结果
unordered_set<int> nums_set(nums1.begin(), nums1.end());
for (int num : nums2) {
// 发现nums2的元素 在nums_set里又出现过
if (nums_set.find(num) != nums_set.end()) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
C语言代码如下:由于leetcode的输入数据还是比较小,所以此题可用数组当作哈希表
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
/* 题目里没说元素的大小是多少,为什么判断还能通过? */
#define MAX_SIZE 1024
int* intersection(int* nums1, int nums1Size, int* nums2, int nums2Size, int* returnSize){
int* hash = (int*)calloc(MAX_SIZE, sizeof(int));
int* res = (int*)calloc(MAX_SIZE, sizeof(int));
for (int i = 0; i < nums1Size; i++) {
if (hash[nums1[i]] == 0) {
hash[nums1[i]]++;
}
}
*returnSize = 0;
for (int i = 0; i < nums2Size; i++) {
if (hash[nums2[i]]) {
hash[nums2[i]]--;
res[(*returnSize)++] = nums2[i];
}
}
return res;
}
leetcode 202. 快乐数
leetcode 202. 快乐数
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。
如果 n 是快乐数就返回 True ;不是,则返回 False 。
示例:
- 输入:19
- 输出:true
- *解释:
- 1^2 + 9^2 = 82
- 8^2 + 2^2 = 68
- 6^2 + 8^2 = 100
- 1^2 + 0^2 + 0^2 = 1
还是如果数据过大使用数组不合适
C++代码:
class Solution {
public:
// 取数值各个位上的单数之和
int getSum(int n) {
int sum = 0;
while (n) {
sum += (n % 10) * (n % 10);
n /= 10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> set;
while(1) {
int sum = getSum(n);
if (sum == 1) {
return true;
}
// 如果这个sum曾经出现过,说明已经陷入了无限循环了,立刻return false
if (set.find(sum) != set.end()) {
return false;
} else {
set.insert(sum);
}
n = sum;
}
}
};
C语言代码:此题leetcode的输入数据还不是很大
#define SIZE 1024
int getSum(int n) {
int sum = 0;
while (n) {
sum += (n % 10) * (n % 10);
n /= 10;
}
return sum;
}
bool isHappy(int n){
int* hash = (int*)calloc(SIZE, sizeof(int));
while (1) {
int sum = getSum(n);
if (sum == 1) {
return true;
}
if (hash[sum] != 0) {
return false;
} else {
hash[sum]++;
}
n = sum;
}
}
leetcode 15.三数之和(哈希复杂,双指针简单)
leetcode 15.三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意: 答案中不可以包含重复的三元组。
示例:
- 给定数组 nums = [-1, 0, 1, 2, -1, -4],
- 满足要求的三元组集合为: [ [-1, 0, 1], [-1, -1, 2] ]
此题可以使用set当做哈希表来求解,但是题目要求不能出现重复的三元组,所以哈希表求解不合适参考代码随想录的讲解
C++使用set:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[j], c = -(a + b)
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么不可能凑成三元组
if (nums[i] > 0) {
continue;
}
if (i > 0 && nums[i] == nums[i - 1]) { //三元组元素a去重
continue;
}
unordered_set<int> set;
for (int j = i + 1; j < nums.size(); j++) {
if (j > i + 2
&& nums[j] == nums[j-1]
&& nums[j-1] == nums[j-2]) { // 三元组元素b去重
continue;
}
int c = 0 - (nums[i] + nums[j]);
if (set.find(c) != set.end()) {
result.push_back({nums[i], nums[j], c});
set.erase(c);// 三元组元素c去重
} else {
set.insert(nums[j]);
}
}
}
return result;
}
};
此题使用双指针:
C语言代码:
/**
* Return an array of arrays of size *returnSize.
* The sizes of the arrays are returned as *returnColumnSizes array.
* Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
*/
/* qsort的比较函数 */
int cmp(const void* a, const void* b) {
return *(int*)a - *(int*)b;
}
int** threeSum(int* nums, int numsSize, int* returnSize, int** returnColumnSizes){
/* 先记录返回的行数为0 */
*returnSize = 0;
/* 输入为空、或元素个数小于3则返回NULL */
if (nums == NULL || numsSize < 3) {
return NULL;
}
/* 将nums排序为升序排列 */
qsort(nums, numsSize, sizeof(int), cmp);
/* 分配返回数组、返回数组的列数 */
int** ret = (int**)malloc(numsSize * numsSize * sizeof(int*));
*returnColumnSizes = (int*)malloc(numsSize * numsSize *sizeof(int));
/* 排序后的数组从头到尾进行遍历 */
for (int i = 0; i < numsSize; i++) {
/* 当前数组大于0,表示后续没有三数之和为0,结束遍历 */
if (nums[i] > 0) {
break;
}
/* 当前元素与上一次相等,跳过此次计算,去重 */
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
/* 定义左右指针 */
int left = i + 1, right = numsSize - 1;
/* 开始查找三数之和为0 */
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
ret[*returnSize] = (int*)malloc(sizeof(int) * 3);
ret[*returnSize][0] = nums[i];
ret[*returnSize][1] = nums[left];
ret[*returnSize][2] = nums[right];
/* 返回数组当前行的列数为3 */
(*returnColumnSizes)[*returnSize] = 3;
/* 返回数组的行数自加1 */
(*returnSize)++;
/* 对左右指针进行去重 */
while (left < right && nums[left] == nums[++left]);
while (left < right && nums[right] == nums[--right]);
} else if (sum < 0) {
left++;
} else {
right--;
}
}
}
return ret;
}
leetcode 18.四树之和(哈希复杂,双指针简单)
leetcode 18.四树之和(哈希复杂,双指针简单)
题意:
- 给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。
注意:答案中不可以包含重复的四元组。
示例:
- 给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。 满足要求的四元组集合为: [ [-1, 0, 0, 1], [-2, -1, 1, 2], [-2, 0, 0, 2] ]
C语言代码:
- 此题也是可以使用哈希表进行求解,但是比较复杂,涉及到重复元素去重问题,所以改为双指针求解
/**
* Return an array of arrays of size *returnSize.
* The sizes of the arrays are returned as *returnColumnSizes array.
* Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
*/
int cmp(const void* a, const void* b) {
return *(int*)a - *(int*)b;
}
int** fourSum(int* nums, int numsSize, int target, int* returnSize, int** returnColumnSizes){
*returnSize = 0;
if (nums == NULL || numsSize < 4) {
return NULL;
}
qsort(nums, numsSize, sizeof(int), cmp);
int** ret = (int**)malloc(numsSize * numsSize * sizeof(int*));
*returnColumnSizes = (int*)malloc(numsSize * numsSize * sizeof(int));
for (int i = 0; i < numsSize; i++) {
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
for (int j = i + 1; j < numsSize; j++) {
int sum1 = nums[i] + nums[j];
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
int L = j + 1, R = numsSize - 1;
while (L < R) {
int sum2 = nums[L] + nums[R];
if (sum1 + sum2 == target) {
ret[*returnSize] = (int*)malloc(sizeof(int) * 4);
ret[*returnSize][0] = nums[i];
ret[*returnSize][1] = nums[j];
ret[*returnSize][2] = nums[L];
ret[*returnSize][3] = nums[R];
(*returnColumnSizes)[*returnSize] = 4;
(*returnSize)++;
while (L < R && nums[L] == nums[++L]);
while (L < R && nums[R] == nums[--R]);
} else if (sum1 + sum2 < target) {
L++;
} else {
R--;
}
}
}
}
return ret;
}
4、map作为哈希表
leetcode 1. 两数之和
leetcode 1. 两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
- 输入:nums = [2,7,11,15], target = 9
- 输出:[0,1]
- 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
C++代码:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
/* 此题不需要排序,使用无需map更高效 */
std::unordered_map<int, int> map;
for (int i = 0; i < nums.size(); i++) {
/* 在map中查找target - nums[i] */
auto iter = map.find(target - nums[i]);
/* 找到目标就返回 */
if (iter != map.end()) {
return {iter->second, i};
}
/* 找不到就将当前元素插入map */
map.insert(pair<int, int>(nums[i], i));
}
return {};
}
};
leetcode 454. 四数相加 II(C语言的hashtable)
leetcode 454. 四数相加 II
给定四个包含整数的数组列表 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 。
例如:
- 输入: A = [ 1, 2] B = [-2,-1] C = [-1, 2] D = [ 0, 2] 输出: 2 解释: 两个元组如下:
- (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0
- (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0
将A、B之和当做键值key,再对C、D进行双重循环遍历进行查找即可:
C语言代码:
struct hashTable {
int key; /* 键值 */
int value; /* 键值出现的次数 */
UT_hash_handle hh; /* uthash需要用户定义自己的数据结构,一个包含UT_hash_handle hh的结构体 */
};
int fourSumCount(int* A, int ASize, int* B, int BSize, int* C, int CSize, int* D, int DSize){
struct hashTable* hashtable = NULL;
for (int i = 0; i < ASize; i++) {
for (int j = 0; j < BSize; j++) {
int nkey = A[i] + B[j];
struct hashTable* tmp;
HASH_FIND_INT(hashtable, &nkey, tmp);
if (!tmp) { /* 此键值未出现过 */
struct hashTable* new_hashNode = malloc(sizeof(struct hashTable));
new_hashNode->key = nkey;
new_hashNode->value = 1;
HASH_ADD_INT(hashtable, key, new_hashNode); /* 这里的key就是结构体中的key、不是ikey */
} else { /* 此键值出现过 */
tmp->value++;
}
}
}
int ret = 0; /* 返回满足条件的次数 */
for (int i = 0; i < CSize; i++) {
for (int j = 0; j < DSize; j++) {
int mkey = -(C[i] + D[j]);
struct hashTable* tmp;
HASH_FIND_INT(hashtable, &mkey, tmp);
if (tmp) {
ret += tmp->value;
}
}
}
return ret;
}
C++代码:
class Solution {
public:
int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
unordered_map<int, int> umap; //key:a+b的数值,value:a+b数值出现的次数
// 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中
for (int a : A) {
for (int b : B) {
umap[a + b]++;
}
}
int count = 0; // 统计a+b+c+d = 0 出现的次数
// 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来。
for (int c : C) {
for (int d : D) {
if (umap.find(0 - (c + d)) != umap.end()) {
count += umap[0 - (c + d)];
}
}
}
return count;
}
};