搜索结构之哈希表(线性探测法)

来源:互联网 发布:软件测试周期算法举例 编辑:程序博客网 时间:2024/05/17 20:31

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

若关键字为k,则其值存放在f(k)的存储位置上。由此,不需比较便可直接取得所查记录。称这个对应关系f为散列函数,按这个思想建立的表为散列表。

对不同的关键字可能得到同一散列地址,即k1≠k2,而f(k1)=f(k2),这种现象称为碰撞(英语:Collision)。具有相同函数值的关键字对该散列函数来说称做同义词。综上所述,根据散列函数f(k)和处理碰撞的方法将一组关键字映射到一个有限的连续的地址集(区间)上,并以关键字在地址集中的“像”作为记录在表中的存储位置,这种表便称为散列表,这一映射过程称为散列造表或散列,所得的存储位置称散列地址。

若对于关键字集合中的任一个关键字,经散列函数映象到地址集合中任何一个地址的概率是相等的,则称此类散列函数为均匀散列函数(Uniform Hash function),这就是使关键字经过散列函数得到一个“随机的地址”,从而减少碰撞。

散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位。散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位。

构造哈希函数需要注意以下几点:
1、哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域
2、哈希函数计算出来的地址能均匀分布在整个空间中
3、哈希函数应该比较简单

不管做什么事情都要达到优是不可能的,既要付出尽可能的少,又要得到大化的多,因此设计散 列函数,可以参看以下两个原则:
1、计算简单
2、散列地址尽量分布均匀

【常见哈希函数】

(1)直接定址法
取关键字的某个线性函数为散列地址,Hash(Key)= Key 或 Hash(Key)= A*Key + B,A、B为常数。

这样的散列函数优点是简单、均匀,也不会产生冲突,但问题是需要 事先知道关键字的分布情况,适合查找表较小且连续的情况。

(2) 除留余数法
设散列表中允许的地址数为m,取一个不大于m,但接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key % p p<=m,将关键码转换成哈希地址。

(3)平方取中法
假设关键字是1234,那么它的平方就是1522756,再抽取中间的3位就是227作为散列地址;再比如关键字是4321,那么它的平方就是18671041,抽取中间的3位就可以是671或者710用作散列地址。

平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况。

(3) 折叠法
折叠法是将关键字从左到右分割成位数相等的几部分(注意:后一部分位数不够时可 以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。

比 如:关键字是9876543210,散列表表长为三位,我们将它分成四组987|654|321|0|,然后将它们叠加求和987+654+321+0=1962,再求后3位得到散列地址为962。有时可能 这还不能够保证分布均匀,不妨从一段向另一端来回折叠后对齐相加。比如将987和321反转,再与654和0相加,编程789+654+123+0=1566,此时的散列地址为566。

折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况。

(4)随机数法
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中 random为随机数函数,通常应用于关键字长度不能采用此法。

(5) 数学分析法
分析一组数据,比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。

散列冲突处理方法

任何一种散列函数也不能避免产生冲突,因此选择好的解决冲突溢出的方法十分重要。为了减少 冲突必须对散列表加以改造。 设散列表HT有m个地址,将其改为m个桶(bucket)。其桶号与散列地址一一对应,第i(0<= i < m)个桶的桶号即为第i个散列地址。每个桶可存放s个表项,这些表项的关键码应互为同义词。如 果对两个不同表项的关键码用散列函数计算得到同一个散列地址,就产生了冲突,它们可以放在 同一个桶内的不同位置。只有当桶内所有s个表项位置都放满表项后再加入表项才会产生溢出。

(1)【闭散列法】
闭散列,也叫开地址法。所有桶都直接放在散列表数组中。因此每个桶只有一个表项(s = 1)。若设散列表中各桶的编址为0到m-1,当要加入一个表项R是,用它的关键码R.key,通过散列 函数hash(R.key)的计算,得到它的存放桶号j,但是在存放时发现这个桶已经被另一个表项R占 据了,这时不但发生了冲突,还必须处理该溢出,为此必须把R存放到表中“下一个”空桶中。 如果表未被装满,则在允许的范围内必须还有空桶。

简而言之:就是一旦发生冲突,就去寻找下 一个空的散列表地址,只要散列表足够大,空的散列地址总能找到。

找空桶的方法如下:

1、线性探查

设给出一组元素,它们的关键码为:37,25,14,36,49,68,57,11,散列表为HT[12],表的大小m = 12,假设采用Hash(x) = x % p; // (p = 11) 11是接近m的质数,就有:
Hash(37) = 4 Hash(25) = 3 Hash(14) = 3 Hash(36) = 3 Hash(49) = 5 Hash(68) = 2 Hash(57) = 2 Hash(11) = 0
采用线性探查法处理冲突

这里写图片描述

需要加入一个元素时,使用散列函数进行计算,确定元素的桶号H0,按此桶号查看该桶,如果是所 要搜索的元素的关键码,则说明表中已有此元素,不再进行此元素的插入,否则即为冲突,再查看紧 随其后的下一个桶,如果是空桶,则搜索失败,新元素插入即可。
在闭散列的情形下不能随便物理删除表中已有的元素。因为若删除元素会影响其他元素的搜索。

散列表的装填因子
散列表的装填因子定义为:α= 填入表中的元素个数 / 散列表的长度
α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,填入表中的元素较多,产生冲突的可能性就越大;α越小,填入表中的元素较少,产生冲突的可能性就越小

线性探查方法容易产生“堆积”问题,即不同探查序列的关键码占据了可利用的空桶,使得为寻 找某一关键码需要经历不同的探查序列的元素,导致搜索时间增加。因此,当散列表经常变动 时,好不要用闭散列法处理冲突,可以利用二次探查法可以改善上述的“堆积”问题,减少为完成 搜索所需的平均探查次数。

2、二次探查

使用二次探查法,在表中寻找“下一个”空桶的公式为: Hi = (H0 + i^2)%m, Hi = (H0 - i^2)%m, i = 1,2,3…,(m-1)/2 H0 = Hash(x)是通过散列函数Hash()对元素的关键码x进行计算得到的桶号,它是一个非负整数。 m是表的大小,它应该是一个质数。

假设数组的关键码为37,25,14,36,49,68,57,11,假设取m=19,这样可设定为HT[19],采用散列函 数Hash(x) = x % 19 Hash(37)=18 Hash(25)=6 Hash(14)=14 Hash(36)=17 Hash(49)=11 Hash(68)=11 Hash(57)=0 Hash(11)=11
采用二次探查法处理冲突:
这里写图片描述

研究表明当表的长度TableSize为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而 且任何一个位置都不会被探查两次。因此只要表中有一半的空的,就不会有表满的问题。在搜索时
可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5;如果超出必须考虑增容。

代码实现:

#include <iostream>using namespace std;#include<vector>#include<string>#include <assert.h>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);}enum State{    EMPTY,    DELETE,    EXIST};template<class K>struct _HashFun_{    size_t operator[](const K& key)    {        return key;     }};template<> //模板特化struct _HashFun_<string>{private:    static size_t BKDRHash(const char* str) //key为字符串时的哈希函数    {        unsigned int seed = 131; // 31 131 1313 13131 131313        unsigned int hash = 0;        while (*str )        {            hash = hash * seed + (*str++);        }        return (hash & 0x7FFFFFFF);    }};template<class K, class V>struct HashNode{    HashNode()        :_s(EMPTY)    {}    pair<K,V> _kv;    State _s;};template<class K, class V, class HashFun = _HashFun_<K>, bool IsLine = true>class HashTable{    typedef HashNode<K, V> Node;    typedef HashTable<K, V, HashFun, IsLine> Self;public:    HashTable(size_t size = 10)        :_size(0)    {        _table.resize(_GetNextPrime(size));    }    bool Insert(const K& key, const V& value)    {        CheckSize();  //检测容量        size_t HashAddr = HashFunc(key);        size_t index = HashAddr;        while(_table[index]._s == EXIST)        {            if(_table[index]._kv.first == key) //要插入的节点已经存在                return false;            if(IsLine)  //线性探测            {                index = DetectedLine(HashAddr);            }            else  //二次探测            {                size_t i = 1;                index = DetectedSquare(HashAddr, i);                i++;            }            if(index == _table.size())//如果探测到最后,就返回第一个结点继续探测                index = 0;        }        _table[index]._kv.first = key;        _table[index]._kv.second = value;        _table[index]._s = EXIST;        _size++;        return true;    }    pair<HashNode<K, V>*, bool> Find(const K& key)    {        size_t index = HashFunc(key);        size_t HashAddr = index;        HashNode<K, V>& elem = _table[index];        if(elem._kv.first != key)        {            index++;            if(index == _table.size())                index = 0;  //找到哈希表结尾,返回开始处,继续寻找            if(index == HashAddr)                return make_pair(&elem, false);        }        if(_table[index]._s == EXIST) //找到与key相等且存在的节点            return make_pair(&elem, true);        else   //找到与key相等但是不存在的节点            return make_pair(&elem, false);    }    bool Remove(const K& key)    {        pair<Node*, bool> ret = Find(key);        if(ret.second == true)        {            ret.first->_s == DELETE;            _size--;            return true;        }        return false;    }    size_t Size()    {        return _size;    }private:    size_t DetectedLine(size_t hashAddr)//线性探测    {        hashAddr += 1;        if(hashAddr == _table.size())            hashAddr = 0;        return hashAddr;    }    size_t DetectedSquare(size_t hashAddr, size_t i)//二次探测    {        hashAddr = hashAddr + (i<<2) +1;        if(hashAddr == _table.size())            hashAddr = 0;        return hashAddr;    }    size_t _GetNextPrime(size_t size)    // 使用素数表对齐做哈希表的容量,降低哈希冲突    {        const int _PrimeSize = 28;        static const unsigned long _PrimeList[_PrimeSize] =        {            52ul, 97ul, 193ul, 389ul, 769ul,            1543ul, 3079ul, 6151ul, 12289ul, 24593ul,            49157ul, 98317ul, 196613ul, 393241ul, 786433ul,            1572869ul, 3145739ul, 6291469ul, 12582917ul, 24165843ul,            50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,            1610612741ul, 3221225473ul, 4294967291ul        };        for (size_t i = 0; i < _PrimeSize; ++i)          {              if (_PrimeList[i] > size)              {                  return _PrimeList[i];              }          }         return _PrimeList[_PrimeSize-1];    }    void CheckSize()    {        if(_size*10/_table.size() >= 6)        {            _table.resize(_GetNextPrime(_size));            HashTable<K, V> ht;            for(size_t idx=0; idx<_table.size(); ++idx)            {                if(_table[idx]._s == EXIST)                ht.Insert(_table[idx]._kv.first,_table[idx]._kv.second);            }            this->Swap(ht);        }    }    void Swap(HashTable<K, V> ht)    {        swap(_size, ht._size);        _table.swap(ht._table);    }    size_t HashFunc(const K& key)    {        return key%_table.size();    }protected:    vector<HashNode<K, V>> _table;    size_t _size;};int main(){    HashTable<int, int> ht;    ht.Insert(25, 1);    ht.Insert(14, 2);    ht.Insert(36, 3);    ht.Insert(49, 4);    ht.Insert(68, 5);    ht.Insert(57, 6);    ht.Insert(11, 7);    ht.Insert(37, 8);    cout<<ht.Size()<<endl;    pair<HashNode<int, int>*, bool> ret = ht.Find(11);    cout<<ret.first->_kv.first<<endl;    ht.Remove(25);    ht.Remove(14);    cout<<ht.Size()<<endl;    system("pause");    return 0;}
原创粉丝点击