关于哈希表的学习笔记

前言

在C++98中,stl提供了底层为红黑树的几个关联式容器,查询时最差的情况就是需要比较红黑树的高度次,当节点变多的时候,查询的效率就不那么明显了,而在C++11中新增的unordered系列的关联式容器,它们的用法和map/set类似,但底层采用的是哈希结构。

关于unordered系列容器

1、unordered_map

属于<Key,Value>键值对的关联性容器,通过key来快速索引到其对应的Value。

这里的value可以和key的类型不同,它们两者之间只是映射关系。

unordered_map在内部没有对键值对进行按顺序排序,同时将相同的哈希值的键值对放置在同一个桶中。

unordered_map在通过key索引到value的速度相比map是较快的,但是遍历其元素子集的范围迭代效率就较慢了

迭代器是前向迭代器。

2、unordered_set

是以特定顺序存储唯一元素的容器,它允许根据单个元素的值快速检索它们。

在unordered_set中,元素的值同时是其键,用于唯一标识它。键是不可变的,因此,unordered_set中的元素在容器中一次就不能修改 ,但是可以插入和删除它们。

在内部,unordered_set中的元素不按任何特定顺序排序,而是根据其哈希值组织到存储桶中,以便直接按其值快速访问单个元素(平均平均时间复杂度恒定)。

unordered_set容器通过其键访问单个元素的速度比设置容器更快,尽管它们通常通过其元素的子集进行范围迭代的效率较低。

容器中的迭代器是前向迭代器。

3、关于哈希结构

一般的平衡树中查找,需要对目标和经过的关键码进行比较,而对于哈希结构而言,不经过比较,而是通过某种函数使元素储存的位置和关键码建立映射关系,从而在查询中能更快的找到元素。

常见的哈希函数有:

1、直接定址法,优点简单、均匀,缺点需要事先知道关键字的大小,适用于比较小而连续的情况。

2、除留余数法,假设地址数为p,取一个比p小的m,类似于hash(key)=key%m(m<=P)进行取余,将这个结果转换成哈希地址

3、平方取中法,适用于不知道关键字分布,而位数又不大的情况。

4、折叠法,适用于不需事先知道关键字分布,而关键字位数比较多的情况。

5、随机数法,适用于关键字长度不等的情况。

哈希冲突:不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突 或哈希碰撞。

产生哈希冲突的原因就是哈希函数设计的不合理,常见解决方法有闭散列和开散列。

1、闭散列

又称开放定址法,发生哈希冲突时,如果哈希表未被填满,会优先往下一个空的位置放置。

而找到下一个位置的方法有:

线性探测::从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

eg:给一个序列{1,4,3,7,44,9},假设p的10,去留余数的话,

则hash(1)=1%10=1,hash(4)=4%10=4,...但是在hash(44)=44%10=4;

这里就发生了哈希冲突,原本索引为4的位置有了,那么就会从5开始向后查询,直到有一个空位置就插入进去。当哈希表的载荷因子在0.7左右的时候就需要控制扩容了,载荷因子=填入表中的元素个数/散列表的长度。

缺点是不好删除,采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素 会影响其他元素的搜索。因此线性探测采用标记的伪删除法来删除一个元素。

//赋予空间状态
enum State
{
    EMPTY, 
    EXIST, 
    DELETE
}; 
template<class K, class V>
struct Data
{   
    pair<K, V> _kv;
    State _state=EMPTY;
};
template<class K, class V>
class HashTable
{
private:
    vector<Data> _tables;
    size_t _n;//储存的个数
public:
    bool Insert(const pair<K,V>& kv)
    {
        if(Find(kv.first)
        {
            return false;
        }
        if(_table.size()==0||_n*10/_tables.size()>=7)//这样就不用强转成double了
        {
            size_t newsize=_table.size()==0?10:_tables.size()*2;
            HashTable<K,V> newtable;
            for(auto& data:_tables)
            {
                newtable.Insert(data._kv);
            }
            _tables.swap(newtable._tables);
        }

        size_t hash=kv.first%_tables.size();
        size_t i=1;
        size_t index=hash;
        while(_tables[index]._state==EXIST)
        {
            index=i+hash;
            index%=_tables.size();
            i++;
        }
        _table[hash]._kv=kv;
        _table[hash]._state=EXIST;
        _n++;
        return true;
    }
    HashTable<K,V>* Find(const K& key)
    {
        if(_table.size()==0)
        {
            return false;
        }
        size_t hash=kv.first%_table.size();
        size_t i=1;
        size_t index=hash;
        while(_table[index]._state!=EMPTY)
        {
            if(_table[index]._kv.first==key
                &&_table[index]._state==EXIST)
            {
                return &_table[index];    
            }
            index=hash+i;
            index%=_tables.size();
            ++i;
            if(hash==index)
            {
                break;//排除全是delete的状态。
            }
        }
        return nullptr;
    }
    bool Erase(const K& key)
    {
        HashTable<K,V> *ret=Find(key);
        if(key)
        {
            ret->_state=DELETE;
            --_n;
            return true;
        }
        else
        {
            reutrn false;
        }        
    }
}

线性探测优点:实现非常简单, 线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易导致不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。 

2、开散列

又称开链法,首先对关键码集合用散列函数计算散列地址,具有相同地 址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链 接起来,各链表的头结点存储在哈希表中,而这里的扩容则是在某个桶的链表长度等于这个哈希表的长度后,就需要扩容了。

template<class K, class V>
struct HashNode
{
    HashNode<K,V>* _next;
    pair<K,V> _kv;
    HashNode(const pair<K,V>& kv)
        :_next(nullptr),
         _kv(kv)
    {};
}
template<class K,class V>
class Hashtale
{
    typedef HashNode<K,V> Node;
private:
    vector<Node*> _tables;
    size_t _n=0;//储存的有效个数 
public:   
    ~HashTable()
    {
        for(auto cur: _tables)
        {
            while(cur)
            {
                Node* next=cur->next;
                delete cur;
                cur=next;
            }
            cur=nullptr;
        }
    }
    Node* Find(const K& key)
    {
        if(_tables.size()==0)
        {
            return nullptr;
        }
        size_t hash=key%_tables.size();
        Node* cur= _tables[hash];
        while(cur)
        {
            if(cur->_kv.first==key)
            {
                return cur;
            }
            cur=cur->next;
        }
        return nullptr;
    }
    bool Insert(const pair<K,V>& kv)
    {
        if(Find(kv.first))
        {
            return false;
        }
        if(_n==_tables.size())
        {
            size_t newsize=_tables.size()==0?10:_tables.size*2;
            vector<Node*> newtables(newsize,nullptr);
            for(auto& cur :_tables)
            {
                while(cur)
                {
                    Node* next=cur->next;
                    size_t hash=cur->_kv.first%newtables.size();
                    cur->next=newtable[hash];
                    newtables[hash]=cur;
                }
            }
            _tables.swap(newtables);
        }
        size_t hash =kv.first%_tables.size();
        //头插
        Node* newnode=new Node(kv);
        newnode->_next=_tables[hash];
        _tables[hash]=newnode;
        ++_n;
        return true;
    }
    bool Erase(const K& key)
    {    
        size_t hash=key%_tables.size();
        Node* prev=nullptr;
        Node* cur=_tables[hash];
        while(cur)
        {
            if(cur->kv.first==key)
            {
                if(prev==nullptr)
                {
                    _tables[hash]=cur->next;
                }
                else
                {
                    prev->next=cur->next;
                }
                delete cur;
                return true;
            }
            else
            {
                prev=cur;
                cur=cur->next;
            }
        }
        return false;
    }
     
}

 #先记录在闭散列和开散列的部分,后面如果有需要补充的笔记继续下面进行记录。


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值