保留余数法
我们知道用数组存数据的好处是直接存取,但是我们又没办法知道哪个元素到底在哪个位置,每次查找很不方便,那么我们能不能设计一个映射f(x),使得f(x)对应数组得一处下标,使用时只需要输入x就可以了?这就是今天得散列表。
首先构造函数f(x)有很多方法,比如
第一种: f(x) = x,直接把数据存入到它本身值的数组下标里,比如a[num] = num。但这样的弊端很明显,不说浪费空间吧,也只能存1e7左右的数据,再大一个数量级就会爆掉。因此存的数据也非常的小。
第二种:
平方取中法,这种方法是对关键码平方后,按照散列表的大小,取中间的若干位数组作为散列地址。对于位数不大,并且事先不知道关键码的分布可以使用这种方法。
第三种:
上面的平方其实还不是最完美的,我们直到黄金分割率,而与黄金分割率吻合的序列就是我们熟知的斐波那契数列,因此
1,对于16位整数而言,这个乘数是40503
2,对于32位整数而言,这个乘数是2654435769
3,对于64位整数而言,这个乘数是11400714819323198485
对我们常见的32位整数而言,公式:
index = (value * 2654435769) >> 28
第三种
就是下面代码实现用到的方法,也就是保留余数法,什么意思呢,我们可以找到一个数字p,存数据的时候存到data%p的位置,这样做的好处很明显,多大的数据都能存下来,而且空间浪费的多少取决于你的p。同样的,缺点也非常明显,就是两个数字如果取模p后相等怎么办,而且应该是一个接近表长并且是一个质数,如果它不是一个质数,并且p = m * n,那么所有含有因子为m或n的数字再取模后都是n或m的倍数,这大大增加了冲突的可能,还有就是模p相等怎么办。这就是接下来的二次探测法了。
二次探测法: 上面说可能会有两个数字取模p后相等的情况发生,这时候假设t = data % p, 然后对t进行t+=di,如果已经有数据,在进行t-=di {di = 12, 22, 32…},直到找到一个位置然后存数据。这种方法称为开放定址法,得到的散列表叫做闭散列表(二次探测实际上是一个定理,有兴趣的可以了解一下)
最后的问题: 上面的构造和插入都完事了,还有一个问题就是删除,删除操作需要我们再删除一个操作的时候不能影响其他的元素,并且删除后还可以再次被使用。这就要我们额外添加一个标记,删除后这个曾经有过元素的标记并不取消,这样再查找和它同类(取模相等算是同类)的时候可以当作跳板继续查找。
代码实现:
下面是我自己打的代码实现,不知道对错,如果错误忘斧正。
#include <iostream>
#include <cmath>
using namespace std;
int find(int);
int insert(int);
const int mod = 11;
struct node
{
int data;
bool flag;
node ()
{
data = 0;
flag = false;
}
};
struct node data[11];
bool insert(int n)
{
int t = n%mod;
if (find(n)) //插入前先判断这个元素再表中有没有,有的话就不能再插入
{
return true;
}
for (int i=0; i<=sqrt(mod); i++)
{
int q = t+i*i;
int p = t-i*i;
if (q<mod && !data[q].data)
{
data[q].data = n;
data[q].flag = true;
return true;
}
else if (p>=0 && !data[p].data)
{
data[p].data = n;
data[p].flag = true;
return true;
}
}
return false;
}
int find(int n)
{
int t = n%mod;
int m = sqrt(mod);
if (!data[t].flag) //如果这一类的第一个元素没有被使用过直接返回
{
return -1;
}
for (int i=0; i<=m; i++)
{
int p = t+i*i;
int q = t-i*i;
if (p<mod && data[p].data == n)
{
return p;
}
else if (q>=0 && data[q].data == n)
{
return q;
}
//发现两边的使用标记都没用过h或者越界了,说明这个元素不再这个表里直接返回
if ((!data[p].flag && !data[q].flag) || (p>=mod && q<0))
return -1;
}
return -1;
}
int Delete(int n)
{
int t = n%mod;
int m = sqrt(mod);
if (!data[t].flag)
{
return -1;
}
for (int i=0; i<=m; i++)
{
int p = t + i * i;
int q = t - i * i;
if (p<mod && data[p].data == n) //找到了
{
data[p].data = 0;
return p;
}
else if (q>=0 && data[q].data == n)
{
data[q].data = 0;
return q;
}
//发现两边的使用标记都没用过h或者越界了,说明这个元素不再这个表里直接返回
if ((!data[p].flag && !data[q].flag) || (p>=mod && q<0))
return -1;
}
return -1;
}
int main()
{
int n, m;
cin >> n;
for (int i=0; i<n; i++)
{
int num;
cin >> num;
if (!insert(num))
{
cout << num << "无法插入了" << endl;
}
}
for (int i=0; i<11; i++)
cout << data[i].data << " ";
while (cin >> m && m)
{
cout << Delete(m) << endl;
}
for (int i=0; i<11; i++)
cout << data[i].data << " ";
return 0;
}
拉链法
上面介绍了使用二次探测法防止冲突的发生,这里再介绍另外一种方法,叫做拉链法,使用拉链法构建出来的散列表叫做开散列表。
那么它是怎么做到的呢?上面我们提了一句,就是取模相等的数字我们给看作一类,因此,就可以把他们放在一个链表里面,如下图。
下面是我自己打的实现代码,如果有问题望斧正。
#include <iostream>
using namespace std;
const int mod = 11;
bool find(int);
void insert(int);
struct node //每一个节点
{
int data;
struct node *next;
node()
{
data = 0;
next = NULL;
}
};
struct list //整个主链的结构体
{
struct node *head;
list()
{
head = new node;
}
};
struct list data[11];
void insert(int n) //采用头插法,为了删除方便,我们要设置头节点
{
int t = n%mod;
struct node *p = new node;
if (!find(n))
{
p->data = n;
p->next = data[t].head->next;
data[t].head->next = p;
}
}
bool find(int n)
{
int t = n%mod;
struct node *p = data[t].head->next;
while (p)
{
if (p->data == n)
{
return true;
}
p = p->next;
}
return false;
}
bool Delete(int n)
{
int t = n%mod;
struct node *p = data[t].head;
while (p->next)
{
if (p->next->data == n)
{
struct node *s = p->next;
p->next = s->next;
delete s;
return true;
}
p = p->next;
}
return false;
}
int main()
{
int n;
cin >> n;
for (int i=0; i<n; i++)
{
int num;
cin >> num;
insert(num);
}
int m;
while (cin >> m)
{
cout << Delete(m) << endl;
}
return 0;
}