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

hash函数的引入

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

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

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

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

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

string *buckets = new string[nBuckets];

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

string.length() % 6

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

从Element ->int

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

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

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

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

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

此时计算所得的结果发现,两者的长度相等,把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;
}

碰撞(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

这个函数跟我们之前写的函数有什么不一样? 我们只是把%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

猜你喜欢

转载自blog.csdn.net/redRnt/article/details/81170156