hash.h
/**************************************************************************
Description:
哈希表说白了就是有一个数组,然后数组的每个元素都装了一条链表
哈希查找复杂度为O(1)
可用于数据存储,数据查询,字符串比较等
**************************************************************************/
#pragma once
#define MAXSIZE 128
//链表 节点的数据结构
typedef struct _node_t
{
int key;
const void* value;
_node_t* next;
}node_t;
typedef node_t* pNode;
typedef struct _hash_t
{
int nTotalNum; // 总数量
pNode* pArray; // 定义链表类型的数组
}hash_t;
class MyHash
{
public:
explicit MyHash();
~MyHash();
static MyHash* getInstance();
bool getStatus();
int getHashIndex(int nKey);//简单取模法
int getBKDRHashIndex(const char *str, int nTotal);//BKDR
void insertHash(int nKey, const void* value);
void deleteHash(int nKey);
pNode searchNodeByKey(int nKey);
const void* getValue(pNode node);
static int m_nTotal;
private:
hash_t* m_pHash;
bool m_bStatus;
static MyHash* m_pInstance;
};
hash.cpp
/**************************************************************************
Description:
哈希表说白了就是有一个数组,然后数组的每个元素都装了一条链表
哈希查找复杂度为O(1)
可用于数据存储,数据查询,字符串比较等
**************************************************************************/
#include "hash.h"
#include <iostream>
using namespace std;
int MyHash::m_nTotal = MAXSIZE;
MyHash::MyHash()
{
m_bStatus = true;
m_pHash = new hash_t;
if(!m_pHash) //判断分配内存是否成功
{
m_bStatus = false;
}
else
{
m_pHash->nTotalNum = MyHash::m_nTotal;
m_pHash->pArray = new pNode[m_pHash->nTotalNum];
if(!m_pHash->pArray) //判断指针数组是否分配内存成功
{
delete m_pHash;
m_bStatus = false;
}
else
{
for(int i = 0; i < m_pHash->nTotalNum; i++)
{
m_pHash->pArray[i] = new node_t;
if(!m_pHash->pArray[i]) //判断节点是否分配内存成功
{
delete[] m_pHash->pArray;
delete m_pHash;
m_bStatus = false;
}
else
{
memset(m_pHash->pArray[i], 0, sizeof(node_t));//初始化节点都置为0
}
}
}
}
}
MyHash::~MyHash()
{
pNode pHead = NULL, pCur = NULL, pTmp = NULL;
for(int i = 0; i < m_pHash->nTotalNum; i++)//循环销毁每一个链表
{
pHead = m_pHash->pArray[i];//获取链表的头节点
pCur = pHead->next;
while(pCur != NULL) //循环销毁链表中内容
{
pTmp = pCur;
pCur = pCur->next;
delete pTmp;
}
delete pHead; //销毁链表头部节点
}
delete[] m_pHash->pArray;
delete m_pHash;
}
//单例
MyHash* MyHash::m_pInstance = nullptr;
MyHash* MyHash::getInstance()
{
if(m_pInstance == nullptr)
{
m_pInstance = new MyHash;
}
return m_pInstance;
}
//获取哈希表创建状态
bool MyHash::getStatus()
{
return m_bStatus;
}
//简单取模法
//散列函数的作用就是用来提供节点应该随机分布在哪个链表当中,用来确定添加的位置
int MyHash::getHashIndex(int nKey)
{
return nKey % m_pHash->nTotalNum;
}
//BKDR 最出众的一种字符串散列函数
int MyHash::getBKDRHashIndex(const char* str, int nTotal)
{
unsigned int seed = 131; // 31 131 1313 13131 131313 etc..
unsigned int key = 0;
while (*str)
{
key = key * seed + (*str++);//每一位乘一个系数
}
/*0x7FFFFFFF是一个用16进制表示的整型,是整型里面的最大值,hash与其按位与得到一个正数,
*/
return (key & 0x7FFFFFFF)%nTotal;
}
//插入节点
void MyHash::insertHash(int nKey, const void *value)
{
pNode pHead = NULL, pCur = NULL, pTmp = NULL;
//判断key值,不能有相同的key值
pCur = searchNodeByKey(nKey);
if(pCur == NULL)
{
pTmp = new node_t;
if(!pTmp)
{
cout << "Insert node failed.(mem alloc fail)" << endl;
return;
}
pHead = m_pHash->pArray[getHashIndex(nKey)];
//头插法
pTmp->next = pHead->next;
pTmp->key = nKey;
pTmp->value = value;
pHead->next = pTmp;
}
else
cout << "key exist." << endl;
}
//删除节点
void MyHash::deleteHash(int nKey)
{
pNode pHead = NULL, pCur = NULL, pPre = NULL;
pHead = m_pHash->pArray[getHashIndex(nKey)];
pCur = pHead->next; //获取第一个有效节点,因为头节点是空的,所以需要next
pPre = pHead;
while(pCur != NULL && pCur->key != nKey)
{
pPre = pCur; //记录上一次的节点,方便把链表重新连接起来
pCur = pCur->next;
}
if(pCur) //重新连接链表
{
pPre->next = pCur->next;
delete pCur;
}
}
//根据key值查找节点
pNode MyHash::searchNodeByKey(int nKey)
{
pNode pHead = NULL, pCur = NULL;
pHead = m_pHash->pArray[getHashIndex(nKey)];
pCur = pHead->next;
while(pCur != NULL && pCur->key != nKey)
{
pCur = pCur->next;
}
return pCur;
}
//获取节点的值
const void *MyHash::getValue(pNode node)
{
return node ? node->value : NULL;
}
main.cpp
#include "hash.h"
#include <iostream>
using namespace std;
#define MaxKey 1000
int main(int argc, char *argv[])
{
//哈希表存储数据,操作数据
const char* test_arr[] = {"AAA","BBB","CCC","DDD","EEE","FFF"};
MyHash::m_nTotal = 10;
MyHash* hash = MyHash::getInstance();
//插入节点
for (int i = 0; i < 6; ++i)
{
hash->insertHash(i+1, test_arr[i]);
}
//输出内容
for (int i = 0; i < MyHash::m_nTotal; ++i)
{
pNode node = hash->searchNodeByKey(i);
if(node)
cout << "Key: " << i + 1 << ", Value: " << (const char*)(hash->getValue(node)) << endl;
else
cout << "Not find node, key:" << i + 1 << endl;
}
//删除key为2的节点
hash->deleteHash(2);
//再次输出内容
for (int i = 0; i < MyHash::m_nTotal; ++i)
{
pNode node = hash->searchNodeByKey(i);
if (node)
cout << "Key: " << i + 1 << ", Value: " << (const char*)(hash->getValue(node)) << endl;
else {
cout << "Not find node, key:" << i + 1 << endl;
}
}
//BKDR Hash 简单使用
const char* pStr[4] = {"how","old","are","you" };
const char* pHash_table[MaxKey] = {NULL};
for (int i = 0; i < 4; i++)
{
cout << "i:" << i << ",str:" << pStr[i] << ", key:" << hash->getBKDRHashIndex(pStr[i], MaxKey) << endl;
pHash_table[hash->getBKDRHashIndex(pStr[i], MaxKey)] = pStr[i];
}
return 0;
}
其他补充
哈希算法的应用
1)加密 (MD5)
2)唯一标识
3)数据校验 保存文件时对文件去哈希值
4)散列表 (根据键(Key)而直接访问在内存存储位置的数据结构) 散列函数(hash(key) )
5)负载均衡 映射表
6)分布式存储
将数据分布在多台机器上,通过一致性哈希算法,保持了各个机器上数据数量的均衡
7)区域链
解决哈希冲突
哈希冲突就是key1=key2,而hash(key1) = hash(key2)的情况
链地址法
再哈希法( 提供多个哈希函数,如果第一个哈希函数计算出来的key的哈希值冲突了,则使用第二个哈希函数计算key的哈希值)
优点:
不易产生聚集
缺点:
增加了计算时间
建立公共溢出区 (将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表)
开放定址法(当关键字key的哈希地址p =H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,若p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中)
4.1 线性探测再散列
顺序查看下一个单元,直到找出一个空单元或查遍全表
di=1,2,3,…,m-1
4.2 二次(平方)探测再散列
在表的左右进行跳跃式探测,直到找出一个空单元或查遍全表
di=1^2,-1^2,2^2,-2^2,…,k^2,-k^2 ( k<=m/2 )
4.3 伪随机探测再散列
建立一个伪随机数发生器,并给一个随机数作为起点
di=伪随机数序列。具体实现时,应建立一个伪随机数发生器,(如i=(i+p) % m),并给定一个随机数做起点。
例如,已知哈希表长度m=11,哈希函数为:H(key)= key % 11,则H(47)=3,H(26)=4,H(60)=5,假设下一个关键字为69,则H(69)=3,与47冲突。
如果用线性探测再散列处理冲突,下一个哈希地址为H1=(3 + 1)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 + 2)% 11 = 5,还是冲突,继续找下一个哈希地址为H3=(3 + 3)% 11 = 6,此时不再冲突,将69填入5号单元。
如果用二次探测再散列处理冲突,下一个哈希地址为H1=(3 + 12)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 - 12)% 11 = 2,此时不再冲突,将69填入2号单元。
如果用伪随机探测再散列处理冲突,且伪随机数序列为:2,5,9,……..,则下一个哈希地址为H1=(3 + 2)% 11 = 5,仍然冲突,再找下一个哈希地址为H2=(3 + 5)% 11 = 8,此时不再冲突,将69填入8号单元。
优点:
容易序列化
若可预知数据总数,可以创建完美哈希数列
缺点:
1.占空间很大。(开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间)
2.删除节点很麻烦。不能简单地将被删结点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。