C语言HashTable基本理解
文章目录
- 一、哈希表概念
- 1. 哈希表的基本概念
- 2. 哈希表的核心组件
- 2.1 哈希函数
- 2.2 冲突处理(哈希碰撞)
- 3.哈希表的三种结构
- (1) 数组作为哈希表
- 示例:
- 2. Set(集合)
- 示例:查找数组中的重复元素
- 1. Set 基础概念
- 2. TypeScript 泛型约束
- 3. Map(字典)
- 示例:两数之和
- 1. Map 基础概念
- 4.哈希表性能分析
- 1. 时间复杂度
- 2. 影响性能的关键因素
- (1)哈希函数的质量
- (2)冲突处理方式
- (3)负载因子(Load Factor)
- 3. 性能优化策略
- (1)选择高效的哈希函数
- (2)优化冲突处理
- (3)动态扩容
- 4. 实际应用中的性能对比
- 5.与其他数据结构对比
- 1. 时间复杂度对比
- 2. 空间复杂度对比
- 3. 特性与适用场景对比
- 4. 典型应用场景
- 5. 选择建议
- 6.哈希表的内存管理
- 1. 基本内存结构
- 2. 内存分配策略
- (1)静态分配
- (2)动态分配
- 3. 冲突处理与内存开销
- (1)链地址法
- 4. 动态扩容与内存重分配
- (1)触发条件
- (2)扩容步骤
- (3)内存开销
- 5. 内存优化策略
- (1)预分配与分批扩容
- (2)内存池技术
- (3)压缩哈希表
- 6. 内存泄漏风险
- 7. 不同语言的实现差异
- 总结
- 二、C 语言实现哈希表
- 三、例题示例
- (1)四数相加2
- (2)随机链表的复制
- (3)有效的字母异位词
一、哈希表概念
1. 哈希表的基本概念
哈希表(Hash Table),也称为散列表,是一种根据键(Key)直接访问存储位置的数据结构。哈希表的核心构成部分有两个:哈希函数和数组。
它通过哈希函数(Hash Function)将键映射到一个固定大小数组的索引上,这个数组被称为哈希表,存储在该索引位置的数据被称为值(Value)。我们能把键(Key)转换为数组的索引,进而快速地进行数据的插入、查找和删除操作。
2. 哈希表的核心组件
2.1 哈希函数
哈希函数是哈希表的核心,它的作用是将键转换为哈希表中的一个索引。一个好的哈希函数应该满足以下几个条件:
- 确定性:对于相同的键,哈希函数必须始终返回相同的索引。
- 均匀性:哈希函数应该尽可能均匀地将键分布到哈希表的各个位置,以减少冲突的发生。
- 高效性:哈希函数的计算应该尽可能快,以保证哈希表的操作效率。
常见的哈希函数有取模法,直接定址法,除留余数法,平方取中法。
- 取模法:
index = key % table_size
,其中key
是键,table_size
是哈希表的大小。
- 直接定址法:一种将键值直接作为哈希地址的方法。具体来说,它通过一个线性变换来确定哈希地址。
H(key) = a·key + b
。假设要统计不同年龄的人数,年龄范围是 0-100,那么可以直接用年龄作为索引,即:
int count[101] = {0}; // 索引0-100对应年龄0-100
- 除留余数法:一种较为常用的哈希函数构造方法,它通过取键值除以哈希表大小的余数来得到哈希地址。
H(key) = key % m
。若哈希表的大小 m 为 10,对于键值 23,其哈希地址为:
H(23) = 23 % 10 = 3
若 m 是 2 的幂次方,那么哈希地址就相当于键值的低位部分,这可能会导致更多的冲突。
- 平方取中法:先计算键值的平方,然后取中间的几位作为哈希地址。
步骤:
- 计算键值的平方,即
key²
。- 取平方结果中间的几位数字作为哈希地址。
示例:假设哈希表的大小为 1000(即需要 3 位哈希地址):
- 若键值为 25,
25² = 625
,那么哈希地址就是 625。- 若键值为 400,
400² = 160000
,取中间三位为 000,所以哈希地址是 0。
方法 优点 缺点 适用场景 直接定址法 无冲突,简单直接 可能浪费大量空间 键分布连续且密集 除留余数法 实现简单,应用广泛 m 选择不当易产生冲突 大多数场景 平方取中法 分布均匀,对键的每一位都敏感 计算相对复杂 键的每一位都有影响的场景 取模法 空间利用率为 100%,键值任意 扩容时, m
值改变,所有键需要重新计算哈希地址键的分布较为随机且均匀,哈希表大小固定且不需要动态扩容
2.2 冲突处理(哈希碰撞)
如图所示:当目标数组经过
Hash Function
将Key
转化为Hash Table
的Index
时,有可能发生重复利用一个Index
的情况,若不采取操作,可能会覆盖原来存储的数据,我最常用的是采用链表的方法,即在Hash Table
每个Index
所指的地方维护一个链表,将具有相同Index
的数据存放到链表中。
由于哈希函数的映射是多对一的,不同的键可能会映射到相同的索引,这种情况称为冲突。常见的冲突处理方法有以下两种:
- 开放地址法:当发生冲突时,通过某种探测方法(如线性探测、二次探测等)寻找下一个空闲的位置来存储数据。
- 链地址法:在哈希表的每个位置上维护一个链表(或其他数据结构),当发生冲突时,将具有相同哈希值的数据存储在该位置的链表中。
3.哈希表的三种结构
哈希结构(Hash Structures)是计算机科学中非常重要的数据结构,主要用于高效地存储和查找数据。以下是三种最常见的哈希结构及其详细介绍和示例。
(1) 数组作为哈希表
特点:
- 适用于键的范围已知且较小的情况
- 直接使用数组索引作为键
- 访问时间复杂度:O(1)
适用场景:
- 字符统计(如小写字母统计)
- 固定范围的数字统计
示例:
const count: number[] = new Array(26).fill(0);const aCode: number = 'a'.charCodeAt(0);
2. Set(集合)
特点:
- 存储唯一值
- 快速判断元素是否存在
- 添加、删除、查找时间复杂度:O(1)
适用场景:
- 去重
- 快速查找元素是否存在
示例:查找数组中的重复元素
const numSet: Set<number> = new Set<number>();
在 TypeScript 中,const numSet = new Set<number>();
创建了一个强类型的集合(Set),它只能存储 数值类型(number) 的元素。下面详细解释其功能、用法和注意事项:
1. Set 基础概念
Set
是 ES6 引入的一种数据结构,类似于数组,但具有以下特点:
- 元素唯一:不允许重复值。
- 无序性:元素没有固定顺序。
- 高效查找:基于哈希表实现,插入、删除、查找的时间复杂度接近 O (1)。
2. TypeScript 泛型约束
<number>
是泛型语法,用于约束 Set 中元素的类型。
3. Map(字典)
特点:
- 存储键值对
- 键可以是任意类型(对象引用需注意)
- 添加、删除、查找时间复杂度:O(1)
适用场景:
- 需要保存键值映射关系
- 统计频率
- 缓存
示例:两数之和
const numMap = new Map<number, number>();
在 TypeScript 中,const numMap = new Map<number, number>();
创建了一个强类型的映射(Map),它只能存储 键和值均为数值类型(number) 的键值对。下面详细解释其功能、用法和注意事项:
1. Map 基础概念
Map
是 ES6 引入的一种数据结构,类似于对象(Object),但具有以下特点:
- 键的类型灵活:键可以是任意类型(对象、函数、基本类型等),而对象的键只能是字符串或 Symbol。
- 有序性:按插入顺序存储键值对,遍历时顺序与插入顺序一致。
- 高效查找:基于哈希表实现,插入、删除、查找的时间复杂度接近 O (1)。
4.哈希表性能分析
1. 时间复杂度
哈希表的核心操作(插入、查找、删除)的时间复杂度在平均情况下为 O(1),但在最坏情况下可能退化为 O(n)。具体分析如下:
操作 | 平均时间复杂度 | 最坏时间复杂度 | 说明 |
---|---|---|---|
插入(Insert) | O(1) | O(n) | 需计算哈希值并处理冲突(如链表遍历)。 |
查找(Search) | O(1) | O(n) | 需计算哈希值并遍历可能存在的冲突链表。 |
删除(Delete) | O(1) | O(n) | 需先查找元素,再从链表或数组中删除。 |
最坏情况发生在所有键都映射到同一哈希地址时。
2. 影响性能的关键因素
(1)哈希函数的质量
- 均匀性:好的哈希函数应使键均匀分布在哈希表中,减少冲突。
- 计算效率:哈希函数本身的计算复杂度应尽量低(如取模法)。
(2)冲突处理方式
- 链地址法:每个槽位维护一个链表,冲突元素依次插入链表。
- 优点:实现简单,适用于冲突频繁的场景。
- 缺点:链表过长时查询效率下降,需额外指针空间。
- 开放寻址法:冲突时通过探测序列(如线性探测、二次探测)寻找下一个空位。
- 优点:无需额外指针,空间利用率高。
- 缺点:易产生聚集现象,导致后续冲突概率增加。
(3)负载因子(Load Factor)
负载因子定义为:
α = 键的数量 / 哈希表大小
- α 越小:冲突概率越低,性能越好,但空间利用率低。
- α 越大:冲突概率越高,性能下降,需扩容以维持效率。
经验法则:
- 链地址法:通常将 α 控制在 0.75~1.0 之间。
- 开放寻址法:α 应控制在 0.5 以下,避免聚集。
3. 性能优化策略
(1)选择高效的哈希函数
- 除留余数法:
H(key) = key % m
,其中m
取质数以减少冲突。 - 字符串哈希:使用多项式哈希(如 DJB2 算法):
DJB2 算法是一种由 Daniel J. Bernstein 设计的字符串哈希函数,其核心思想是通过移位和加法操作将字符串转换为一个无符号整数哈希值。
hash(i) = hash(i-1) * 33 + str[i]
其中:
hash(i)
是前i
个字符的哈希值。str[i]
是字符串的第i
个字符(ASCII 值)。- 初始值:
hash(0) = 5381
。
unsigned int hash(char* str) {unsigned int hash = 5381;//研究表明,这个值能减少哈希冲突的概率。//33 是一个奇质数,实验证明它在哈希函数中表现良好,能有效减少冲突。while (*str) hash = ((hash << 5) + hash) + (*str++); return hash;
}
(2)优化冲突处理
- 链地址法改进:
- 使用红黑树代替链表,将最坏时间复杂度从 O (n) 降至 O (log n)(如 Java 的
HashMap
在链表长度超过 8 时转换为红黑树)。
- 使用红黑树代替链表,将最坏时间复杂度从 O (n) 降至 O (log n)(如 Java 的
(3)动态扩容
当负载因子超过阈值时,扩容并重新哈希:
- 步骤:
- 创建更大的新哈希表(通常扩容为原大小的 2 倍)。
- 遍历原哈希表,将所有键重新计算哈希值并插入新表。
4. 实际应用中的性能对比
场景 | 链地址法哈希表 | 红黑树 |
---|---|---|
平均查找时间 | O(1) | O(log n) |
最坏查找时间 | O(n) | O(log n) |
空间利用率 | 中等(需额外指针) | 低(需左右子树指针) |
适用负载因子 | α ≤ 1.0 | 无限制 |
动态扩容开销 | 较高(需重新哈希) | 低(支持动态调整) |
结论:
- 哈希表在平均情况下性能远优于树结构(如红黑树)。
- 当键数量不确定或冲突频繁时,链地址法更灵活。
5.与其他数据结构对比
1. 时间复杂度对比
数据结构 | 插入(平均) | 插入(最坏) | 查找(平均) | 查找(最坏) | 删除(平均) | 删除(最坏) |
---|---|---|---|---|---|---|
哈希表 | O(1) | O(n) | O(1) | O(n) | O(1) | O(n) |
数组 | O(1) | O(n) | O(1) | O(1) | O(n) | O(n) |
链表 | O(1) | O(1) | O(n) | O(n) | O(n) | O(n) |
二叉搜索树 | O(log n) | O(n) | O(log n) | O(n) | O(log n) | O(n) |
红黑树 | O(log n) | O(log n) | O(log n) | O(log n) | O(log n) | O(log n) |
说明:
- 哈希表的最坏情况发生在所有键冲突时(如哈希函数设计不当)。
- 数组的插入 / 删除最坏情况发生在需要扩容或移动元素时。
- 二叉搜索树的最坏情况发生在树退化为链表时(如插入有序数据)。
2. 空间复杂度对比
数据结构 | 空间复杂度 | 额外开销 |
---|---|---|
哈希表 | O(n) | 哈希表数组、链表指针 |
数组 | O(n) | 连续内存块,可能浪费空间 |
链表 | O(n) | 每个节点的指针 |
二叉搜索树 | O(n) | 每个节点的左右子树指针 |
红黑树 | O(n) | 每个节点的颜色位、指针 |
说明:
- 哈希表的空间利用率取决于负载因子(通常为 0.75~1.0)。
- 红黑树的每个节点额外存储一个颜色位(1 bit),用于维持平衡。
3. 特性与适用场景对比
数据结构 | 有序性 | 动态扩容 | 优势场景 | 劣势场景 |
---|---|---|---|---|
哈希表 | 无序 | 支持 | 快速查找、插入、删除(平均 O (1)),适合键值对存储(如缓存、数据库索引) | 不支持有序遍历,哈希函数设计不当会导致性能下降 |
数组 | 有序 | 需重建 | 随机访问(O (1)),元素类型一致且数量固定 | 插入 / 删除效率低,扩容成本高 |
链表 | 有序 | 动态 | 频繁插入 / 删除(O (1)),元素数量不确定 | 随机访问效率低(O (n)) |
二叉搜索树 | 有序 | 动态 | 有序遍历(O (n)),支持范围查询 | 最坏情况退化为链表(O (n)),需维护平衡性 |
红黑树 | 有序 | 动态 | 高效的插入、删除、查找(O (log n)),适合需频繁修改的有序集合 | 实现复杂,空间开销略高 |
4. 典型应用场景
数据结构 | 应用场景 |
---|---|
哈希表 | 缓存(如 Redis)、编程语言内置字典(如 Python 的 dict)、数据库索引 |
数组 | 科学计算(如 NumPy)、矩阵运算、需要随机访问的固定大小数据 |
链表 | 内存管理(如操作系统的空闲内存块管理)、LRU 缓存淘汰策略 |
二叉搜索树 | 文件系统目录结构、简单的排序与查找 |
红黑树 | C++ STL 中的 map/set、Java 的 TreeMap/TreeSet、Linux 内核进程调度 |
5. 选择建议
- 优先选哈希表:若需快速查找且无需有序遍历(如缓存、去重)。
- 优先选数组:若需随机访问且元素数量固定(如矩阵、向量)。
- 优先选链表:若需频繁插入 / 删除且无需随机访问(如实现栈、队列)。
- 优先选红黑树:若需有序遍历或范围查询(如数据库索引、有序集合)。
6.哈希表的内存管理
1. 基本内存结构
哈希表通常由两部分组成:
- 哈希数组:存储桶(Bucket)的固定大小数组,每个桶可指向一个链表或其他数据结构。
- 链表 / 红黑树:处理哈希冲突时,每个桶可能链接多个元素。
示例:
链地址法哈希表的内存布局:
2. 内存分配策略
(1)静态分配
- 特点:哈希数组大小固定,初始化时分配内存。
- 优点:实现简单,无需动态扩容。
- 缺点:可能浪费空间或导致冲突频繁。
示例:
#define SIZE 100
Node* table[SIZE]; // 静态数组,大小为100
(2)动态分配
- 特点:运行时根据需要调整哈希数组大小。
- 优点:空间利用率高,减少冲突。
- 缺点:扩容操作耗时(O (n))。
示例:
int size = 100;
Node** table = (Node**)calloc(size, sizeof(Node*)); // 动态分配
3. 冲突处理与内存开销
(1)链地址法
- 每个桶维护链表:每个节点包含键值对和指向下一个节点的指针。
- 内存开销:
- 哈希数组:
O(m)
(m 为桶数量)。 - 链表节点:
O(n)
(n 为键值对数量),每个节点需额外指针。
- 哈希数组:
优化:
当链表过长时,可将链表转换为红黑树(如 Java 的 HashMap
),将最坏时间复杂度从 O (n) 降至 O (log n),但会增加每个节点的内存开销(额外存储颜色位和父节点指针)。
4. 动态扩容与内存重分配
(1)触发条件
当负载因子(α = n/m
)超过阈值(通常为 0.75)时,需扩容。
(2)扩容步骤
- 创建更大的新哈希数组(通常扩容为原大小的 2 倍)。
- 遍历原哈希表,重新计算每个元素的哈希值并插入新数组。
- 释放原哈希数组内存。
示例:
void resize(HashTable* ht) {int new_size = next_prime(ht->size * 2); // 扩容为原大小的2倍Node** new_table = (Node**)calloc(new_size, sizeof(Node*));// 重新哈希所有元素for (int i = 0; i < ht->size; i++) {Node* curr = ht->table[i];while (curr) {Node* next = curr->next;unsigned int idx = hash(curr->key, new_size);curr->next = new_table[idx];new_table[idx] = curr;curr = next;}}free(ht->table); // 释放原数组内存ht->table = new_table;ht->size = new_size;
}
(3)内存开销
- 扩容期间需额外
O(m)
空间存储新数组。 - 若元素数量波动较大,频繁扩容会导致内存碎片。
5. 内存优化策略
(1)预分配与分批扩容
- 预分配:预估元素数量,初始化时分配足够空间。
- 分批扩容:在高并发场景下,将扩容操作分摊到多次插入中,减少单次扩容的内存压力。
(2)内存池技术
- 预先分配大块内存,按需分配给节点,减少频繁 malloc/free 的开销。
- 降低内存碎片,提高内存利用率。
示例:
// 内存池结构
typedef struct MemoryPool {Node* free_list;size_t block_size;
} MemoryPool;// 从内存池分配节点
Node* pool_alloc(MemoryPool* pool) {if (!pool->free_list) {// 分配新块Node* block = (Node*)malloc(pool->block_size * sizeof(Node));// 将新块中的节点连接到空闲列表for (int i = 0; i < pool->block_size - 1; i++) {block[i].next = &block[i + 1];}block[pool->block_size - 1].next = NULL;pool->free_list = block;}Node* node = pool->free_list;pool->free_list = node->next;return node;
}
(3)压缩哈希表
- 对于稀疏哈希表(负载因子极低),可使用压缩技术减少空间浪费。
- 例如,使用 BitArray 标记已使用的桶,仅存储有效元素。
6. 内存泄漏风险
- 未释放链表节点:删除元素时,若未正确释放链表节点内存,会导致泄漏。
- 循环引用:在双向链表或树结构中,若存在循环引用,垃圾回收机制无法回收内存。
正确释放示例:
void free_hash_table(HashTable* ht) {for (int i = 0; i < ht->size; i++) {Node* curr = ht->table[i];while (curr) {Node* temp = curr;curr = curr->next;free(temp->key); // 释放键内存free(temp); // 释放节点内存}}free(ht->table); // 释放哈希数组free(ht); // 释放哈希表结构
}
7. 不同语言的实现差异
语言 | 内存管理方式 | 特点 |
---|---|---|
C | 手动管理(malloc/free) | 需自行处理内存分配、释放和扩容,灵活性高但易出错 |
Java | 自动垃圾回收(GC) | 无需手动释放内存,但扩容时可能触发 GC,影响性能 |
Python | 引用计数 + GC | 字典底层使用哈希表,内存由解释器自动管理 |
Go | 自动垃圾回收 + 内存池 | 标准库 map 使用哈希表,并发场景下通过内存池优化 |
总结
哈希表的内存管理需平衡空间利用率和性能:
- 链地址法:空间开销较大,但实现简单,适合冲突频繁的场景。
- 开放寻址法:空间利用率高,但需严格控制负载因子,避免聚集。
- 动态扩容:需权衡扩容时机,避免频繁扩容导致的性能下降和内存碎片。
- 内存池和压缩技术:可进一步优化内存使用,但会增加实现复杂度。
二、C 语言实现哈希表
// 定义哈希表节点结构
typedef struct HashNode {int key;int value;struct HashNode* next;
} HashNode;// 定义哈希表结构
typedef struct HashTable {int size;HashNode** table;
} HashTable;
- 哈希表节点(
HashNode
)- 包含三个字段:
key
(键)、value
(值)和next
(指向下一个节点的指针)。 - 通过链表结构处理哈希冲突(不同的键映射到相同索引时)。
- 包含三个字段:
- 哈希表(
HashTable
)- 包含两个字段:
size
(哈希表大小)和table
(指向指针数组的指针)。 table
数组的每个元素是一个链表头指针。
- 包含两个字段:
// 初始化哈希表
HashTable* createHashTable(int size) {HashTable* hashTable = (HashTable*)malloc(sizeof(HashTable));hashTable->size = size;hashTable->table = (HashNode**)malloc(size * sizeof(HashNode*));for (int i = 0; i < size; i++) {hashTable->table[i] = NULL;}return hashTable;
}// 创建新的哈希表节点
HashNode* createHashNode(int key, int value) {HashNode* newNode = (HashNode*)malloc(sizeof(HashNode));newNode->key = key;newNode->value = value;newNode->next = NULL;return newNode;
}
- 分配哈希表结构内存
- 使用
malloc
动态分配HashTable
结构体的内存空间。
- 使用
- 设置哈希表大小
- 将传入的
size
参数赋值给哈希表的size
字段,决定哈希表的桶bucket
数量。
- 将传入的
- 分配哈希表数组内存
- 分配一个指针数组,每个元素是一个指向
HashNode
的指针(即链表头指针)。
- 分配一个指针数组,每个元素是一个指向
- 初始化所有桶为空
// 哈希函数
int hashFunction(int key, int size) {return key % size;
}// 插入节点到哈希表
void insert(HashTable* hashTable, int key, int value) {int index = hashFunction(key, hashTable->size);HashNode* newNode = createHashNode(key, value);if (hashTable->table[index] == NULL) {hashTable->table[index] = newNode;} else {HashNode* current = hashTable->table[index];while (current->next != NULL) {current = current->next;}current->next = newNode;}
}// 在哈希表中查找节点
int search(HashTable* hashTable, int key) {int index = hashFunction(key, hashTable->size);HashNode* current = hashTable->table[index];while (current != NULL) {if (current->key == key) {return current->value;}current = current->next;}return -1;
}// 从哈希表中删除节点
void deleteNode(HashTable* hashTable, int key) {int index = hashFunction(key, hashTable->size);HashNode* current = hashTable->table[index];HashNode* prev = NULL;while (current != NULL && current->key != key) {prev = current;current = current->next;}if (current == NULL) {return;}if (prev == NULL) {hashTable->table[index] = current->next;} else {prev->next = current->next;}free(current);
}
- 哈希函数
使用取模运算将键映射到数组索引。- 作用:决定键值对应该存储在哈希表的哪个 “桶” 中。
- 插入操作
- 通过哈希函数计算索引。
- 如果对应索引为空,直接插入。如果不为空,遍历链表将新节点添加到尾部(尾插法)。
- 查找操作
- 通过哈希函数定位索引。
- 遍历对应链表,比较键值。找到则返回值,未找到返回
- 1
。
- 删除操作
- 通过哈希函数定位索引
- 遍历链表查找目标节点,调整前驱节点指针跳过目标节点,释放目标节点内存。
// 释放哈希表内存
void freeHashTable(HashTable* hashTable) {for (int i = 0; i < hashTable->size; i++) {HashNode* current = hashTable->table[i];while (current != NULL) {HashNode* temp = current;current = current->next;free(temp);}}free(hashTable->table);free(hashTable);
}
- 遍历哈希表数组
- 获取当前桶的链表头指针,若链表不为空,则释放链表中的所有节点。
- 释放链表节点
- 保存当前节点指针到临时变量,移动到下一个节点,释放临时保存的节点内存,重复直到链表末尾。
- 释放哈希表结构
- 释放哈希表的数组(存储链表头指针的数组)。
- 释放哈希表本身的结构体。
三、例题示例
(1)四数相加2
454. 四数相加 II
给你四个整数数组
nums1
、nums2
、nums3
和nums4
,数组长度都是n
,请你计算有多少个元组(i, j, k, l)
能满足:
0 <= i, j, k, l < n
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
示例 1:
输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2] 输出:2 解释: 两个元组如下: 1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0 2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0
示例 2:
输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0] 输出:1
- 定义哈希表节点和哈希表结构体:
typedef struct HashNode {int key;int count;struct HashNode* next;
} HashNode;typedef struct HashTable {int size;HashNode** table;
} HashTable;
HashNode
结构体表示哈希表中的一个节点,包含一个键 key
、一个计数 count
(用于记录该键出现的次数)以及指向下一个节点的指针 next
。HashTable
结构体表示整个哈希表,包含哈希表的大小 size
和一个指向 HashNode
指针数组的指针 table
,用于存储哈希表的各个桶。
- 创建哈希表函数
createHashTable
:
HashTable* createHashTable(int size) {HashTable* hashTable = (HashTable*)malloc(sizeof(HashTable));hashTable->size = size;hashTable->table = (HashNode**)malloc(size * sizeof(HashNode*));for (int i = 0; i < size; i++) {hashTable->table[i] = NULL;}return hashTable;
}
该函数动态分配一个 HashTable
结构体的内存,并设置其大小 size
。然后,为哈希表的桶数组 table
分配内存,并将每个桶的指针初始化为 NULL
,最后返回创建好的哈希表指针。
- 哈希函数
hashFunction
:
int hashFunction(int key, int size) { return (key % size + size) % size; }
该函数接受一个键 key
和哈希表的大小 size
,通过取模运算将键映射到哈希表的索引位置。(key % size + size) % size
确保了即使 key % size
为负数,也能得到一个有效的非负索引。
- 插入节点到哈希表函数
insert
:
void insert(HashTable* hashTable, int key) {int index = hashFunction(key, hashTable->size);HashNode* current = hashTable->table[index];while (current != NULL) {if (current->key == key) {current->count++;return;}current = current->next;}HashNode* newNode = (HashNode*)malloc(sizeof(HashNode));newNode->key = key;newNode->count = 1;newNode->next = hashTable->table[index];hashTable->table[index] = newNode;
}
该函数首先计算键 key
的哈希索引 index
。然后,遍历哈希表中该索引位置的链表,如果找到相同的键,则将其计数 count
加 1 并返回。如果没有找到相同的键,则创建一个新的 HashNode
,将其计数初始化为 1,并将其插入到链表的头部。
- 获取键的计数函数
getCount
:
int getCount(HashTable* hashTable, int key) {int index = hashFunction(key, hashTable->size);HashNode* current = hashTable->table[index];while (current != NULL) {if (current->key == key) {return current->count;}current = current->next;}return 0;
}
该函数计算键 key
的哈希索引 index
,然后遍历该索引位置的链表,查找键 key
。如果找到,则返回其计数 count
;如果没有找到,则返回 0。
- 释放哈希表内存函数
freeHashTable
:
void freeHashTable(HashTable* hashTable) {for (int i = 0; i < hashTable->size; i++) {HashNode* current = hashTable->table[i];while (current != NULL) {HashNode* temp = current;current = current->next;free(temp);}}free(hashTable->table);free(hashTable);
}
该函数遍历哈希表的每个桶,释放链表中的每个节点的内存,然后释放桶数组的内存,最后释放哈希表结构体的内存。
- 四数之和计数函数
fourSumCount
:
int fourSumCount(int* nums1, int nums1Size, int* nums2, int nums2Size,int* nums3, int nums3Size, int* nums4, int nums4Size) {HashTable* hashTable = createHashTable(400);int result = 0;for (int i = 0; i < nums1Size; i++) {for (int j = 0; j < nums2Size; j++) {int sum12 = nums1[i] + nums2[j];insert(hashTable, sum12);}}for (int k = 0; k < nums3Size; k++) {for (int l = 0; l < nums4Size; l++) {int sum34 = nums3[k] + nums4[l];result += getCount(hashTable, -sum34);}}freeHashTable(hashTable);return result;
}
首先,创建一个哈希表 hashTable
。然后,遍历数组 nums1
和 nums2
,计算它们元素的和 sum12
,并将其插入到哈希表中,同时记录出现的次数。接着,遍历数组 nums3
和 nums4
,计算它们元素的和 sum34
,并在哈希表中查找 -sum34
的计数,将计数累加到 result
中。最后,释放哈希表的内存并返回 result
。
(2)随机链表的复制
138. 随机链表的复制
给你一个长度为
n
的链表,每个节点包含一个额外增加的随机指针random
,该指针可以指向链表中的任何节点或空节点。构造这个链表的 深拷贝。 深拷贝应该正好由
n
个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的next
指针和random
指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。例如,如果原链表中有
X
和Y
两个节点,其中X.random --> Y
。那么在复制链表中对应的两个节点x
和y
,同样有x.random --> y
。返回复制链表的头节点。
用一个由
n
个节点组成的链表来表示输入/输出中的链表。每个节点用一个[val, random_index]
表示:
val
:一个表示Node.val
的整数。random_index
:随机指针指向的节点索引(范围从0
到n-1
);如果不指向任何节点,则为null
。你的代码 只 接受原链表的头节点
head
作为传入参数。
- 定义数据结构:
typedef struct HashNode {struct Node* key;struct Node* value;struct HashNode* next;
} HashNode;typedef struct HashTable {int size;HashNode** table;
} HashTable;
-
首先定义了链表节点
Node
的结构体,包含值val
、指向下一个节点的指针next
和指向随机节点的指针random
。 -
接着定义了哈希表节点
HashNode
的结构体,包含键key
(指向原链表节点的指针)、值value
(指向复制链表节点的指针)和指向下一个哈希表节点的指针next
。 -
然后定义了哈希表
HashTable
的结构体,包含哈希表的大小size
和一个指向HashNode
指针数组的指针table
。
- 创建链表节点函数
creatNode
:
struct Node* creatNode(int val) {struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));newNode->val = val;newNode->next = NULL;newNode->random = NULL;return newNode;
}
该函数动态分配一个 Node
结构体的内存,并初始化其值 val
,同时将 next
和 random
指针初始化为 NULL
,最后返回创建好的链表节点指针。
- 创建哈希表节点函数
createHashNode
:
HashNode* createHashNode(struct Node* key, struct Node* value) {HashNode* newNode = (HashNode*)malloc(sizeof(HashNode));newNode->key = key;newNode->value = value;newNode->next = NULL;return newNode;
}
该函数动态分配一个 HashNode
结构体的内存,并初始化其键 key
和值 value
,同时将 next
指针初始化为 NULL
,最后返回创建好的哈希表节点指针。
- 创建哈希表函数
createHashTable
:
HashTable* createHashTable(int size) {HashTable* hashTable = (HashTable*)malloc(sizeof(HashTable));hashTable->size = size;hashTable->table = (HashNode**)malloc(size * sizeof(HashNode*));for (int i = 0; i < size; i++) {hashTable->table[i] = NULL;}return hashTable;
}
该函数动态分配一个 HashTable
结构体的内存,并设置其大小 size
。然后,为哈希表的桶数组 table
分配内存,并将每个桶的指针初始化为 NULL
,最后返回创建好的哈希表指针。
- 哈希函数
hashFunction
:
int hashFunction(struct Node* key, int size) {return ((unsigned long)key) % size;
}
该函数接受一个指向链表节点的指针 key
和哈希表的大小 size
,通过将指针转换为无符号长整型并取模运算,将键映射到哈希表的索引位置。
在 C 语言中:
- 指针本质:指针是一个内存地址,通常表示为无符号整数。
- 类型安全:直接对指针进行算术运算(如取模)会导致类型错误,因此需要将指针转换为整数类型。
- 平台兼容性:
unsigned long
类型通常足够大,可以容纳指针的完整值(在 32 位和 64 位系统中均适用)。通过
(unsigned long)key
,将指针值转换为无符号长整型,确保其作为整数参与后续的取模运算。
- 插入节点到哈希表函数
insert
:
void insert(HashTable* hashTable, struct Node* key, struct Node* value) {int index = hashFunction(key, hashTable->size);HashNode* newNode = createHashNode(key, value);if (hashTable->table[index] == NULL) {hashTable->table[index] = newNode;} else {HashNode* current = hashTable->table[index];while (current->next != NULL) {current = current->next;}current->next = newNode;}
}
该函数首先计算键 key
的哈希索引 index
。然后,创建一个新的 HashNode
,并将其插入到哈希表中该索引位置的链表中。如果该索引位置的链表为空,则直接插入;否则,遍历链表并将新节点插入到链表的末尾。
- 在哈希表中查找节点函数
search
:
struct Node* search(HashTable* hashTable, struct Node* key) {int index = hashFunction(key, hashTable->size);HashNode* current = hashTable->table[index];while (current != NULL) {if (current->key == key) {return current->value;}current = current->next;}return NULL;
}
该函数计算键 key
的哈希索引 index
,然后遍历该索引位置的链表,查找键 key
。如果找到,则返回其对应的值 value
;如果没有找到,则返回 NULL
。
- 释放哈希表内存函数
freeHashTable
:
void freeHashTable(HashTable* hashTable) {for (int i = 0; i < hashTable->size; i++) {HashNode* current = hashTable->table[i];while (current != NULL) {HashNode* temp = current;current = current->next;free(temp);}}free(hashTable->table);free(hashTable);
}
该函数遍历哈希表的每个桶,释放链表中的每个节点的内存,然后释放桶数组的内存,最后释放哈希表结构体的内存。
- 复制随机链表函数
copyRandomList
:
struct Node* copyRandomList(struct Node* head) {if (head == NULL)return NULL;HashTable* hashTable = createHashTable(300);struct Node* cur = head;while (cur != NULL) {struct Node* newNode = creatNode(cur->val);insert(hashTable, cur, newNode);cur = cur->next;}cur = head;while (cur != NULL) {struct Node* newNode = search(hashTable, cur);if (cur->next != NULL) {newNode->next = search(hashTable, cur->next);}if (cur->random != NULL) {newNode->random = search(hashTable, cur->random);}cur = cur->next;}struct Node* new = search(hashTable, head);freeHashTable(hashTable);return new;
}
首先,创建一个哈希表 hashTable
。然后,遍历原链表,为每个节点创建一个复制节点,并将原节点和复制节点的对应关系插入到哈希表中。接着,再次遍历原链表,通过哈希表找到复制节点,并设置其 next
和 random
指针。最后,释放哈希表的内存,并返回复制链表的头节点。
(3)有效的字母异位词
有效的字母异位词
给定两个字符串
s
和t
,编写一个函数来判断t
是否是s
的 字母异位词。示例 1:
输入: s = "anagram", t = "nagaram" 输出: true
示例 2:
输入: s = "rat", t = "car" 输出: false
- 检查字符串长度
if (strlen(s) != strlen(t))return false;
如果两个字符串的长度不相等,那么它们肯定不是字母异位词,直接返回 false
。
- 创建哈希表
int* hashTable = (int*)malloc(sizeof(int) * 26);
for (int i = 0; i < 26; i++) {hashTable[i] = 0;
}
动态分配一个大小为 26 的整型数组 hashTable
,用于记录每个字符出现的次数。因为英文字母有 26 个,所以数组大小为 26。然后将数组的每个元素初始化为 0。
- 统计字符串
s
中字符的出现次数
for (int i = 0; i < strlen(s); i++) {hashTable[(s[i] - 'a')] += 1;
}
遍历字符串 s
,对于每个字符 s[i]
,计算其在 hashTable
中的索引位置 s[i] - 'a'
(假设字符都是小写英文字母),并将对应位置的计数器加 1。
- 检查字符串
t
中字符的出现次数
for (int i = 0; i < strlen(t); i++) {if (hashTable[(t[i] - 'a')] == 0) {return false;} else {hashTable[(t[i] - 'a')] -= 1;}
}
遍历字符串 t
,对于每个字符 t[i]
,计算其在 hashTable
中的索引位置 t[i] - 'a'
。如果对应位置的计数器为 0,说明字符串 t
中出现了字符串 s
中没有的字符,那么它们不是字母异位词,返回 false
。否则,将对应位置的计数器减 1。
- 检查哈希表中所有计数器是否为 0
for (int i = 0; i < 26; i++) {if (hashTable[i] != 0) {return false;}
}
遍历哈希表 hashTable
,如果有任何一个计数器不为 0,说明两个字符串中字符的出现次数不相等,它们不是字母异位词,返回 false
。
- 返回结果
return true;
如果上述所有检查都通过,说明两个字符串包含相同的字符且出现次数相同,即它们是字母异位词,返回 true
。