hash表
首先说hash函数,hash其实就是一个映射函数
y
=
H
a
s
h
(
x
)
y=Hash(x)
y=Hash(x),具体做的事情就是把一个值y(数字,字符串等)映射成一个数组hash_arr
的下标index
(数字),然后把这个值存到这个下标对应的位置。
H
a
s
h
Hash
Hash函数需要自己定义,hash_arr
也需要自己额外定义(大小自己看着办)
i = Hash(y) //值y通过hash函数映射成一个下标
hash_arr[i] = y // 将y存到这个下标对应的数组位置
Hash函数特点:
1.输入域无穷,输出域有限。例如:有无穷多个(在工程中可以具体到多少个,例如1000)输入参数经过hash函数映射后得到有限的输出域{1,2,3,4}。
2.输入参数确定,经过hash函数映射出的返回值一样。(不是随机函数,不同的输入参数可能得到相同的返回值)。
3.输入域上的值经过函数值映射后会几乎均等的分布在输出域上。
但是这样什么用呢,简单的一个例子,有一个数组
[
11
,
12
,
13
,
15
,
10
]
[11,12,13,15,10]
[11,12,13,15,10],我们现在想要查一下数字10是否在数组里。很简单,直接一个for循环遍历一下就可以,这样的复杂度为
O
(
N
)
O(N)
O(N)。hash函数怎么做的呢,假设定义一个大小为10的数组hash_arr
,hash函数为
y
=
x
m
o
d
6
y = x \ mod\ 6
y=x mod 6(hash函数和数组大小都需要自定义的),11对6取余为5,将11存到hash_arr[5]
的地方
hash(11) = 5
hash_arr[5] = 11
再看12,12对6取余数后为0
hash(12) = 0
hash_arr[0] = 12
再看13,13对6取余数后为1
hash(13) = 1
hash_arr[1] = 13
下一个数字是15,对6取余数后为3
hash(15) = 3
hash_arr[3] = 15
下一个数字是10,对6取余数后为4
hash(10) = 4
hash_arr[4] = 10
这样原来的数组就可以存储为
值 | 12 | 13 | 15 | 10 | 11 | |||||
---|---|---|---|---|---|---|---|---|---|---|
坐标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
我们现在想要查一下10是否存在,直接对10进行hash映射即
h
a
s
h
(
10
)
=
4
hash(10) = 4
hash(10)=4,然后查找hash_arr[4]
,发现数组是存在的。假如要查找数字8在不在,
h
a
s
h
(
8
)
=
2
hash(8) = 2
hash(8)=2,hash_arr[2]
是空的,也就是说8是不再数组里的。我们只需要查找一次就可以了,复杂度为
O
(
1
)
O(1)
O(1)。
说白了,hash函数其实就是将一个数值映射到一个下标,并存储到这个下标到位置,查找的时候直接取找这个下标里值就可以了
紧接着就会出现问题,两个不同的数会映射在一个地方吗,看看数字12和18
hash(12) = 0
hash(18) = 0
hash_arr[0] = (12 or 18)
现在问题来了,12和18都映射在同一个地方0,那么0这个位置该存储什么呢,动动脑子就知道,所有6的倍数都会映射在0,要是只能存一个数据,那hash表真的没有任何价值了,因为无法解决映射冲突的问题。所以,可以把数组存储的地方改成拉链结构
数组index | 列表 |
---|---|
0 | [6,12,18,24,30…] |
1 | [7,13,19,25,31…] |
2 | [8,14,20,26,32…] |
3 | [9,15,21,27,33…] |
4 | [10,16,22,28,34…] |
5 | [11,17,23,29,35…] |
大致就是这样的,链表形式是这样的
这样,通过hash我们先找到12或18的位置,然后再去查询是否存在。
查找过程中,关键码的比较次数,取决于产生冲突的多少,产生的冲突少,查找效率就高,产生的冲突多,查找效率就低。因此,影响产生冲突多少的因素,也就是影响查找效率的因素。影响产生冲突多少有以下三个因素:
1. 散列函数是否均匀;
2. 处理冲突的方法;
3. 散列表的装填因子。
如何处理冲突的方法这里就不说了,具体请参考解决hash冲突的三个方法和Hash算法的讲解。
hash底层原理
Hash的应用
有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
最简单的解决办法就是把所有的数据都加载到内存,然后直接hashset解决就行了,但是这里有一个要求内存限制大小为1M,真实的世界中我们的电脑通常又4G,然而处理的数据其实可能又1T,殊途同归而已。那么这个问题该怎么解决呢?
大的文件无法加载到内存中,那么切割成小的文件就可以直接加载了。总量有1G,每次最多可以加载1M,也就是1G/1M = 1024个文件,(每个文件中有1M/16B=2^16个单词),当然不可能吧1M都加载进来,这里咱们可以吧小文件适当增多一些,比如切割成2000个文件,每个文件大小为524KB。然后把每个单词通过hash函数进行映射,相同的单词存储到一个小文件中,不同的单词大概率放到了不同的文件中,一个文件中有不同的单词着很正常,因为hash存在冲突在正常不过了。这里其实会有一个问题,某个单词数量非常多,这个单词会映射到同一个文件中,导致1个小文件无法完全存储,这就需要我们继续向下拆分,例如第一次分割完之后存在了一个2M的文件,我们需要再次将2M的文件进行切割,变成4个524KB的小文件,最好可以递归检测文件大小进行切割。
不同的单词可能存储在一个小文件中,但是不同小文件中的单词一定是不同
这个其实很好理解,因为hash函数映射后的值是有限的,而输入是无限的,你想输啥就输啥,这就导致不同的输入对应的是同一个输出,但是不同的输出对应的一定是不同输入
如果不同的输出对应了同一个输入,这样hash函数其实就是有问题的,想想函数的一个特性,多对1,但是没有1对多
我这里写一个简单的文件切割,不考虑小文件大于1M的这种case了。
with open('1G.txt') as lines:
for line in lines:
y = hash(line.strip())
with open('{}.txt'.format(y)) as f:
f.write(line)
通过hash函数把所有的单词都打散在了各个小文件中,然后我们通过hashset对各个小文件中的单词进行聚合
for i in range(2000):
with open('{}.txt'.format(i)) as lines:
word_dict = {}
for line in lines:
word = line.strip()
if word in word_dict:
word_dict[word] += 1
else:
word_dict[word] = 1
还得再啰嗦一遍,因为我会忘记,不同的文件中word_dict
中的单词一定是不同的。
然后维护一个最大堆,把每个小文件中频次出现最多的100个单词拿出来,一个一个输到堆中,留下的最大的100就是了
题目描述:给A,B两个文件,各存放50亿条URL,每条URL占用64个字节,内存限制为4G,找出A,B中相同的URL。
这个问题可能简单一点,同样的可以把AB这两个文件切分成众多的小文件,例如2000个吧,我随便说的,具体要自己计算。A1,A2,…A2000,B1,B2,…B2000,然后通过hash函数把每个url分到各个小文件中,同样不同小文件中的url一定是不同的,所以我们只需要比较A1和B1中的url的就可以,因为A2和B1中的url肯定是不同的
一致性hash算法
假如有3台缓存服务器,每天都有大量的请求,我们希望能把这些请求分别派发到每个服务器上,这样可以减缓服务器的压力,我们把请求来的ip或者mac地址输入到hash函数中,得到hashcode(就是把一个字符串转化成数字),然后将hashcode % 3映射到3台服务器上。
hashcode = Hash(ip/mac)
server_y = hashcode % 3
例如有一个用户ip为192.168.1.110,通过hash函数后得到的hashcode为10,对3取余之后为1,即映射的服务器为1。
10 = Hash(192.168.1.110)
1 = 10 % 3
但是这样会有一个问题,例如3台缓存服务器不够用了,我们要增加两台服务器,这个时候,映射关系就会改变,原本映射1的现在突然就变成了0,用户就得跳过缓冲服务器,到服务器取请求数据。
10 = Hash(192.168.1.110)
0 = 10 % 5
这要是隔三差五的增加或删除,那基本都全乱套了。所以就需要白话解析一致性hash算法