目录
哈希概念
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素
时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即
O(),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立
一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:
- 插入元素
- 根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
- 搜索元素
- 对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快
哈希冲突
对于两个数据元素的关键字和
(i != j),有
!=
,但有:Hash(
) ==Hash(
),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
哈希函数
引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
哈希函数设计原则:
- 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
- 哈希函数计算出来的地址能均匀分布在整个空间中
- 哈希函数应该比较简单
常见哈希函数:
- 直接定址法--(常用)
- 取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
- 优点:简单、均匀
- 缺点:需要事先知道关键字的分布情况
- 使用场景:适合查找比较小且连续的情况
- 除留余数法--(常用)
- 设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
解决哈希冲突的方法
解决哈希冲突两种常见的方法是:闭散列和开散列
闭散列
线性探测
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE};
线性探测的实现
namespace OpenAddress
{
enum State
{
EMPTY=0,
EXIT,
DELETE
};
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
State _state = EMPTY;
};
template<class K,class V>
class HashTable
{
public:
bool Insert(const pair<K, V>& kv)
{
//扩容||判断载荷因子是否大于0.7
if (_table.size() == 0 || _n * 10 / _table.size() > 7)
{
size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;
HashTable<K, V> newHash;
newHash._table.resize(newsize);
for (const auto& e : _table)
{
newHash.Insert(e._kv);
}
std::swap(*this, newHash);
}
size_t hashi = kv.first % _table.size();
size_t index = hashi;
size_t i = 1;
//找到对应的映射位置
while (_table[index]._state != EMPTY)//解决哈希冲突问题
{
index = (hashi + i) % _table.size();
++i;
}
_table[index]._kv = kv;
_table[index]._state = EXIT;
++_n;
return true;
}
HashData<K, V>* Find(const K& key)
{
if (_table.size() == 0)
{
return nullptr;
}
size_t hashi = key % _table.size();
size_t index = hashi;
size_t i = 1;
while (_table[index]._state == EXIT)
{
if (_table[index]._kv.first == key)
{
return &_table[index];
}
index = (hashi + i)%_table.size();
++i;
if (index == hashi)
{
return nullptr;
}
}
return nullptr;
}
bool Erase(const K& key)
{
HashData<K, V>* ret = Find(key);
if (ret == nullptr)return false;
ret->_state = DELETE;
--_n;
return true;
}
private:
vector<HashData<K,V>> _table;
int _n = 0;
};
void TestHashTable1()
{
int a[] = { 3, 33, 2, 13, 5, 12, 1002 };
HashTable<int, int> ht;
for (auto e : a)
{
ht.Insert(make_pair(e, e));
}
if (ht.Find(13))
{
cout << "13在" << endl;
}
else
{
cout << "13不在" << endl;
}
ht.Erase(13);
if (ht.Find(13))
{
cout << "13在" << endl;
}
else
{
cout << "13不在" << endl;
}
}
}
思考:哈希表什么情况下进行扩容?如何扩容?
二次探测
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位
置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法
为: = (
+
)% m, 或者:
= (
-
)% m。其中:i =1,2,3…,
是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。
开散列
开散列概念
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
开散列的实现
namespace HashBucket
{
template<class K,class V>
struct HashNode
{
pair<K, V>_kv;
HashNode<K, V>* _next = nullptr;
HashNode(const pair<K,V>&kv)
:_kv(kv)
{}
};
template<class K,class V>
class HashTable
{
typedef HashNode<K, V> Node;
public:
~HashTable()
{
for (auto e : _table)
{
Node* cur = e;
while (cur != nullptr)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
}
}
public:
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))return false;
if (_table.size() == 0 || _n / _table.size() == 1)
{
size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;
HashTable<K, V> newht;
newht._table.resize(newsize,nullptr);
for (auto& e : _table)
{
Node* cur = e;
while (cur != nullptr)
{
size_t hashi = cur->_kv.first % newht._table.size();
Node* next = cur->_next;
cur->_next = newht._table[hashi];
newht._table[hashi] = cur;
cur = next;
}
e = nullptr;
}
std::swap(*this, newht);
}
size_t hashi = kv.first %_table.size();
Node* newnode = new Node(kv);
newnode->_next = _table[hashi];
_table[hashi]=newnode;
++_n;
return true;
}
Node* Find(const K&key)
{
if (_table.size() == 0)return nullptr;
size_t hashi = key % _table.size();
Node* cur = _table[hashi];
while (cur != nullptr)
{
if (cur->_kv.first == key)return cur;
else cur = cur->_next;
}
return nullptr;
}
bool Erase(const K& key)
{
if (Find(key) == nullptr)return false;
size_t hashi = key % _table.size();
Node* cur = _table[hashi];
Node* pre = nullptr;
while (cur != nullptr)
{
if (cur->_kv.first == key)
{
if (pre == nullptr)
{
_table[hashi] = cur->_next;
}
else
{
pre->_next = cur->_next;
}
delete cur;
return true;
}
else
{
pre = cur;
cur = cur->_next;
}
}
return false;
}
size_t MaxBucketSize()
{
size_t max = 0;
int i = 0;
for (auto e : _table)
{
size_t size = 0;
Node* cur = e;
while (cur != nullptr)
{
++size;
cur = cur->_next;
}
printf("[%d]->%d\n", i++, size);
max = size > max ? size : max;
}
return max;
}
private:
vector<Node*>_table;
size_t _n = 0;
};
void TestHashTable1()
{
int a[] = { 3, 33, 2, 13, 5, 12, 1002 };
HashTable<int, int> ht;
for (auto e : a)
{
ht.Insert(make_pair(e, e));
}
ht.Insert(make_pair(15, 15));
ht.Insert(make_pair(25, 25));
ht.Insert(make_pair(35, 35));
ht.Insert(make_pair(45, 45));
if (ht.Find(2))cout << "yes" << endl;
else cout << "No" << endl;
ht.Erase(2);
if (ht.Find(2))cout << "yes" << endl;
else cout << "No" << endl;
}
void TestHashTable2()
{
size_t N = 900000;
HashTable<int, int> ht;
srand(time(0));
for (size_t i = 0; i < N; ++i)
{
size_t x = rand() + i;
ht.Insert(make_pair(x, x));
}
cout << ht.MaxBucketSize() << endl;
}
}
开散列增容
桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。
开散列的思考
- 只能存储key为整形的元素,其他类型怎么解决?
// 哈希函数采用处理余数法,被模的key必须要为整形才可以处理,此处提供将key转化为
整形的方法
// 整形数据不需要转化
template<class T>
class DefHashF
{
public:
size_t operator()(const T& val)
{
return val;}
};
// key为字符串类型,需要将其转化为整形
class Str2Int
{
public:
size_t operator()(const string& s)
{
const char* str = s.c_str();
unsigned int seed = 131; // 31 131 1313 13131 131313
unsigned int hash = 0;
while (*str)
{
hash = hash * seed + (*str++);
}
return (hash & 0x7FFFFFFF);
}
};
2. 除留余数法,最好模一个素数,如何每次快速取一个类似两倍关系的素数?
size_t GetNextPrime(size_t prime)
{
const int PRIMECOUNT = 28;
static const size_t primeList[PRIMECOUNT] =
{
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 = 0;
for (; i < PRIMECOUNT; ++i)
{
if (primeList[i] > prime)
return primeList[i];
}
return primeList[i];
}