散列表(hash表)学习

保留余数法

我们知道用数组存数据的好处是直接存取,但是我们又没办法知道哪个元素到底在哪个位置,每次查找很不方便,那么我们能不能设计一个映射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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值