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+树族的混用,而不是直接使用树族;因为树族的精髓是排序,海量数据往往不能直接这么排

   

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值