数据结构 -- 哈希表和内核链表

哈希表(Hash Table)核心概念

  • 定义:一种支持高效存储与查找的数据结构,目标查找复杂度为 O(1)∼O(lgN) ,通过哈希函数映射数据到存储位置,平衡存储与查询效率。

  • 核心要素

    1. 哈希函数(Hash Fun)
      • 作用:将输入的 key(要存储的数据)转换为哈希表的下标,确定数据存储位置。
      • 要求:计算快捷(降低时间开销)、地址分布均匀(减少冲突概率)。
      • 示例(数字场景):常用求余运算 (如 key % 哈希表长度 )。
    2. 哈希表(Hash Table):存储数据的连续空间,通过哈希函数映射的下标访问,是哈希逻辑的载体。
  • 冲突(Collision) :不同 key 经哈希函数计算得到相同下标(即 fun(key1) == fun(key2) ),需通过探测策略解决。常见探测方式:

    探测策略 描述 示例探测序列(假设基础下标为 i
    线性探测 按顺序遍历后续下标 i+1, i+2, i+3, ...(或循环回表首)
    二次探测 按 "平方级偏移" 遍历下标 i+1, i-1, i+2, i-2, ...(避免连续扎堆)
    随机探测 用随机数生成偏移量 i + rand()(依赖随机算法,分布更灵活)

关键逻辑梳理

  1. 存储流程
    数据 → 哈希函数计算下标 → 若下标位置空闲,直接存储;若冲突,按探测策略找新位置存储。

  2. 查找流程
    目标 key → 哈希函数计算下标 → 比对数据,若匹配则命中;若不匹配,按探测策略遍历查找(或判定不存在)。

  3. 设计权衡

    • 哈希函数越简单、分布越均匀,存储 / 查询效率越高,但冲突仍难完全避免。

    探测策略影响冲突解决效率:线性探测实现简单但易引发 "聚集";二次 / 随机探测可分散冲突,却增加实现复杂度。

操作函数

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
  • 头节点是链表的 "锚点",不包含业务数据,仅用于标记链表范围;
  • 空链表状态:头节点的nextprev均指向自身。

内核链表与传统链表的对比

特性 传统双向链表 内核链表(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(&current->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;
}
相关推荐
kfepiza15 小时前
CAS (Compare and Swap) 笔记251007
java·算法
墨染点香15 小时前
LeetCode 刷题【103. 二叉树的锯齿形层序遍历、104. 二叉树的最大深度、105. 从前序与中序遍历序列构造二叉树】
算法·leetcode·职场和发展
啊我不会诶15 小时前
23ICPC澳门站补题
算法·深度优先·图论
Brookty16 小时前
【算法】二分查找(一)朴素二分
java·学习·算法·leetcode·二分查找
黑色的山岗在沉睡17 小时前
LeetCode 2761. 和等于目标值的质数对
算法·leetcode·职场和发展
bawangtianzun17 小时前
重链剖分 学习记录
数据结构·c++·学习·算法
ChoSeitaku21 小时前
NO.14数据结构红黑树|树高|转化4阶B树|插入操作|删除操作
数据结构·b树
T1an-121 小时前
力扣70.爬楼梯
算法·leetcode·职场和发展
T1an-121 小时前
力扣169.多数元素
数据结构·算法·leetcode
violet-lz1 天前
数据结构:七大线性数据结构从结构体定义到函数实现的的区别
数据结构