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] |
---|---|---|
data1 | data4 | data6 |
data2 | data5 | |
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;
}