哈希表学习过程
一、哈希表定义
哈希表(Hash Table):也叫做散列表。是根据关键码值(Key Value)直接进行访问的数据结构。
哈希表通过「键 key 」和「映射函数 Hash(key) 」计算出对应的「值 value」,把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做「哈希函数(散列函数)」,存放记录的数组叫做「哈希表(散列表)」。
在上图例子中,我们使用 Hash(key) = key // 1000 作为哈希函数。// 符号代表整除。我们以这个例子来说明一下哈希表的插入和查找策略。
- 向哈希表中插入一个关键码值:通过哈希函数解析关键字,并将对应值存放到该区块中。
比如:0138 通过哈希函数 Hash(key) =0138 // 100 = 0,得出应将 0138 分配到0 所在的区块中。 - 在哈希表中搜索一个关键码值:通过哈希函数解析关键字,并在特定的区块搜索该关键字对应的值。
比如:查找 2321,通过哈希函数,得出 2321 应该在 2 所对应的区块中。然后我们从 2 对应的区块中继续搜索,并在 2 对应的区块中成功找到了 2321。查找 3214,通过哈希函数,得出 3214 应该在 3 所对应的区块中。然后我们从 3 对应的区块中继续搜索,但并没有找到对应值,则说明 3214 不在哈希表中。
二、哈希函数
哈希函数(Hash Function):将哈希表中元素的关键键值映射为元素存储位置的函数。
2.1 哈希函数满足条件
- 哈希函数应该易于计算,并且尽量使计算出来的索引值均匀分布。
- 哈希函数计算得到的哈希值是一个固定长度的输出值。
- 如果 Hash(key1) 不等于 Hash(key2),那么 key1、key2 一定不相等。
- 如果 Hash(key1) 等于 Hash(key2),那么 key1、key2 可能相等,也可能不相等(会发生哈希碰撞)。
在哈希表的实际应用中,关键字的类型除了数字类,还有可能是字符串类型、浮点数类型、大整数类型,甚至还有可能是几种类型的组合。一般我们会将各种类型的关键字先转换为整数类型,再通过哈希函数,将其映射到哈希表中。
2.2 哈希函数运用方法
2.2.1 直接定址法
2.2.2 除留余数法
2.2.3 平方取中法
2.2.4 基数转换法
三、哈希冲突
哈希冲突(Hash Collision):不同的关键字通过同一个哈希函数可能得到同一哈希地址,即 key1 ≠ key2,而
Hash(key1) = Hash(key2),这种现象称为哈希冲突。
3.1 哈希冲突是怎么发生的?
理想状态下,我们的哈希函数是完美的一对一映射,即一个关键字(key)对应一个值(value),不需要处理冲突。但是一般情况下,不同的关键字 key 可能对应了同一个值 value,这就发生了哈希冲突。
3.2 解决哈希冲突的办法【哈希表核心】
设计再好的哈希函数也无法完全避免哈希冲突。所以就需要通过一定的方法来解决哈希冲突问题。常用的哈希冲突解决方法主要是两类:「开放地址法(Open Addressing)」 和 「链地址法(Chaining)」。
3.2.1 开放地址法
**开放地址法(Open Addressing):**指的是将哈希表中的「空地址」向处理冲突开放。当哈希表未满时,处理冲突时需要尝试另外的单元,直到找到空的单元为止。
当发生冲突时,开放地址法按照下面的方法求得后继哈希地址:H(i) = (Hash(key) + F(i)) % m,i = 1, 2, 3, …, n (n ≤ m - 1)。
- H(i) 是在处理冲突中得到的地址序列。即在第 1 次冲突(i = 1)时经过处理得到一个新地址 H(1),如果在 H(1) 处仍然发生冲突(i = 2)时经过处理时得到另一个新地址 H(2) …… 如此下去,直到求得的 H(n) 不再发生冲突。
- Hash(key) 是哈希函数,m 是哈希表表长,对哈希表长取余的目的是为了使得到的下一个地址一定落在哈希表中。
- F(i) 是冲突解决方法,取法可以有以下几种:
- 线性探测法:
F
(
i
)
=
1
,
2
,
3
,
.
.
.
,
m
−
1
F(i) = 1, 2, 3, ..., m - 1
F(i)=1,2,3,...,m−1。
二次探测法: F ( i ) = 1 2 , − 1 2 , 2 2 , − 2 2 , . . . , ± n 2 ( n ≤ m / 2 ) F(i) = 1^2, -1^2, 2^2, -2^2, ..., \pm n^2(n \le m / 2) F(i)=12,−12,22,−22,...,±n2(n≤m/2)。
伪随机数序列: F ( i ) = 伪随机数序列 F(i) = 伪随机数序列 F(i)=伪随机数序列。
举个例子说说明一下如何用以上三种冲突解决方法处理冲突,并得到新地址 H(i):
例如,在长度为 11 的哈希表中已经填有关键字分别为 28、49、18 的记录(哈希函数为 Hash(key) = key %
11)。现在将插入关键字为 38 的新纪录。根据哈希函数得到的哈希地址为 5,产生冲突。接下来分别使用这三种冲突解决方法处理冲突。
- 使用线性探测法:得到下一个地址 H(1) = (5 + 1) % 11 = 6,仍然冲突;继续求出 H(2) = (5 + 2) % 11 = 7,仍然冲突;继续求出 H(3) = (5 + 3) % 11 = 8,8 对应的地址为空,处理冲突过程结束,记录填入哈希表中序号为 8 的位置。
- 使用二次探测法:得到下一个地址 H(1) = (5 + 11) % 11 = 6,仍然冲突;继续求出 H(2) = (5 - 11) % 11 = 4,4 对应的地址为空,处理冲突过程结束,记录填入哈希表中序号为 4 的位置。
- 使用伪随机数序列:假设伪随机数为 9,则得到下一个地址 H(1) = (9 + 5) % 11 = 3,3 对应的地址为空,处理冲突过程结束,记录填入哈希表中序号为 3 的位置。
3.2.2 链地址法
链地址法(Chaining):将具有相同哈希地址的元素(或记录)存储在同一个线性链表中。
我们假设哈希函数产生的哈希地址区间为 [0, m - 1],哈希表的表长为 m。则可以将哈希表定义为一个有 m 个头节点组成的链表指针数组 T。
- 这样在插入关键字的时候,我们只需要通过哈希函数 Hash(key) 计算出对应的哈希地址 i,然后将其以链表节点的形式插入到以 T[i] 为头节点的单链表中。在链表中插入位置可以在表头或表尾,也可以在中间。如果每次插入位置为表头,则插入操作的时间复杂度为O(1)。
- 而在在查询关键字的时候,我们只需要通过哈希函数 Hash(key) 计算出对应的哈希地址 i,然后将对应位置上的链表整个扫描一遍,比较链表中每个链节点的键值与查询的键值是否一致。查询操作的时间复杂度跟链表的长度 k 成正比,也就是O(k)。对于哈希地址比较均匀的哈希函数来说,理论上讲,k = n // m,其中 n 为关键字的个数,m 为哈希表的表长。
举个例子来说明如何使用链地址法处理冲突:
假设现在要存入的关键字集合 keys = [88, 60, 65, 69, 90, 39, 07, 06, 14, 44, 52, 70, 21, 45, 19, 32]。再假定哈希函数为 Hash(key) = key % 13,哈希表的表长 m = 13,哈希地址范围为 [0, m - 1]。将这些关键字使用链地址法处理冲突,并按顺序加入哈希表中(图示为插入链表表尾位置),最终得到的哈希表如下图所示:
与开放地址法相比的优势:
采用链地址法处理冲突要多占用一些存储空间(主要是链节点占用空间)。但它可以减少在进行插入和查找具有相同哈希地址的关键字的操作过程中的平均查找长度。这是因为在链地址法中,待比较的关键字都是具有相同哈希地址的元素,而在开放地址法中,待比较的关键字不仅包含具有相同哈希地址的元素,而且还包含哈希地址不相同的元素。
四、哈希表题目
1.0217. 存在重复元素
存在重复元素力扣链接
这里我就不复制题目了;
因为这里我的学习内容是哈希表,那代码逻辑自然而生
struct hashTable {//定义了一个结构体 hashTable
int key; //整数类型的键 key
UT_hash_handle hh;//UT_hash_handle 结构体
};
bool containsDuplicate(int* nums, int numsSize) {
struct hashTable* set = NULL;//初始化定义了一个名为 set 的指针,指向哈希表
for (int i = 0; i < numsSize; i++) {//遍历整数数组 nums
struct hashTable* tmp;
HASH_FIND_INT(set, nums + i, tmp);//使用 HASH_FIND_INT 宏在哈希表中查找是否存在相同的键(即当前数组元素的值)
/**如果 tmp 为 NULL,表示当前元素是第一次出现,
动态分配内存创建一个新的哈希表节点,
并将当前数组元素的值作为键存储到哈希表中
*/
if (tmp == NULL) { //tmp 用于存储查找结果
tmp = malloc(sizeof(struct hashTable));
tmp->key = nums[i];
HASH_ADD_INT(set, key, tmp);
} else {//如果 tmp 不为 NULL,表示当前元素已经在哈希表中存在,说明存在重复元素,函数返回 true
return true;
}
}
return false;//如果整个数组都遍历完毕,都没有找到重复元素,则函数返回 false
}
在哈希表中查找元素的时间复杂度是 O(1),整体算法的平均时间复杂度是 O(n)
这是我看见的有趣大佬干了个手写哈希表
#include <assert.h>
#include <stdlib.h>
#ifndef MY_TINY_STL_HASHMAP_C_H
#define MY_TINY_STL_HASHMAP_C_H
#define DEFAULT_CAPACITY 32 //初始的表长
#define DEFAULT_FACTOR 0.75f //初始的装载因子
/*类型定义 和 装载因子初始化*/
typedef int key_t;
typedef int val_t;
static const float factor = DEFAULT_FACTOR; //装载因子
typedef struct node {//每个哈希表的键值对
key_t key;
val_t val;
struct node *next;
} Node;
typedef struct {
size_t size; //记录已经存下的键值对数目
size_t capacity; //记录表长
Node **buckets; //桶子:用于记录的哈希桶,桶子中每个元素是Node*
} HashMap;
/*函数的声明*/
HashMap *init();
void Put(HashMap *, key_t, val_t);
void insert(HashMap *, Node *); //直接把已经分配好的内存插入哈希表
static void putVal(HashMap*,key_t, val_t); //这个是put的委托函数,用于直接更新桶子,并更新HashMap的size
static inline size_t getHashcode(key_t); //得到key对应的扰动函数
static inline size_t strHashcode(char *); //得到字符串的哈希值,用的java的31为底的算法,这个哈希值再经过扰动函数
static inline size_t getIndex(key_t, size_t); //通过桶的大小和key映射位置,算是包含了关键的哈希函数:由于C不支持泛型也就无法针对不同类型作出不同的哈希了,我这里默认key为int
static void resize(HashMap *); //如果插入的元素过多,*2进行重新哈希分配
static void rehash(HashMap *, Node **); //重新设置长度则需要重新哈希一些key的位置
val_t *Get(HashMap *, key_t); //得到key对应的val
static void del_nodes(Node *); //把单个链表销毁
void destroy(HashMap *); //把哈希表的内存销毁
/*函数实现*/
HashMap *init() { //初始化得到一个哈希表
HashMap *ret = (HashMap *) malloc(sizeof(HashMap));
if(ret==NULL)
return NULL;
ret->size = 0;
ret->capacity = DEFAULT_CAPACITY;
ret->buckets = (Node **) calloc(DEFAULT_CAPACITY, sizeof(Node *));
if(ret->buckets==NULL)
return NULL;
return ret;
}
void insert(HashMap *map, Node *node) {
assert(map != NULL && node != NULL);
size_t index = getIndex(node->key, map->capacity);
node->next = map->buckets[index];
map->buckets[index] = node;
}
void Put(HashMap *map, key_t key, val_t val) {
assert(map != NULL);
//判断是否需要扩容
if (map->size >=factor * map->capacity) resize(map);
putVal(map, key,val);
}
static inline size_t strHashcode(char *key) {
size_t hash = 0;
size_t index = 0;
while (key[index] != '\0') {
hash = hash * 31 + key[index++];
}
return hash;
}
static inline size_t getHashcode(key_t key) {
return key ^ (key >> 16);//这是32位数的扰动函数
}
static inline size_t getIndex(key_t key, size_t bucket_size) {//由于bucketsize一定是2的次方,所以size-1和key相与得到的就是下标
return getHashcode(key) & (bucket_size - 1);
}
static void putVal(HashMap* map,key_t key,val_t val) {
assert(map!=NULL);
//获取位置
size_t index = getIndex(key, map->capacity);
Node *x = map->buckets[index];
if (x == NULL) {//插入位置为空
x = (Node *) malloc(sizeof(Node));
x->val = val;
x->key = key;
x->next = NULL;
map->buckets[index] = x;
map->size++; //哈希表内的元素增加
return;
}
//插入位置不为空,说明发生冲突,使用链地址法,遍历链表
while (x) {
//如果key相同就覆盖
if (x->key == key) {
x->val = val;
return;
}
x = x->next;
}
//当前的key不在链表中,则插入链表头部
x = (Node *) malloc(sizeof(Node));
x->key = key;
x->val = val;
x->next = map->buckets[index];
map->buckets[index] = x;
map->size++; //哈希表内元素增加
}
static void resize(HashMap *map) {
map->capacity <<= 1; //扩大两倍容量,相当于左移一位
Node **tmp = map->buckets; //存下之前的内存地址
map->buckets = (Node **) calloc(map->capacity, sizeof(Node *)); //重新分配
rehash(map, tmp);//重新哈希处理
free(tmp); //释放之前的内存
}
static void rehash(HashMap *map, Node **preTable) {//采取java1.7的方式进行rehash也就是最简单直接的直接重新哈希插入
size_t preCap = map->capacity >>1,i; //改变前的有效区域
Node* preNode,*curNode;
for (i = 0; i < preCap; i++) {
if (preTable[i] != NULL) {//判断对应的key是否需要重新换位置,如果对最新掩码多出来的1敏感则需要rehash
curNode = preTable[i];
while (curNode) {
preNode = curNode;
curNode = curNode->next;
insert(map, preNode);
}
}
}
}
val_t *Get(HashMap *map, key_t key) {//前面的写好后,那么get就很好写了
int index = getIndex(key, map->capacity);
Node *node = map->buckets[index];
while (node != NULL) {
if (node->key == key) {
return &(node->val);
}
node = node->next;
}
return NULL;//没找到返回NULL指针
}
static void del_nodes(Node *head) {//删除单条链表
Node *pre;
Node *cur = head;
while (cur != NULL) {
pre = cur;
cur = cur->next;
free(pre);
}
}
void destroy(HashMap *map) {//销毁整个哈希表
size_t sz = map->capacity;
Node **tmp = map->buckets;
for (size_t i = 0; i < sz; i++) {
if (tmp[i] != NULL)
del_nodes(tmp[i]);
}
free(tmp);
free(map);
}
#endif //MY_TINY_STL_HASHMAP_C_H
bool containsDuplicate(int* nums, int numsSize) {
HashMap* set = init();
for (int i = 0; i < numsSize; i++) {
if(Get(set,nums[i])!=NULL)
return true;
Put(set,nums[i],0);
}
return false;
}
作者:加油!!!欧里给
链接:https://leetcode.cn/problems/contains-duplicate/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2.0219. 存在重复元素 II
当我看到题目的时候,以为是给我一个数组,以及一个数字,让我判断数组中会不会有数字和我自己给的数字相同
随后仔细去读题,发现有个“存在两个不同的索引 i 和 j 满足 nums[i] == nums[j] 且 abs(i - j) <= k”
这句是啥意思呢?
其实是在问,
- 在给定的数组中,判断是否存在两个相同的元素;
- 假设有两个相同的元素,那么给它们不同的索引为i和j,其中i和j的差不能超过k;
这就很好理解题目的意思了吧!
再举个小例子:假设给的数组nums=[1,2,3,1] 给了k=3
遍历数组,看看是否存在相同元素
对于
元素1,index为0
元素2,index为1
元素3,index为2
元素1,index为3
显而易见,可以看见存在两个相同元素为1,索引一个为0,一个为3
因此,索引之差为3 3=k。 最后返回true
#include <stdbool.h>
#include <stdlib.h>
// 定义哈希表节点的结构体
struct hashTable {
int key; // 哈希表节点的键,即数组元素的值
int index; // 哈希表节点的值,即数组元素的索引
UT_hash_handle hh; // 用于哈希表操作的结构
};
// 函数签名,判断数组中是否存在两个不同索引 i 和 j,满足 nums[i] == nums[j] 且 abs(i - j) <= k
bool containsNearbyDuplicate(int* nums, int numsSize, int k) {
// 如果 k 小于等于 0,不满足题意条件,直接返回 false
if (k <= 0) {
return false;
}
// 定义哈希表的指针,初始为空
struct hashTable* set = NULL;
// 遍历数组
for (int i = 0; i < numsSize; i++) {
// 在哈希表中查找当前元素值是否已存在
struct hashTable* tmp;
HASH_FIND_INT(set, &nums[i], tmp);
// 如果找到相同的元素值且索引之差不超过 k,则返回 true
if (tmp != NULL && i - tmp->index <= k) {
return true;
}
// 如果没有找到相同的元素值或索引之差超过 k,将当前元素值和索引加入哈希表
struct hashTable* newEntry = malloc(sizeof(struct hashTable));
newEntry->key = nums[i];
newEntry->index = i;
HASH_ADD_INT(set, key, newEntry);
}
// 如果整个数组都遍历完毕,都没有找到满足条件的索引 i 和 j,则返回 false
return false;
}
使用哈希表存储已经遍历过的数组元素值及其索引,每次迭代时查找哈希表中是否存在相同的元素值,如果存在且索引之差不超过 k,则返回 true;否则,将当前元素值和索引加入哈希表。如果整个数组都遍历完毕,都没有找到满足条件的索引 i 和 j,则返回 false。
时间复杂度是 O(n)
3.0036. 有效的数独
数独就是一个由 9 行、9列、9个小格子(3x3的方块)组成的网格,要求在这个网格中填入数字 1 到9,每个数字在每一行、每一列和每个小格子中都不能重复。
所以,题目的任务就是要检查给定的数独是否符合这些规则。如果符合,就返回 true,否则返回false。在代码中,通过使用哈希表记录每个数字在每一行、每一列和每个小格子中是否已经出现,来检查数独的有效性。
bool isValidSudoku(char** board, int boardSize, int* boardColSize) {
// 定义行、列和子数独的哈希表
bool rowHash[9][9] = {false}; // 用于记录每一行中数字是否出现
bool colHash[9][9] = {false}; // 用于记录每一列中数字是否出现
bool subBoxHash[9][9] = {false}; // 用于记录每一个3x3子数独中数字是否出现
for (int i = 0; i < boardSize; i++) {
for (int j = 0; j < *boardColSize; j++) {
char c = board[i][j];
if (c != '.') {
int num = c - '1'; // 将字符转换为数字
// 检查当前数字在行、列和子数独中是否已经出现过
if (rowHash[i][num] || colHash[j][num] || subBoxHash[i/3*3 + j/3][num]) {
return false; // 如果已经出现过,则数独无效
}
// 将当前数字标记为已经出现过
rowHash[i][num] = true;
colHash[j][num] = true;
subBoxHash[i/3*3 + j/3][num] = true;
}
}
}
return true; // 遍历完所有数字,数独有效
}
时间复杂度是 O(1)
4.0349. 两个数组的交集
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int* intersection(int* nums1, int nums1Size, int* nums2, int nums2Size, int* returnSize) {
int nums1Cnt[1000] = {0};
int lessSize = nums1Size < nums2Size ? nums1Size : nums2Size;
int * result = (int *) calloc(lessSize, sizeof(int));
int resultIndex = 0;
int* tempNums;
int i;
/* Calculate the number's counts for nums1 array */
for(i = 0; i < nums1Size; i ++) {
nums1Cnt[nums1[i]]++;
}
/* Check if the value in nums2 is existing in nums1 count array */
for(i = 0; i < nums2Size; i ++) {
if(nums1Cnt[nums2[i]] > 0) {
result[resultIndex] = nums2[i];
resultIndex ++;
/* Clear this count to avoid duplicated value */
nums1Cnt[nums2[i]] = 0;
}
}
* returnSize = resultIndex;
return result;
}
5.0350. 两个数组的交集 II
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int* intersect(int* nums1, int nums1Size, int* nums2, int nums2Size, int* returnSize) {
int table[1001];
int count=0;
int* ans=(int*)malloc(sizeof(int)*nums2Size);
memset(table,0,sizeof(table));
for(int i=0;i<nums1Size;i++){
table[nums1[i]]+=1;
}
for(int i=0;i<nums2Size;i++){
if(table[nums2[i]]>0){
ans[count++]=nums2[i];
table[nums2[i]]-=1;
}
}
*returnSize=count;
return ans;
}
6.0706. 设计哈希映射
struct List {
int key;
int val;
struct List* next;
};
void listPush(struct List* head, int key, int val) {
struct List* tmp = malloc(sizeof(struct List));
tmp->key = key;
tmp->val = val;
tmp->next = head->next;
head->next = tmp;
}
void listDelete(struct List* head, int key) {
for (struct List* it = head; it->next; it = it->next) {
if (it->next->key == key) {
struct List* tmp = it->next;
it->next = tmp->next;
free(tmp);
break;
}
}
}
struct List* listFind(struct List* head, int key) {
for (struct List* it = head; it->next; it = it->next) {
if (it->next->key == key) {
return it->next;
}
}
return NULL;
}
void listFree(struct List* head) {
while (head->next) {
struct List* tmp = head->next;
head->next = tmp->next;
free(tmp);
}
}
const int base = 769;
int hash(int key) {
return key % base;
}
typedef struct {
struct List* data;
} MyHashMap;
MyHashMap* myHashMapCreate() {
MyHashMap* ret = malloc(sizeof(MyHashMap));
ret->data = malloc(sizeof(struct List) * base);
for (int i = 0; i < base; i++) {
ret->data[i].key = 0;
ret->data[i].val = 0;
ret->data[i].next = NULL;
}
return ret;
}
void myHashMapPut(MyHashMap* obj, int key, int value) {
int h = hash(key);
struct List* rec = listFind(&(obj->data[h]), key);
if (rec == NULL) {
listPush(&(obj->data[h]), key, value);
} else {
rec->val = value;
}
}
int myHashMapGet(MyHashMap* obj, int key) {
int h = hash(key);
struct List* rec = listFind(&(obj->data[h]), key);
if (rec == NULL) {
return -1;
} else {
return rec->val;
}
}
void myHashMapRemove(MyHashMap* obj, int key) {
int h = hash(key);
listDelete(&(obj->data[h]), key);
}
void myHashMapFree(MyHashMap* obj) {
for (int i = 0; i < base; i++) {
listFree(&(obj->data[i]));
}
free(obj->data);
}
/**
* Your MyHashMap struct will be instantiated and called as such:
* MyHashMap* obj = myHashMapCreate();
* myHashMapPut(obj, key, value);
* int param_2 = myHashMapGet(obj, key);
* myHashMapRemove(obj, key);
* myHashMapFree(obj);
*/
五、学习地址
注:由datawhale提供学习
推荐算法与数据结构相关书籍:
- 算法(第 4 版)- 谢路云 译
- 大话数据结构 - 程杰 著
- 趣学算法 - 陈小玉 著
- 算法图解 - 袁国忠 译
- 算法竞赛入门经典(第 2 版) - 刘汝佳 著
- 数据结构与算法分析 - 冯舜玺 译
- 算法竞赛进阶指南 - 李煜东 著
- 算法导论(原书第 3 版) - 殷建平 / 徐云 / 王刚 / 刘晓光 / 苏明 / 邹恒明 / 王宏志 译
- 数据结构与算法 Python语言描述 - 裘宗燕 著
- Python 数据结构与算法分析 第 2 版 - 吕能 习寿钧 译
参考资料
博文
博文:掘金—面试官:哈希表都不知道,你是怎么看懂HashMap的?
文章:小灰的算法之旅
博文:oi-wiki
【书籍】数据结构(C 语言版)- 严蔚敏 著
【书籍】数据结构教程(第 3 版)- 唐发根 著
【书籍】数据结构与算法 Python 语言描述 - 裘宗燕 著
剩余练习题目
会员题目——https://leetcode.cn/problems/logger-rate-limiter/ (简单)