哈希表(Hash Table)核心概念
-
定义:一种支持高效存储与查找的数据结构,目标查找复杂度为 O(1)∼O(lgN) ,通过哈希函数映射数据到存储位置,平衡存储与查询效率。
-
核心要素:
- 哈希函数(Hash Fun) :
- 作用:将输入的
key
(要存储的数据)转换为哈希表的下标,确定数据存储位置。 - 要求:计算快捷(降低时间开销)、地址分布均匀(减少冲突概率)。
- 示例(数字场景):常用求余运算 (如
key % 哈希表长度
)。
- 作用:将输入的
- 哈希表(Hash Table):存储数据的连续空间,通过哈希函数映射的下标访问,是哈希逻辑的载体。
- 哈希函数(Hash Fun) :
-
冲突(Collision) :不同
key
经哈希函数计算得到相同下标(即fun(key1) == fun(key2)
),需通过探测策略解决。常见探测方式:探测策略 描述 示例探测序列(假设基础下标为 i
)线性探测 按顺序遍历后续下标 i+1, i+2, i+3, ...
(或循环回表首)二次探测 按 "平方级偏移" 遍历下标 i+1, i-1, i+2, i-2, ...
(避免连续扎堆)随机探测 用随机数生成偏移量 i + rand()
(依赖随机算法,分布更灵活)
关键逻辑梳理
-
存储流程 :
数据 → 哈希函数计算下标 → 若下标位置空闲,直接存储;若冲突,按探测策略找新位置存储。 -
查找流程 :
目标key
→ 哈希函数计算下标 → 比对数据,若匹配则命中;若不匹配,按探测策略遍历查找(或判定不存在)。 -
设计权衡 :
- 哈希函数越简单、分布越均匀,存储 / 查询效率越高,但冲突仍难完全避免。
探测策略影响冲突解决效率:线性探测实现简单但易引发 "聚集";二次 / 随机探测可分散冲突,却增加实现复杂度。
操作函数
cs
#include <stdio.h> // 标准输入输出库,用于printf等函数
#include <stdlib.h> // 标准库,用于malloc、free等内存管理函数
#include <string.h> // 字符串处理库,用于memcpy等内存拷贝函数
// 定义哈希表存储的数据类型为int
typedef int DATATYPE;
// 哈希表结构体定义
typedef struct
{
DATATYPE* head; // 指向哈希表数组的首地址
int tlen; // 哈希表的长度(数组大小)
} HS_Table;
/**
* @brief 创建哈希表
*
* @param len 哈希表的长度
* @return HS_Table* 成功返回哈希表指针,失败返回NULL
*/
HS_Table* CreateHsTable(int len)
{
// 为哈希表结构体分配内存
HS_Table* hs = malloc(sizeof(HS_Table));
if (NULL == hs) // 内存分配失败检查
{
perror("CreateHsTable malloc1"); // 打印错误信息
return NULL;
}
// 为哈希表数组分配内存
hs->head = malloc(sizeof(DATATYPE) * len);
if (NULL == hs->head) // 内存分配失败检查
{
perror("CreateHsTable malloc2"); // 打印错误信息
free(hs); // 释放已分配的结构体内存,防止内存泄漏
return NULL;
}
hs->tlen = len; // 设置哈希表长度
// 初始化哈希表,将所有元素设为-1(表示该位置为空)
int i = 0;
for (i = 0; i < len; i++)
{
hs->head[i] = -1;
}
return hs; // 返回创建好的哈希表
}
/**
* @brief 哈希函数:根据需要存储的数据计算下标
* 设计原则:1.计算过程尽可能简单 2.使下标均匀分布
*
* @param h 哈希表指针
* @param data 需要计算下标的数据
* @return int 计算得到的下标
*/
int HS_Fun(HS_Table* h, DATATYPE* data)
{
// 使用取模运算作为哈希函数,data对哈希表长度取模
return *data % h->tlen;
}
/**
* @brief 向哈希表中插入数据
*
* @param h 哈希表指针
* @param data 要插入的数据
* @return int 0表示成功
*/
int HS_Insert(HS_Table* h, DATATYPE* data)
{
// 计算数据应插入的初始位置
int ind = HS_Fun(h, data);
// 处理哈希冲突:当计算的位置已有数据时,使用线性探测法寻找下一个空位置
// 线性探测法:如果当前位置被占用,则检查下一个位置,直到找到空位置
while (-1 != h->head[ind]) // -1表示位置为空
{
printf("data:%d ind:%d 发生冲突\n", *data, ind); // 打印冲突信息
ind = (ind + 1) % h->tlen; // 计算下一个位置,循环到表的开始
}
// 将数据插入到找到的空位置
memcpy(&h->head[ind], data, sizeof(DATATYPE));
return 0; // 插入成功
}
/**
* @brief 在哈希表中查找数据
*
* @param h 哈希表指针
* @param data 要查找的数据
* @return int 找到返回数据所在下标,未找到返回-1
*/
int HS_Search(HS_Table* h, DATATYPE* data)
{
// 计算数据可能存在的初始位置
int ind = HS_Fun(h, data);
int old_ind = ind; // 保存初始位置,用于判断是否遍历完整个表
//由于哈希表是 "循环数组"(通过% h->tlen实现循环),需要一个 "起点标记" 来判断是否已经遍历了整个哈希表。old_ind就是这个起点,当后续查找绕回old_ind时,说明所有位置都已检查过
// 查找数据,处理可能的哈希冲突
while (*data != h->head[ind]) //// 若当前位置不是目标数据,继续查找
{
ind = (ind + 1) % h->tlen; // 检查下一个位置
// 如果回到初始位置,说明整个表已遍历完毕且未找到数据
if (ind == old_ind)
{
return -1; // 未找到
}
}
return ind; // 找到数据,返回下标
}
// 哈希表销毁函数声明(未实现)
int HS_Destroy(HS_Table* h);
/**
* @brief 主函数:演示哈希表的创建、插入和查找操作
*/
int main(int argc, char** argv)
{
// 创建长度为12的哈希表
HS_Table* hs = CreateHsTable(12);
// 准备要插入的数据
int array[12] = {12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34};
// 将数据插入哈希表
int i = 0;
for (i = 0; i < 12; i++)
{
HS_Insert(hs, &array[i]);
}
// 在哈希表中查找数据12
int want_num = 12;
int ret = HS_Search(hs, &want_num);
if (-1 == ret)
{
printf("未找到 %d\n", want_num);
}
else
{
printf("找到 %d,位于下标 %d\n", want_num, ret);
}
// 注意:此处缺少哈希表的销毁操作,实际应用中应调用HS_Destroy释放内存
return 0;
}
内核链表(Kernel list)
内核链表是一种源自 Linux 内核的双向循环链表实现,其核心设计特点是将链表的 "指针域" 与 "数据域" 解耦,从而实现高度的通用性和灵活性。以下是其精确定义及核心组成:
1. 内核链表的本质定义
内核链表是一种纯指针驱动的双向循环链表 ,通过一个仅包含前后指针的基础结构体(struct list_head
),实现对任意数据结构的链接管理。它不直接存储数据,而是通过 "嵌入" 到用户自定义的数据结构中,间接关联数据,从而一套链表操作逻辑可适配所有数据类型。
2. 核心结构体定义
内核链表的基础是struct list_head
,定义如下(源自<linux/list.h>
):
struct list_head {
struct list_head *next; // 指向后一个链表节点
struct list_head *prev; // 指向前一个链表节点
};
- 该结构体仅包含指针域,无任何数据字段,是链表的 "连接核心";
- 所有链表操作(插入、删除、遍历等)均围绕
struct list_head
进行。
3. 数据与链表的关联方式
用户需将struct list_head
作为成员嵌入自定义数据结构中,实现数据与链表的绑定:
// 示例:包含内核链表节点的自定义数据结构
typedef struct {
int id; // 自定义数据字段1
char name[50]; // 自定义数据字段2
struct list_head node; // 嵌入内核链表节点(关键!)
} Person;
node
成员是数据结构与链表的 "连接点";- 多个
Person
结构体通过各自的node
成员形成链表。
4. 核心特性
- 双向循环 :链表首尾相连,头节点的
prev
指向尾节点,尾节点的next
指向头节点,无NULL
指针,简化边界处理; - 解耦设计 :链表逻辑(
list_head
)与数据(用户结构体)分离,一套操作适配所有数据类型; - 通用操作 :内核提供标准化宏(如
INIT_LIST_HEAD
初始化、list_add
插入、list_del
删除、list_for_each_entry
遍历等),无需重复开发; - 安全遍历 :通过
list_for_each_entry_safe
宏支持遍历中动态删除节点,避免野指针问题。
5. 初始化与基本形态
内核链表需通过INIT_LIST_HEAD
宏初始化头节点,形成空链表:
struct list_head head; // 定义头节点(不存储数据)
INIT_LIST_HEAD(&head); // 初始化:head->next = head->prev = &head
- 头节点是链表的 "锚点",不包含业务数据,仅用于标记链表范围;
- 空链表状态:头节点的
next
和prev
均指向自身。
内核链表与传统链表的对比
特性 | 传统双向链表 | 内核链表(Linux) |
---|---|---|
结构设计 | 数据域与指针域耦合 | 数据域与指针域解耦(list_head 独立) |
通用性 | 仅支持固定数据类型,复用性差 | 一套逻辑适配所有数据类型 |
扩展性 | 数据结构变更需修改链表逻辑 | 数据变更不影响链表操作 |
遍历安全性 | 动态删除易出现野指针 | list_for_each_entry_safe 保障安全 |
边界条件处理 | 需判断头 / 尾节点 NULL,逻辑复杂 | 循环结构,边界逻辑统一 |
工业级支持 | 需自行实现所有操作(易出错) | 内核提供成熟宏(list.h ),稳定性高 |
内核链表操作函数实现
cs
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "list.h"
/**
* @brief 内核链表的一个demo
* 1. 定义自己需要的数据类型,其中必须包含一个 struct list_head 的变量
* 2. 定义头节点,并初始化
* 3. 增加结点:malloc自己的结构体,填入数据后调用list_add将节点加入链表
* 4. 遍历所有元素:使用list_for_each_entry_safe宏
*/
// 内核链表节点定义(通常在list.h中)
// struct list_head {
// struct list_head *next, *prev; // 双向指针,指向前后节点
// };
// 自定义数据结构,包含实际业务数据和内核链表节点
typedef struct {
int id; // 编号
char name[50]; // 名称
struct list_head node; // 链表节点(内核链表的核心)
} Person;
/**
* @brief 添加节点:创建新的Person结构体,初始化数据后插入链表
*
* @param head 链表头节点指针
* @param id 人员ID
* @param name 人员姓名(字符串,传指针类型)
* @return int 0表示成功,1表示失败
*/
int add_person(struct list_head *head, int id, const char *name) {
// 为新节点分配内存
Person *person = malloc(sizeof(Person));
if (NULL == person) {
perror("add_person malloc error");
return 1;
}
// 初始化节点数据
person->id = id;
// 使用strncpy替代strcpy增强安全性,并确保字符串结束
strncpy(person->name, name, sizeof(person->name) - 1);
person->name[sizeof(person->name) - 1] = '\0';
// 尾插:新节点插入到头节点之前,成为最后一个元素(因是循环链表)
// 头插方式:list_add(&person->node, head);(新节点插入到头节点之后,成为第一个元素)
list_add_tail(&person->node, head);
return 0;
}
/**
* @brief 遍历打印:遍历链表中所有节点,打印每个节点的id和name
*
* @param head 链表头节点指针
* @return int 0表示成功
*/
int show_persons(struct list_head *head) {
Person *current; // 当前要访问的Person结构体指针
Person *next; // 临时变量,保存current的下一个节点(避免删除current后丢失后续节点)
// 遍历所有数据的宏:list_for_each_entry_safe(pos, n, head, member)
// 参数说明:
// pos:当前访问的结构体指针
// n:保存下一个节点的临时指针
// head:链表的头结点
// member:在自定义结构体中链表节点的变量名
list_for_each_entry_safe(current, next, head, node) {
printf("ID: %d, 姓名: %s\n", current->id, current->name);
}
return 0;
}
/**
* @brief 容器指针技术说明(内核链表核心原理)
*
* 如何从链表节点指针(struct list_head*)找到它所在的完整数据结构体(Person*):
* 1. 生活化例子:
* 快递盒(Person结构体)包含物品(id和name)和标签(struct list_head node),
* 标签上有前后快递盒标签的位置(prev和next)。通过标签位置找到整个快递盒位置。
*
* 2. 内存布局理解:
* Person结构体在内存中是连续空间,包含id、name、node三部分。
* 假设Person起始地址为p,则:
* - id地址 = p + 0(偏移0字节)
* - name地址 = p + 4(假设int占4字节,偏移4字节)
* - node地址 = p + 54(name占50字节,4+50=54,偏移54字节)
* 即:node地址 = Person起始地址 + node在Person中的偏移量
* 反过来:Person起始地址 = node地址 - node在Person中的偏移量
*
* 3. 代码实现:
* list_for_each_entry_safe宏内部通过计算偏移量实现转换:
* Person* current = (Person*)((char*)curr_node - offsetof(Person, node))
* 其中offsetof(Person, node)计算node在Person中的偏移量
*/
/**
* @brief 根据id查找并删除链表中的节点
*
* @param head 链表头节点指针
* @param id 需要删除数据的编号
* @return int 0表示成功
*/
int delete_person(struct list_head *head, int id) {
Person *current; // 当前访问的Person结构体指针
Person *next; // 临时变量,保存current的下一个节点(避免删除current后丢失后续节点)
list_for_each_entry_safe(current, next, head, node) {
if (current->id == id) { // 根据id查找节点
list_del(¤t->node); // 从链表中移除该节点
free(current); // 释放节点内存
printf("已删除ID为%d的节点\n", id);
break;
}
}
return 0;
}
/**
* @brief 通过id查找节点
*
* @param head 链表头节点指针
* @param id 需要查找的数据编号
* @return int 0表示找到,1表示未找到
*/
int find_person(struct list_head *head, int id) {
Person *current;
Person *next;
int found = 0;
list_for_each_entry_safe(current, next, head, node) {
if (current->id == id) {
printf("找到节点 - ID: %d, 姓名: %s\n", current->id, current->name);
found = 1;
break;
}
}
if (!found) {
printf("未找到ID为%d的节点\n", id);
return 1;
}
return 0;
}
/**
* @brief 修改指定id节点的id值
*
* @param head 链表头节点指针
* @param old_id 原ID
* @param new_id 新ID
* @return int 0表示成功
*/
int modify_person_id(struct list_head *head, int old_id, int new_id) {
Person *current;
Person *next;
list_for_each_entry_safe(current, next, head, node) {
if (current->id == old_id) {
current->id = new_id;
printf("修改成功 - 新ID: %d, 姓名: %s\n", current->id, current->name);
break;
}
}
return 0;
}
int main(int argc, char **argv) {
// 头结点:不包含有效数据,head->next是链表中的第一个有效数据
// 头节点作用:标记链表的起点和终点,简化链表操作
struct list_head head;
// 初始化双向循环链表:将头节点的next和prev指针都指向自身,形成空链表
INIT_LIST_HEAD(&head);
// 向链表添加节点
add_person(&head, 1, "张三");
add_person(&head, 2, "李四");
add_person(&head, 3, "王麻子");
add_person(&head, 4, "关二哥");
add_person(&head, 5, "刘备");
// 显示所有节点
printf("----- 初始链表 -----\n");
show_persons(&head);
// 删除指定节点
printf("\n----- 删除ID=1的节点 -----\n");
delete_person(&head, 1);
show_persons(&head);
// 查找节点
printf("\n----- 查找ID=3的节点 -----\n");
find_person(&head, 3);
// 修改节点ID
printf("\n----- 修改ID=2的节点为ID=6 -----\n");
modify_person_id(&head, 2, 6);
show_persons(&head);
return 0;
}