哈希表理论基础
哈希表
哈希表是根据关键码的值而直接进行访问的数据结构。
哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:
一般哈希表都是用来快速判断一个元素是否出现集合里。
(例如要查询一个名字是否在这所学校里,把学生的姓名直接映射为哈希表上的索引,然后通过查询索引下标快速知道这位同学是否在这所学校里。要枚举的话时间复杂度是O(n),但如果使用哈希表只需要O(1)。)
哈希函数
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
如果hashCode得到的数值大于哈希表的大小(大于tableSize),为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,这样就保证了学生姓名一定可以映射到哈希表上。
哈希碰撞
如果学生的数量大于哈希表的大小,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表同一个索引下标的位置。
如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞。
一般哈希碰撞有两种解决方法, 拉链法和线性探测法。
拉链法
小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王。
拉链法要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
线性探测法
数据规模是dataSize, 哈希表的大小为tableSize,使用线性探测法,一定要保证tableSize大于dataSize。 需要依靠哈希表中的空位来解决碰撞问题。
例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放冲突的数据。如图所示:
242.有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
示例 1: 输入: s = "anagram", t = "nagaram" 输出: true
示例 2: 输入: s = "rat", t = "car" 输出: false
暴力解法:
两层for循环,记录字符是否重复出现。 时间复杂度: O(n^2)
哈希表法:
定义一个数组,来记录字符串s里字符出现的次数。
- 创建一个大小为26的数组,用于存储每个字符出现的次数。
- 对于字符串s,遍历其中的每个字符,并将其在数组中对应的计数器加1。
- 对于字符串t,同样遍历其中的每个字符,并将其在数组中对应的计数器减1。
- 最后检查数组中所有计数器是否都为0,若是则说明t是s的字母异位词。
- 时间复杂度: O(n)
- 空间复杂度: O(1)
例,判断一下字符串s= "aee", t = "eae"。
操作动画如下:
bool isAnagram(char* s, char* t) {
int count[26] = {0}; // 创建大小为26的数组,初始化为0
int len_s = strlen(s);
int len_t = strlen(t);
if (len_s != len_t) { // 长度不相等必不是字母异位词
return false;
}
// 对字符串s进行遍历
for (int i = 0; i < len_s; i++) {
count[s[i] - 'a']++; // 将对应计数器加1
}
// 对字符串t进行遍历
for (int i = 0; i < len_t; i++) {
count[t[i] - 'a']--; // 将对应计数器减1
}
// 检查所有计数器是否都为0
for (int i = 0; i < 26; i++) {
if (count[i] != 0) {
return false;
}
}
return true;
}
java写法
class Solution {
public boolean isAnagram(String s, String t) {
int[] record = new int[26];
for (int i = 0; i < s.length(); i++) {
record[s.charAt(i) - 'a']++; // 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
}
for (int i = 0; i < t.length(); i++) {
record[t.charAt(i) - 'a']--;
}
for (int count: record) {
if (count != 0) { // record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。
return false;
}
}
return true; // record数组所有元素都为零0,说明字符串s和t是字母异位词
}
}
349. 两个数组的交集
给定两个数组,编写一个函数来计算它们的交集。
int* intersection(int* nums1, int nums1Size, int* nums2, int nums2Size, int* returnSize) {
// 创建一个计数数组来存储nums1中每个元素的出现次数
int nums1Cnt[1000] = {0};
// 根据nums1和nums2的大小决定结果数组的大小
int lessSize = nums1Size < nums2Size ? nums1Size : nums2Size;
// 为结果数组分配内存空间
int* result = (int*)calloc(lessSize, sizeof(int));
// 初始化结果数组的索引
int resultIndex = 0;
// 计算nums1数组中每个元素的出现次数
for (int i = 0; i < nums1Size; i++) {
nums1Cnt[nums1[i]]++;
}
// 检查nums2中的每个元素是否存在于计数数组中
for (int i = 0; i < nums2Size; i++) {
if (nums1Cnt[nums2[i]] > 0) {
// 如果元素存在,则将其添加到结果数组中
result[resultIndex] = nums2[i];
resultIndex++;
// 将计数数组中对应元素的计数清零,以避免重复添加
nums1Cnt[nums2[i]] = 0;
}
}
*returnSize = resultIndex;// 将结果数组的长度保存到returnSize中
return result;// 返回结果数组
}
java写法
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
int[] hash1 = new int[1002];
int[] hash2 = new int[1002];
for(int i : nums1)
hash1[i]++;
for(int i : nums2)
hash2[i]++;
List<Integer> resList = new ArrayList<>();
for(int i = 0; i < 1002; i++)
if(hash1[i] > 0 && hash2[i] > 0)
resList.add(i);
int index = 0;
int res[] = new int[resList.size()];
for(int i : resList)
res[index++] = i;
return res;
}
}
202. 快乐数
对于一个正整数,如果对其每一位的数字平方求和,重复这个过程最终可以得到1,那么这个数就是快乐数。
数组
int get_sum(int n) { // 定义函数,输入一个整数n,返回各位数字平方和
int sum = 0; // 定义一个变量sum,用于存储平方和,初始值为0
div_t n_div = { .quot = n }; // 定义一个div_t结构体n_div,初始化其quot成员为n
while (n_div.quot != 0) { // 循环直到n_div.quot等于0(即已处理完所有位上的数字)
n_div = div(n_div.quot, 10); // 使用div函数将n_div.quot除以10,得到商和余数,更新n_div
sum += n_div.rem * n_div.rem; // 将余数的平方加入sum中
}
return sum; // 返回计算结果
}
bool isHappy(int n) {
uint8_t visited[163] = { 0 }; // 定义一个数组visited,用于记录已经出现过的中间结果,初始值为0
int sum = get_sum(get_sum(n)); // 计算输入n的平方和的平方和
int next_n = sum; // 将计算结果赋值给next_n
while (next_n != 1) { // 循环直到next_n等于1(即找到了符合条件的快乐数)
sum = get_sum(next_n); // 计算next_n的平方和
if (visited[sum]) return false; // 如果计算结果在visited数组中已经被标记为已访问过,说明进入了一个循环,函数返回false
visited[sum] = 1; // 否则,将计算结果在visited数组中标记为已访问过
next_n = sum; // 将计算结果赋值给next_n,继续下一轮循环
};
return true; // 最终找到符合条件的快乐数,函数返回true
}
快慢指针
在循环中,慢指针每次移动一步,而快指针每次移动两步。如果存在循环,快慢指针最终会相遇;如果不存在循环,快指针最终会达到1。
(假设我们要判断数字 19 是否是快乐数。我们将按照如下步骤进行计算:
- 初始化慢指针
slow
和快指针fast
,都指向初始数值 19。 - 根据规则,计算
slow
的下一个数值:1^2 + 9^2 = 82。 - 根据规则,计算
fast
的下一个数值:8^2 + 2^2 = 68。 - 再次计算
fast
的下一个数值:6^2 + 8^2 = 100。 - 继续计算
fast
的下一个数值:1^2 + 0^2 + 0^2 = 1。 - 计算
slow
的下一个数值:8^2 + 2^2 = 68。
在第6步,我们发现 slow
和 fast
相等了,它们都等于 68。这意味着我们在计算过程中进入了一个循环,而不会再产生新的数值。因此19 是快乐数。
快指针的速度更快,它能更早地进入循环并与慢指针相遇。如果序列最终收敛到1,那么快指针和慢指针都会等于1,这意味着数字是一个快乐数。)
int get_sum(int n) { // 定义函数,输入一个整数n,返回各位数字平方和
int sum = 0; // 定义一个变量sum,用于存储平方和,初始值为0
div_t n_div = { .quot = n }; // 定义一个div_t结构体n_div,初始化其quot成员为n
while (n_div.quot != 0) { // 循环直到n_div.quot等于0(即已处理完所有位上的数字)
n_div = div(n_div.quot, 10); // 使用div函数将n_div.quot除以10,得到商和余数,更新n_div
sum += n_div.rem * n_div.rem; // 将余数的平方加入sum中
}
return sum; // 返回计算结果
}
bool isHappy(int n) {
int slow = n; // 定义慢指针slow,并将其初始化为n
int fast = n; // 定义快指针fast,并将其初始化为n
do {
slow = get_sum(slow); // 慢指针每次移动一步,计算下一个数的平方和
fast = get_sum(get_sum(fast)); // 快指针每次移动两步,计算下一个数的平方和的平方和
} while (slow != fast); // 当快慢指针相等时退出循环,说明找到了循环
return (fast == 1); // 返回快指针最后的值是否为1,若为1则是快乐数,否则不是
}
java写法
class Solution {
public boolean isHappy(int n) {
Set<Integer> record = new HashSet<>();
while (n != 1 && !record.contains(n)) {
record.add(n);
n = getNextNumber(n);
}
return n == 1;
}
private int getNextNumber(int n) {
int res = 0;
while (n > 0) {
int temp = n % 10;
res += temp * temp;
n = n / 10;
}
return res;
}
}
1. 两数之和
给定一个整数数组 nums 和一个目标值 target,在该数组中找出和为target的那两个整数,并返回他们的数组下标。
在函数中,先使用 hashMapAdd
函数将数组中每个元素的值和下标插入哈希表中。然后通过hashMapFind函数
遍历数组,在哈希表中查找与目标值相差的值,即 target - nums[i]
,如果有匹配成功,则返回这两个数的下标,如果没有,就把目前遍历的元素放进map中。最后,在函数执行结束时使用 hashMapCleanup
函数清除哈希表。
(map用来存放访问过的元素,map中的存储结构为 {key:数据元素,value:数组元素对应的下标}。)
过程如下:
typedef struct {
int key; // 键值,代表数组中的元素值
int value; // 值,代表元素值在数组中的下标
UT_hash_handle hh; // 用于让结构体能够被哈希的宏
} map;
map* hashMap = NULL; // 声明一个哈希表
// 向哈希表中添加键值对
void hashMapAdd(int key, int value){
map* s;
// 判断 key 是否已经在哈希表中
HASH_FIND_INT(hashMap, &key, s);
if(s == NULL){ // 如果 key 不存在,则新建一个 map 结构体,并插入哈希表
s = (map*)malloc(sizeof(map));
s -> key = key;
HASH_ADD_INT(hashMap, key, s);
}
s -> value = value; // 更新值
}
// 在哈希表中查找指定键的值,并返回对应的 map 结构体
map* hashMapFind(int key){
map* s;
// *s: 输出指针
HASH_FIND_INT(hashMap, &key, s);
return s;
}
// 清除哈希表
void hashMapCleanup(){
map* cur, *tmp;
HASH_ITER(hh, hashMap, cur, tmp){
HASH_DEL(hashMap, cur);
free(cur);
}
}
// 打印哈希表中的所有键值对
void hashPrint(){
map* s;
for(s = hashMap; s != NULL; s=(map*)(s -> hh.next)){
printf("key %d, value %d\n", s -> key, s -> value);
}
}
// 解决 LeetCode 上的两数之和问题
int* twoSum(int* nums, int numsSize, int target, int* returnSize){
int i, *ans;
// 用于存储哈希表查找的结果
map* hashMapRes;
hashMap = NULL; // 初始化哈希表为空
ans = malloc(sizeof(int) * 2); // 分配用于存储结果的数组
for(i = 0; i < numsSize; i++){
// 将数组中每个元素的值和下标插入哈希表中,键为元素的值,值为元素的下标
hashMapAdd(nums[i], i);
}
hashPrint(); // 打印哈希表中的所有键值对
for(i = 0; i < numsSize; i++){
// 在哈希表中查找与目标值相差的值,即 target - nums[i]
hashMapRes = hashMapFind(target - nums[i]);
if(hashMapRes && hashMapRes -> value != i){ // 如果匹配成功,则返回这两个数的下标
ans[0] = i;
ans[1] = hashMapRes -> value ;
*returnSize = 2; // 返回数组大小为 2
return ans; // 返回结果数组
}
}
hashMapCleanup(); // 清除哈希表
return NULL;
}
java写法
public int[] twoSum(int[] nums, int target) {
int[] res = new int[2];
if(nums == null || nums.length == 0){
return res;
}
Map<Integer, Integer> map = new HashMap<>();
for(int i = 0; i < nums.length; i++){
int temp = target - nums[i]; // 遍历当前元素,并在map中寻找是否有匹配的key
if(map.containsKey(temp)){
res[1] = i;
res[0] = map.get(temp);
break;
}
map.put(nums[i], i); // 如果没找到匹配对,就把访问过的元素和下标加入到map中
}
return res;
}