当前位置: 代码网 > it编程>编程语言>C/C++ > STL容器之哈希

STL容器之哈希

2024年08月02日 C/C++ 我要评论
哈希表/散列表的简单使用,与线性探测实现哈希表

哈希

​ 哈希本质上还是一个数组,只是数组的每一个位置要存储的值进行了映射;

​ 哈希也可以叫做散列;

​ 哈希比红黑树快是因为,不需要重复进行比较大小,直接用映射关系进行查找;

​ 哈希函数的设计应该注意要减少哈希冲突;

1.unordered系列关联式容器的使用

​ java设计的是hashmap与hashset,treemap与treeset。

​ stl的map与set的底层是红黑树,unorderedmap与unorderedset的底层设计的是哈希表/桶;

​ 与map/set的区别是,1.迭代器使用的都是单向迭代器;2.遍历出来不是有序的,只能去重;3.哈希表的性能比起红黑树更高一些;

​ 注意插入重复数据时哈希更快,非重复时红黑树更快,其他如查找,删除都是哈希更快。

​ 对于有序数据还是红黑树更快,包括插入,查找,删除;

1.1unordered_set

在这里插入图片描述

在这里插入图片描述

1.2unordered_map

在这里插入图片描述

在这里插入图片描述

2.不同查找方式的对比

​ 1.暴力查找时间按复杂度o(n);

​ 2.有序数组的二分查找o(lgn),存在排序o(n*logn)和增删改需要挪动数据o(n)的问题。

​ 3.平衡搜索树o(lgn),天然有序,增删查改也比较方便;

​ 4.哈希(散列)存储的值和存储的位置建立一个对应关系,根据对应关系进行快速查找;

3.哈希

​ 1.计数排序使用的就是一种哈希;

​ 2.图书馆找书,根据字母排序进行哈希查找相关书籍;

​ 如果数据很分散,那就退化成了暴力查找;

3.1常见的哈希函数

​ 1.直接定址法,使用直接映射查找,一对一,适合数据集中使用,问题:数据分散就会退化成暴力查找;

​ 2.除留余数法,开辟capacity个位置的空间,int i = k % capacity,然后将key放到第i个位置,问题:会产生哈希冲突/碰撞,即不同的值可能会映射到同一个位置,值和位置的关系是多对一的关系;

​ 如果是整型直接进行映射,如果是对象或者字符串就需要先与整数进行映射,然后在用整数和位置进行映射;

3.2解决哈希冲突

​ 1.闭散列的开放定址法,缺陷是冲突会互相影响,如连续空间的值会互相影响;

​ 思路是:位置被占了,就找下一个位置(本质上找的是别人的位置),会造成踩踏拥堵;

	线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止,空间不够会进行扩容;

问题是:1.会不断的抢占位置;2.哈希表不能太满,否则会导致下降,所以是不可能找不到空位置的;3.删除会导致查找出错,所以需要存在三个状态标记,1.exist,2.empty,3.delete ;

​ 二次探测,与线性探测唯一的区别就是每次移动走的是平方,1 4 9 …,好处是每次查找的时候不是走连续的空间,若一段连续空间内存在多个值,就会降低哈希冲突的概率;

​ 2.开散列的拉链法(哈希桶):

​ 在原有顺序表的位置使用类似链表的方式,在顺序表外将多个值链接在此位置之后;

4.线性探测实现哈希表

​ 1.使用枚举变量,来控制状态;2.对于哈希数据要存放kv和状态枚举变量;3.对于哈希表使用vector来开辟空间存放哈希数据,使用size记录有效数据;4.设计负载因子,负载因子为表中的元素个数除以长度;

​ 对于插入函数的设计,1.要注意负载因子越大,产生哈希冲突的概率越大,但是空间利用率更高,所以综合一下控制负载因子到一定值就扩容,不能满了才扩容或空间利用率过低;2.扩容先计算新的空间大小,然后重新开辟一个哈希表,并且使用resize使得大小和空间相同,遍历旧表插入新表,然后交换新旧表;3.使用除留余数法来获取顺序表的下标;4…不断线性探测空位置;5.找到了空位置,插入哈希数据,设置kv并修改状态变量;

​ 对于删除函数的设计,较为简单先find,然后置为delete并且size–。

​ 由于映射使用的是下标位置是自然数,所以要注意对于非无符号整型要先建立无符号整型映射,推荐使用仿函数;对于常见的类型可以使用模板的特化;如果只是使用字符转整型相加还是右很多场景得到的值相同,难以避免哈希冲突,所以需要使用哈希算法bkdr算法,即每次加之前乘131,使用131是因冲突数相较于其他算法较少;

代码实现:

#pragma once

#include <vector>

namespace hashtable
{
    enum status
    {
        exist = 0,
        empty,
        delete,
    };

    template <class k, class v>
    struct hashdata
    {
        std::pair<k, v> kv_;
        status status_ = empty;
    };

    template <class k>
    struct default
    {
        size_t operator()(const k &key)
        {
            return (size_t)key;
        }
    };
    template <>
    struct default<std::string>
    {
        size_t operator()(const std::string &key)
        {
            size_t i = 0;
            for (const auto &ch : key)
            {
                i *= 131;
                i += ch;
            }
            return i;
        }
    };
    // struct stringfunc
    // {
    //     size_t operator()(const std::string& key)
    //     {
    // 		return key[0];
    //     }
    // };

    template <class k, class v, class hashfunc = default<k>>
    class hashtable
    {
    public:
        hashtable()
        {
            table_.resize(10); // 使用resize保证size和capacity的值是一样的
        }
        bool insert(const std::pair<k, v> &kv) // 要注意不可以使得这个表太满,否则会降低查找效率,甚至表满了会不断死循环下去
        {
            if (find(kv.first))
            {
                return false;
            }
            hashfunc func;
            // 扩容逻辑,要注意的是扩容完成之后映射关系会发生改变,因为扩容后除数变大了
            if ((double)size_ / (double)table_.size() >= 0.7)
            {
                size_t newsize = table_.size() * 2;
                hashtable<k, v, hashfunc> hashtable;
                hashtable.table_.resize(newsize);
                // 遍历旧表重新插入到新表
                for (const auto &e : table_)
                {
                    if (e.status_ == exist)
                    {
                        hashtable.insert(e.kv_);
                    }
                }
                // 交换新旧表
                table_.swap(hashtable.table_);
            }

            // 线性探测
            size_t hashi = func(kv.first) % table_.size(); // 不能%capacity因为断言检查的是size,会有越界风险,最好是使得size与capacity相等
            // 如果这个位置存在值,就需要探测到空停止
            if (func(table_[hashi].kv_.first) != func(kv.first))
            {
                while (table_[hashi].status_ == exist)
                {
                    hashi++;
                    hashi %= table_.size();
                }
                // 找到了空位置进行插入
                table_[hashi].kv_ = kv;
                table_[hashi].status_ = exist;
                size_++;
                return true;
            }
            else
            {
                return false;
            }
        }
        hashdata<const k, v> *find(const k &key)
        {
            default<k> func;
            size_t hashi = func(key) % table_.size();
            while (table_[hashi].status_ != empty) // 由于delete状态可能是数据再后面位置,不能停止
            {
                if (table_[hashi].status_ != delete && table_[hashi].kv_.first == key)
                {
                    return (hashdata<const k, v> *)&table_[hashi]; // 会产生隐式类型转化,有的编译器不支持需要显式转换
                }
                hashi++;
                hashi %= table_.size();
            }
            return nullptr;
        }
        bool erase(const k &key)
        {
            hashdata<const k, v> *pos = find(key);
            if (pos)
            {
                pos->status_ = delete;
                size_--;
                return true;
            }
            return false;
        }

    private:
        std::vector<hashdata<k, v>> table_; // 物理空间上就是一个是顺序表,逻辑上是一个哈希
        size_t size_ = 0;                   // 存放有效数据的个数
    };
}

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com