一、散列函数
散列表是表示集合和字典的另一种有效方法,通过将关键码映射到表中某个位置来存储元素,然后根据关键码用同样的方法直接访问。
散列方法使用的转换函数即散列函数,散列函数不能太复杂。
散列函数定义域必须包括全部关键码,值域必须在0到m-1之间(m为散列表允许的地址数),通常关键码集合比散列地址集合大得多,因此散列函数是一种压缩映像函数。对不同关键码,通过散列函数的计算可能得到同一散列地址,称这些散列地址相同的不同关键码为同义词。
如果key是从关键码集合中任意取出的一个关键码,散列函数应该能以同等概率取0到m-1中每一个值:
1.除留余数法
取一个不大于m,但最接近或者等于m的质数p作为除数。
质数p不能是接近2的幂,否则散列函数值仅仅是关键码最低k位数的值。
2.数学分析法
3.平方取中法
一般取散列表地址为8的某次幂。即m=8r。
先计算构成关键码的标识符的内码(每个字符用两个八进制数表示)的平方,然后按照散列表的大小取中间r位。
4.折叠法
把关键码自左向右分为位数相等的几部分,每一部分和散列表地址相等,最后一部分较短,叠加得到具有该关键码记录的散列地址。
一般关键码位数很多,而且关键码的每一位上数字分布大致比较均匀时使用。
有两种叠加方法:
(1)移位法
把各部分最后一位对齐相加。
(2)分界法
沿各部分分界来回折叠然后对齐相加。
二、溢出处理技术
将散列表HT的m个地址改为m个桶(bucket),桶号与散列地址一一对应。桶的大小通常比较小,因此在桶内一般采用顺序搜索。
1.闭散列法(开地址法)
所有的桶都直接放在散列表数组中,并把该数组组织成环形结构。每个桶中只有一个元素。
因此容易产生堆积问题,不同探查序列的关键码占据了可利用的空桶,使得为寻找某一关键码需要经历不同的探查序列的元素。
找到一个元素的比较次数与当初将它存入时的探查次数相等。
不能随便物理删除表中已有元素,否则会影响其它元素的搜索。
(1)线性探查法(相继溢出法)
(2)二次探查法
散列表的大小TableSize必须是满足4k+3的质数。
当表的长度TableSize为质数且表的装载因子α不超过0.5时,表的新项就一定可以插入,且任何一个位置不会被探查两次。
装载因子α超过0.5时应将表长度扩大一倍。
(3)双散列法
双散列法的思路是应用伪随机探查方法,使用一个伪随机数产生器(再散列函数)。
发生冲突时利用再散列函数ReHash(),根据元素的关键码key,计算该元素向后到达下一个桶的位移量。位移量的取值与key的值有关,应当为小于地址空间大小TableSize且与其互质(公约数只有1)的正整数。
最多经过m-1次探查会遍历表中所有位置回到H0。
使用闭散列法组织的散列表如下所示:
const int DefaultSize=100; enum KindOfStatus{Active,Empty,Deleted}; //元素分类(活动/空/删) template <class E,class K> class HashTable{ public: HashTable(const int d,int sz=DefaultSize); ~HashTable(){delete []ht; delete []info;} HashTable<E,K>& operator=(const HashTable<E,K>& ht2); bool Search(const K kl,E& el)const; bool Insert(const E& el); bool Remove(const K kl,E& el); void makeEmpty(); private: int divitor; //散列函数的除数 int CurrentSize,TableSize; //当前桶数、最大桶数 E *ht; //散列表存储数组 KindOfStatus *info; //状态数组 int FindPos(const K kl)const; int operator==(E& el){return *this==el;} int operator!=(E& el){return *this!=el;} }; template <class E,class K> HashTable<E,K>::HashTable(const int d, int sz) { divitor = d; TableSize = sz; CurrentSize = 0; ht = new E[TableSize]; info = new KindOfStatus[TableSize]; for (int i = 0; i < TableSize; i++) info[i] = Empty; } template <class E,class K> int HashTable<E,K>::FindPos(const K kl)const { //使用线性探查法搜索在一个散列表中关键码与kl匹配的元素位置 //搜索成功时移动次数与插入时移动次数相等,搜索不成功则返回插入位置,表满了则返回kl%divitor int i=kl%divitor; int j=i; do{ if(info[j]==Empty || info[j]==Active && ht[j]==kl) return j; j=(j+1)%TableSize; //当作循环表处理,找下一个空桶 }while(j!=i); return j; //转一圈回到开始点,表满了 } template <class E,class K> bool HashTable<E,K>::Search(const K kl, E& el) const { //使用线性探查法在根据关键码kl在散列表中搜索,如果存在则用引用参数el返回找到的值ht[i],并返回true。没找到或者表满了返回false int i=FindPos(kl); if(info[i]!=Active || ht[i]!=kl) return false; //可能info[i]==Empty或者表满了 el=ht[i]; return true; } template <class E,class K> void HashTable::makeEmpty() { for (int i = 0; i <TableSize; i++) { info[i]=Empty; } CurrentSize=0; } template <class E,class K> HashTable<E,K>& HashTable<E,K>::operator=(const HashTable<E, K> &ht2) { if(this!=&ht2){ //防止自我复制 delete []ht; delete []info; TableSize=ht2.TableSize; ht=new E[TableSize]; info=new KindOfStatus[TableSize]; for (int i = 0; i <TableSize; i++) { ht[i]=ht2.ht[i]; info[i]=ht2.info[i]; } CurrentSize=ht2.CurrentSize; } return *this; } template <class E,class K> bool HashTable<E,K>::Insert(const E &el) { K kl = el; int i = FindPos(kl); if(info[i]!=Active){ //书上这段程序有一个问题,如果元素不在ht[kl%divitor],且状态为delete,并不会插入。 ht[i]=el; info[i]=Active; CurrentSize++; return true; } if(info[i]==Active && ht[i]==el){ cout<<"表中已有此元素,不能插入!"<<endl; return false; } cout<<"表已满,不能插入!"<<endl; //表已满时i=FindPos(kl)=kl%divitor,此时不能插入 return false; } //在闭散列情形下不能随便物理删除表中已有元素,否则会影响其它元素的搜索。 template <class E,class K> bool HashTable<E,K>::Remove(const K kl, E &el) { //在ht表中找到删除元素key,返回true,并在引用参数el中得到它 int i=FindPos(kl); if(info[i]==Active && ht[i]==kl){ el=ht[i]; info[i]=Deleted; CurrentSize--; return true; } else return false; //若表中找不到kl,或者它已经逻辑删除过,则返回false }
二次探查法:
template <class E,class K> int HashTable<E,K>::FindPos(const K kl){ //这里书上写得应该有点问题,我给改了一下 int i = kl%divitor; int k = odd = 0; //k为探查次数,odd为控制加减符号 int save0 = save1 = 0; while(info[i]==Active && ht[i]!=kl || info[i]==Deleted){//info[i]==empty或者找到了kl则终止循环 if(odd==0){ save1 = i; i = save0; k++; i=(i+2*k-1)%TableSize; //(k-1)^2和k^2之差为2*k-1 odd=1; } else{ save0 = i; i = save1; i=(i-2*k+1)%TableSize; odd=0; if(i<0)i=i+TableSize; } return i; } template <class E,class K> bool HashTable<E,K>::Insert(const E& el){ //插入时必须保证表的装载因子不超过0.5,否则必须进行分裂 K kl=el; int i=FindPos(kl),j,k; if(info[i]==Active) return false; ht[i]=el; info[i]=Active; if(++CurrentSize<TableSize/2) return true; E *OldHt=ht; KindOfStatus *oldinfo=info; int OldTableSize=TableSize; CurrentSize=0; TableSize=NextPrime(2*OldTableSize); //求大于某数的第一个素数 divitor=TableSize; ht=new E[TableSize]; if(ht==NULL){ cerr<<"存储分配失败!"<<endl; return false; } info=new KindOfStatus[TableSize]; if(info==NULL){ cerr<<"存储分配失败!"<<endl; return false; } for(j=0;j<TableSize;j++) info[j]=empty; for(i=0;i<TableSize;i++) if(oldinfo[i]==Active) Insert(OldHt[i]); delete []OldHt; delete []oldinfo; return true; } int NextPrime(int n){ if(n%2==0) n++; //偶数 for(;!IsPrime(n);n+=2); return n; } int IsPrime(int n){ for(int i=3;i*i<=n;i+=2) if(n%i==0) return 0; return 1; }
2.开散列法(拉链法)
当散列表经常变动时最好不使用闭散列来处理冲突,改用开散列法。
以搜索平均长度n/m的同义词子表代替了搜索长度为n的顺序表,搜索速度加快。
使用开散列法的散列表如下所示:
template <class E,class K> struct ChainNode{ E data; ChainNode<E,K> *link; } template <class E,class K> class HashTable{ public: HashTable(int d,int sz=defaultSize); ~HashTable(){delete []ht;} bool Search(const K kl,E& el); bool Insert(const E& el); bool Remove(const K kl,E& el); private: int divisor; int TableSize; ChainNode<E,K> **ht; ChainNode<E,K> *FindPos(const K kl); } template <class E,class K> HashTable<E,K>::HashTable(int d,int sz){ divisor=d; TableSize=sz; ht=new ChainNode<E,K> *[sz]; assert(ht!=NULL); } template <class E,class K> ChainNode<E,K> *HashTable<E,K>::FindPos(const K& kl){ //在散列表中搜索关键字为kl的元素,函数返回一个指向散列表中某个位置的指针。若元素不存在,返回NULL int j=kl%divisor; ChainNode<E,K> *p=ht[j]; while(p!=NULL && p->data!=kl) p=p->link; return p; }