写在前面
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( ),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
哈希冲突
对于两个数据元素的关键字 和 (i != j),有 != ,但有:Hash(i ) == Hash(j ),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。发生哈希冲突该如何处理呢?
哈希冲突解决
解决哈希冲突两种常见的方法是:闭散列和开散列
闭散列的实现
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?
类的成员变量:
private:
vector<elem> _ht;
int _size;
/////////////////////////////////////////
enum state{
empty,
exist,
deleted
};
typedef struct elem{
pair<K, V> val;
state sta;
}elem;
//1.闭散列
#include<map>
#include<vector>
#include<utility>
#include<iostream>
using namespace std;
const int PRIMECOUNT = 28;
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
};
enum state{
empty,
exist,
deleted
};
template<class K,class V>
class hash_1{
typedef struct elem{
pair<K, V> val;
state sta;
}elem;
public:
hash_1(int n = 3)
:_size(0)
, _ht(n)
{
for (int i = 0; i < _ht.capacity(); ++i){
_ht[i].sta = empty;
}
}
bool insert(const K& key){
check_capacity();
size_t _hashaddr = hash_func(key);
pair<K, V>_val = { key, _hashaddr }; //pair类型的变量初始化
size_t _start = _hashaddr;
while (_ht[_hashaddr].sta == exist)
{
if (_ht[_hashaddr].sta == exist && _ht[_hashaddr].val.first == key)
return false;
_hashaddr++;
if (_hashaddr == _ht.capacity())
_hashaddr == 0;
if (_hashaddr == _start)
return false;
}
//如果为空,可以直接插入
_ht[_hashaddr].val = _val;
_ht[_hashaddr].sta = exist;
++_size;
return true;
}
void check_capacity(){
//增容的条件是: α>=0.7
if (_size*10/_ht.capacity()>=7){
hash_1<K, V>newht(getnextprime(_ht.capacity()));
for (int i = 0; i < _ht.capacity(); ++i){
if (_ht[i].sta == exist){
newht.insert(_ht[i].val.first);
}
}
Swap(newht);
}
}
int getnextprime(size_t n){
for (int i = 0; i < PRIMECOUNT; ++i){
if (primeList[i]>n)
return primeList[i];
}
return 0;
}
int find(const K& key){
//如果找到了,返回下标;没有找到就打印“不存在”;
int _hashaddr = hash_func(key);
size_t start = _hashaddr;
while ( _ht[_hashaddr].sta != empty ){
if (_ht[_hashaddr].sta == exist && _ht[_hashaddr].val.first == key)
return _hashaddr;
++_hashaddr;
if (_hashaddr == _ht.capacity()){
_hashaddr = 0;
}
if (_hashaddr == start)
{
cout << "不存在" << endl;
return -1;
}
}
cout << "不存在" << endl;
return -1;
}
void erase(const K& key){
int index = find(key);
if (index != -1){
_ht[index].sta = deleted;
++_size;
}
return;
}
void Swap(hash_1<K, V>& ht){
swap(_ht, ht._ht);
swap(_size, ht._size);
}
private:
size_t hash_func(const K& key){
return key % _ht.capacity();
}
private:
vector<elem> _ht;
int _size;
};
/////////////////////////////////////////华丽分割////////////////////////////////////////////////////////////
开散列的实现
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
类的成员变量:
private:
vector<Node *> _ht;
size_t _size;
///////////////////////////////////////////////
template<class v>
struct node{
node(const v& data)
:_val(data),
_pnext(nullptr)
{}
int _val;
node<v>* _pnext;
};
typedef node<V> Node;
typedef node* PNode;
//哈希---开散列--哈希桶
//开散列的实现
template<class v>
struct node{
node(const v& data)
:_val(data),
_pnext(nullptr)
{}
int _val;
node<v>* _pnext;
};
template<class V, class HF = DefHashF<T> >
class HashBucket
{
typedef node<V> Node;
typedef node* PNode;
public:
//构造函数
HashBucket(size_t capacity = 3)
: _size(0)
{
_ht.resize(GetNextPrime(capacity), nullptr);
}
//哈希桶中的元素插入----- 哈希桶中的元素不能重复
PNode* Insert(const V& data)
{
// 确认是否需要扩容。。。
// _CheckCapacity();
// 1. 计算元素所在的桶号
size_t bucketNo = HashFunc(data);
// 2. 检测该元素是否在桶中
PNode pCur = _ht[bucketNo];
while (pCur)
{
if (pCur->_data == data)
return pCur;
pCur = pCur->_pNext;
}
// 3. 插入新元素
pCur = new Node(data);
// 采用头插法插入,效率高
pCur->_pNext = _ht[bucketNo];
_ht[bucketNo] = pCur;
_size++;
return pCur
}
// 删除哈希桶中为data的元素(data不会重复),返回删除元素的下一个节点
PNode* Erase(const V& data)
{
size_t bucketNo = HashFunc(data);
PNode pCur = _ht[bucketNo];
PNode pPrev = nullptr;
//PNode pRet = nullptr;
while (pCur!=nullptr && pCur->_val!=data)
{
pPrev = pCur;
pCur = pCur->_pnext;
}
//要删除的节点不存在
if (pCur == nullptr)
return nullptr;
//如果是头删
if (pPrev == nullptr){
_ht[bucketNo] = pCur->_pnext;
return _ht[bucketNo];
}
//数据存在,并且非首元素
pPrev->_pnext = pCur->_pnext;
return pPrev->_pnext;
}
// 查找data是否在哈希桶中
PNode Find(const V& data)
{
size_t bucketNo = HashFunc(data);
PNode pCur = _ht[bucketNo];
while (pCur)
{
if (pCur->_data == data)
return pCur;
pCur = pCur->_pNext;
}
return nullptr;
}
size_t Size()const
{
return _size;
}
bool Empty()const
{
return 0 == _size;
}
void Clear()
{
for (size_t bucketNo = 0; bucketNo < _ht.capacity(); ++bucketNo)
{
PNode pCur = _ht[bucketNo];
while (pCur)
{
PNode pNext = pCur->_pNext;
delete pCur;
pCur = pNext;
}
}
_size = 0;
}
bool BucketCount()const
{
return _ht.capacity();
}
void Swap(HashBucket<V, HF>& ht)
{
swap(_ht, ht._ht);
swap(_size, ht._size);
}
~HashBucket()
{
Clear();
}
/*桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一
个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件
怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发
生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。*/
void _CheckCapacity(){
size_t bucketCount = BucketCount();
if (_size == bucketCount){
//增容
HashBucket<V, HF> newHt(getnextprime(_ht.capacity()));
for (int i = 0; i < newHt._ht.capacity; ++i)
newHt._ht[i] = nullptr;
for (int j = 0; j < _ht.capacity(); ++j){
PNode cur = _ht[j];
int hashNo = -1;
while (cur){
//取旧哈希桶i号桶的第一个节点
_ht[j] = cur->_pnext; //头删
//计算当前节点在新空间的桶号
hashNo = newHt.HashFunc(cur->_val); //计算在那个哈希桶中
//头插法将该节点插入新空间
cur->_pnext = newHt[hashNo];//连接新桶中的内容
newHt[hashNo] = cur;//将给定的第一个节点放入新空间
//取旧哈希桶i号桶的第next个节点
cur = ht[j]//cur取原链表中的下一个节点
}
}
}
newHt._size = _size;
this->Swap(newHt);
}
private:
size_t HashFunc(const V& data)
{
return HF()(data) % _ht.BucketCount();
}
private:
vector<Node *> _ht;
size_t _size; // 哈希表中有效元素的个数
};
开散列与比散列比较:
应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上: 由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。