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

哈希表(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;
}
相关推荐
lifallen3 小时前
Hadoop MapReduce 任务/输入数据 分片 InputSplit 解析
大数据·数据结构·hadoop·分布式·算法
Ghost-Face4 小时前
并查集提高——种类并查集(反集)
算法
董董灿是个攻城狮4 小时前
5分钟搞懂大模型微调的原始能力退化问题
算法
Univin5 小时前
8.25作业
数据结构·windows
胡萝卜3.07 小时前
数据结构初阶:详解单链表(一)
数据结构·笔记·学习·单链表
艾醒8 小时前
大模型面试题剖析:大模型微调与训练硬件成本计算
人工智能·后端·算法
闪电麦坤958 小时前
数据结构:红黑树(Red-Black Tree)
数据结构··红黑树
啊嘞嘞?9 小时前
力扣(滑动窗口最大值)
算法·leetcode·职场和发展