当前位置: 首页 > news >正文

哈希表与哈希算法:原理、实现与优化

哈希表与哈希算法:原理、实现与优化

1. 哈希表基础原理

哈希表(Hash Table)是一种基于键值对(key-value)存储数据的数据结构,它通过计算键的哈希值来确定数据在内存中的存储位置,从而实现O(1)平均时间复杂度的插入、查找和删除操作。

1.1 哈希表的核心组件

  1. 哈希函数:将任意大小的输入数据映射到固定大小的输出(哈希值)
  2. 哈希桶(数组):存储数据的实际容器
  3. 冲突解决机制:处理不同键映射到相同位置的情况

1.2 哈希表的基本原理

哈希表通过以下步骤存储和检索数据:

  1. 通过哈希函数计算键的哈希值
  2. 将哈希值映射到哈希桶的索引位置
  3. 在该位置存储或检索数据
  4. 如果发生冲突,使用冲突解决策略处理

2. 哈希函数设计

哈希函数是哈希表性能的关键因素,一个好的哈希函数应该具备以下特性:

  1. 高效计算:计算速度快
  2. 均匀分布:不同输入产生的哈希值均匀分布在整个输出范围内
  3. 确定性:相同的输入总是产生相同的输出
  4. 雪崩效应:输入的微小变化会导致输出的显著变化

2.1 常见的哈希函数

2.1.1 整数哈希

对于整数类型的键,常用的哈希函数包括:

// 简单的取模哈希函数
size_t hash_mod(int key, size_t table_size) {
   
    return key % table_size;
}

// 乘法哈希函数(Knuth推荐)
size_t hash_multiply(int key, size_t table_size) {
   
    const double A = 0.6180339887; // (sqrt(5) - 1) / 2
    double val = key * A;
    val = val - floor(val);
    return floor(val * table_size);
}
2.1.2 字符串哈希

对于字符串类型的键,几种经典哈希算法:

// BKDR哈希算法
size_t BKDRHash(const char* str, size_t table_size) {
   
    unsigned int seed = 131; // 31, 131, 1313, 13131... 都是不错的选择
    unsigned int hash = 0;
    while (*str) {
   
        hash = hash * seed + (*str++);
    }
    return hash % table_size;
}

// FNV哈希算法
size_t FNVHash(const char* str, size_t table_size) {
   
    const unsigned int FNV_PRIME = 16777619;
    const unsigned int OFFSET_BASIS = 2166136261;
    unsigned int hash = OFFSET_BASIS;
    while (*str) {
   
        hash ^= *str++;
        hash *= FNV_PRIME;
    }
    return hash % table_size;
}

// Jenkins哈希算法(one-at-a-time)
size_t JenkinsHash(const char* str, size_t table_size) {
   
    unsigned int hash = 0;
    while (*str) {
   
        hash += *str++;
        hash += (hash << 10);
        hash ^= (hash >> 6);
    }
    hash += (hash << 3);
    hash ^= (hash >> 11);
    hash += (hash << 15);
    return hash % table_size;
}

2.2 选择哈希表大小

哈希表的大小也是影响性能的重要因素。通常,选择质数作为哈希表大小可以减少冲突:

// 检查一个数是否为质数
bool isPrime(int n) {
   
    if (n <= 1) return false;
    if (n <= 3) return true;
    if (n % 2 == 0 || n % 3 == 0) return false;
    for (int i = 5; i * i <= n; i += 6) {
   
        if (n % i == 0 || n % (i + 2) == 0) return false;
    }
    return true;
}

// 找到大于n的下一个质数
int nextPrime(int n) {
   
    if (n <= 1) return 2;
    int prime = n;
    bool found = false;
    while (!found) {
   
        prime++;
        if (isPrime(prime)) found = true;
    }
    return prime;
}

3. 冲突解决策略

哈希冲突是指不同的键通过哈希函数计算后得到相同的哈希值。解决冲突的主要策略有两种:开放寻址和链式寻址。

3.1 链式寻址(Chaining)

链式寻址使用链表存储映射到同一位置的所有元素:

  1. 每个哈希桶维护一个链表
  2. 发生冲突时,将新元素添加到链表中
  3. 查找时,遍历链表找到目标元素

优点:

  • 实现简单
  • 对负载因子不敏感
  • 插入操作不受表大小限制

缺点:

  • 需要额外的内存存储链表
  • 可能导致缓存不友好
  • 搜索时间与链表长度相关

3.2 开放寻址(Open Addressing)

开放寻址在冲突发生时,通过某种探测序列查找下一个可用位置:

3.2.1 线性探测(Linear Probing)

冲突发生时,按顺序检查下一个位置:

size_t linear_probe(size_t hash, size_t i, size_t table_size) {
   
    return (hash + i) % table_size;
}

优点:

  • 缓存友好
  • 内存利用率高

缺点:

  • 容易造成聚集(clustering)
  • 负载因子增大时性能下降明显
3.2.2 二次探测(Quadratic Probing)

使用二次方程确定探测序列:

size_t quadratic_probe(size_t hash, size_t i, size_t table_size) {
   
    return (hash + i*i) % table_size;
}

优点:

  • 减少了聚集问题
  • 比线性探测性能更稳定

缺点:

  • 可能无法探测表中所有位置
3.2.3 双重哈希(Double Hashing)

使用第二个哈希函数确定步长:

size_t double_hash(size_t hash1, size_t hash2, size_t i, size_t table_size) {
   
    return (hash1 + i * hash2) % table_size;
}

优点:

  • 消除二次聚集
  • 提供更均匀的探测序列

缺点:

  • 计算开销略大

4. 哈希表的C++实现

以下是使用链式寻址实现的哈希表:

#include <iostream>
#include <vector>
#include <list>
#include <string>
#include <functional>

template<typename K, typename V>
class HashMap {
   
private:
    // 哈希表的桶,每个桶是一个链表
    std::vector<std::list<std::pair<K, V>>> buckets;
    size_t size_;
    size_t capacity_;
    double load_factor_threshold_;

    // 哈希函数
    size_t hash(const K& key) const {
   
        return std::hash<K>{
   }(key) % buckets.size();
    }

    // 重新哈希
    void rehash() {
   
        size_t new_capacity = nextPrime(capacity_ * 2);
        std::vector<std::list<std::pair<K, V>>> new_buckets(new_capacity);
        
        for (const auto& bucket : buckets) {
   
            for (const auto& pair : bucket) {
   
                size_t bucket_idx = std::hash<K>{
   }(pair.first) % new_capacity;
                new_buckets[bucket_idx].push_back(pair);
            }
        }
        
        buckets = std::move(new_buckets);
        capacity_ = new_capacity;
    }

    // 查找大于n的

相关文章:

  • C语言-动态内存管理
  • 【区块链 + 商贸零售】基于区块链的消费积分系统 | FISCO BCOS 应用案例
  • Nginx基于SSL的TCP代理
  • Vue3.如何把一个对象内的键值变成响应式变量
  • Bigemap Pro 拾取CAD地形图教程
  • uniapp工程中解析markdown文件
  • 在 JIT 编译的函数中调用非 JAX 函数
  • OpenAI Agents SDK 使用自定义的 OpenAI-Compatible API
  • 将对象内的键值转换为响应式变量后,在setup函数中用这些属性的时候为什么不用像ref那样加value
  • 冯・诺依曼架构深度解析
  • WPF-实现按钮的动态变化
  • OMRON Corporation Programming Contest 2025 (AtCoder Beginner Contest 397)题解
  • 对接豆包大模型
  • SvelteKit 最新中文文档教程(6)—— 状态管理
  • 【微服务】基于Lambda ESM的预留模式调整Kafka ESM吞吐量的实战
  • 【海螺AI视频】蓝耘智算 | AI视频新浪潮:蓝耘MaaS与海螺AI视频创作体验
  • leetcode33.搜索旋转排序数组-medium
  • 【八股文】volatile关键字的底层原理是什么
  • 实现搜索功能:第一部分
  • 穿越是时空之门(java)
  • 手机表面细菌菌落总数可能比马桶高10倍,医生详解如何洗手
  • 蚊媒传染病、手足口病……上海疾控发布近期防病提示
  • 深圳拟出让3宗居住用地,共计用地面积6.77公顷
  • 季子文化与江南文化的根脉探寻与融合
  • 《淮水竹亭》:一手好牌,为何打成这样
  • 欧阳娜娜担任江西吉安文化旅游大使