数据结构——hash函数——hash函数基础

hash函数的引入

在介绍hash函数之前,先说个实际的例子。我是个比较乱的男生,袜子啊,书籍什么的都乱扔。那么哪天如果要找某件东西,在最坏的情况下,你需要找遍你房间的所有角落。但是,如果你是个爱收拾的男生,那么你要找某件东西的话,直接去对应的地方去寻找就好了。如果用算法复杂度表示,那么前者就是O(N)和后者是O(1)。我们现在思考,能不能将这样的结构用于数据结构当中呢?看下图: 
图一

那么这个映射有什么用呢? 
- 我能直接找到衣服存放的地方 
- 寻找效率提升了 
- 插入的效率也提升了 
- 删除的效率也提升了

假设你有n件衣服,那么上述操作的复杂度都是O(1)。我们把这种思想用到数据结构当中,我们把上图的右边看成一个个命名空间,那么我们把一个个不一样的字符串放进对应的空间中,像下面的图一样: 
这里写图片描述

那么我们应该怎么去实现这个stringMap功能呢? 
1. 我们要定义一个容器来装我们的数据,那么这个容器可以是数组 
2. 定义一个函数,这个函数将你要放入箱子的字符串映射到对应的空间的下标

因此我们可以这样的语句来定义一个动态的字符串数组:

string *buckets = new string[nBuckets];
  • 1

然后我们的对应规则可以是:

string.length() % 6
  • 1

就像下图这样: 
这里写图片描述

从Element ->int

将上述的结论推广到一般的形式:

<type> *buckets = new <type>[nBuckets];
  • 1

现在我们来看看我们应该将什么元素放入盒子中,并且我们实施查找的时候会发生什么。假设我们已经有一个键值对(“brother”,1)存在于map中,我们应该怎么实现当我们调用Map.get(“banter”)的时候,返回它对应的值1呢? 
这里写图片描述 
显然这里的???应该填写的是1. 从这里我们看到,我们可以把字符串通过某个hash算法,将它变成对应的数组的下标,然后将数值存入对应的数组空间。就像这样: 
这里写图片描述

这个时候我们的map中就有了这几个元素:: { “banter”: 1, “:)”:10, “C++”:42} 那么如果我们把(“Razzmatazzes”, 13)放入map中会发生什么呢?我们按照上面的流程算一下:

azzmatazzes.length() % 6 == brother.length() % 6
  • 1

此时计算所得的结果发现,两者的长度相等,把azzmatazzes插入势必会影响brother里面的内容。 
那我们应该怎么将这个单词插入map中,而不覆盖掉brother这个单词呢?一种可能的做法是,用下标来替代字符,下标 形式,而里面存储的是这些单词下标的数组,如图所示: 
这里写图片描述 
1 和 13分别代表的单词的下标。那么问题来了,那我们怎么知道哪个数字代表哪个单词呢?显然这一串数字就没有了什么意义了。那么到底应该怎么做呢?这里有个很好的方式: 
我们可以把字符串跟其下标进行绑定,形成一种结构(key - value)结构,将一对对的结构分别存入对应的方块中,有: 
这里写图片描述

如何进行hash函数声明?

hash函数要求: 
确定性:相同的输入总是给出相同的输出(类型) 
快速:快速运行 
分布均匀的输出 
因此上述的hash功能可以这样描述

// hashFunction(“banter”) is always 0
int hashFunction(const string &s) {  
    return s.length() % 6;
}
  • 1
  • 2
  • 3
  • 4

碰撞(Collisions)

如果我们按照每个事物都给一个空间存放,那么除非我有无限的空间,否则我不能保证每一个事物都具有自己的存储空间。当两个事物通过同一个hash函数映射到同一个空间的时候,就发生了碰撞。(就像上文提到的azzmatazzes 和brother)。但是碰撞未必就是坏事。 
我们常常使用装填因子(load factor )来描述hash表的装满程度。 
hash表的的加载因子是N / n 
N是map中的键数 
n是map中的空间数 
如果加载因子较低,并且散列函数分布良好,则操作为O(1) 
如果加载因子很高(N >> n),或散列函数分布不均,则操作为O(N),

那么装填因子太高了怎么办?这个时候我们就可能需要看看我们的hash函数了,如下图 
这里写图片描述 
我们的算法使得所有的单词都在0 - 5的空间中存放,这样词汇量多了,自然碰撞的概率大大提高,而下面的空间我们几乎没有用到。那么怎么才能让我们的空间更大呢?当然我们得更改我们压缩字符串长度的程度。

压缩函数

看下面一条语句

string.length() % nBuckets
  • 1

这个函数跟我们之前写的函数有什么不一样? 我们只是把%6换成了% nBuckets,现在我们想象一下,我们有一个map< int, int >类型的hash表,我们分析一下装载过程 
1. 一开始我们初始定义nBuckets为6,刚刚开始的时候,元素为空,于是有: 
这里写图片描述 
2. 这个时候我们试着往里面装元素(3,7),即调用map.put(3,7),应该是这样的情况: 
这里写图片描述 
此时,nBuckets = 6 nElems = 1 
3. 同理我们再添加元素(16,10),记录值 nBuckets: 6 nElems: 2 
这里写图片描述 
4. 我们调用map.get(16),则有 
这里写图片描述 
5. 那么我们此时输入(10,3)会出现什么?当然会出现碰撞,此时 nBuckets: 6 nElems: 3所以情况如下: 
这里写图片描述 
6.发生了碰撞,于是我们生成新的数列,其大小一般为旧数列的一半,然后将发生碰撞的值,放到对应的新空间,从而避免碰撞。 
这里写图片描述 
此时 nBuckets: 12 nElems: 3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值