数据结构——哈希表

一、哈希表(Hash Table)

1. 哈希表概念

1.1 定义

哈希表是一种存储和查询技术 ,通过哈希函数 将关键字映射到表中位置,实现 O(1) 的平均查找时间。

1.2 核心思想

复制代码
存储位置 = f(key)
  • 存储位置:数组中存储数据的地址

  • f:哈希函数,根据存储内容计算下标的函数

  • key:需要存储的数据

1.3 特点

  • 使用顺序表实现,需要支持随机访问数据

  • 在大量数据中快速查找(O(1)或O(log n))

  • 理想情况下查找时间为常数

  • 既可用于存储 ,也可用于查询

2. 哈希函数设计

2.1 设计要点

  1. 计算相对简单:快速计算哈希值

  2. 地址分布均匀:减少冲突概率

2.2 常见哈希函数

2.2.1 直接定址法
复制代码
h(key) = a × key + b
  • 优点:简单,无冲突

  • 缺点:需要关键字分布连续

  • 适用:关键字分布连续且范围小

2.2.2 除留余数法(最常用)
复制代码
h(key) = key % p
  • p的选择:通常为质数,且不大于表长m

  • 优点:简单,分布相对均匀

  • 缺点:容易产生聚集

2.2.3 平方取中法
复制代码
h(key) = (key²的中间几位)
  • 步骤:key平方 → 取中间几位作为地址

  • 适用:不知道关键字分布,位数适中

2.2.4 折叠法

将关键字分成几部分,然后相加

  • 移位折叠:各部分直接相加

  • 边界折叠:相邻部分反向相加

  • 适用:关键字位数较多

2.2.5 数字分析法

抽取关键字中分布均匀的若干位作为哈希地址

  • 适用:关键字位数较多,且某些位分布均匀

3. 哈希冲突

3.1 冲突定义

复制代码
f(key1) == f(key2), key1 != key2

需要存储的数据不同,但哈希函数计算出的下标相同。

3.2 冲突解决方法

3.2.1 开放定址法

线性探测法

复制代码
hᵢ(key) = (h(key) + i) % m, i = 0,1,2,...,m-1
  • 优点:简单易实现

  • 缺点 :容易产生堆积(一次聚集)

  • 探测序列:+1, +2, +3, ..., +n (n < 数组容量)

二次探测法

复制代码
hᵢ(key) = (h(key) ± i²) % m, i = 0,1,2,...,m-1
  • 优点:减少堆积现象

  • 缺点:不能探测所有位置

  • 探测序列:+1², -1², +2², -2², +4², -4², ...

随机探测法

复制代码
hᵢ(key) = (h(key) + rand()) % m
  • 优点:避免聚集

  • 缺点:查找时需相同随机序列

双散列法

复制代码
hᵢ(key) = (h₁(key) + i × h₂(key)) % m
  • h₁和h₂是两个不同的哈希函数

  • 优点:冲突少,分布均匀

  • 缺点:计算复杂

3.2.2 链地址法
复制代码
typedef struct HashNode {
    KeyType key;
    DataType value;
    struct HashNode *next;
} HashNode;

typedef struct {
    HashNode **table;  // 指针数组
    int size;          // 表长
    int count;         // 元素个数
} HashTable;
  • 优点:无堆积现象,删除方便

  • 缺点:需要额外指针空间

3.2.3 公共溢出区法
  • 哈希表分为基本表溢出表

  • 冲突元素放入溢出表

  • 优点:实现简单

  • 缺点:查找效率降低

4. 哈希表ADT实现

4.1 数据结构定义

复制代码
typedef int DATATYPE;

typedef struct {
    DATATYPE* head;  // 数据数组
    int tlen;        // 表长度
    int count;       // 元素个数
    int* status;     // 状态数组:0-空,1-占用,2-删除
} HS_TABLE;

4.2 基本操作实现

4.2.1 创建哈希表
复制代码
HS_TABLE* CreateHsTable(int len) {
    HS_TABLE* hs = (HS_TABLE*)malloc(sizeof(HS_TABLE));
    if (hs == NULL) return NULL;
    
    hs->head = (DATATYPE*)malloc(sizeof(DATATYPE) * len);
    hs->status = (int*)malloc(sizeof(int) * len);
    hs->tlen = len;
    hs->count = 0;
    
    if (hs->head == NULL || hs->status == NULL) {
        free(hs->head);
        free(hs->status);
        free(hs);
        return NULL;
    }
    
    // 初始化状态为0(空)
    for (int i = 0; i < len; i++) {
        hs->status[i] = 0;
    }
    
    return hs;
}
4.2.2 哈希函数(除留余数法)
复制代码
int HashFunc(DATATYPE key, int size) {
    return key % size;
}
4.2.3 插入元素(线性探测)
复制代码
int InsertHsTable(HS_TABLE* hs, DATATYPE data) {
    if (hs == NULL || hs->count >= hs->tlen) {
        return -1;  // 表满或无效
    }
    
    int index = HashFunc(data, hs->tlen);
    int i = 0;
    
    // 线性探测
    while (i < hs->tlen) {
        int pos = (index + i) % hs->tlen;
        
        // 找到空位置或删除位置
        if (hs->status[pos] == 0 || hs->status[pos] == 2) {
            hs->head[pos] = data;
            hs->status[pos] = 1;  // 标记为占用
            hs->count++;
            return pos;  // 返回插入位置
        }
        // 如果已存在相同元素
        else if (hs->status[pos] == 1 && hs->head[pos] == data) {
            return -2;  // 元素已存在
        }
        
        i++;
    }
    
    return -1;  // 未找到插入位置
}
4.2.4 查找元素
复制代码
int SearchHsTable(HS_TABLE* hs, DATATYPE data) {
    if (hs == NULL) return -1;
    
    int index = HashFunc(data, hs->tlen);
    int i = 0;
    
    while (i < hs->tlen) {
        int pos = (index + i) % hs->tlen;
        
        // 遇到空位置,说明元素不存在
        if (hs->status[pos] == 0) {
            return -1;
        }
        // 找到元素
        if (hs->status[pos] == 1 && hs->head[pos] == data) {
            return pos;
        }
        
        i++;
    }
    
    return -1;  // 未找到
}
4.2.5 删除元素
复制代码
int DeleteHsTable(HS_TABLE* hs, DATATYPE data) {
    if (hs == NULL) return -1;
    
    int pos = SearchHsTable(hs, data);
    if (pos == -1) {
        return -1;  // 元素不存在
    }
    
    // 标记为删除状态(惰性删除)
    hs->status[pos] = 2;
    hs->count--;
    
    return pos;
}
4.2.6 销毁哈希表
复制代码
int DestroyHsTable(HS_TABLE* hs) {
    if (hs == NULL) return -1;
    
    free(hs->head);
    free(hs->status);
    free(hs);
    
    return 0;
}
4.2.7 显示哈希表
复制代码
void DisplayHsTable(HS_TABLE* hs) {
    if (hs == NULL) return;
    
    printf("哈希表(长度=%d,元素数=%d):\n", hs->tlen, hs->count);
    printf("索引\t状态\t数据\n");
    printf("-------------------\n");
    
    for (int i = 0; i < hs->tlen; i++) {
        printf("%d\t", i);
        
        switch (hs->status[i]) {
            case 0:
                printf("空\t-\n");
                break;
            case 1:
                printf("占用\t%d\n", hs->head[i]);
                break;
            case 2:
                printf("删除\t-\n");
                break;
        }
    }
}

5. 哈希表性能分析

5.1 装载因子

复制代码
α = n / m
  • n:表中元素个数

  • m:哈希表长度

  • α越大,冲突概率越高

  • 建议:α < 0.75

5.2 平均查找长度(ASL)

5.2.1 查找成功
复制代码
ASL成功 = Σ(查找每个元素的比较次数) / n
5.2.2 查找失败
复制代码
ASL失败 = Σ(到空位置比较次数) / m

5.3 不同方法的ASL

冲突解决方法 ASL成功(平均) ASL失败(平均)
线性探测法 (1 + 1/(1-α)²)/2 (1 + 1/(1-α))/2
二次探测法 -ln(1-α)/α 1/(1-α)
链地址法 1 + α/2 α + e⁻α

6. 完整示例代码

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

#define EMPTY 0
#define OCCUPIED 1
#define DELETED 2

// 哈希表ADT实现
typedef int DATATYPE;

typedef struct {
    DATATYPE* data;    // 数据数组
    int* status;       // 状态数组
    int size;          // 表大小
    int count;         // 元素个数
} HashTable;

// 创建哈希表
HashTable* CreateHashTable(int size) {
    HashTable* ht = (HashTable*)malloc(sizeof(HashTable));
    if (!ht) return NULL;
    
    ht->data = (DATATYPE*)malloc(sizeof(DATATYPE) * size);
    ht->status = (int*)calloc(size, sizeof(int));  // 初始化为0
    ht->size = size;
    ht->count = 0;
    
    if (!ht->data || !ht->status) {
        free(ht->data);
        free(ht->status);
        free(ht);
        return NULL;
    }
    
    return ht;
}

// 哈希函数(除留余数法)
int HashFunction(DATATYPE key, int size) {
    return abs(key) % size;  // 取绝对值避免负数
}

// 插入元素(线性探测)
int HashInsert(HashTable* ht, DATATYPE key) {
    if (!ht || ht->count >= ht->size) return -1;
    
    int index = HashFunction(key, ht->size);
    int i = 0;
    
    while (i < ht->size) {
        int pos = (index + i) % ht->size;
        
        // 空位置或删除位置
        if (ht->status[pos] == EMPTY || ht->status[pos] == DELETED) {
            ht->data[pos] = key;
            ht->status[pos] = OCCUPIED;
            ht->count++;
            return pos;
        }
        // 已存在相同元素
        else if (ht->status[pos] == OCCUPIED && ht->data[pos] == key) {
            return -2;  // 元素已存在
        }
        
        i++;
    }
    
    return -1;  // 表满
}

// 查找元素
int HashSearch(HashTable* ht, DATATYPE key) {
    if (!ht) return -1;
    
    int index = HashFunction(key, ht->size);
    int i = 0;
    
    while (i < ht->size) {
        int pos = (index + i) % ht->size;
        
        // 遇到空位置,停止搜索
        if (ht->status[pos] == EMPTY) {
            return -1;
        }
        // 找到元素
        if (ht->status[pos] == OCCUPIED && ht->data[pos] == key) {
            return pos;
        }
        
        i++;
    }
    
    return -1;  // 未找到
}

// 删除元素
int HashDelete(HashTable* ht, DATATYPE key) {
    if (!ht) return -1;
    
    int pos = HashSearch(ht, key);
    if (pos == -1) return -1;  // 元素不存在
    
    ht->status[pos] = DELETED;  // 惰性删除
    ht->count--;
    
    return pos;
}

// 销毁哈希表
void DestroyHashTable(HashTable* ht) {
    if (!ht) return;
    
    free(ht->data);
    free(ht->status);
    free(ht);
}

// 显示哈希表
void DisplayHashTable(HashTable* ht) {
    if (!ht) return;
    
    printf("\n哈希表状态(大小=%d,元素=%d):\n", ht->size, ht->count);
    printf("索引\t状态\t数据\n");
    printf("--------------------\n");
    
    for (int i = 0; i < ht->size; i++) {
        printf("%d\t", i);
        
        switch (ht->status[i]) {
            case EMPTY:
                printf("空\t-\n");
                break;
            case OCCUPIED:
                printf("占用\t%d\n", ht->data[i]);
                break;
            case DELETED:
                printf("删除\t-\n");
                break;
        }
    }
}

// 测试函数
int main() {
    // 创建哈希表
    HashTable* ht = CreateHashTable(10);
    if (!ht) {
        printf("创建哈希表失败!\n");
        return 1;
    }
    
    printf("=== 哈希表测试 ===\n");
    
    // 插入测试
    int keys[] = {19, 29, 39, 49, 59, 69, 79, 89, 99, 109};
    for (int i = 0; i < 10; i++) {
        int result = HashInsert(ht, keys[i]);
        if (result >= 0) {
            printf("插入 %d 成功,位置: %d\n", keys[i], result);
        } else if (result == -2) {
            printf("插入 %d 失败,元素已存在\n", keys[i]);
        } else {
            printf("插入 %d 失败,表满\n", keys[i]);
        }
    }
    
    DisplayHashTable(ht);
    
    // 查找测试
    printf("\n查找测试:\n");
    int search_keys[] = {39, 50, 89};
    for (int i = 0; i < 3; i++) {
        int pos = HashSearch(ht, search_keys[i]);
        if (pos >= 0) {
            printf("查找 %d 成功,位置: %d\n", search_keys[i], pos);
        } else {
            printf("查找 %d 失败,元素不存在\n", search_keys[i]);
        }
    }
    
    // 删除测试
    printf("\n删除测试:\n");
    int delete_key = 49;
    int del_pos = HashDelete(ht, delete_key);
    if (del_pos >= 0) {
        printf("删除 %d 成功,位置: %d\n", delete_key, del_pos);
    } else {
        printf("删除 %d 失败\n", delete_key);
    }
    
    DisplayHashTable(ht);
    
    // 再次插入测试删除位置
    printf("\n在删除位置插入新元素:\n");
    int new_key = 119;
    int ins_pos = HashInsert(ht, new_key);
    if (ins_pos >= 0) {
        printf("插入 %d 成功,位置: %d\n", new_key, ins_pos);
    }
    
    DisplayHashTable(ht);
    
    // 销毁哈希表
    DestroyHashTable(ht);
    
    return 0;
}

7. 应用场景

7.1 适合使用哈希表的场景

  1. 快速查找:字典、电话簿、用户数据库

  2. 去重操作:统计唯一元素

  3. 缓存系统:Redis、Memcached

  4. 数据库索引:快速定位记录

  5. 编译器符号表:变量名查找

  6. 拼写检查:单词快速查找

7.2 不适合使用哈希表的场景

  1. 需要有序数据:哈希表无序

  2. 范围查询:如"查找10-20之间的所有值"

  3. 前缀搜索:如"查找以'abc'开头的所有词"

  4. 数据量极小:直接线性查找更简单

8. 总结

特性 哈希表
查找速度 O(1)平均,O(n)最坏
插入速度 O(1)平均
删除速度 O(1)平均
空间复杂度 O(n)
是否有序 无序
最佳适用 快速查找,无需排序

核心优势:在理想情况下,哈希表提供了最快的查找速度(O(1)),特别适合需要频繁查找操作的场景。

注意事项:需要合理设计哈希函数和处理冲突的方法,否则可能退化为O(n)的查找效率。

相关推荐
FY_20184 小时前
Stable Baselines3中调度函数转换器get_schedule_fn 函数
开发语言·人工智能·python·算法
CoderYanger4 小时前
动态规划算法-子数组、子串系列(数组中连续的一段):26.环绕字符串中唯一的子字符串
java·算法·leetcode·动态规划·1024程序员节
闲聊MoonL4 小时前
Microsoft Azure Cobalt 200 Launched with 132 Arm Neoverse V3 Cores
笔记
van久4 小时前
.Net Core 学习:DbContextOptions<T> vs DbContextOptions 详细解析
java·学习·.netcore
Jane-6667774 小时前
C语言——表达式、语句、函数
c语言·开发语言·算法
Tony_yitao4 小时前
12.华为OD机试 - N个选手比赛前三名、比赛(Java 双机位A卷 100分)
java·算法·华为od·algorithm
小龙报4 小时前
【算法通关指南:数据结构与算法篇】树形结构遍历指南:DFS 递归深搜与 BFS 队列广搜实战解析
c语言·数据结构·c++·算法·链表·深度优先·visual studio
业精于勤的牙4 小时前
浅谈:快递物流与算法的相关性(六)
算法
HalvmånEver4 小时前
Linux:进程替换(进程控制四)
linux·运维·服务器·学习·进程