C++哈希表之闭散列方法的模拟实现详解

 更新时间:2022年11月10日 11:46:22   作者:Gaze!  
闭散列指(开放定址法)发生冲突时,如果哈希表没有被填满,则表内一定还有其他空闲位置,可以把冲突值放到下一个没有被占用的空余位置上。本文将模拟实现闭散列方法,需要的可以参考一下

哈希

概念

可以不经过任何比较,直接从表中得到要搜索的元素。 关键在于通过某种函数,使元素的存储位置与它的关键码之间能够建立 一一映射的关系。这样就可以通过o(1)的时间复杂度来寻找到元素。

例如数据集合{1,7,4,5,9,6},哈希函数hash(key)=key&capacity 

冲突

hash(7)=7 hash(17)=7,两个不同的数通过哈希函数映射到了一个位置,产生了冲突。哈希函数设计的越精妙,产生冲突的可能性就越低,但无法避免。

解决方法:

  • 闭散列(开放定址法)
  • 开散列(拉链法)

闭散列

闭散列,(开放定址法)发生冲突时,如果哈希表没有被填满,则表内一定还有其他空闲位置,可以把冲突值放到下一个没有被占用的空余位置上。

如何找到下一个没有被占用的空位?答:采用线性探测方法。从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

线性探测

线性探测的插入

如:在上述的哈希表中插入元素44,由于下标为4的位置放入了元素4,于是从该位置往后++,找到第一个不为空的位置,将44放入。

线性探测的删除

在寻找要删除的元素时,依然会根据存放在哈希表的下标开始寻找,比如在上述哈希表中寻找4,在4下标位置直接就可以找到该元素。但如果直接将其删除,那后续寻找元素44时,就会因为4下标没有元素,而认为元素44不存在于这张哈希表。所以我们需要设置一个状态来表示删除。

哈希表闭散列的模拟实现

我们写在一个自定义类域 Closehash 里面

准备工作

哈希表中元素状态

namespace Closehash
{
    //哈希表中元素的状态
    enum State
    {
        EMPTY,
        EXIT,
        DELETE
    };    
}

存储类型用pair即可,但是数据中要包含状态,我们进行一次封装

//由于数据需要一个状态,所以需要将pair<K,V>封装一层
    template<class K,class V>
    struct HashDate
    {
        pair<K, V>_kv;
        State _state;
    };

开始(画饼)构建哈希表的内容

template<class K,class V>
    class HashTable
    {
    public:
        bool Insert(const pair<K,V>& kv);
        HashDate<K, V>* find(const K& key);
        bool Erase(const K& key);
    private:
        vector<HashDate<K,V>> _tables;
        size_t _size = 0;
    };

闭散列的插入

        bool Insert(const pair<K, V>& kv)
        {
            //if (Find(kv.first)) return false; 
            //Find实现了再去掉注释
 
            if (_tables.size() == 0 
                || 10 * _size / _tables.size() >= 7)//相当于存了70%
            {
                //开始扩容
                size_t newsize = _tables.size()== 0 ? 10 : _tables.size() * 2;
                HashTable<K, V> newHash;
                newHash._tables.resize(newsize);
                for (auto e: _tables)//注意_tables是HashDate类型 里面有_kv 和_state
                {
                    if (e._state == EXIST)
                    {
                        newHash.Insert(e._kv);
                    }
                }
                //资本家拷贝方法
                _tables.swap(newHash._tables);
            }
            //走到这里扩容完成 或者空间足够大
            size_t hashi = kv.first % _tables.size();//寻找在表中对应的下标是什么
            while (_tables[hashi]._state==EXIST)
            {
                hashi++;
                //走到头了得回来
                hashi%=_tables.size();
            }
            _tables[hashi]._kv = kv;
            _tables[hashi]._state = EXIST;
            _size++;
            return true;
        }

测试用例

void TestHT1()
    {
        int a[] = { 1, 11, 4, 15, 26, 7, 44 };
        HashTable<int, int> ht;
        for (auto e : a)
        {
            ht.Insert(make_pair(e, e));
        }
 
        ht.Print();
    }

添加个99以验证扩容功能

闭散列的查找

HashDate<K, V>* Find(const K& key)
        {
            if (_tables.size() == 0) return nullptr;
            size_t hashi = key % _tables.size();
            while (_tables[hashi]._state != EMPTY)
            {
                if (_tables[hashi]._state != DELETE 
                    && _tables[hashi]._kv.first == key)
                {
                    return &_tables[hashi];
                }
                hashi++;
                hashi% _tables.size();
            }
            return nullptr;
        }

测试用例

    cout << ht.Find(4)->_kv.first << endl; 

闭散列的删除

bool Erase(const K& key)
        {
            HashDate<K,V>* ret = Find(key);
            if (ret)
            {
                ret->_state = DELETE;
                --_size;
                return true;
            }
            else
            {
                return false;
            }
        }

模拟实现的闭散列中的问题与改进

上述测试用例中使用的是pair<int,int>那我要是用pair<string,int>呢?我的key还可以直接对数组长度取模吗?

文档中对这一问题采用了仿函数的解决方法,我们这里也按照该方法模拟一个。

    template<class K>
    struct HashFunc
    {
        size_t operator()(const K& key)
        {
            return (size_t)key;
        }
    };
 
    // 特化  
    template<>
    struct HashFunc<string>
    {
        // BKDR
        size_t operator()(const string& key)
        {
            size_t val = 0;
            for (auto ch : key)
            {
                val *= 131;
                val += ch;
            }
 
            return val;
        }
    };
 
    template<class K,class V,class Hash=HashFunc<K>>
    class HashTable
    {
    public:
        bool Insert(const pair<K,V>& kv);
        HashDate<K, V>* find(const K& key);
        bool Erase(const K& key);
    private:
        vector<HashDate<K,V>> _tables;
        size_t _size = 0;
    };

在每次求 在哈希表中位置的前面添加

Hash hash;
size_t hashi = hash(kv.first) % _tables.size()

测试用例

void TestHT2()
    {
        string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
 
        //HashTable<string, int, HashFuncString> countHT;
        HashTable<string, int> countHT;
        for (auto& str : arr)
        {
            auto ptr = countHT.Find(str);
            if (ptr)
            {
                ptr->_kv.second++;
            }
            else
            {
                countHT.Insert(make_pair(str, 1));
            }
        }
    }

测试用例没加打印...让我来回看了好几遍代码...蠢到无语

到此这篇关于C++哈希表之闭散列方法的模拟实现详解的文章就介绍到这了,更多相关C++哈希表实现闭散列内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • c语言switch反汇编的实现

    c语言switch反汇编的实现

    本文主要介绍了c语言switch反汇编,在分支较多的时候,switch的效率比if高,在反汇编中我们即可看到效率高的原因,感兴趣的可以了解一下
    2021-06-06
  • Opencv 视频转为图像序列的实现

    Opencv 视频转为图像序列的实现

    今天小编就为大家分享一篇Opencv 视频转为图像序列的实现,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-12-12
  • C语言冒泡排序超全面实现流程

    C语言冒泡排序超全面实现流程

    算法中排序是十分重要的,而每一个学习计算机的都会在初期的时候接触到这种排序,下面这篇文章主要给大家介绍了关于c语言冒泡排序的相关资料,需要的朋友可以参考下
    2023-01-01
  • C语言进阶可变参数列表

    C语言进阶可变参数列表

    这篇文章主要为大家介绍了C语言进阶可变参数列表的示例详解有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步
    2022-02-02
  • C++中使用FFmpeg适配自定义编码器的实现方法

    C++中使用FFmpeg适配自定义编码器的实现方法

    本文介绍了在C++中使用FFmpeg库进行自定义编码器适配的实现方法。文章通过具体的代码示例,介绍了FFmpeg的基本使用方法和自定义编码器的实现过程,帮助读者了解如何在C++中进行音视频编码和解码的开发工作,并能够实现自定义的编码器适配
    2023-04-04
  • C语言 超详细模拟实现单链表的基本操作建议收藏

    C语言 超详细模拟实现单链表的基本操作建议收藏

    单链表是后面要学的双链表以及循环链表的基础,要想继续深入了解数据结构以及C语言,我们就要奠定好这块基石!接下来就和我一起学习吧
    2022-03-03
  • 探讨register关键字在c语言和c++中的差异

    探讨register关键字在c语言和c++中的差异

    建议不要用register关键字定义全局变量,因为全局变量的生命周期是从执行程序开始,一直到程序结束才会终止,而register变量可能会存放在cpu的寄存器中,如果在程序的整个生命周期内都占用着寄存器的话,这是个相当不好的举措
    2013-10-10
  • 使用OpenGL实现3D立体显示的程序代码

    使用OpenGL实现3D立体显示的程序代码

    本篇文章是对使用OpenGL实现3D立体显示的方法进行了详细的分析介绍,需要的朋友参考下
    2013-05-05
  • C语言模拟实现memmove的示例代码

    C语言模拟实现memmove的示例代码

    memmove函数用于拷贝字节,如果目标区域和源区域有重叠的话,memmove能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中,但复制后源内容会被更改。本文主要介绍了C语言模拟实现memmove的示例代码,需要的可以参考一下
    2022-12-12
  • C语言用递归函数实现汉诺塔

    C语言用递归函数实现汉诺塔

    大家好,本篇文章主要讲的是C语言用递归函数实现汉诺塔,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下
    2022-01-01

最新评论