C++/数据结构:哈希表知识点

news2025/4/2 18:50:33

目录

哈希表

理解哈希表

哈希值(整形)

BKDR哈希 

 异或组合

 hash_combine

哈希函数

直接定址法

除留余数法

平方取中法

基数转换法

哈希冲突

开放定址法

哈希桶

unordered_map和unorder_set如何共用一个哈希桶模板类

stl的哈希桶中Insert如何得到的键值

键为自定义类型的处理


        前言:本篇文章前半部分内容为哈希表的原理, 从上到下按照理解链逐层递进。 最后三个小标题占了比较大的篇幅, 是结合c++代码来叙述, 主要内容为stl中的哈希桶如何封装的。 如果有错误的地方, 欢迎友友们指正哦。

        ps:本篇文章一直到哈希桶,除了最后三个小标题,c++和java的同学都可以看, 讲的是数据结构, 即便有c++代码也很简单哦。

哈希表

        首先要理解哈希和哈希表有什么不同。 哈希就是映射, 是一种算法思想。 哈希表就是映射表, 是利用映射这种思想写出的一种数据结构。 

        所有的哈希表的算法流程都是类似的——拿到一个key, 利用哈希函数进行hasher(key), 得到的空间位置存放我们想要存放的数据Value。

        或者——拿到一个key, 利用哈希函数进行hasher(key), 从得到的空间位置取出我们想要查找的数据Value。

理解哈希表

        博主认为理解哈希表, 我们要分为三层去学习。 第一层是理解哈希值, 第二层是学习各种哈希函数, 第三层是解决哈希冲突。

        为什么要这么学习,因为stl哈希表中的底层哈希函数, 其实都是对哈希值去进行Hash。这个哈希值是一个整形, 整形它本身就是哈希值;其他的自定义类型不管你Key的类型是string, 还是vector。这些自定义类型, 想要存储到哈希表中, 上层最终都要让它们能够转为哈希值。

         所以我们要先得到哈希值。 然后再去使用哈希函数进行hash得到对应的映射位置。 

         另外, 其实最基本的哈希表, 博主认为逻辑上就是可以看作是一个数组。 既然是数组, 那么他就一定有大小。 有大小, 那么就一定存在hash的数据太多, 数组空间不够的情况。这时再hash, 就有了哈希冲突。 更不用提两个不同的自定义类型的数量远远大于哈希值的数量, 自定义类型可能哈希值相同,就更会存在哈希冲突。 所以, 哈希是哈希表的功能。 哈希冲突, 是这个功能产生的一种可能存在的结果。 所以两者存在因果的关系。

        所以我们的理解链应该是: 哈希值——》哈希函数——》哈希冲突

哈希值(整形)

        为什么要有哈希值? 是因为哈希函数都是对一个整形进行哈希, 比如直接定址、除留余数、平方取中,基数转换等等。 最重要的是stl里面使用的也是除留余数(只不过不是传统的除留余数, 大佬们有其他优化)。

        我们在用stl的时候, 如果想要对一个自定义类型进行哈希, 那么就必须提供这个自定义类型向哈希值的转换方法。 本篇文章中我们以后称为“转换策略”。

        有了这个转换策略, 就可以将自定义类型转化为一个哈希值。然后再将这个哈希值交给stl底层的哈希函数进行哈希。 得到的结果经过哈希冲突的处理得到映射位置, 这个映射位置就是最后这一次哈希要存储的位置了。

        转化哈希值一般要定义为一个仿函数, 然后作为unordered_map的第三个模板参数传进去。 这样救能让一个任意类型能够去进行哈希了。 

       要注意转化哈希值要注意速度快, 离散高。 常见的转化策略比如string类型向整形转化的BKDR哈希,DJB哈希、多成员复杂结构,结构中含有整形和string的hash_combine、以及对含少量成员的异或组合。里面有嵌套容器的递归哈希等等。

        这里博主只挑选博主熟悉的演示:

BKDR哈希 

        优点:实现简单,计算快;离散高,冲突少;不同种子可以计算出不同哈希值。

        实现方法是选取一个种子seed, 然后对字符串里面的每一个字符进行处理:

size_t BKDRHash(const string &str) {
    size_t seed = 131; // 31 131 1313 13131 131313 
    size_t hash = 1;
    
    for (auto e : str)
    {
        hash *= seed;
        hash += e;
    }
    
    return hash & 0x7FFFFFFF; // 确保返回正数
}

        这个种子的值可以是31, 131, 1313, 13131, 131313...。

 异或组合

         优点:实现简单,计算快。 缺点:冲突率高

struct PointHash
{
    size_t operator()(const vector<int> &vec)
    {
        int hash = 0;
        for (auto e : vec)
        {
            hash ^= (e << 1);
        }
        return hash;
    }
};

 hash_combine

          优点是冲突率低, 比较推荐。

template <typename T>
void hash_combine(std::size_t& seed, const T& val) 
{
    seed ^= std::hash<T>()(val) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}

struct Person 
{
    std::string name;
    int age;
    //重载==符号是为了在哈希桶的桶内Find
    bool operator==(const Person& p) const { return name == p.name && age == p.age; }
};

struct PersonHash 
{
    std::size_t operator()(const Person& p) const 
    {
        std::size_t seed = 0;
        hash_combine(seed, p.name);
        hash_combine(seed, p.age);
        return seed;
    }
};

哈希函数

直接定址法

         哈希表进行哈希有可能产生哈希冲突。 但是我们说哈希其实就是映射, 在数学中也有映射, 一元一次方程, 一元二次方程其实都是映射。 有没有那么一种映射关系, 不会存在两个哈希值同时映射到一个空间上呢? 其实是有的, 比如直接定址法。

        直接定址法就是数学中一个具有单调性的函数, 每一个哈希值都对应唯一的一个映射位置(x映射唯一的y)。比如下图:

         这样做的方法有利有坏, 好处是没有哈希冲突, 因为每一个哈希都有唯一的数组下标与其对应;坏处是如果这一组整形很分散,假如有1, 99999, 那么消耗的空间很大。所以对于直接定址法只适用于很集中的一组数。

除留余数法

        对哈希表的大小的进行取模。 得到一个结果, 这个就叫做除留余数法。 一个哈希表的大小为m, 就要取一个不大于m的,接近m的质数p作为模数。  

        假如一个哈希表大小为10, 那么p可以取7。想要对键值为12, 13, 1的键值对进行映射, 那么就是用他们模7, 然后就能得到映射的区域。

平方取中法

        平方取中法是通过取关键字的哈希值的平方数中间几位作为哈希地址。

        例子1: 假如有一个哈希表1000(0 ~ 999), 一个键值对的key为1234。 运用平方取中法就是: 1234 ^ 2 = 1522756。 因为哈希表的大小为1000, 那么就要取平方数的中间三位。 取得227, 这个227就是最后的哈希地址。

        例子2: 假如有一个哈希表的大小为50(0 ~ 49), 一个键值对的key为68。运用平方取中法就是: 68 * 68 = 4624。 因为哈希表的大小为50, 那么就要去平方数的中间两位。 取得62。 但是62比50大, 所以我们可以让他们取模得到12。 最后12就是哈希地址。

        平方取中法的优点

        对于一些连续的键值能够将它们打散。 比如123, 124, 125等。

        不像除留余数法那样需要取值, 防止取值不合适加剧冲突。

基数转换法

        基数转换法就是将一个十进制数转化为其他进制的数字(比如十二进制, 十三进制, 十六进制), 然后把转化后的数字看成是十进制, 最后再把这个转化后的数字对哈希表的大小进行取模。但是如果转化后的数字里面有字母, 就把这个字母看成他的ASCII码值。 比如255, 转化成十六进制为FF,看成十进制就是7070。 所以255进制基数转换后就是7070。如果哈希表大小为10, 那么取模后就是7070 % 10 = 0。

哈希冲突

        上面的所有的哈希函数除了直接定址法, 都有一个问题。 就是有可能两个不同的键值映射到了同一个哈希地址上面。 这种情况就叫做哈希冲突。哈希冲突也是哈希表的核心问题之一。

        哈希表里面有负载因子的概念。 负载因子 = 已插入数据的个数 / 表大小。 负载因子越大, 越容易发生哈希冲突。因为负载因子大, 说明哈希表中的数据很多,再插入数据时就很容易映射到有数据的地址, 就发生了冲突。

        很明显, 如果插入数据映射到有数据的地址, 就要重新映射, 这样效率非常低。所以哈希冲突会影响哈希表的效率。 那么为了缓解冲突带来的影响, 哈希冲突有两种常见的解决方案:开放定址法、哈希桶。 

开放定址法

        解决方法: 开放定址法就是当哈希冲突时, 键值对会向后偏移。 比如a本来映射到1号位, 但是1号位被占用了, 那么a就按照规律向后偏移, 直到遇到一个没有被占用的位置。 

        (这里的规律有两种:一种叫做线性探测, 即一格一格的向后遍历, 1号位被占用, 去探测2号位。 2号位没有, 去探测3号位,依此类推。  第二种叫做二次探测, 即按照平方数进行探测。 假如探测位置为 i, 那么线性探测就是每次i++, 二次探测就是每次 (i++) ^ 2。)

        负载因子:负载因子通常需要小于0.7。 如果太大探测次数就很多, 效率就会很低。 甚至插入数据的时间复杂度可能逼近O(n)了。

        这里举个例子模拟开放定址法的处理方法:

       此时已经插入了三个数据1, 12, 3。 如果又要插入一个11, 那么11 % 10 等于1。 应该映射到 1 号位。 但是1号位已经被占用了。 假如是线性探测,那么就要响应后进行遍历, 找到2号位,发现不为空,继续向后遍历。 找到3号位, 发现不为空, 继续向后遍历。 找到4号位, 发现为空, 说明找到了, 那么就将11插入到这个位置就行了。 结果如下图:

        下面是开放定址法的demo:

   template <class K, class V>
    class HashData
    {
    public:
        /*状态值为什么要有DELETE? 因为要进行查找Key。 查找Key的时候
        , 遇到DELETE和EXIST不会停止,遇到EMPTY停止查找, 说明没有找到。
           因为查找操作, 应该是只要该位置有过数据, 就要向后查找, 直到遇到一个
           没有数据插入过的位置, 就结束。因为会出现Key与这里的数据发生冲突, 向后
           便宜了, 但是之后这里的数据又删除了。 这样如果没有DELETE, 查找到这里就停止了
             就错了。 所以要有DELETE。*/
        enum State
        {
            EMPTY,
            EXIST,
            DELETE
        };

    public:
        pair<K, V> _kv;
        State _state;
    };

    template <class K, class V>
    class HashTable
    {
    public:
        bool insert(const pair<K, V> &kv)
        {
            /*使用开放定址法:
            会不会找不到合适的位置, 不会。因为有负载因子。
            哈希表要判断负载因子, 负载因子越大,哈希冲突越多, 效率越低。
            负载因子越小, 哈希冲突越小, 效率越高, 但是会占用大量空间。*/

            if (_tables.size() == 0 || (_n * 10) / _tables.size() >= 7)
            {
                /*超过了负载因子, 那么就要扩容了。*/
                UpMemory();
            }
            if (Find(kv.first))
                return false;
            /*插入主逻辑, 就是向后去找, 直到遇到没有数据的地方。*/
            int hashi = kv.first % _tables.size();
            while (_tables[hashi]._state == HashData<K, V>::State::EXIST)
            {
                hashi++;
                hashi %= _tables.size();
            }

            _tables[hashi]._kv = kv;
            _tables[hashi]._state = HashData<K, V>::State::EXIST;
            _n++;
        }

        HashData<K, V> *Find(const K &key) /*查找, 如果找到了,就返回对应的地址, 如果没找到, 就返回nullptr*/
        {
            /*这里不用循环遍历找? 直接hashi, 直到hashi对应的值是key*/
            int hashi = key % _tables.size(); /*先映射,这个位置应该是原本应该在的位置, 但是可能被别人占用了, 这个
                                               时候就要向后遍历, 只要一发现, 这个位置不是空的,DELETE也算,
                                               因为DELETE说明这个位置之前有数据, 不知道这个数据在元数据之前是否
                                               插入的。  就要继续向后找。*/
            int tmp = hashi;
            while (_tables[hashi]._state != HashData<K, V>::EMPTY)
            {
                if (_tables[hashi]._state == HashData<K, V>::EXIST && _tables[hashi]._kv.first == key)
                {
                    return &_tables[hashi];
                }
                hashi++;
                hashi %= _tables.size();
                if (hashi == tmp)
                    return nullptr;
            }

            return nullptr;
        }

        bool Erase(const K &key) /*删除操作, 查找到对应的地址, 然后就将对应的地址位置变成Delete*/
        {
            HashData<K, V> *pdata = Find(key);
            if (pdata == nullptr)
                return false; /*找到了, 不是空, 那么久false*/
            pdata->_state = HashData<K, V>::DELETE;
            --_n;
            return true;
        }

        void Order()
        {
            cout << _n << endl;
            for (int i = 0; i < _tables.size(); i++)
            {
                if (_tables[i]._state == HashData<K, V>::EXIST)
                    cout << _tables[i]._kv.first << " : " << _tables[i]._kv.second << endl;
            }
        }

    private:
        void UpMemory()
        {
            /*扩容步骤:
                创建一段新空间
                重新映射
                删除旧空间
            */
            int newsize = (_tables.size() == 0) ? 5 : 2 * _tables.size();  /*如果空间为零就初始化空间为5, 否则就扩容2倍。*/

            //*创建一个新的哈希表, 让这个哈希表去扩容newsize个大小的空间并把是数据都重新插入一遍。*/
            HashTable<K, V> NewHT;   
            NewHT._tables.resize(newsize);
            for (int i = 0; i < _tables.size(); i++)
            {
                if (_tables[i]._state == HashData<K, V>::EXIST)
                {
                    NewHT.insert(_tables[i]._kv);
                }
            }
            /*新表处理完成后, 新表的_tables里面就是扩容后我们需要的哈希表。所以把新表的_tables和哈希表的_tables换一下位置。*/
            _tables.swap(NewHT._tables);
        }

    private:
        vector<HashData<K, V>> _tables;
        size_t _n = 0;

    public:
        static void Test1()
        {
            HashTable<int, int> hash;
            hash.insert({1, 1});
            hash.insert({12, 1});
            hash.insert({13, 1});
            hash.insert({16, 1});
            hash.insert({161, 1});
            hash.insert({162, 1});
            hash.insert({163, 1});
            hash.insert({164, 1});
            hash.insert({165, 1});
            hash.insert({16, 1});
            hash.insert({-16, 1});

            hash.Erase(16);
            hash.Erase(161);
            hash.Erase(162);
            hash.Erase(163);
            hash.insert({16, 1});
            hash.Erase(16);
            hash.insert({16, 1});

            hash.Order();
        }
    };

哈希桶

        如果说开放定址法的结构是一个数组。 那么哈希桶本质上其实结构已经不单单是一个数组了, 而是一个数组, 但是这个数组里面挂上了一串串链表, 如下图:

        解决方法: 哈希桶解决哈希冲突的方法是在数组本该存放数据的位置不放数据了, 改为数据节点的指针。 以后如果有数据映射到了这个位置, 就创建节点, 将数据链入到该位置的链表里面。 因为链表挂在数组上面就像一个桶, 所以就叫做哈希桶。 数组里面的每一个元素都叫做一个桶。 

        哈希桶效率比开放定址法要高。 在负载因子为1的情况下(stl下也为1), 只要不出现极端情况, 插入几百万条随机数据,最长的桶的长度也会保持在6, 7, 8左右。 也就是说, 每次我们hash查找数据, 是一个常数级别的时间复杂度。

        c++unordered_map和unordered_set的底层就是用的哈希桶。 接下来, 我们就要讲解一下, 在stl中,是怎么实现哈希桶的。

unordered_map和unorder_set如何共用一个哈希桶模板类

        首先, 它们的定义分别是:

/*哈希桶*/
template<class Key, class Value,
            class Alloc, class ExtractKey, 
            class Hash, class __Pred, .....>

/*unordered_set*/
template<class Key, class Hash, class Pred.......>


/*unordered_map*/
template<class Key, class T, class Hash, class Pred......>

         先来只看哈希桶里面的前两个模板参数。 这里的Key和Value并不是传统意义上的键值对。因为set没有键值对, 只有键值。 而map有键值对。 这就说明unordered_set不需要两个模板参数, unordered_map需要两个模板参数。 所以, 为了解决unordered_set和unordered_map复用模板类的问题, 大佬们把unordered_set里面那个哈希桶的第二个模板参数也传成了Key。 把unordered_map里面那个哈希桶的第二个模板参数直接传承了pair<Key, T>即:

template<class Key, class Hash, class Pred.....>
class unordered_set
{
    //省略...........
    
    typedef HashBucket<Key, Key, ....>


    //省略......
};



template<class Key, class T, class Hash, class Pred.....>
class unordered_map
{
    //省略...........
    
    typedef HashBucket<Key, pair<Key, T>, ....>


    //省略......
};

        这样有有什么用? 博主下面讲解:

        对于哈希桶的查找和删除,是一定, 并且只会用到Key,不管上层存储的是键值对还是单个键。 就是利用键Key进行查找和删除。

/*哈希桶*/
template<class Key, class Value,
            class Alloc, class ExtractKey, 
            class Hash, class __Pred, .....>
class HashBucket
{
    /*省略.........*/
    
    Find(const Key &key);

    Erase(const Key &key);
    
    /*省略.........*/
};


template<class Key, class Hash, class Pred.....>
class unordered_set
{
    //省略...........
    
    typedef HashBucket<Key, Key, ....> HT;
    HT _ht;   /*定义一个哈希桶对象*/
    

    Find(const Key &key) { return _ht.Find(key); }

    Erase(const Key &key) { return _ht.Erase(key); }

    //省略......
};



template<class Key, class T, class Hash, class Pred.....>
class unordered_map
{
    //省略...........
    
    typedef HashBucket<Key, pair<Key, T>, ....> HT;
    HT _ht;   /*定义一个哈希桶对象*/
    

    Find(const Key &key) { return _ht.Find(key); }

    Erase(const Key &key) { return _ht.Erase(key); }

    //省略......
};

        但是对于插入, unordered_set和unordered_map是不一样的。因为前者插入的是一个Key类型数据, 而后者插入的是一个pair<Key, T>类型的数据。 正好unordered_set里面的哈希桶第二个模板参数是Key, 又恰好unordered_map里面的哈希桶的第二个模板参数是pair<Key, T>。 所以, 在哈希桶内部的Insert的参数是Value类型的数据。 但是在外层, 其实unordered_set传的是Key, unordered_map传的是pair<Key, T>。如下:

/*哈希桶*/
template<class Key, class Value,
            class Alloc, class ExtractKey, 
            class Hash, class __Pred, .....>
class HashBucket
{
    /*省略.........*/
    
    Find(const Key &key);

    Erase(const Key &key);

    Insert(const Value &data);
    
    /*省略.........*/
};


template<class Key, class Hash, class Pred.....>
class unordered_set
{
    //省略...........
    
    typedef HashBucket<Key, Key, ....> HT;
    HT _ht;   /*定义一个哈希桶对象*/
    

    Find(const Key &key) { return _ht.Find(key); }

    Erase(const Key &key) { return _ht.Erase(key); }
    
    /*调用的哈希桶的Insert, 传的是Key类型数据, 此时哈希桶的Insert用的也是unordermap传给他的            
     pair<Key, T>*/
    Insert(const Key &data) { return _ht.Insert(data); }
    //省略......
};



template<class Key, class T, class Hash, class Pred.....>
class unordered_map
{
    //省略...........
    
    typedef HashBucket<Key, pair<Key, T>, ....> HT;
    HT _ht;   /*定义一个哈希桶对象*/
    

    Find(const Key &key) { return _ht.Find(key); }

    Erase(const Key &key) { return _ht.Erase(key); }

    /*调用的哈希桶的Insert, 传的是pair<Key, T>类型数据, 此时哈希桶的Insert用的也是unordermap传给他的pair<Key, T>*/
    Insert(const pair<K, T> &data) { return _ht.Insert(data); }

    //省略......
};

        上面我们讲解的是大佬们如何解决的unordered_set和unordered_map公用一个底层哈希桶的问题,是框架。 现在我们讲解了这个框架的结构。 然后就要去实现里面的细节。

stl的哈希桶中Insert如何得到的键值

        这里有两处细节, 首先第一处细节:Insert怎么得到键值?

        我们上面说哈希桶的删除和查找, 是一定, 并且是只会用到键Key的。 不管上层存储的是键值对还是单个键。  所以unordered_set和unordered_map的Find和Erase直接按照逻辑实现代码就行。 

        而哈希桶的插入unordered_set和unordered_map传入的类型不同。 那么得到键的方法就不一样了。 unordered_set是直接拿过来用就行。 unordered_map是要拿到里面的first。这个时候聪明的小伙伴已经想到办法了, 没错, 就是利用仿函数。对于哈希桶, 它要对外提供一个模板参数来来接受获取Value里面键值的方法。 这, 就是我们的哈希桶里面的第四个模板参数ExtractKey(第三个是一个空间配置器,与本节没有关系)。以后, unordered_set传送他的拿到键值的方法, unordered_map传送他的拿到键值对方法, 哈希桶只管接收, 然后Insert想要用键Key的时候, 就用传过来的方法对data进行处理, 就能拿到这个键Key。 

/*哈希桶*/
template<class Key, class Value,
            class Alloc, class ExtractKey, 
            class Hash, class __Pred, .....>
class HashBucket
{
    /*省略.........*/
    
    Find(const Key &key);

    Erase(const Key &key);

    Insert(const Value &data);
    
    /*省略.........*/
};


template<class Key, class Hash, class Pred.....>
class unordered_set
{
    //省略...........
///
    
    struct KeyOfValue
    {
        const K &operator()(const K &key)
        {
            return key;
        }
    };


///    
    typedef HashBucket<Key, Key, Alloc, KeyOfValue..> HT;
    HT _ht;   /*定义一个哈希桶对象*/
    

    Find(const Key &key) { return _ht.Find(key); }

    Erase(const Key &key) { return _ht.Erase(key); }
    
    /*调用的哈希桶的Insert, 传的是Key类型数据, 此时哈希桶的Insert用的也是unordermap传给他的            
     pair<Key, T>*/
    Insert(const Key &data) { return _ht.Insert(data); }
    //省略......
};



template<class Key, class T, class Hash, class Pred.....>
class unordered_map
{
    //省略...........

    struct KeyOfValue
    {
        const K& operator()(const pair<K, V> &_kv)
        {
            return _kv.first;
        }
    };
/
    
    typedef HashBucket<Key, pair<Key, T>, Alloc, KeyOfValue, ...> HT;
    HT _ht;   /*定义一个哈希桶对象*/
    

    Find(const Key &key) { return _ht.Find(key); }

    Erase(const Key &key) { return _ht.Erase(key); }

    /*调用的哈希桶的Insert, 传的是pair<Key, T>类型数据, 此时哈希桶的Insert用的也是unordermap传给他的pair<Key, T>*/
    Insert(const pair<K, T> &data) { return _ht.Insert(data); }

    //省略......
};

键为自定义类型的处理

        最后一个细节, unordered_set和unordered_map是怎么处理自定义类型的键的?

        其实本篇文章在一开始就在为这一点做铺垫了, 就是上面的哈希值。 对于任意的自定义类型, 都可以作为键, 但是前提要实现这个自定义类型向整形转化的仿函数和自定义类型的==重载或equal仿函数。 

        哈希桶的底层插入或者查找或者删除, 哈希函数用的是类似于除留余数法(大佬们肯定做了特殊处理), 如果是一个整形, 比如10, 桶大小为5。 那么10 % 5 == 0. 此时这个值就放到0号桶里面了。下面insert的伪代码

insert(const Value &data)
{
    KeyOfT key;
    //_tables是整个数组,里面存放了一串串的桶, _tables.size()是桶的个数
    int hashi = key(data) % _tables.size();   /*得到存储的桶*/
    
    //头插将新数据链入桶内
    Node* newnode = new Node(data);  
    newnode->_next = _tables[hashi];
    _tables[hashi] = newnode;
}

        但是一个自定义类型是不能进行取模的。 所以大佬们就想到了办法:哈希值。 就是先将自定义类型转化成一个哈希值。然后用哈希值去取模找桶。 所以哈希桶就有提供了第五个模板参数Hash。并且, 又因为自定义类型是上层使用unordered_map和unorder_set的人自定义的, 所以必须由上层的人提供这个转化策略。 继而unordered_map和unordered_set也都新增了一个模板参数Hash。 这个模板参数就是unorder_set或者unordered_map接收到上层传过来的转化策略。然后unordered_map或者unordered_set传给哈希桶,哈希桶就能用这个方法拿到键的哈希值, 然后就能找桶了。

/*哈希桶*/
template<class Key, class Value,
            class Alloc, class ExtractKey, 
            class Hash, class __Pred, .....>
class HashBucket
{
    /*省略.........*/
    
    Find(const Key &key);

    Erase(const Key &key);

    Insert(const Value &data);
    
    /*省略.........*/
};


template<class Key, class Hash, class Pred.....>
class unordered_set
{
    //省略...........
///
    
    struct KeyOfValue
    {
        const K &operator()(const K &key)
        {
            return key;
        }
    };


///    
    typedef HashBucket<Key, Key, Alloc, KeyOfValue, Hash, ...> HT;
    HT _ht;   /*定义一个哈希桶对象*/
    

    Find(const Key &key) { return _ht.Find(key); }

    Erase(const Key &key) { return _ht.Erase(key); }
    
    /*调用的哈希桶的Insert, 传的是Key类型数据, 此时哈希桶的Insert用的也是unordermap传给他的            
     pair<Key, T>*/
    Insert(const Key &data) { return _ht.Insert(data); }
    //省略......
};



template<class Key, class T, class Hash, class Pred.....>
class unordered_map
{
    //省略...........

    struct KeyOfValue
    {
        const K& operator()(const pair<K, V> &_kv)
        {
            return _kv.first;
        }
    };
/
    
    typedef HashBucket<Key, pair<Key, T>, Alloc, KeyOfValue, Hash, ...> HT;
    HT _ht;   /*定义一个哈希桶对象*/
    

    Find(const Key &key) { return _ht.Find(key); }

    Erase(const Key &key) { return _ht.Erase(key); }

    /*调用的哈希桶的Insert, 传的是pair<Key, T>类型数据, 此时哈希桶的Insert用的也是unordermap传给他的pair<Key, T>*/
    Insert(const pair<K, T> &data) { return _ht.Insert(data); }

    //省略......
};

        但是, 这里还存在一个问题, 就是查找的问题。 查找要判断一个数据是否存在, 就一定会用到比较。 这个比较不能用哈希值进行比较。 因为哈希值是整形, 是有限的, 而自定义类型的对象是无限的。 所以一定会存在两个不同的自定义类型对象有着相同的哈希值。 所以想要查找这个数据存在不存在, 还是要用原生类型的比较。 这里哈希桶和unordered_map, unorder_set都提供了一个模板参数Pred。 就是一个equal的仿函数。 这里其实也可以直接重载==符号。

/*哈希桶*/
template<class Key, class Value,
            class Alloc, class ExtractKey, 
            class Hash, class __Pred, .....>
class HashBucket
{
    /*省略.........*/
    
    Find(const Key &key);

    Erase(const Key &key);

    Insert(const Value &data);
    
    /*省略.........*/
};


template<class Key, class Hash, class Pred.....>
class unordered_set
{
    //省略...........
///
    
    struct KeyOfValue
    {
        const K &operator()(const K &key)
        {
            return key;
        }
    };


///    
    typedef HashBucket<Key, Key, Alloc, KeyOfValue, Hash, Pred, ...> HT;
    HT _ht;   /*定义一个哈希桶对象*/
    

    Find(const Key &key) { return _ht.Find(key); }

    Erase(const Key &key) { return _ht.Erase(key); }
    
    /*调用的哈希桶的Insert, 传的是Key类型数据, 此时哈希桶的Insert用的也是unordermap传给他的            
     pair<Key, T>*/
    Insert(const Key &data) { return _ht.Insert(data); }
    //省略......
};



template<class Key, class T, class Hash, class Pred....>
class unordered_map
{
    //省略...........

    struct KeyOfValue
    {
        const K& operator()(const pair<K, V> &_kv)
        {
            return _kv.first;
        }
    };
/
    
    typedef HashBucket<Key, pair<Key, T>, Alloc, KeyOfValue, Hash, Pred, ...> HT;
    HT _ht;   /*定义一个哈希桶对象*/
    

    Find(const Key &key) { return _ht.Find(key); }

    Erase(const Key &key) { return _ht.Erase(key); }

    /*调用的哈希桶的Insert, 传的是pair<Key, T>类型数据, 此时哈希桶的Insert用的也是unordermap传给他的pair<Key, T>*/
    Insert(const pair<K, T> &data) { return _ht.Insert(data); }

    //省略......
};

 以上, 就是关于哈希表理解的全部内容,我们下一篇文章再见喽..........

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2325847.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于SpringBoot的求职招聘网站系统(源码+数据库)

473基于SpringBoot的求职招聘网站系统&#xff0c;本系统共分为2个角色&#xff1a;系统管理员、用户&#xff0c;主要功能如下 【前台功能】 用户角色功能&#xff1a; 1. 注册和登录&#xff1a;注册账户并登录系统&#xff0c;以便访问更多功能。 2. 个人信息管理&#x…

Linux : System V 共享内存

目录 一 前言 二 共享内存概念 三 共享内存创建 四 查看共享内存 五 共享内存的删除 六 共享内存的关联 七 共享内存去关联 八 共享内存的使用&#xff08;通信&#xff09; 九 共享内存的特点 一 前言 共享内存区是最快的IPC形式&#xff08;进程间通信&#xff1…

端到端语音识别案例

《DeepSeek大模型高性能核心技术与多模态融合开发&#xff08;人工智能技术丛书&#xff09;》(王晓华)【摘要 书评 试读】- 京东图书 语音识别这一技术正如其名&#xff0c;是通过精密地解析说话人的语音来识别并准确转写出其所说的内容。它不仅仅是一个简单的转录过程&#…

【软件系统架构】微服务架构

一、引言 随着互联网技术的快速发展&#xff0c;传统的单体应用架构在面对复杂业务需求时逐渐暴露出诸多问题&#xff0c;如开发效率低、部署困难、扩展性差等。为了解决这些问题&#xff0c;微服务架构应运而生。本文将详细介绍微服务架构的定义、发展历史、特点、细分类型、优…

Linux内核设计——(一)进程管理

目录 一、进程及线程简介 二、进程描述符 2.1 进程描述符简介 2.2 分配进程描述符 2.3 进程标识值 2.4 进程状态 2.5 进程上下文 三、进程创建 3.1 写时拷贝 3.2 fork()和vfork() 四、线程 4.1 Linux线程实现 4.2 内核线程 五、进程终结 5.1 删除进程描述符 5.…

22 安装第三方包

一、什么是第三方包 在 Python 的世界里&#xff0c;包就像是一个个功能强大的工具箱&#xff0c;它将多个 Python 模块收纳其中&#xff0c;而每个模块又蕴含着丰富多样的具体功能。可以说&#xff0c;一个包就是一系列同类功能的集合体&#xff0c;它们就像紧密协作的团队&a…

oracle 常用函数的应用

在使用开发中会经常遇到数据类型转换、显示系统时间等情况&#xff0c;需要使用函数来实现。通过函数来实现业务需求会非常的省事便捷&#xff0c;函数可以用在适当的dml语句和查询语句中。 Oracle 数据库中主要使用两种类型的函数&#xff1a; (1)单行函数&#xff1a;对每一个…

“上云入端” 浪潮云剑指组织智能化落地“最后一公里”

进入2025年&#xff0c;行业智能体正在成为数实融合的核心路径。2025年初DeepSeek开源大模型的横空出世&#xff0c;通过算法优化与架构创新&#xff0c;显著降低算力需求与部署成本&#xff0c;推动大模型向端侧和边缘侧延伸。其开源策略打破技术垄断&#xff0c;结合边缘计算…

CentOS 7 如何挂载ntfs的移动硬盘

CentOS 7 如何挂载ntfs的移动硬盘 前言一、查看硬盘并尝试挂载(提示无法挂载)二、yum安装epel-release提示yum被锁定三、强行终止yum的进程四、yum安装epel-release完成五、yum安装ntfs-3g六、此时可正常挂载NTFS硬盘 前言 CentOS 7默认情况下是不支持NTFS的文件系统&#xff…

pytorch+maskRcnn框架训练自己的模型以及模型导出ONXX格式供C++部署推理

背景 maskrcnn用作实例分割时&#xff0c;可以较为精准的定位目标物体&#xff0c;相较于yolo只能定位物体的矩形框而言&#xff0c;优势更大。虽然yolo的计算速度更快。 直接开始从0到1使用maskrCNN训练自己的模型并并导出给C部署&#xff08;亲测可用&#xff09; 数据标注…

①EtherCAT/Ethernet/IP/Profinet/ModbusTCP协议互转工业串口网关

型号 协议转换通信网关 EtherCAT 转 Modbus TCP MS-GW15 概述 MS-GW15 是 EtherCAT 和 Modbus TCP 协议转换网关&#xff0c;为用户提供一种 PLC 扩展的集成解决方案&#xff0c;可以轻松容易将 Modbus TCP 网络接入 EtherCAT 网络 中&#xff0c;方便扩展&#xff0c;不受限…

《Oracle服务进程精准管控指南:23c/11g双版本内存优化实战》 ——附自动化脚本开发全攻略

正在学习或者是使用 Oracle 数据库的小伙伴&#xff0c;是不是对于那个一直启动且及其占用内存的后台进程感到烦躁呢&#xff1f;而且即使是手动去开关也显得即为麻烦&#xff0c;所以基于我之前所学习到的方法&#xff0c;我在此重新整理&#xff0c;让大家动动手指就能完成开…

Java单列集合[Collection]

目录 1.Collection单列集合 1.1单列集合各集合特点 1.2、Collection集合 1.2.1、Collection方法 1.2.2、Collection遍历方式 1.2.2.1、迭代器遍历集合 1.2.2.2、增强for遍历集合 1.2.2.3、forEach遍历集合&#xff08;JDK8之后&#xff09; 1.2.2.4、遍历案例 1.3、Li…

如何在ONLYOFFICE插件中添加自定义AI提供商:以通义千问和Kimi为例

随着 ONLYOFFICE AI 插件的发布&#xff0c;我们极大地提升了编辑器的默认功能。在ONLYOFFICE&#xff0c;我们致力于提供强大且灵活的解决方案&#xff0c;以满足您的特定需求。其中一项便是能够在 AI 插件中添加自定义提供商。在这篇文章中&#xff0c;我们将展示如何将通义千…

Spark,配置hadoop集群1

配置运行任务的历史服务器 1.配置mapred-site.xml 在hadoop的安装目录下&#xff0c;打开mapred-site.xml&#xff0c;并在该文件里面增加如下两条配置。 eg我的是在hadoop199上 <!-- 历史服务器端地址 --> <property><name>mapreduce.jobhistory.address…

FPGA实现4K MIPI视频解码H265压缩网络推流输出,基于IMX317+VCU架构,支持4K60帧,提供工程源码和技术支持

目录 1、前言工程概述免责声明 2、相关方案推荐我已有的所有工程源码总目录----方便你快速找到自己喜欢的项目我这里已有的 MIPI 编解码方案我这里已有的视频图像编解码方案 3、详细设计方案设计框图FPGA开发板IMX317摄像头MIPI D-PHYMIPI CSI-2 RX Subsystem图像预处理Sensor …

【Linux】网络概念

目录 网络模型 OSI七层模型 TCP/IP五层(或四层)模型 网络传输 网络传输基本流程 封装与分用 以太网通信&#xff08;局域网传输&#xff09; 跨网络传输 网络模型 OSI七层模型 TCP/IP五层(或四层)模型 网络层和传输层就是操作系统的一部分 网络传输 网络传输基本流程…

【模拟CMOS集成电路设计】电荷泵(Charge bump)设计与仿真(示例:栅极开关CP+轨到轨输入运放+基于运放CP)

【模拟CMOS集成电路设计】电荷泵&#xff08;Charge bump&#xff09;设计与仿真 0前言1电荷泵1.1 PFD/CP/电容器级联1.2 PFD/CP/电容传递函数 2基本电荷泵(CP)结构2.1“漏极开关”结构2.2“源极开关”结构2.3“栅极开关”结构 3 CP的设计与仿真13.1 P/N电流源失配仿真3.2 电荷…

Kafka消息丢失全解析!原因、预防与解决方案

作为一名高并发系统开发工程师&#xff0c;在使用消息中间件的过程中&#xff0c;无法避免遇到系统中消息丢失的问题&#xff0c;而Kafka作为主流的消息队列系统&#xff0c;消息丢失问题尤为常见。 在这篇文章中&#xff0c;将深入浅出地分析Kafka消息丢失的各种情况&#xf…

VS Code 云服务器远程开发完整指南

VS Code Ubuntu 云服务器远程开发完整指南 远程开发是现代开发者的标配之一&#xff0c;特别是在使用云服务器&#xff08;如 Ubuntu&#xff09;进行部署、测试或大项目开发时&#xff0c;利用 VS Code 的 Remote-SSH 插件&#xff0c;可以像本地一样顺滑操作远程服务器。本…