【数据结构】哈希表的理论与实现

哈希表的理论

哈希表是一种基于哈希函数实现高效查找的数据结构,其核心思想是通过哈希函数将关键字映射到存储位置。

哈希表示例分析

12 18 21 24 33 45 67 72

21 45 24 18 12 33

哈希函数:除数留余法

数值 哈希计算 (mod 7) 结果位置 冲突情况
12 12 % 7 5 与33冲突
33 33 % 7 5 与12冲突
18 18 % 7 4 -
45 45 % 7 3 与24冲突
21 21 % 7 0 -
24 24 % 7 3 与45冲突

哈希冲突/哈希碰撞了。

解决办法:1、线性探测法 2、链地址法

哈希表的搜索操作: O(1)

18 % 7 = 4 arr[4] 18

45 % 7 = 3 arr[3]

45 % 7 = 3 arr[3] ≠ 45 产生哈希冲突了,线性探测法继续找 O(1) → O(n)

哈希冲突是不可避免的,怎么减少哈希冲突?(下面讲的是线性探测法)

1、哈希函数。 如,除留余数法 让哈希表(桶)的长度:素数

2、哈希表的装载因子。 loadfactor = 已占用的桶的个数/桶的总个数 > 阈值(0.75)------→ 哈希表就需要扩容了。相当于数组的扩容(对于线性探测哈希表来说),原来哈希表中的元素,需要在新的哈希表中重新哈希。------ O(n)

均摊时间复杂度 O(1)

线性探测哈希表

增加元素:

通过哈希函数计算数据存放的位置

该位置空闲,直接存储元素,完成

该位置被占用,从当前位置向后找空闲的位置,存放该元素

查询元素:

通过哈希函数计算数据存放的位置,从该位置取值(判断状态 STATE_USEING)

该值==要查询的元素值,找到了!

该值 ≠ 要查询的元素值(之前往这个位置放元素时,发生哈希冲突了),继续遍历往后找该元素

【补充】往后遍历到什么时候结束呢?

位置是空的有两种情况:1、这个位置是空的,没放过元素 (不需要继续往后搜索) 2、这个位置是空的,以前放过元素,后来被删除了(需要继续往后搜索)

会发现桶里面只放元素是不行的,还要放桶的状态

struct Node{

int val;

State state; //当前位置的状态

}

enumState{

STATE_USEING, //正在使用

STATE_UNUSE, //从来没用过

STATE_DEL //当前位置的元素被删除

}

删除元素:

通过哈希函数计算数据存放的位置,从该位置取值,判断状态STATE_USING

该值==要删除的值,直接修改当前位置的状态就可以 STATE_DEL

该值 ≠ 要删除的值,继续往后遍历,找到该元素,修改状态,如果遇到 STATE_UNUSE,结束

实现:

cpp 复制代码
#include <iostream>
using namespace std;

enum State{
    STATE_UNUSE,    //从未使用过的桶
    STATE_USING,    //正在使用的桶
    STATE_DEL,      //元素被删除了的桶
};

//桶的类型
struct Bucket{
    Bucket(int key = 0, State state = STATE_UNUSE)
    : key_(key)
    , state_(state)
    {}

    int key_;     //存储的数据
    State state_;     //桶的当前状态
};

//线性探测哈希表类型
class HashTable{
public:
    HashTable(int size = primes_[0], double loadFactor = 0.75)
        : useBucketNum_(0)
        , loadFactor_(loadFactor)
        , primeIdex_(0)
    {
        //把用户传入的size调整到最近的比较大的素数上
        if(size != primes_[0]){
            for(; primeIdex_ < PRIME_SIZE; primeIdex_++){
                if(primes_[primeIdex_] > size)
                    break;
            }

            //用户传入的size过大,已经超过最后一个素数,调整到最会一个素数
            if(primeIdex_ == PRIME_SIZE){
                primeIdex_--;
            }
        }

        tableSize_ = primes_[primeIdex_];
        table_ = new Bucket[tableSize_];
    }

    ~HashTable(){
        delete[]table_;
        table_ = nullptr;
    }

public:
    //插入元素
    bool insert(int key){
        //考虑扩容
        double factor = useBucketNum_*1.0 / tableSize_;
        cout << "factor:" << factor << endl;
        if(factor > loadFactor_){
            //哈希表开始扩容
            expand();
        }

        int idx = key % tableSize_;
        int i = idx;

        do{
            if(table_[i].state_ != STATE_USING){
                table_[i].state_ = STATE_USING;
                table_[i].key_ = key;

                useBucketNum_++;
                return true;
            }
            i = (i+1)%tableSize_;
        }while(i != idx);

        return false;
    }

    //删除元素
    bool erase(int key){
        int idx = key % tableSize_;
        int i = idx;
        do{
            if(table_[i].state_ == STATE_USING && table_[i].key_ == key){
                table_[i].state_ = STATE_DEL;
                useBucketNum_--;
            }
            i = (i+1) % tableSize_;
        }while(table_[i].state_ != STATE_UNUSE && i != idx);

        return true;
    }

    //查询
    bool find(int key){
        int idx = key % tableSize_;
        int i = idx;
        do{
            if(table_[i].state_ == STATE_USING && table_[i].key_ == key){
                return true;
            }
            i = (i+1) % tableSize_;
        }while(table_[i].state_ != STATE_UNUSE && i != idx);

        return false;       
    }
private:
    void expand(){
        ++primeIdex_;
        if(primeIdex_ ==  PRIME_SIZE){
            throw "HashTable is too large! can not expand anymore!";
        }

        Bucket* newTable = new Bucket[primes_[primeIdex_]];
        for(int i = 0; i < tableSize_; i++){
            if(table_[i].state_ == STATE_USING){   //旧表有效数据放到新表
                int idx = table_[i].key_ % primes_[primeIdex_];

                int k = idx;
                do{
                    if(newTable[k].state_ != STATE_USING){
                        newTable[k].state_ = STATE_USING;
                        newTable[k].key_ = table_[i].key_;
                        break;
                    }
                    k = (k+1) % primes_[primeIdex_];
                }while(k != idx);
            }
        }

        delete[]table_;
        table_ = newTable;
        tableSize_ = primes_[primeIdex_];
    }
private:
    Bucket* table_;      //指向动态开辟的哈希表
    int tableSize_;      //哈希表当前的长度
    int useBucketNum_;   //已经使用的桶的个数
    double loadFactor_;  //哈希表的装载因子

    static const int PRIME_SIZE = 10;   //素数表的大小
    static int primes_[PRIME_SIZE];    //素数表
    int primeIdex_;                   //当前使用的素数下标
};

int HashTable::primes_[PRIME_SIZE] = {3, 7, 23, 47, 97, 251, 443, 911, 1471, 42773};

int main(){
    HashTable htable;
    htable.insert(21);
    htable.insert(32);
    htable.insert(14);
    htable.insert(15);
    htable.insert(22);

    cout << htable.find(14) << endl;
    htable.erase(14);
    cout << htable.find(14) << endl;

    return 0;
}

链式哈希表

线性探测哈希表的缺陷:

1、发生哈希冲突时,靠近O(n)的时间复杂度,存储变慢

2、多线程环境中,线性探测所用到的基于数组实现的哈希表,只能给全局的表用互斥锁来保证哈希表的原子操作,保证线程安全!

链式哈希表可以用:分段的锁!既保证了线程安全,又有一定的并发量,提高了效率!

例如:12 18 21 24 33 45 67 72 哈希函数采用除留余数法,哈希表长度7

哈希表O(1) 无线趋近于O(1)---→哈希冲突的存在

每个桶的链表比较长,链表搜索花费的时间就大

优化一:当链表长度大于某个阈值时,把桶里面的这个链表转化成红黑树(搜索时间复杂度O(logn))

优化二:链式哈希表每个桶都可以创建自己的互斥锁,不同桶中的链表操作,可以互斥起来

实现:

cpp 复制代码
#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
using namespace std;

//链式哈希表
class HashTable{
public:
    HashTable(int size = primes_[0], double loadFactor = 0.75)
    : useBucketNum_(0)
    , loadFactor_(loadFactor)
    , primeIdex_(0)
    {
        if(size != primes_[0]){
            for(; primeIdex_ < PRIME_SIZE; primeIdex_++){
                if(primes_[primeIdex_] >= size){
                    break;
                }
            }

            if(primeIdex_ == PRIME_SIZE)
                primeIdex_--;            
        }
        table_.resize(primes_[primeIdex_]);
    }

public:
    //增加元素  不能重复插入key
    void insert(int key){
        //判断扩容
        double factor = useBucketNum_*1.0/table_.size();
        cout << "factor:" << factor << endl;
        if(factor > loadFactor_){
            expand();
        }

        int idx = key % table_.size();
        if(table_[idx].empty()){
            useBucketNum_++;
            table_[idx].emplace_front(key);
        }
        else{
            //使用全局的::find泛型算法,而不是调用自己的成员方法
            auto it = ::find(table_[idx].begin(), table_[idx].end(), key);
            if(it == table_[idx].end()){
                //key不存在
                table_[idx].emplace_front(key);
            }
        }
    }

    //删除元素
    void erase(int key){
        int idx = key % table_.size();
        auto it = ::find(table_[idx].begin(), table_[idx].end(), key);
        if(it != table_[idx].end()){
            table_[idx].erase(it);
            if(table_[idx].empty()){
                useBucketNum_--;
            }
        }
    }

    //搜索元素
    bool find(int key){
        int idx = key % table_.size();
        auto it = ::find(table_[idx].begin(), table_[idx].end(), key);
        
        return it != table_[idx].end();
    }

private:
    //扩容函数
    void expand(){
        if(primeIdex_ + 1 ==  PRIME_SIZE){
            throw "HashTable is too large! can not expand anymore!";
        }
        primeIdex_++;
        useBucketNum_ = 0;

        vector<list<int>> oldTable;
        //swap交换两个容器的成员变量(两个容器Allocator一样时),不涉及数据拷贝,效率很高 
        table_.swap(oldTable);   //table_ 与 oldTable交换后,table_变为空
        table_.resize(primes_[primeIdex_]);

        for(auto list : oldTable){
            for(auto key : list){
                int idx = key % table_.size();
                if(table_[idx].empty()){
                    useBucketNum_++;
                }
                table_[idx].emplace_front(key);
            }
        }
    }

private:
    vector<list<int>> table_;
    int useBucketNum_;      //记录使用的桶的个数
    double loadFactor_;     //记录哈希表的装载因子

    static const int PRIME_SIZE = 10;   //素数表的大小
    static int primes_[PRIME_SIZE];    //素数表
    int primeIdex_;                 //当前使用的素数下标
};

int HashTable::primes_[PRIME_SIZE] = {3, 7, 23, 47, 97, 251, 443, 911, 1471, 42773};

int main(){
    HashTable htable;
    htable.insert(21);
    htable.insert(32);
    htable.insert(14);
    htable.insert(15);
    htable.insert(22);
    htable.insert(23);

    cout << htable.find(15) << endl;
    htable.erase(15);
    cout << htable.find(15) << endl;

    return 0;
}

哈希表总结

哈希表的核心定义:存储位置=f(关键字) 一个关键字通过散列函数进行映射,得到其存储位置。这种技术称为散列技术。f称为哈希函数或者散列函数。采用散列技术将记录存储在一块连续的存储空间中,这块连续的存储空间称为哈希表。

优势:快速查找,时间复杂度O(1)

缺点:链式哈希表每一个节点既要存数据,又要存地址,内存空间占用了比较大。空间换时间。

散列函数:

设计特点: 计算简单(复杂度会降低查找的时间)、散列地址分布均匀(减少哈希冲突)

相关推荐
Aqua Cheng.4 小时前
代码随想录第七天|哈希表part02--454.四数相加II、383. 赎金信、15. 三数之和、18. 四数之和
java·数据结构·算法·散列表
zym大哥大4 小时前
哈希表封装myunordered_map以及set
数据结构·散列表
怀揣小梦想4 小时前
跟着Carl学算法--哈希表
数据结构·c++·笔记·算法·哈希算法·散列表
Nebula_g4 小时前
Java哈希表入门详解(Hash)
java·开发语言·学习·算法·哈希算法·初学者
Kent_J_Truman4 小时前
【模拟散列表】
数据结构·算法·蓝桥杯·散列表·常识类
努力努力再努力wz4 小时前
【C++进阶系列】:万字详解unordered_set和unordered_map,带你手搓一个哈希表!(附模拟实现unordered_set和unordered_map的源码)
java·linux·开发语言·数据结构·数据库·c++·散列表
加油=^_^=4 小时前
【C++】哈希表
数据结构·c++·散列表
对纯音乐情有独钟的阿甘4 小时前
【C++庖丁解牛】哈希表/散列表的设计原理 | 哈希函数
c++·哈希算法·散列表
励志不掉头发的内向程序员4 小时前
【STL库】哈希表的原理 | 哈希表模拟实现
开发语言·c++·学习·散列表