[C++] Hash_map 实现原理和源码[散列表][散列映射][泛式数据支持][STL]

Hash_map
作为非线性数据结构.
在阅读前,你需要确定你已经真正的理解了什么是指针.
并且在不看任何源码的情况下,通过原理实现出链表数据结构.
如果不太确定的话,建议以下文章:
[C/C++]指针的原理和对指针的运用及理解(包括函数指针和多级指针)
[C++]什么是引用,以及引用和指针的关系(对内存和指针运用的理解)
[C++] LinkList:双向不循环链表的原理和实现(基础)

或者不想看长篇大论,可以直接滑动到最下面,copy源码.
源码内有注释和例子,复制粘贴就能用.如果可以,请点个免费的赞,支持一下.
纯爱发电,也得需要爱.


我不会跟大家说太多的废话,比如时间复杂度之类的玩意,大家过来找Hash_map的文章,必然是知道它大概是个什么东西.
下面正式开讲.
Hash_map 或者说,STL标准容器中的 unordered_map.
它们说白了,就是一个链表指针数组.

而它之所以可以快速查找数据,通过较少次数的循环,就能在上百,上千,上万次的数据中,找到你想要的.
就因为,它并不真的是挨个遍历.
而是在指定的链表内查找.

如果没有这个概念.
我们先在脑海中抽象一下.

LinkList<int> *list[3];
LinkList [0]LinkList [1]LinkList [2]
data1data4data6
data2data5
data3

往里面new了6个数据.

如果用数组或者链表的方式,去找data6,那么就要从头遍历.
你最少要循环6次可以找到.
那可能会说,我可以选择从尾遍历,这样第一次就能找到.
那么问题来了,如果要找data1呢?
循环次数还是这么多.
因为你不可能未卜先知的知道自己的数据,在这个数据结构中的那一部分.
它可能位于前部,中部,尾部.
最糟糕的就是,你找的数据,它并不存在,而计算机又不知道你找的数据存不存在,它会整个遍历一圈.
效率特别的低.

而用Hash_map来遍历数据data6,它会直接去LinkList[2]里面找数据.
如果LinkList[2]没有,那就证明整个数据结构都不存在这项数据.

这其中的关键,就是数组的下标.
它知道这个数据,如果真的存在,它会在第几个数组元素.

它是如何实现的这个功能的.
那么就要引入一个独特的东西.
hash_value,哈希值.
哈希值是一个无符号正整数.
Hash_map是通过哈希值来确定数据的存放链表.

而要让Hash_map这个数据结构,拥有哈希值.
则要再引入一个概念.
就是Key.
这个Key即是哈希值的来源,也是能否找寻到这个数据的关键.

我们把这个key的原始数据,和你需要存放的数据,一起放进节点.
而哈希值,则需要通过key转换.


using hash = unsigned int;
hash HashFunction(keyType key)
    {
        uint modle{ 53 }; // mapSize >32 <64 M53

        hash_value = ((hash)key % modle) % 数组大小;    // 最后不M数组大小,必然会出现越界问题
        return hash_value;
    }

这就是将key转换成哈希值的哈希函数.

有了哈希值的概念,我们就要知道一个名词.
哈希冲突
哈希冲突就是当两个不一样的key转换成了一样的哈希值.

如果你不把key的原始数据扔进哈希节点里面.
你就会导致,不一样的数据,会被新数据覆写掉.

而解决哈希冲突的方式有很多种.
我个人采用的是在指定数组的大小区间内%一个质数.
[HashCollision][哈希冲突][HashValue]:最佳哈希质数
该链接有已经计算好的质数.

只要你的数组大小,在这个区间内,就可以使用相应的质数,减少哈希冲突.
如果你觉得这个方法不适合你,也可以去网上找找其他的方法.


我们拥有了哈希值,我们也有了链表数值.
现在你只需要让程序,自己计算哈希值,然后将数据写入对应的链表即可.

hash_map的原理非常简单.

实现的关键,就是Hash_value

可以通过下面的代码,来进一步理解hash_map


代码


// ReSharper disable CppClangTidyClangDiagnosticInvalidSourceEncoding
// 停用ReSharper插件的字符串拼写检查

#include <iostream>

#ifndef NULL
#define NULL 0x0
#endif

using uint = unsigned int;
using hash = unsigned int;
using BOOL = short;

char* StringCopy(char* dest, const char* source)
{
    if (!(dest != nullptr && source != nullptr))
    {
        return nullptr;
    }
    char* tmp = dest;
    while ((*dest++ = *source++) != '\0') {}
    return tmp;
}

class Student
{
    const char* name;
    uint age;
public:
    Student(uint age_ = 18, const char* name_ = "Name:Unknown") :name(name_), age(age_)
    {

    }
    Student(const Student& copyObj) // 深复制构造函数:存在外部指针
    {
        this->age = copyObj.age;
        this->name = new char[strlen(copyObj.name) + 1];
        StringCopy(const_cast<char*>(this->name), copyObj.name);
    }
    bool operator==(const Student& rightObj)    // 运算符重载:相等逻辑判断
    {
        if (this->age == rightObj.age && strcmp(this->name, rightObj.name) != 0)
            return true;
        return false;
    }
};

template <typename keyType, typename dataType>
class Hash_node                                 // 和链表的节点实现方式差不多
{
    keyType hashkey;                            // hashKey 存储钥匙,这个钥匙是hash_map能正常实现同hash_value下,在一个index中进行覆写还是链接的关键
    dataType data;                              // data area
    Hash_node* nPoint;                          // nPoint
public:
    Hash_node(keyType hashKey_ = NULL, dataType data_ = NULL, Hash_node* nPoint_ = nullptr)
        :hashkey(hashKey_), data(data_), nPoint(nPoint_)
    {

    }

    keyType& GetHashKey()
    {
        return hashkey;
    }

    dataType& GetData()
    {
        return this->data;
    }

    Hash_node*& GetNextPoint()      // 返回对象的nPoint指针引用
    {                           // 如果返回类型是Hash_node的指针类型,那么只会返回里面的值
        return this->nPoint;
    }

    Hash_node* GetObject()
    {
        return this;
    }

};

// 模板参数1:钥匙类型 模板参数2 : 数据类型 模板参数3 : map大小
template <typename keyType, typename dataType, size_t mapSize>
class Hash_map
{
    hash hash_value;
    Hash_node<keyType, dataType>* map[mapSize];

    hash HashFunction(keyType key)
    {
        uint modle{ 57 }; // mapSize >32 <64 M57
        					// https://blog.csdn.net/qq_42468226/article/details/117166210 最优数

        hash_value = ((hash)key % modle) % mapSize;    // 最后不 m数组大小,必然会出现越界问题
        return hash_value;
    }

public:
    Hash_map(keyType key = NULL, const dataType& data = NULL) :hash_value(HashFunction(key)), map{ nullptr }
    {
        this->Hash_push(key, data);
    }

    // key相同覆写,hashValue相同追加
    bool Hash_push(keyType key, const dataType& data)
    {
        /*Hash_node<keyType, dataType>** */
        this->HashFunction(key);    // 计算哈希值
        Hash_node<keyType, dataType>** hnpp = &this->map[hash_value];
        while (*hnpp)   //进入循环证明 map的该元素位置已经被某些数据写入过,所以需要检测是需要覆盖还是追加到尾.
        {
            // 判断key是否相同,相同覆写,否则追加
            if (this->map[hash_value]->GetHashKey() == key)
            {
                this->map[hash_value]->GetData() = data;
                return true;
            }
            hnpp = &(*hnpp)->GetNextPoint();
        }
        if (!*hnpp)
        {
            *hnpp = new Hash_node<keyType, dataType>{ key,data };
            return true;
        }

        return false;   //safe return
    }

    // 0 钥匙:不匹配 1 钥匙:匹配 2 钥匙和数据:匹配
    BOOL Map_check(keyType key, const dataType& data = NULL)
    {
        this->HashFunction(key);    // 计算哈希值
        Hash_node<keyType, dataType>** hnpp = &this->map[hash_value];
        while (*hnpp)   //进入循环证明 map的该元素位置已经被某些数据写入过,所以需要检测是需要覆盖还是追加到尾.
        {
            // 判断key是否相同,相同覆写,否则追加
            if ((*hnpp)->GetHashKey() == key) // hnpp[hash_value]->GetHashKey()
            {
                if ((*hnpp)->GetData() == data)
                    return 2; // 钥匙匹配 数据匹配
                return 1;   // 钥匙匹配 数据不匹配
            }
            hnpp = &(*hnpp)->GetNextPoint();
        }

        return 0;  // 0 钥匙不匹配,数据不匹配
    }

};

int main(void)
{
    std::cout << "\t\tHash_map testing program" << std::endl;

    // 模板参数1:钥匙类型 模板参数2:数据类型 模板参数3:map大小.
    Hash_map<int, int, 50>hash_map{};   // 压第一个数据,来源于缺省构造,一般会在 [0]-0的位置

    hash_map.Hash_push(15, 99);     // 压第二个数据
    hash_map.Hash_push(15, 100);    // 覆盖第二个数据

    switch (hash_map.Map_check(15,100))    //switch 使用 Map_check 函数 例子
    {
    case 0:
        std::cout << "[KEY]:未匹配" << std::endl;
        break;
    case 1:
        std::cout << "[KEY]:匹配" << std::endl;
        break;
    case 2:
        std::cout << "[DATA]:匹配" << std::endl;
        break;
    default:
        std::cout << "Hello,World!" << std::endl;
        break;
    }

    if (hash_map.Map_check(15))     // if 使用 Map_check 函数 例子
        hash_map.Hash_push(15, 999);    // 覆盖第二个数据

    for (uint count{ 0 }; count < 100; count++) // 快压一百个数据
        hash_map.Hash_push(count, count);

    hash_map.Map_check(99, 99); // 数据查找速度例子,最多3次循环查询完毕.

    // 泛型结构支持
    Hash_map<const char*, Student, 30> studentMap{ "学号:1001",Student{18,"张三"} };

    studentMap.Hash_push("学号:1002", Student{ 18,"李四" });
    studentMap.Hash_push("学号:1003", Student{ 19,"王二麻子" });

    if (studentMap.Map_check("学号:1003"))
        studentMap.Hash_push("学号:1003",Student{5,"Hello,World"});

    return 0;
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

八宝咸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值