现在不懂没关系,积累多了就懂了。
转自某个勤劳的小蜜蜂https://blog.csdn.net/qq_24642743/article/details/80412398
还有一只:https://blog.csdn.net/yyyljw/article/details/80903391
another:https://blog.csdn.net/tayanxunhua/article/details/20528389
一、什么是哈希表
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表或哈希表。具体表现为:
存储位置=f(key)
数组的特点是:寻址容易,插入和删除困难;
而链表的特点是:寻址困难,插入和删除容易。
那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法——拉链法,我们可以理解为“链表的数组”,如图:
左边很明显是个数组,数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多。我们根据元素的一些特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素。
二、哈希表查找步骤
1.在存储时,通过散列函数计算记录的散列地址,并按此散列地址存储该记录。
2.当查找记录时,我们通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录。简单说,怎么存进去的,就怎么取出来。由于存取用的同一个散列函数,因此结果当然也是相同的。
因此,散列技术,既是一种存储方法,也是一种查找方法。散列表最适合的求解问题是查找与给定值相等的记录,但不适合能对应很多记录的情况。比如,一个班级的所有男同学,给定关键字为“男”去查找。另外,它也不适合范围查找。比如,查找一个班级18~22岁的同学,则在散列表中没法进行。还有获得记录中的最大值和最小值等结果也无法从散列表中计算出来等等。
三、哈希表的特点
一个好的hash函数应同时具备以下两个条件:
1.计算简单
可能设计一个算法可以保证所有的关键字都不会产生冲突,但是这个算法需要很复杂的计算,会耗费很多时间,这对于需要频繁地查找来说,就会大大降低查找的效率。因此,散列函数的计算时间不应该超过其他查找技术与关键字比较的时间。
2.散列地址分布均匀
散列地址均匀地分布在存储空间中,这样可以保证存储空间的有效利用,并减少了为处理冲突而耗费的时间。
优点:不论哈希表中有多少数据,查找、插入、删除(有时包括删除)只需要接近常量的时间即0(1)的时间级。实际上,这只需要几条机器指令。
哈希表运算得非常快,在计算机程序中,如果需要在一秒种内查找上千条记录通常使用哈希表(例如拼写检查器)哈希表的速度明显比树快,树的操作通常需要O(N)的时间级。哈希表不仅速度快,编程实现也相对容易。
如果不需要有序遍历数据,并且可以提前预测数据量的大小。那么哈希表在速度和易用性方面是无与伦比的。
缺点:它是基于数组的,数组创建后难于扩展,某些哈希表被基本填满时,性能下降得非常严重,所以程序员必须要清楚表中将要存储多少数据(或者准备好定期地把数据转移到更大的哈希表中,这是个费时的过程)。
四、哈希函数的构造
1.直接定址法
存储位置 = f(key) = a*key+b(a,b为常数)
这样的散列函数的优点就是简单、均匀,也不会产生冲突,但问题是这需要事先知道关键字的分布情况,适合查找表较小且连续的情况。
2.数字分析法
分析关键字的特点,选取关键字的一部分来计算散列存储位置的方法。数字分析法,通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀。
3.平方取中法
假设关键字是1234,那么它的平方就是1522756,再抽取中的3位就是227,用作散列地址。平方取中法比较适合于不知道关键字的分布,而位数又不是很大的情况。
4.除留余数法
存储位置 = f(key) = key mod p(p≤m,m为散列表长);
mod取模的意思。事实上,这方法不仅可以对关键字直接取模,也可以在折叠、平方取中后再取模。很显然,本方法的关键在于选择合适的p,p如果选得不好,就可能会容易产生冲突。因此,根据前辈们的经验,若散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。
5.随机数法
存储位置 = f(key) = random(key)(random是随机函数,key为随机函数的种子);
选择一个随机函数,取关键字的随机函数值为它的散列地址。当关键字的长度不等时,采用这个方法构造散列函数是比较合适的。
以上五种哈希函数构造方法,不论采用哪一种,都有可能存在冲突,因此处理hash冲突也是需要被考虑的。
五、冲突处理
1.开放定址法
一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。具体表现:
fi(key) = (f(key)+di) MOD m (di=1,2,3,......,m-1);
用开放定址法解决冲突的做法是:当冲突发生时,使用某种探测技术在散列表中形成一个探测序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探测到开放的地址则表明表中无待查的关键字,即查找失败。
比如说,我们的关键字集合为{12,67,56,16,25,37,22,29,15,47,48,34},表长为12。 我们用散列函数f(key) = key mod l2。
当计算前S个数{12,67,56,16,25}时,都是没有冲突的散列地址,直接存入:
计算key = 37时,发现f(37) = 1,此时就与25所在的位置冲突。于是我们应用上面的公式f(37) = (f(37)+1) mod 12 = 2。于是将37存入下标为2的位置。这其实就是房子被人买了于是买下一间的作法:。
接下来22,29,15,47都没有冲突,正常的存入:
到了 key=48,我们计算得到f(48) = 0,与12所在的0位置冲突了,不要紧,我们f(48) = (f(48)+1) mod 12 = 1,此时又与25所在的位置冲突。于是f(48) = (f(48)+2) mod 12=2,还是冲突……一直到 f(48) = (f(48)+6) mod 12 = 6时,才有空位,机不可失,赶快存入:
我们把这种解决冲突的开放定址法称为线性探测法。
从这个例子我们也看到,我们在解决冲突的时候,还会碰到如48和37这种本来都不是同义词却需要争夺一个地址的情况,我们称这种现象为堆积。很显然,堆积的出现,使得我们需要不断处理冲突,无论是存入还是査找效率都会大大降低。
考虑深一步,如果发生这样的情况,当最后一个key=34,f(key)=10,与22所在的位置冲突,可是22后面没有空位置了,反而它的前面有一个空位置,尽管可以 不断地求余数后得到结果,但效率很差。因此我们可以改进di = 12, -12, 22, -22,……, q2, -q2 (q <= m/2),这样就等于是可以双向寻找到可能的空位置。对于34来说,我 们取di即可找到空位置了。另外增加平方运算的目的是为了不让关键字都聚集在 某一块区域。我们称这种方法为二次探测法。
fi(key) = (f(key)+di) MOD m (di = 1^2, -1^2, 2^2, -2^2,……, q^2, -q^2, q <= m/2);
还有一种方法是,在冲突时,对于位移量 di 采用随机函数计算得到,我们称之为随机探测法。
此时一定会有人问,既然是随机,那么查找的时候不也随机生成办吗?如何可以获得相同的地址呢?这是个问题。这里的随机其实是伪随机数。伪随机数是说,如果我们设置随机种子相同,则不断调用随机函数可以生成不会重复的数列,我们在査找时,用同样的随机种子,它每次得到的数列是相同的,相同的 di 当然可以得到相同的散列地址。
fi(key) = (f(key)+di) MOD m (di是一个随机数列);
总之,开放定址法只要在散列表未填满时,总是能找到不发生冲突的地址,是我们常用的解决冲突的办法。
2.再散列函数法
对于我们散列表来说,我们事先准备多个散列函数。
fi(key) = RHi(key) (i=1,2,3...,k);
这里RHi就是不同的散列函数,把之前的5个哈希函数都可以用上。这种方法能够使得关键字不产生聚集,但相应地增加了计算的时间。
3.链地址法
将所有关键字为同义词的记录存储在一个单链表中,我们称这种表尾同义词子表,在散列表中只存储所有同义词子表的头指针。此时,已经不存在什么冲突地址的问题,无论有多少个冲突,都只是在当前位置给单链表增加结点的问题。链地址法对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保障。当然,也就带来了查找时需要遍历单链表的性能损耗。
4.公共溢出区法
将冲突的部分,单独存放在一个公共的溢出区里面。在查找时,对给定值通过散列函数计算出散列地址后,先于基本表的相应位置进行比对,如果相等,则查找成功;如果不相等,则到溢出表去进行顺序查找。如果相对于基本表而言,有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。
五、应用
1、Hash主要用于信息安全领域中加密算法,它把一些不同长度的信息转化成杂乱的128位的编码,这些编码值叫做Hash值. 也可以说,Hash就是找到一种数据内容和数据存放地址之间的映射关系。
2、查找:哈希表,又称为散列,是一种更加快捷的查找技术。我们之前的查找,都是这样一种思路:集合中拿出来一个元素,看看是否与我们要找的相等,如果不等,缩小范围,继续查找。而哈希表是完全另外一种思路:当我知道key值以后,我就可以直接计算出这个元素在集合中的位置,根本不需要一次又一次的查找!
举一个例子,假如我的数组A中,第i个元素里面装的key就是i,那么数字3肯定是在第3个位置,数字10肯定是在第10个位置。哈希表就是利用利用这种基本的思想,建立一个从key到位置的函数,然后进行直接计算查找。
3、Hash表在海量数据处理中有着广泛应用。
六、小结
如果没有冲突,散列表查找是所有查找算法中效率最高的,因为它的时间复杂度为O(1)。散列查找的平均查找长度取决于哪些因素呢?1.散列函数是否均匀。2.处理冲突的方法。3.散列表的装填因子α=填入表中的记录个数/散列表长度。α标志散列表的装满的程度.
题目:海量日志数据,提取出某日访问百度次数最多的那个IP
算法思想:分而治之+Hash
1.IP地址最多有2^32=4G种取值情况,所以不能完全加载到内存中处理;
2.可以考虑采用“分而治之”的思想,按照IP地址的hash(ip)%1024,把海量IP日志分别存储到1024个小文件中。这样,每个小文件最多包含4MB个IP地址;
3.对于每个小文件,可以构建一个ip为key,出现次数为value的Hash_map,同时记录当前出现次数最多的那个ip地址;
4.可以得到1024个小文件中的出现次数最多的ip,再依据常规的排序算法得到总体上出现次数最多的ip;