前序:
一、关于查找,其实说白了,查找的本质就是已知对象查找位置;那么在已知了对象以后都有什么办法去快速查找对象的位置;
(1)【无序查找】在前面的学习中,我们看到如果这些对象是无序的装在一个容器中(数组,顺序表,链表,普通二叉树)那么就拿着这个对象去容器中一个一个的找,如果找到了,就查到了该对象的位置,如果没有找到说明对象没在这个容器中;这种情况下的查找时间复杂度为O(N)
(2)【全序查找】如果我们将顺序表,数组,链表中的元素按照升序或者降序的规则完全排列好以后再拿着这个对象去找,那么我们可以利用二分查找的方法去寻找,这样每次排除剩下的要查询的数据的一排,就会快很多,这种查找的时间复杂度为O(log2N);
(3)【半序查找】当使用树的结构去存储数据元素时,我们不能像顺序结构那样让数据完全的升序或者降序储存,这时我们只能让数据满足某些大小关系储存,如果这颗树为BST树,那么查找效率为O(H)【H为树的高度】;如果这颗树为AVL树,那么查找效率为O(log2N);
二、前面学习的查找方法都是基于数字的查找,虽然AVL树的查找效率已经相当可观,但是,试想当我们给AVL树中储存字符串时,尤其是储存一些长的字符串是,那么在每次拿着一个要寻找的字符串去AVL树中寻找时,每次虽然能到达淘汰一半的数据的高效率,但是每次都要将要查找的字符串和当前的结点作比较,我们知道字符串的比较是一个一个的字符的比较,也是很费时间的,所以,这样来看,当AVL树中存储的是字符串时,查找效率也不是很客观;
基于上面的两点查找的不足,我们提出了散列查找,就是通过一个散列函数,每次将要储存的对象放入散列函数映射出一个位置整数,那么该位置就是这个对象在顺序表中的存储位置,然后将该对象放入顺序表当中,就这样,通过这种方法将所有已存在的对象放入散列表中,之后如果我们要进行查找,那么直接将要查找的对象通过散列函数转换为位置下标,然后查找该位置下的散列表是否存在数字;这样 就可以一次完成查找,所以散列查找的理论时间复杂度为O(1);
正文:
一、散列表(哈希表)
上面所说的散列查找是理论情况,在实际中,会出现不同的对象通过散列函数映射到相同的位置下标的情况;所以这时,当我们在插入时,就会出现一个对象通过散列函数计算出来的散列位置值已经存在对象,那么这种情况就是散列冲突(又叫哈希冲突);那么我们学习散列表就是着重学习如何获得一个很好的散列函数,使其哈希冲突降到最低,但是哈希冲突时不可避免的,所以第二个学习的重点就是如何处理哈希冲突;
把这两个问题解决好了,那么散列表的查找基本所用的时间就是=散列函数计算位置所用时间+从该位置起查找对象所用的时间;
二、哈希表的构造
(1)哈希表(散列表),是通过关键字key而直接访问在内存存储位置的一种数据结构,它就是 以空间换取时间。通过多开辟几个空间,来实现查找的高效率。
(2)它通过一个关键值的函数将所需的数据映射到表中的位置来访问数据,这个映射函数叫做散列函数,存放记录的数组叫做散列表。
(3)构造哈希表的几种方法
1.直接定址法–取关键字的某个线性函数为散列地址,
Hash(Key)= Key 或Hash(Key)= A*Key + B,A、B为常数。
2. 除留余数法–取关键值被某个不大于散列表长m的数p除后的所得的余数为散列地址。
Hash(Key)= Key%P。
3. 平方取中法
4. 折叠法
5. 随机数法
6数学分析法
以上六种方法,我只掌握了两种;
对于直接定址法:给定一个key,就可以对应一个地址(这种比较适用于key值比较集中的情况,如果key值过于分散,就需要浪费很多空间)
对于除留余数法:不同的key有可能对应同一个散列地址。当多个key对应一个散列地址值时,这样就发生了冲突,这种冲突叫做哈希冲突或者哈希碰撞。 任意的散列函数都不能避免产生冲突。
三、处理(解决)哈希冲突的方法(哈希冲突已经出现,这是解决方法)
- 1 处理哈希冲突的闭散列方法-开放定址法
(1)线性探测:对于一个给定的key,当有别的key值占了它的散列地址的位置,他就应该以线性方式去找下一个空位置
(2)二次探测:实际是二次方探测,当发生冲突时,以二次方的方式去找下一个空位置。
- 2 处理哈希冲突的开链法/拉链法(哈希桶)
(1)开链法:
四、减少哈希冲突的方法(只是尽可能让冲突达到最低,不能完全避免冲突)
- 1 对于hashtable的大小使用素数,使用素数做除数可以减少哈希冲突
使用素数表对齐做哈希表的容量,降低哈希冲突
constint_PrimeSize= 28;
staticconstunsignedlong_PrimeList[_PrimeSize] =
{
53ul, 97ul, 193ul, 389ul, 769u1543ul, 3079ul, 6151ul, 12289ul, 245949157ul, 98317ul, 196613ul,393241ul,786433ul,1572869ul,
3145739ul, 6291469ul, 12582917ul,25165843ul,
50331653ul, 100663319ul,201326611ul,
402653189ul,805306457ul,
1610612741ul, 3221225473ul, 4294967291ul
};
- 2 对于字符串对象,使用正确的字符串哈希算法,可以大大减低哈希碰撞
字符串哈希算法
staticsize_tBKDRHash(constchar*str)
{
unsignedintseed= 131;// 31 131 1313 13131 131313
unsignedinthash= 0;
while(*str)
{
hash=hash*seed+ (*str++);
}
return(hash& 0x7FFFFFFF);
}
3 对于开放定址法,严格控制载荷因子在0.7–0.8以下。载荷因子表明了哈希表的装填程度;
为了防止哈希冲突,我们引入了负载因子这一概念,负载因子就是哈希表中元素的个数与哈希表的大小的比值就是负载因子。对于开放定址法,载荷因子是特别重要因素,应将其控制在0.7–0.8以下,查过0.8,查表时,CPU的缓存不命中。(cache的使用,解决了主存速度不足的问题。当CPU要读取一段数据时,先在cache中查找,如果找到了,说明命中;如果找不到,去主存中查找,然后根据调度算法将要读取的数据从主存调入cache)
四、下面详细介绍解决哈希冲突的三种方法
(1)线性探测法
(2)二次探测法
(3)拉链法/开链法/哈希桶
五、代码实现
(1)开放定值法–线性探测法
#include<iostream>
#include<vector>
#include <string>
using namespace std;
namespace Open
{
enum State//元素的状态类型
{
EMPTY,//空状态,表示还没有放元素
EXIST,//存在状态,表示此处有元素
DELETE//删除状态,表示以前有元素,现在删了
};
template<typename K,typename V>
struct HashNode//hashtable存放的元素类型
{
K _key;//健值
V _value;//实际存放的元素
State _state;//表示该元素的状态
HashNode(const K& key=K(),const V& value=V())
:_key(key)
,_value(value)
,_state(EMPTY)
{}
};
template<typename K>//仿函数,获取元素的hashtable的位置值
struct _GetK
{
size_t operator()(const K& key)
{
return key;
}
};
struct _GetStrK//仿函数,获取string类元素的hashtable的位置值
{
static size_t BKDRHash(const char* str)
{
unsigned int seed = 131;// 31 131 1313 13131 131313
unsigned int hash = 0;
while (*str)
{
hash = hash * seed + (*str++);
}
return(hash & 0x7FFFFFFF);
}
size_t operator()(const string& key)
{
return BKDRHash(key.c_str());
}
};
template<typename K,typename V,typename __GetK=_GetK<K>>
class HashTable
{
public:
typedef HashNode<K,V> Node;
public:
HashTable()//构造函数
:_sz(0)
{
_table.resize(2);
}
~HashTable()//析构,vector自动释放内存
{}
void Swap(HashTable<K,V,__GetK>& hashtable)
{
_table.swap(hashtable._table);
std::swap(_sz,hashtable._sz);
}
bool Insert(const K& key,const V& value)//增加数据
{
//1.判容,虽然顺序表在插入元素时,有自动增容的功能,但是,当顺序表作为哈希表来用时;
//不光是简单的每次让顺序表自己增容,然后继续给顺序表后面放元素那么简单,还需要在增容的同时,将原先的顺序表
//中的元素,按照哈希表的插入规则,重新放入新的已经增容的顺序表中,
_CheakCapacity(_sz);
//2.获取hashtable位置
size_t index=_GetIndex(key);
size_t begin=index;
//3.开始插入---线性探测法插入
//3.1寻找可以插入的位置
while (_table[index]._state==EXIST)//【删除】和【空】两种状态的位置都可以插入
{
if (_table[index]._key==key)//hashtable 不存放key相等的两个元素
{
cout<<key<<"->"<<value<<"元素已存在"<<endl;
return false;
}
index++;
if (index==_table.size())
{
index=0;
}
if (index==begin)
{
return false;
}
}
//3.2找到位置,开始正式插入
_table[index]._key=key;
_table[index]._value=value;
_table[index]._state=EXIST;
_sz++;
return true;
}
bool Remove(const K& key,const V& value)//删除数据
{
//1.如果hashtable为空,不用删除,直接返回失败
if (_table.empty())
{
return false;
}
//2.hashtable不为空
//2.1寻找该元素的
size_t index=_GetIndex(key);
size_t begin=index;
while (_table[index]._state!=EMPTY&&_table[index]._key!=key)
{
index++;
if (index==_table.size())//找到最后一个没找到,返回第一个继续找
{
index=0;
}
if (index==begin)//找了一圈没找到,说明没有在hashtable中
{
return false;
}
}
//2.2找到位置。开始删除--不是真删,而是将元素的状态改为删除,避免后面在来元素时找不到该删除位置后面的元素
if (_table[index]._key==key&&_table[index]._state==EXIST)
{
_table[index]._state=DELETE;
_sz--;
return true;
}
else
return false;
}
Node* /*bool*/ Find(const K& key,const V& value)//查找数据
{
//1.现寻找元素在hashtable中的映射位置
size_t index=_GetIndex(key);
size_t begin=index;
//2.寻找
while (_table[index]._state!=EMPTY&&_table[index]._key!=key)
{
index++;
if (index==_table.size())
{
index=0;
}
if (index==begin)
{
return /*false*/NULL;
}
}
//删除的元素的状态为delete,也有可能和key相等
if (_table[index]._key==key&&_table[index]._state==EXIST)
{
return /*true*/&_table[index];
}
else
{
return/* false*/NULL;
}
}
void Printf()
{
for (size_t i=0;i<_table.size();i++)
{
if (_table[i]._state==EXIST)
{
cout<<i<<"【"<<_table[i]._key<<"->"<<_table[i]._value<<"】"<<" ";
}
}
}
protected:
void _CheakCapacity(size_t _sz)//判容
{
//1.容量不够,需要增加容量
if ((_sz*10)/_table.size()>=8)//散列因子控制在0.8以内
{
size_t NewSize=_GetNewSize(_table.size());
HashTable<K,V,__GetK> hashtable;
hashtable._table.resize(NewSize);
for (size_t i=0;i<_sz;i++)
{
if (_table[i]._state=EXIST)
{
hashtable.Insert(_table[i]._key,_table[i]._value);
}
}
this->Swap(hashtable);
}
//2.容量够用,不用增容
else
return;
}
size_t _GetIndex(const K& key)//获取hash表位置
{
__GetK Getk;
return Getk(key)%_table.size();
}
int _GetNewSize(size_t num)//当哈希表的载荷因子超过0.8,那么重载增容哈希表,该函数为获取新的哈希表大小为下一个大的素数
{
const int _PrimeSize = 28;
static const unsigned long _PrimeList[_PrimeSize] = {
53ul, 97ul, 193ul, 389ul, 769ul,
1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
1572869ul, 3145739ul, 6291469ul, 12582917ul,
25165843ul, 50331653ul, 100663319ul, 201326611ul, 402653189ul,
805306457ul, 1610612741ul, 3221225473ul, 4294967291ul
};
for (int i = 0; i < _PrimeSize; ++i)
{
if (_PrimeList[i] > (unsigned long)num)
return _PrimeList[i];
}
return _PrimeList[_PrimeSize - 1];
}
private:
vector<Node> _table;//利用顺序表当做hashtable,存放元素
size_t _sz;//记录hashtable中存放元素的个数
};
}
int main()
{
//Open::HashTable<int,int> ht;
//ht.Insert(8,0);//
//ht.Insert(18,0);//
//ht.Insert(58,0);//
//ht.Insert(89,0);//
//ht.Insert(2,0);//
//ht.Insert(3,0);//
//ht.Insert(4,0);//
//ht.Insert(9,0);//
//ht.Insert(0,0);//
//ht.Insert(3,0);//
// ht.Insert(52,0);
//cout<<"原始哈希表元素:";
//cout<<endl;
//ht.Printf();
//cout<<endl;
//
//cout<<"删除(5,55)和(9,99)以后的哈希表元素:";
//cout<<endl;
//ht.Remove(4,0);
//ht.Remove(3,0);
//ht.Printf();
// cout<<endl;
//
//cout<<ht.Find(18,0)<<endl;
//cout<<ht.Find(52,0)<<endl;
//cout<<ht.Find(100,0)<<endl;
// cout<<endl;
//return 0;
/*统计单词个数*/
Open::HashTable<string, int, Open::_GetStrK> ht2;
char* array2[] = { "hello", "world", "sort", "find", "sort" };
for (int i = 0; i < sizeof(array2) / sizeof(array2[0]); ++i)
{
Open::HashTable<string,int,Open::_GetStrK>::Node* ret = ht2.Find(array2[i], 0);
if (ret)//结点已经存在
{
ret->_value++;
}
else
{
ht2.Insert(array2[i], 0);
}
}
ht2.Printf();
return 0;
}
(2)拉链法
//拉链法
namespace Link
{
template<typename K,typename V>
struct HashNode//hashtable存放的元素类型
{
K _key;//健值
V _value;//实际存放的元素
HashNode<K,V>* _next;
HashNode(const K& key=K(),const V& value=V())
:_key(key)
,_value(value)
,_next(NULL)
{}
};
template<typename K>//仿函数,获取元素的hashtable的位置值
struct _GetK
{
size_t operator()(const K& key)
{
return key;
}
};
struct _GetStrK//仿函数,获取string类元素的hashtable的位置值
{
static size_t BKDRHash(const char* str)
{
unsigned int seed = 131;// 31 131 1313 13131 131313
unsigned int hash = 0;
while (*str)
{
hash = hash * seed + (*str++);
}
return(hash & 0x7FFFFFFF);
}
size_t operator()(const string& key)
{
return BKDRHash(key.c_str());
}
};
template<typename K,typename V,typename __GetK=_GetK<K>>
class HashTable
{
public:
typedef HashNode<K,V> Node;
public:
HashTable()//构造函数
:_sz(0)
{
_table.resize(2);//初始化开辟2个空间
}
~HashTable()//析构,vector自动释放内存
{
for (size_t i=0;i<_table.size();i++)
{
Node* cur=_table[i];
while (cur)
{
Node* del=cur;
cur=cur->_next;
delete del;
del=NULL;
}
}
_table.clear();
_sz=0;
}
void Swap(HashTable<K,V,__GetK>& TmpHashTable)
{
_table.swap(TmpHashTable._table);
std::swap(_sz,TmpHashTable._sz);
}
bool Insert(const K& key,const V& value)//增加数据
{
_CheakCapacityOP();
//2.获取hashtable位置
size_t index=_GetIndex(key);
//插入右两种情况1.插入的结点时第一个结点2.插入的接单不是第一个结点
Node* NewNode=new Node(key,value);
NewNode->_next=_table[index];//将该节点尾部连到到下一个结点
_table[index]=NewNode;//将该节点连接到哈希表的第一个结点处
_sz++;
return true;
}
bool Remove(const K& key)//删除数据
{
//1.如果hashtable为空,不用删除,直接返回失败
if (_table.empty())
{
return false;
}
//2.哈希表不为空
size_t index=_GetIndex(key);
//2.1寻找接结点并删除
Node* cur=_table[index];
Node* prev=NULL;//记录要删除结点的上一个结点
while (cur)
{
//找到结点
//删除分3种情况
//1.删除第一个节点
//2.删除中间结点
//3.删除最后一个结点
if (cur->_key==key)
{
if (prev==NULL)
{
_table[index]=cur->_next;
}
else
{
prev->_next=cur->_next;
}
delete cur;
cur=NULL;
_sz--;
return true;
}
prev=cur;
cur=cur->_next;
}
return false;
}
Node* Find(const K& key)//查找数据
{
//1.现寻找元素在hashtable中的映射位置
size_t index=_GetIndex(key);
Node* cur=_table[index];
while (cur)
{
if (cur->_key==key)
{
return cur;
}
cur=cur->_next;
}
return NULL;
}
void Printf()
{
for (size_t i=0;i<_table.size();i++)
{
Node* cur=_table[i];
if (cur)
{
while(cur)
{
cout<<"["<<i<<"]->"<<"【"<<cur->_key<<"-"<<cur->_value<<"】"<<"->";
cur=cur->_next;
}
cout<<endl;
}
}
}
protected:
//判容--重新开辟新结点连接到新的哈希表的对应位置的下面(要借用顺序表,释放一次,开辟一次)
//多一次释放,多一次开辟,导致空间 利用率不是很高
void _CheakCapacity()//判容
{
//1.容量不够,需要增加容量
if (_sz==_table.size())
{
size_t NewSize=_GetNewSize(_table.size());
HashTable<K,V,__GetK> TmpHashTable;
TmpHashTable._table.resize(NewSize);
//把旧的哈希表的key全部装入vector,然后删除旧的哈希表的结点
vector<K> v;
for (size_t i=0;i<_table.size();i++)
{
Node* cur=_table[i];
while (cur)
{
v.push_back(cur->_key);
Node* del=cur;
cur=cur->_next;
delete del;
}
_table[i]=NULL;
}
//将vector保存的key全部插入新的临时哈希表中
for (size_t i=0;i<v.size();i++)
{
TmpHashTable.Insert(v[i],0);
}
//交换新旧哈希表
this->Swap(TmpHashTable);
}
//2.容量够用,不用增容
else
return;
}
//判容的优化,不用释放就哈希表的结点,直接将就哈希表的结点连接到新哈希表中
void _CheakCapacityOP()
{
if (_sz==_table.size())
{
size_t NewSize=_GetNewSize(_sz);
HashTable<K,V,__GetK> TmpHashTable;
TmpHashTable._table.resize(NewSize);
for (size_t i=0;i<_sz;i++)
{
if (_table[i])
{
Node* cur=_table[i];
Node* tmp=NULL;
while (cur)
{
size_t index=TmpHashTable._GetIndex(cur->_key);
tmp=cur;
cur=cur->_next;
tmp->_next=TmpHashTable._table[index];
TmpHashTable._table[index]=tmp;
}
}
_table[i]=NULL;
}
this->Swap(TmpHashTable);
}
else
return;
}
size_t _GetIndex(const K& key)//获取hash表位置
{
__GetK Getk;
return Getk(key)%_table.size();
}
int _GetNewSize(size_t num)//当哈希表的载荷因子超过0.8,那么重载增容哈希表,该函数为获取新的哈希表大小为下一个大的素数
{
const int _PrimeSize = 28;
static const unsigned long _PrimeList[_PrimeSize] = {
53ul, 97ul, 193ul, 389ul, 769ul,
1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
1572869ul, 3145739ul, 6291469ul, 12582917ul,
25165843ul, 50331653ul, 100663319ul, 201326611ul, 402653189ul,
805306457ul, 1610612741ul, 3221225473ul, 4294967291ul
};
for (int i = 0; i < _PrimeSize; ++i)
{
if (_PrimeList[i] > (unsigned long)num)
return _PrimeList[i];
}
return _PrimeList[_PrimeSize - 1];
}
private:
vector<Node*> _table;//利用顺序表当做hashtable,存放元素
size_t _sz;//记录hashtable中存放元素的个数
};
}
void Test()
{
// Link::HashTable<int, int> ht1;
// int array1[] = { 89, 21, 8, 58, 53, 12, 3, 4, 9, 0 };
// for (int i = 0; i < sizeof(array1) / sizeof(array1[0]); ++i)
// {
// ht1.Insert(array1[i], 0);
// }
//cout<<"原始哈希表:"<<endl;
// ht1.Printf();
//cout<<"删除了【4,0】和【12,0】以后的哈希表:"<<endl;
// ht1.Remove(4);
// ht1.Remove(12);
// ht1.Printf();
//cout<<"查找【21】和【12】:"<<endl;
//Link::HashTable<int,int>::Node* ret1=ht1.Find(21);
//Link::HashTable<int,int>::Node* ret2=ht1.Find(12);
//if (ret1)
//{
// cout<<ht1.Find(21)->_key<<"-"<<ht1.Find(21)->_value<<endl;
//}
//if (ret2)
//{
// cout<<ht1.Find(12)->_key<<"-"<<ht1.Find(12)->_value<<endl;
//}
Link::HashTable<string,int,Link::_GetStrK> ht2;
char* array2[] = {"hello","yang","hello","sort","wang","zip","huffman"};
for(int i = 0; i < sizeof(array2)/sizeof(array2[0]); ++i)
{
Link::HashTable<string,int,Link::_GetStrK>::Node* ret = ht2.Find(array2[i]);
if(!ret)
ht2.Insert(array2[i],0);
}
cout<<"原始哈希表"<<endl;
ht2.Printf();
ht2.Remove("hello");
ht2.Remove("sort");
cout<<"删除\"hello\"和\"sort\""<<endl;
ht2.Printf();
}