散列(一)

  我们先来看一个问题:

   给出n个整数,再给出m个整数,请问这m个整数在n个整数中是否出现过,你可能会想将每个数字一个一个查找一边就好了,但是这样的话时间复杂度会很高O(nm),那么这里我们就可以用空间换时间:

   我们创建一个数组hashTable[x],初始化为false,在输入n个整数的时候将每个对应hashTable[x]改为true,然后在输入m个数字的时候看看每个数字在hashTable[x]数组中是否为true

   代码奉上:

#include<iostream>
using namespace std;
const int maxn = 10010;
bool hashTable[maxn] = { false };
int main()
{
	int n, m, x;
	cin >> n >> m;
	for (int i = 0; i < n; i++)
	{
		cin >> x;
		hashTable[x] = true;
	}
	for (int i = 0; i < m; i++)
	{
		cin >> x;
		if (hashTable[x] == true)
			cout << "出现过" << endl;
		else
		{
			cout << "没出现过" << endl;
		}
	}
	return 0;
}

这种方法跟桶排序有点像,直接把数字作为数组下标,但是如果数字很大很大,那么我们也会浪费很多空间,那么这时候我们就需要散列表(哈希表)了

什么是哈希表?

   哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表

记录的存储位置=f(关键字)

  这里的对应关系f称为散列函数,又称为哈希(Hash函数),采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(Hash table)。

  哈希表hashtable(key,value) 就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。(或者:把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
    而当使用哈希表进行查询的时候,就是再次使用哈希函数将key转换为对应的数组下标,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位

 

数组的特点是:寻址容易,插入和删除困难;

而链表的特点是:寻址困难,插入和删除容易。

  那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法——拉链法,我们可以理解为“链表的数组”,如图:



这种方法也叫拉链表方法,但是这个是怎样实现的呢?

   其实很简单,因为数字过大我们不可能直接把他作为数组下标,但是我们可以把这个数字做一些变形来让一个下标来代表一些数字,比如把这个一个数字对一个特定的数取余之后,但是可能这样的方法会导致一些数对应同一个下标,这样的情形叫做碰撞比如5mod3=2,但是8mod3=2,所以就把8作为下标2那个点的拉链接到5上,这样就形成了拉链表

  

元素特征转变为数组下标的方法就是散列法。散列法当然不止一种,下面列出三种比较常用的:

1,除法散列法 
最直观的一种,上图使用的就是这种散列法,公式: 
      index = value % 16 
学过汇编的都知道,求模数其实是通过一个除法运算得到的,所以叫“除法散列法”。

2,平方散列法 
求index是非常频繁的操作,而乘法的运算要比除法来得省时(对现在的CPU来说,估计我们感觉不出来),所以我们考虑把除法换成乘法和一个位移操作。公式: 
      index = (value * value) >> 28   右移,除以2^28。记法:左移变大,是乘。右移变小,是除。
如果数值分配比较均匀的话这种方法能得到不错的结果,但我上面画的那个图的各个元素的值算出来的index都是0——非常失败。也许你还有个问题,value如果很大,value * value不会溢出吗?答案是会的,但我们这个乘法不关心溢出,因为我们根本不是为了获取相乘结果,而是为了获取index。

3,斐波那契(Fibonacci)散列法

平方散列法的缺点是显而易见的,所以我们能不能找出一个理想的乘数,而不是拿value本身当作乘数呢?答案是肯定的。

1,对于16位整数而言,这个乘数是40503 
2,对于32位整数而言,这个乘数是2654435769 
3,对于64位整数而言,这个乘数是11400714819323198485

    这几个“理想乘数”是如何得出来的呢?这跟一个法则有关,叫黄金分割法则,而描述黄金分割法则的最经典表达式无疑就是著名的斐波那契数列,即如此形式的序列:01123581321345589144233,377610, 987, 1597, 2584, 4181, 6765, 10946,…。另外,斐波那契数列的值和太阳系八大行星的轨道半径的比例出奇吻合。

    对我们常见的32位整数而言,公式: 
            index = (value * 2654435769) >> 28

    如果用这种斐波那契散列法的话,那上面的图就变成这样了:


如果要查找的不是数字,而是一串字符串怎么办呢?

其实原理一样,只需要把字符串中的字母转化成数字在加起来代表这个字符串就行了,比如现在要一个apple转换成数字,a~z代表0~25,那么就是将26进制转换成10进制

 

int hashFunc(char s[],int len)
{
  int id=0;
  for(int i=0;i<len;i++)
  {
     id=id*26+(s[i]-'a');
  }
  return id;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值