一,简介
哈希表,又称散列表,是一种容器,它的底层一般是数组实现,它可以快速定位一个元素,具有O(1)的时间复杂度。说到这就不得不提,哈希表的原理:哈希表是采用了函数映射的原理,设有函数f(x) (x>=0),则对于任意一个数m(m>=0) ,则有key=f(m),其中key值便是数组中的下标(key>=0),则f(x)便称为哈希函数,在查找一个元素的时候我们便可以通过哈希函数,找到该元素所在的下标。
二,哈希函数的选择。(x>=0)
1,直接定址法
例如,1000个元素,从0开始,我们可以直接用key=f(x)=1000-x。
因为所有的元素都比1000要小。通过哈希函数可以得知1直接存在下标为999(1000-1)的位置。
2,平方取中法。
例如,121的平方为14641;122的平方为14884;此时我们可以取它们平方中的某几位作为它们的key值,例如f(121)=64;
f(122)=88.
3,除留取余法。(常用)
顾名思义,key=f(x)=x%m;
其中m的选择十分重要,它决定了哈希表中元素的分布是否均匀。
例如当我们将m选为2时,对于1,2,3,4,5,6这几个数字,它们的key值分别为f(1)=1%2=1;
f(2)=2%2=0;
f(3)=3%2=1;
f(4)=4%2=0;
f(5)=5%2=1;
f(6)=6%2=0;
此时我们会发现,对于不同的的数x,经过哈希函数将会有相同的key值,这种情况我们称之为哈希冲突,一个好的哈希函数会使造成冲突的可能性十分小。我们上面的例子就是由于m选择不当,使冲突极易发生。
在对m选取的时候我们一般遵循的是选取不大于哈希表容量的最大质数。
4,其他
三,哈希冲突的解决。
1,线性探测。
对于x,如果通过key=f(x),我们发现该位置上已经存有别的数据,则依次查找key+1,key+2…key+n (key+n小于哈希表的容量),当相应位置满足后将x存入相应的位置,结束。如果当key等于哈希表的容量时还没有找到合适的位置,则插入失败。
2,二次探测。
对于x,如果通过哈希函数key=f(x),、发现该位置上已经存在别的元素,则我们依次查找key+1*1,key+2*2……key+n*n (key+n*n小于哈希表的的容量)。
3,拉链法
在哈希表中每一个位置都存放的是类似链表的结点,每次当发生冲突时如果相应位置已经有别的元素存在,则将新的元素链在相应位置的最前面,再把原来已经存在的元素链到新元素的后面。
注:图片来自网络。
如上图所示,采用除留取余法,除数m选择为11,当有一个数为33时,由f(33)=x%m=33%11=0,但是在0位置上已经有别的元素了,于是采用拉链法的方法便是将33放在11所处的位置上,再把33的后面与11链上。
四,负载因子。
当哈希表中数据较少时,此时查找某个数据可以达到哈希表的最高效率即O(1),但如果当数据过多,达到哈希表容量的一半之后,此时产生冲突的可能就变得较大,若采用线性探测的方法,则最坏情况可能需要O(N),N为哈希表的容量。
因此有人提出了负载因子的概念,设变量q;
q=x/y; 其中x为哈希表中已存的数,y为哈希表的容量,如果当q>0.5时哈希表的查找效率便开始下降。其中q便是负载因子,一般选取0.5为最佳。
五,代码实现。
//哈希表线性探测的实现。
#include<iostream>
#include<vector>
using namespace std;
static size_t GetNextPrime(size_t value) //每次选取比当前哈希表容量小的最大素数
{
// 使用素数表对齐做哈希表的容量,降低哈希冲突
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
};
size_t i;
for (i = 0; i < _PrimeSize; i++)
{
if (_PrimeList[i] > value)
{
if (i == 0)
return _PrimeList[0];
else
return _PrimeList[i - 1];
}
}
if (i == _PrimeSize)
return _PrimeList[i - 1];
}
template<class T>
struct node
{
T data; //存放的数据
bool flag; //相应位置的标记,用来判断该位置是否存数了
};
template<class T>
class HashTable
{
private:
vector<node<T>> _a; //数组用vector来完成
size_t _key; //除数
size_t _size;//当前哈希表中存入的数的个数
public:
typedef node<T> Node;
HashTable(size_t n)
{
_a.resize(n);
_size = 0;
_key = GetNextPrime(n);
for (size_t i = 0; i < _a.size(); i++)
{
(_a[i]).flag = false;
}
}
bool isExpand() //判断是否需要扩容
{
size_t x = _size * 10; //存入的个数
size_t y = _a.size(); //哈希表容量
bool temp=( x / y > 5 ? true:false);
return temp;
}
void Expand() //扩容
{
int newSize = _a.size() * 2 + 1;
size_t newKey = GetNextPrime(newSize);
vector<Node> newVector;
newVector.resize(newSize);
for (size_t j = 0; j < newSize; ++j)
{
newVector[j].flag = false;
}
for (size_t i = 0; i < _a.size(); ++i)
{
if (_a[i].flag == true)
{
int j = (_a[i].data)%newKey;
newVector[j].data = _a[i].data;
newVector[j].flag = true;
}
}
swap(_a, newVector);
_key = newKey;
}
void Insert(const T& x)
{
if (isExpand())
Expand();
size_t i = x%_key;
if (i < _a.size())
{
if (_a[i].flag == true)
{
for (i = i + 1; i < _a.size(); i++)
{
if (_a[i].flag == false)
{
_a[i].data = x;
_a[i].flag = true;
++_size;
break;
}
}
}
else
{
_a[i].data = x;
_a[i].flag = true;
++_size;
}
}
else
{
//如果当插入的数远远比当前哈希表大的话,则直接return;或
//采用下面的方法,将它存在一个空的地方。
//产生该问题的原因是我们选取的最小质数为53,如果当前哈希表的长度为10,当
//存入11时便会造成越界问题,但实际上只要哈希表的容量大于该质数便不会出现这种情况。
/*for (size_t i = 0; i < _a.size(); ++i)
{
if (_a[i].flag == false)
{
_a[i].data = x;
_a[i].flag = true;
++_size;
break;
}
}*/
return;
}
}
void print() const
{
for (size_t i = 0; i < _a.size(); i++)
{
if (_a[i].flag == true)
{
cout << "key=" << i << " " << _a[i].data << endl;
}
}
}
size_t getKey() const
{
return _key;
}
bool find(int value) //查找某个元素
{
size_t i = value%_key;
if (_a[i].flag = true)
{
if (_a[i].data == value) //直接找到
return true;
else
{
for (size_t j = i + 1; j < _a.size(); j++) //线性探测
{
if (_a[j].data == value)
return true;
}
return false;
}
}
return false;
}
};
六,总结
1,哈希表之所以存在查找效率降低的问题是因为在冲突处理上,它使得本不同的两个元素出现经哈希函数映射出现在同一个位置的可能。
2,哈希表常用在海量数据处理上,给一定量的数据,查找符合条件的某几个,或1个。如果内存放的下的情况,我们可以直接采取map,hash_map数据结构来查找。当内存有限制的情况,可以使用哈希切分,将大文件平均分成几千份,然后查找每个小文件中满足情况的数据,再通过建堆找出这几千份文件中满足情况的数据。