目录
一、哈希的概念
顺序结构以及平衡树
中,元素关键码与其存储位置之间没有对应的关系,因此在
查找一个元素时,必须要经
过关键码的多次比较
。
顺序查找时间复杂度为
O(N)
,平衡树中为树的高度,即
O(
)
,搜索的效率取决 于搜索过程中元素的比较次数。
理想
的搜索方法:可以
不经过任何比较,一次直接从表中得到要搜索的元素
。
如果构造一种存储结构,通过
某种函数
(hashFunc)
使元素的存储位置与它的关键码之间能够建立一一
映射
的关系,那么在查找时通过该函
数可以很快找到该元素
。
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比
较,若关键码相等,则搜索成功
哈希函数的方法常用
除留余数法
哈希冲突(哈希碰撞)
上述图片中,若要是再插入一个44该怎么插入呢?44明显和4的值发生了冲突,处理好哈希冲突是哈希的一个重要的部分
那怎么处理好冲突呢?
二、开散列和闭散列
2.1闭散列
- 闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?
-
1. 线性探测
- 从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
-
插入通过哈希函数获取待插入元素在哈希表中的位置如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
删除
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他
元素的搜索。比如删除元素
4
,如果直接删除掉,
44
查找起来可能会受影响。因此线性探测采用
标 记
的伪删除法来删除一个元素。
代码实现
#include<iostream>
#include<vector>
using namespace std;
enum State { EMPTY, EXIST, DELETE };
template<class K,class V>
class HashTable
{
public:
struct Elem
{
pair<K, V> _val;
State _state;
};
public:
HashTable(size_t sz):m_ht(sz),m_size(0)
{
for (int i = 0; i < sz; ++i)
{
m_ht[i]._state = EMPTY;
}
}
public:
int Find(const pair<K, V>& key)
{
size_t hash_idx = Hash(key);
size_t origin_idx = hash_idx;
while (m_ht[hash_idx]._state == EXIST && key != m_ht[hash_idx]._val)
{
hash_idx = (hash_idx + 1) % m_ht.capacity(); //空间循环
if (hash_idx == origin_idx)
return -1;
}
if (m_ht[hash_idx]._state == EXIST)
return hash_idx;
return -1;
}
void Insert(const pair<K, V>& val)
{
CheckCapacity();
size_t idx = Hash(val);
size_t origin_idx = idx;//保留起始的地址
while (m_ht[idx]._state==EXIST)
{
idx = (idx + 1) % m_ht.capacity();//空间进行循环
if (idx == origin_idx)//说明转了一圈没找到空位,空间满了
return;//直接返回
}
//没有冲突
Elem e= { val,EXIST };
m_ht[idx] = e;
m_size++;
}
void Remove(const pair<K, V>& key)
{
int idx = Find(key);//先找到要删除的位置
if (idx != -1)
{
m_ht[idx]._state = DELETE;
m_size--;
}
}
int GetNextPrime(int cur_prime)
{
static int prime_table[] = { 7, 13, 19, 23, 29, 43, 53, 93, 103 };
int n = sizeof(prime_table) / sizeof(prime_table[0]);
int i;
for (i = 0; i < n; ++i)
{
if (cur_prime == prime_table[i])
break;
}
return i < n ? prime_table[i + 1] : prime_table[n - 1];
}
void CheckCapacity()
{
if (m_size * 10 / m_ht.capacity() >= 7) // 0.7
{
HashTable<K, V> new_ht(GetNextPrime(m_ht.capacity()));
for (size_t i = 0; i < m_ht.capacity(); ++i)
{
if (m_ht[i]._state == EXIST)
new_ht.Insert(m_ht[i]._val);
}
m_ht.swap(new_ht.m_ht);
}
}
protected:
size_t Hash(const pair<K, V>& val)
{
return val.first % m_ht.capacity();//除留余数法
}
private:
vector<Elem> m_ht;
size_t m_size;
};
-
2.2.开散列
- 开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。也就是说我们解决哈希冲突的方法变成了将节点组织成一个链表挂在哈希冲突的位置
完整代码
#include<iostream>
using namespace std;
template<class Type>
class HashTable;
//装有素数的数组
const size_t primeList[] = { 7,13,17,19,23};
size_t GetNextPrime(size_t prime)
{
int n = sizeof(primeList) / sizeof(primeList[0]);
for (int i = 0; i <n; ++i)
{
if (primeList[i] > prime)
return primeList[i];
}
}
template<class Type>
class HashNode
{
friend class HashTable<Type>;
public:
HashNode(Type d = Type(), HashNode<Type>* n = nullptr) :data(d), next(n)
{}
~HashNode()
{}
private:
Type data;
HashNode* next;
};
template<class Type>
class HashTable
{
public:
HashTable()
{
m_ht = new HashNode<Type> *[Hash_table_size];
memset(m_ht, 0, sizeof(HashNode<Type> *)*Hash_table_size);//全部初始化为空
m_size = 0;
m_bucket_count = Hash_table_size;
}
public:
void Show()const
{
for (int i = 0; i < m_bucket_count; ++i)
{
cout << i << " : ";
HashNode<Type>* p = m_ht[i];
while (p != nullptr)
{
cout << p->data << "-->";
p = p->next;
}
cout << "Nil " << endl;
}
}
HashNode<Type>* Find(const Type& key)
{
size_t idx = Hash(key);
HashNode<Type>* p = m_ht[idx];
while (p != nullptr && key != p->data)
p = p->next;
return p;
}
void Insert(const Type& v)
{
m_size++;
CheckCapacity();
//1、通过hahs函数求哈希地址
size_t idx = Hash(v);
//2、插入数据
HashNode<Type>* node = new HashNode<Type>(v);
node->next = m_ht[idx];
m_ht[idx] = node;
}
void Remove(const Type& key)
{
//查找
size_t idx = Hash(key);
HashNode<Type>* p = m_ht[idx];
HashNode<Type>* pr = nullptr;
while (p != nullptr && p->data != key)
{
pr = p;//前驱追下来
p = p->next;
}
if (p == nullptr)
return;//说明没有找到,直接返回就好了
if (pr == nullptr)//说明第一个就是要删除的节点
m_ht[idx] = p->next;
else
pr->next = p->next;
//删除
}
protected:
size_t Hash(const Type& key)
{
//除留余数法
return key % m_bucket_count;
}
protected:
void CheckCapacity()
{
//开散列最好的情况就是每条链只有一个节点,所以当
if (m_size > m_bucket_count)//这时候就需要去进行扩容了
{
size_t new_bucket_count = GetNextPrime(m_bucket_count);
HashNode<Type>** new_ht = new HashNode<Type> *[new_bucket_count];
memset(new_ht, 0, sizeof(HashNode<Type>*)*new_bucket_count);
for (int i = 0; i < m_bucket_count; ++i)
{
HashNode<Type>* p = m_ht[i];
while (p != nullptr)
{
m_ht[i] = p->next;
size_t hash_idx =p->data % new_bucket_count;//找到新的位置
p->next = new_ht[hash_idx];
new_ht[hash_idx] = p;
p = m_ht[i];
}
}
delete[]m_ht;//释放原来的空间
m_ht = new_ht;//指向新的空间
m_bucket_count = new_bucket_count;//更新桶的大小
}
}
protected:
enum { Hash_table_size = 7 };//默认桶的大小
private:
//HashNode<Type>* m_ht[Hash_table_size];
HashNode<Type>** m_ht;
size_t m_bucket_count;
size_t m_size;
};
void main()
{
int ar[] = {1, 9, 10, 8, 22, 20, 43, 32, 21, 4, 6, 76, 8, 9, 56, 54, 48, 25, 5};
//int ar[] = { 1, 9, 10, 8, 22, 20,43,32};
int n = sizeof(ar) / sizeof(ar[0]);
HashTable<int> ht;
for (int i = 0; i < n; ++i)
{
ht.Insert(ar[i]);
}
ht.Show();
}
1、开散列增容
桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容
我们在每次插入节点之前要多进行一步CheckCapacity的操作,来判断是否需要进行扩容
这里我们增容时不采用的像闭散列那样直接新创建一个对象然后调用对象中的insert函数然后遍历每一个节点将节点插入到新的开散列中,因为我们如果调用insert函数就会重新创建一遍节点那么当前已经存在的节点就没有利用到,这样不仅使得程序需要重新创建节点消耗时间而且如果没有对旧的节点进行释放那么还会造成内存泄漏。
2、当存储类型是非整数类型时
我们可以发现如果我们哈希中放入string类,但是string类又不是一个整数,那么我们想要用整数的方式获取当前string类要存入的位置是不可能的,所以我们必须要找到一个办法来将
字符串转化为整数
三、关于黑客闲着没事攻击你的哈希表
- 尽管我们上面已经尽量使哈希表中一个节点中所挂元素减少,但是如果现在有黑客知道了你解决哈希冲突的办法,和你的哈希函数,那么他针对你的哈希函数存入一堆元素使得这些元素都集中在一条链表上,那么我们又该怎么办呢?
解决方案:我们实现哈希其实就是要作为map和set的底层容器来实现map和set那么我们就可以自然的想到红黑树,如果这里有大部分元素集中在一条链表上,那么我们是否可以将这些元素转化为一颗红黑树,这样即使所有元素都集中在一条链表上那么我们最次的查找效率也就是红黑树的查找效率O(log2N)。具体实现就是给每条链表加一个阈值例如阈值为8链表中元素超过八个的时候我们就将链表结构转化为红黑树,如果此时元素被删除之后,删除元素剩下6个我们就将结构转化为链表(这是因为红黑树对红黑树的删除也需要复杂的旋转操作,我们当元素个数不是对查找效率影响不是那么大的时候我们就可以考虑将结构转化回来)。