Hash总览

2024-05-09 22:18
文章标签 总览 hash

本文主要是介绍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总览的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/974650

相关文章

Redis的Hash类型及相关命令小结

《Redis的Hash类型及相关命令小结》edisHash是一种数据结构,用于存储字段和值的映射关系,本文就来介绍一下Redis的Hash类型及相关命令小结,具有一定的参考价值,感兴趣的可以了解一下... 目录HSETHGETHEXISTSHDELHKEYSHVALSHGETALLHMGETHLENHSET

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

康拓展开(hash算法中会用到)

康拓展开是一个全排列到一个自然数的双射(也就是某个全排列与某个自然数一一对应) 公式: X=a[n]*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[1]*0! 其中,a[i]为整数,并且0<=a[i]<i,1<=i<=n。(a[i]在不同应用中的含义不同); 典型应用: 计算当前排列在所有由小到大全排列中的顺序,也就是说求当前排列是第

hdu1496(用hash思想统计数目)

作为一个刚学hash的孩子,感觉这道题目很不错,灵活的运用的数组的下标。 解题步骤:如果用常规方法解,那么时间复杂度为O(n^4),肯定会超时,然后参考了网上的解题方法,将等式分成两个部分,a*x1^2+b*x2^2和c*x3^2+d*x4^2, 各自作为数组的下标,如果两部分相加为0,则满足等式; 代码如下: #include<iostream>#include<algorithm

usaco 1.2 Milking Cows(类hash表)

第一种思路被卡了时间 到第二种思路的时候就觉得第一种思路太坑爹了 代码又长又臭还超时!! 第一种思路:我不知道为什么最后一组数据会被卡 超时超了0.2s左右 大概想法是 快排加一个遍历 先将开始时间按升序排好 然后开始遍历比较 1 若 下一个开始beg[i] 小于 tem_end 则说明本组数据与上组数据是在连续的一个区间 取max( ed[i],tem_end ) 2 反之 这个

uva 10029 HASH + DP

题意: 给一个字典,里面有好多单词。单词可以由增加、删除、变换,变成另一个单词,问能变换的最长单词长度。 解析: HASH+dp 代码: #include <iostream>#include <cstdio>#include <cstdlib>#include <algorithm>#include <cstring>#include <cmath>#inc

整数Hash散列总结

方法:    step1  :线性探测  step2 散列   当 h(k)位置已经存储有元素的时候,依次探查(h(k)+i) mod S, i=1,2,3…,直到找到空的存储单元为止。其中,S为 数组长度。 HDU 1496   a*x1^2+b*x2^2+c*x3^2+d*x4^2=0 。 x在 [-100,100] 解的个数  const int MaxN = 3000

POJ 1198 双广+Hash

此题采用双广可从bfs的O(16^8)降低到O(2*16^4); 坐标0-7,刚好3位存储, 需要24位存储四个坐标(x,y),也就是[0,2^24) 。 很好的一题。 import java.io.BufferedReader;import java.io.InputStream;import java.io.InputStreamReader;import

C# Hash算法之MD5、SHA

MD5我们用的还是比较多的,一般用来加密存储密码。但是现在很多人觉MD5可能不太安全了,所以都用上了SHA256等来做加密(虽然我觉得都差不多,MD5还是能玩)。 还是跟上一篇说的一样,当一个算法的复杂度提高的同时肯定会带来效率的降低,所以SHA和MD5比较起来的话,SHA更安全,MD5更高效。 由于HASH算法的不可逆性,所以我认为MD5和SHA主要还是应用在字符串的"加密"上。 由于