散列(hashing)是C++STL 字典实现的一种方式,散列的实现一般包括两个步骤:
1.将键值映射成一个整数。
2.将这个整数通过函数映射到一个线性表中
首先介绍第一个问题:怎么将键值映射成为一个整数
第一种转换法:
int stringToInt(string s)
{//把s转换成一个非负整数,这种转换依赖s的所有字符
int length=(int)s.length();
int answer=0;
if(length%2==1)
{//长度为奇数
answer=s.at(length-1);
length--;//先把长度转换为偶数
}
for(int i=0;i<length;i+=2)
{//同时转换两个字符
answer+=s.at(i);
answer+=((int)s.at(i+1))<<8;//这里+=的优先级小于<<.
}
return (answer<0)?-answer:answer;
}
如果键字符串为一个奇数则将其最高位(转换为int)直接加到一个2字节变量里面,然后键长度变为偶数,然后将剩下的键的偶数位加到变量的低八位,奇数位加到变量的高八位。这样就将一个字符串映射成一个整数了,并且不同的字符串映射成同一个整数的可能极小。
第二种转换:
template<>
calss hash<string>
{
public:
size_t operator()(const string theKey)const
{//把关键字theKey转换成一个非负整数
unsigned long hashValue=0;
int length=(int)theKey.length();
for(int i=0;i<length;i++)
hashValue=5*hashValue+theKey.at(i);
return size_t(hashValue)
}
};
上面为C++的STL模板类hash实现散列的一个专业版,它的转换方法非常直接和暴力,直接一个简单的线型变换搞定。
现在介绍第二个问题:怎么将这个整数通过函数映射到一个线性表中
首先建立一个线型表
pair<const K,E>** table;
hash<K> hash;
int dSize;
int divisor;
template<class K,class E>
hashTable<K,E>::hashTable(int divisor)
{
divisor=theDivisor;
dSize=0;
//分配和初始化散列表函数
table=new pair<const K,E>* [divisor];//table每个成员是个指向pair的指针
for(int i=0;i<divisor;i++)
table[i]=NULL;//初始化指针为NULL
}
我们用到的映射公式为f(k)=k%D;我们将整数为K的键,通过取余映射到长度为D的数组里面。当K>D时,就会发生这样的问题即不同的两个键映射到了同样的数组元素里面,这就是所谓的冲突,发生冲突时我们一般采取一种叫做线型探查的方法进行解决。(一般设计的数组的一个元素里面能够容纳很多个pair对,不存在冲突,但是我们这里的设定是,一个元素只能容纳一个pair对,默认会发生冲突)上面函数创建了一个长度为D的数组。
线型探查:如果两个不同的键映射到了相同的数组元素(一般称为起始桶),那么我们就将后加入的那么元素,放到离该元素最近的右边的一个空元素里面,这就叫做线型探查。(认为数组是循环的即,数组的首尾相连)
template<class K,class E>
int hashTable<K,E>::search(const K& theKey)const
{//搜索一个公开地址散列表,查看关键字为theKey的数对
//如果匹配数对存在,返回它的位置,否则,如果散列表不满,
//则返回关键字为theKey的数对可以插入的位置。
int i=(int)hash(theKey)%divisor;//hash()将键映射成一个整数,%divisor找到起始桶的位置
int j=i;//从起始桶开始
do{
if(table[j]==NULL||table[j]->first==theKey)
return j;//如果起始位置为空或者起始位置存储的pair就是我们查找的则返回这个位置
j=(j+1)%divisor;//下一个桶,到最后一个元素后会从第一个元素开始循环
}while(j!=i);
return j;//表满j=i.
}
以上就是线型探查的程序。
template<class K,class E>
pair<const K,E>* hashTable<K,E>::find(const K& theKey)const
{//返回匹配数对的指针
//如果匹配数对不存在,返回NULL
//搜索散列表
int b=search(theKey);
//判断table[b]是否匹配成功
if(table[b]==NULL||table[b]->first!=theKey)
return NULL;//没有找到
return table[b];//找到匹配数对
}
template<class K,class E>
void hashTable<K,E>::insert(const pair<const K,E>& thePair)
{//把数对thePair插入字典。如存在相同的数对,则覆盖
//若表满,则抛出异常
int b=search(thePair.first);
if(table[b]==NULL)
{
tale[b]=new pair<const K,E>(thePair);
dSize++;
}
else
{//检查是否有重复的关键字数对或者是否表满
if(table[b]->first==thePair.first)
{
table[b].second=thePair.second;
}
else
throw hashTableFull();
}
}
以上是查找和插入的程序。
散列和普通线性表的效率公式如下:
U(n)=1/2(1+1/(1-a)^2)
S(n)=1/2(1+1/(1-a)) (散列)
其中a=n/b为负载因子。一般而言散列效率高
D的选取:
理想的D是一个素数,或者是一个不能被小于20的数整除的数。
链表实现
链表实现就是将数组的每个元素都当做一个链表头,将数组存在这些个链表里面这样就不会出现冲突了。
template<class K,class E>
pair<const K,E>* find(const K& theKey)const
{
return table[hash(theKey)%divisor].find(theKey);//因为在元素里面存的是一个链表,所以直接用链表的find方法
}
void insert(const pair<const K,E>& thePair)
{
int homeBucket=(int)hash(thePair.first)%divisor;
int homeSize=table[homeBucket].size();
table[homeBucket].insert(thePair);
if(table[homeBucket].size()>homeSize)
dSize++;//这个我插入了一个新数据难道还会变小不成。
}
void erase(const K& theKey)
{
table[hash(theKey)%divisor].erase(theKey);
}