本文主要是介绍Hash总览,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
一、原理
1、定义:把任意长度的输入,通过散列算法,变换成固定长度的输出,该输出就是Hash值;
散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,即不可能从散列值来唯一的确定输入;
对不同的关键字可能得到同一散列地址,这种现象称冲突。
越优秀的hash算法越会使输入值尽可能得到不同的输出值,即越少的产生冲突。
2、关于典型hashmap:
2.1、使用:对于c++,在c++11之前没有标准的hash容器的实现,而是依靠STL扩展实现,且需要自定义运算符重载。c++11开始STL标准库引入了hash容器。
对于c++11以前的实现的例子,使用起来相对比较麻烦:
#include <ext/hash_map>
#include <iostream>
#include <cstring>using namespace std;
using namespace __gnu_cxx;struct eqstr{bool operator()(const char *s1, const char *s2)const{return strcmp(s1,s2) == 0;}
};int main(){hash_map<const char *,int,hash<const char *>,eqstr> months;months["january"] = 31;months["february"] = 28;months["march"] = 31;cout << "march -> " << months["march"] << endl;
}
从c++11开始,STL正式支持hash容器,使用方式如同二叉排序树的map,
unordered_map,unordered_set,支持迭代器遍历。例子略。
java同样有典型的hash容器的实现,其他动态语言,如python的dict、php的array等均由hashmap实现。
2.2、hashmap原理:一般来说认为hash的查找的时间复杂度为O(1),但如果产生冲突的时候实际上并非真实的O(1);
hashmap是如何处理冲突的:
1、链表法:将相同hash值的对象组织成一个链表放在hash值对应的槽位;
2、开放地址法:通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位;
链表法实际应用的更多,所以hashmap的查找时间复杂度实际应该是:一开始HashMap里面没有出现hash冲突时,hashmap查找元素很快确实是O(1);发生冲突后,对应bucket 里存储的不再是一个 Entry而是一个 Entry 链,只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止。如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。
此外,hashmap的插入、查找的时间消耗,还需要包括hash值计算的时间,往往这个时间忽略不计了。
如果一个hashmap就这样的不断的冲突着,链表长度越来越大,肯定大幅度影响其插入、查找的效率,尤其随数据量越来越大,所以实际上hashmap会在容量到达一定程度时扩容,即rehash,目的就是增加总量避免冲突情况的越来越多。
这个"一定程度"对于hashmap就叫装载因子loadFactor,其实就是当前数据量占总容量的百分比阈值,即hashmap内数据量超过其当前容量的百分比阈值时(如0.6),会把总容量扩大,并且重新计算全部当前已存在元素的hash值并重新落位。
rehash是比较浪费时间的,至少是O(当前数据)的时间消耗,尤其已存在数据极大时。对于后台尤其分布式场景,更是整个系统扩容、容灾的一个根本问题。当增加或减少一个节点,原先对每个节点的hash值如果统统变化,会导致全部紊乱或被迫全部节点的数据均需要转移,这是不可以接受的。这就引入了一个能够尽可能减少损失范围的一致性hash。
这里说一下一致性hash,这是面试一个重点:
如一个"平台 + 多计算节点"的系统,平台需要发送query请求给节点,负载均衡的方式为,平台根据一个hash算法对query做hash,然后用hash值%节点个数,即取模分发给对应的节点;不论写入还是读取,都是这样搞;
忽然,需要加一个节点,或者某个节点死机不可用,导致节点个数变化,原先到各个节点的摸都得变,这时,必须平台用新的取模结果,无法读每个节点的已有数据了;
一致性hash,是对可能计算得出的hash值的范围,如32位正数,0到2^32-1这个范围,根据每个节点计算出的hash值,分割这个范围,比如一个例子:
一共5台机器,分别计算的hash值为100000、200000、300000、400000、500000,那么
0-100000:hash值落在次范围内的请求均算作机器1
100000-200000:hash值落在次范围内的请求均算作机器2
200000-300000:hash值落在次范围内的请求均算作机器3
300000-400000:hash值落在次范围内的请求均算作机器4
300000-2^32 - 1:hash值落在次范围内的请求均算作机器5
如果机器1挂了,100000这个分界点消失了,那么机器0-100000的数据的请求会落在下一个范围即 dao100000-200000,依然将无法正常读,但不会影响范围2、3、4、5,如果机器2也挂了同理。
也就是对应的请求会传递给下一个范围,这样就大幅度减少了受影响的范围。这就是一致性hash的核心思想。
但如果一个机器挂了就导致其请求全都涌入下一个机器,实践会发现下一个机器很快也要挂了,解决方式就是引入虚拟节点:
目的就是把挂了的机器的请求分担在其他正常机器中,比如一共5台机器,每个机器对应100个或1000个或更多的虚拟节点,其实就是故意给每个实体节点,造出更多的hash范围;
因为实际上hash值计算出来并非是按顺序的,比如机器1的全部虚拟节点计算出来的hash值也许是1000000,也有可能是99999999,散列的,这样每个机器"占据"的范围也都是散的,和其他机器的范围随机排列的,这样任何一个机器挂了,所有落在的子范围内的请求会传递给下一个范围,而下一个范围不一定是哪一台机器的,也就实现了负载均衡。当然,对那台机器的原有数据的读还是完蛋了,一致性hash解决的是新数据的写的负载均衡问题。
2.2、更广义的hash:
实际使用中,hash并非必须使用标准库的hashmap或是必须编写一个非常完备的hashmap实现,比如对于ASCII码字符的一些面试题相关的查找算法,仅仅一个"int hashmap[256]"就是hashmap,在海量数据里经常用到的bitmap也是一个hashmap,前缀树trie也是一个hashmap,而且trie对于大量字符串的插入查找的时间复杂度是O(常数),比起hashmap还需要计算hash值、可能存在的解决冲突,trie实际上更快一些。基于多hash实现的bloomfilter,更是一种"在容忍少量的错误下,大幅度减少空间的主要供查找功能的hashmap",在某些场景下适用,如rocksdb的查找优化就使用bloomfilter。
这些非典型hashmap是面试题的重点。一定注重hash的思想并运用,像"int hashmap[256]"这种是某些数组、字符串相关面试题经常用到的技巧,bitmap、trie、bloomfilter经常是海量数据、字符串相关面试题的重点。
2.3、hash与树,以及更实际应用场景下的运用:
对一坨数据的各种查找,无非是hash和树,它们分别代表了两个方向,一个是散列的方向,一个是排序后对数复杂度的方向。
对于它们的比较:
1、hash需要在插入前就提供一片数组空间,供插入
树是不需要的,在插入时动态加入。不论是毫无翻转的简单二叉树、avl树、红黑树、b族树、......
也就是说hash依赖空间而树则不预先依赖,hash有更大的空间复杂度。
2、hash查找速度,可简单认为快于树
在数据量不大时,基本可以认为是这样的,"接近O(1) vs O(logN)";
在数据量很大时,hashmap出现冲突时,实际上说不准谁真的更快,但基本可以认为hashmap更快一些;
3、hash在一些小数据量场景的使用更显灵活技巧,如"int hashmap[256]";
4、hash无序,树有序,则hash无法使用中需要排序的场景,典型就是数据库的范围查找功能,mysql使用b族树存储数据和索引,redis的zset因需要范围查找也无法使用hash(但也没有使用树族,而是使用跳表);
5、在不涉及范围有序的场景中hash运用更广泛,因为结构更简单,速度也比树族更快一些;
6、海量数据处理的面试题,广泛使用hash族或间接使用树族,或hash+树族的混用,而不是直接使用树族;因为树族的精髓是排序,海量数据往往不能直接这么排
这篇关于Hash总览的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!