什么是哈希表:
哈希表是一种存放键值对(<key, value>)的数据结构,又称散列表。
哈希表的作用:
能通过键快速地找到值,原理:哈希表的键值之间存在一种一对一的映射关系:哈希函数。
什么是哈希函数:
哈希函数就是哈希表的键值之间存在的关系。哈希函数为了减少搜索时间一般都是简单函数,但是可能会造成哈希冲突。
常见的哈希函数:
以数组为例,i代表这个键所对应的值存放在数组下标为i的位置。
①直接定制法:
取关键字的某个线性函数为散列地址:i = A*Key + B
适合查找比较小且连续的情况。
②除留余数法:
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:i = key% p,将key转换成哈希地址。
什么是哈希冲突:
当两个不同的键指向同一个值时就叫发生了哈希冲突,发生哈希冲突会导致根据键找到错误的值。
如何解决哈希冲突:
①闭散列(开放定址法)
这里要插入一个载荷因子的概念。载荷因子代表了哈希表中已存入数据占总空间的几分之几。当载荷因子接近于1时(一般大于0.7或0.8),就要考虑增大哈希表的空间大小,防止插入数据失败。
通过闭散列方法,当产生哈希冲突时,将key存放在下一个空位置处。闭散列有如下两个方法寻找下一个空位置。
闭散列方法较为简单便捷,但是容易产生数据“堆积”,即发生哈希冲突的数据将原本正常的数据位置占据了。而且在删除时不能直接删除数据,因为后面的数据可能是与该数据发生哈希冲突而存储的,如果删除了会使得后面的数据按照未发生哈希冲突的情况进行查找。所以删除要用带标记的伪删除法。
一、线性探测:
i = i + j(j = 1, 2, 3, ...)。其中i是存放的值的位置,j是下一个空位置。
二、二次探测:
i = i + j^2(j = 1, 2, 3, ...)。
#include<iostream>
#include<vector>
using namespace std;
enum assign { //用于标记哈希表中的元素状态
empty1 = 0, //为空,未存放元素
exist, //存放元素
cut //存放的元素被删除(伪删除)
};
template<class T1, class T2>
struct data1 { //封装了哈希表的元素
pair<T1, T2> k; //键值对
assign ass_ = empty1; //该位置的状态
};
template<class T1, class T2>
class haxi {
private:
int n = 0; //哈希表的大小
vector<data1<T1, T2> > hx; //哈希表
int yu;
int t_n = 0; //哈希表中已有的大小
public:
haxi(int n = 10) {
this->n = n;
this->yu = n;
extend(n);
}
int haxi_fun(T1 key) { //存放哈希函数,根据key值返回其在哈希表中的下标
return key % yu;
}
void extend(int t = 10) { //扩容函数
int newSize = n == 0 ? t : n * 2;
hx.resize(newSize);
n = newSize;
}
data1<T1, T2>* _find1(T1 key) { //查找该键是否被存储了。
int index = haxi_fun(key);
if (n < index) { return nullptr; }
while (hx[index].ass_ != 0) {
if (hx[index].ass_ == exist && hx[index].k.first == key) {
return &hx[index];
}
else {
index++; //线性探测
//index += i*i; //二次探测
}
index %= n;
}
return nullptr;
}
bool insert(data1<T1, T2> t1) { //将t1插入
int index = haxi_fun(t1.k.first);
if (t_n * 1.0 / n >= 0.7 || index >= n) { //索引大于现有空间,需要扩容
extend();
}
if (_find1(t1.k.first) != nullptr) {
return false; //已经有这个键了,不能插入。
}
int i = 1; // i是二次探测用的。
while (hx[index].ass_ != empty1) { //找到不产生哈希冲突的位置
index++; //线性探测
//index += i*i++; //二次探测
index %= n;
}
hx[index] = t1; //插入
hx[index].ass_ = exist;
t_n++; //记得要给加加。
}
bool erase(T1 key) { //带标记的伪删除
data1<T1, T2>* ret = find(key);
if (ret == nullptr) {
return false;
}
else {
t_n--; //存储的个数减一
ret->ass_ = cut; //伪删除
return true;
}
}
void print() { //输出哈希表中的数据
for (int i = 0; i < n; i++) {
if (hx[i].ass_ == exist) {
cout << hx[i].k.first << " "<< hx[i].k.second << " ";
}
}
cout << endl;
}
};
int main() {
haxi<int, int> a;
data1<int, int> b;
b.k = make_pair(1, 2);
a.insert(b);
a.print();
return 0;
}
②开散列(拉链法、链地址法)
开散列将经过哈希函数后具有相同地址i的集合归并在同一子集中,称为一个桶(即发生哈希冲突的元素放同一个桶里)。将桶中的每个元素用单链表串联起来,头结点放在单链表中。
#include<iostream>
#include<vector>
#include<list>
using namespace std;
template<class T1, class T2>
struct data1 { //封装了哈希表的元素
pair<T1, T2> k; //键值对
data1* next = nullptr; //在同一个位置的下一个节点
};
template<class T1, class T2>
class haxi {
private:
int n = 0; //哈希表的大小
vector<data1<T1, T2> > hx; //哈希表,采用链表的形式
int yu;
int t_n = 0; //哈希表中已有的大小
public:
haxi(int n = 10) {
this->n = n;
this->yu = n;
extend(n);
}
int haxi_fun(T1 key) { //存放哈希函数,根据key值返回其在哈希表中的下标
return key % yu;
}
void extend(int t = 10) { //扩容函数
int newSize = n == 0 ? t : n * 2;
hx.resize(newSize);
n = newSize;
}
data1<T1, T2>* _find1(T1 key) { //查找该键是否被存储了。
int index = haxi_fun(key);
if (n < index) { return nullptr; }
data1<T1, T2>* p = hx[index].next;
while (p != nullptr && p->k.first != key) { //关键值不对,但下标对了。
p = p->next;
}
return p; //找到了,或者没找到返回nullptr。
}
bool insert(data1<T1, T2> *t1) { //将t1插入
int index = haxi_fun(t1->k.first);
if (t_n * 1.0 / n >= 0.7 || index >= n) { //索引大于现有空间,需要扩容
extend();
}
if (_find1(t1->k.first) != nullptr) {
return false; //已经有这个键了,不能插入。
}
data1<T1, T2>* p = &hx[index];
while (p->next != nullptr) { //将其接到最后一个不为空的后面
p = p->next;
}
p->next = t1;
t_n++; //记得要给加加。
return true;
}
bool erase(T1 key){ //带标记的伪删除
data1<T1, T2>* ret = _find1(key);
if (ret == nullptr) { //没找到
return false;
}
else {
t_n--; //存储的个数减一
int index = haxi_fun(key);
data1<T1, T2>* p = hx[index].next;
data1<T1, T2>* q = &hx[index];
while (p->k.first != key) {
q = p;
p = p->next;
}
q->next = q->next->next;
return true;
}
}
void print() { //输出哈希表中的数据
for (int i = 0; i < n; i++) {
data1<T1, T2>* p = _find1(i);
int j = 0;
while (p != nullptr) {
cout << p->k.first << " " << p->k.second << " ";
p = p->next;
j = 1;
}
if(j) cout << endl;
}
cout << endl;
}
};
int main() {
haxi<int, int> a;
data1<int, int> b;
b.k = make_pair(1, 2);
a.insert(&b);
a.print();
a.erase(b.k.first);
a.print();
return 0;
}
③再哈希法
当使用哈希函数后发生哈希冲突时,对结果再调用另一个哈希函数直到不发生冲突的方法。
如:第一个哈希函数为 i = key%n; 发生哈希冲突后,第二个哈希函数为 i1 = 3i%(n+j); 其中j代表发生冲突的次数。
④公共溢出区法
除哈希表外再建立一个溢出表,当发生哈希冲突时,将冲突的将其存放在溢出表中。