'''
本章简单讲讲哈希函数的实现,和大数据问题的处理方法
认识哈希函数
1.输入域是无穷的,输出域是有限的.
2.相同的输入会返回相同的输出
3.不同的输入可能会返回相同的输出(哈希碰撞,因为输入域是无穷的,输出是有限的)
4.输出是均匀的、离散的 (假设输出域是一个大圆圈,里面有n个输出,那么在这个圆圈中任意位置的同大小范围内的输出个数是几乎相同的)
换句话说,即使输入的值相似度很高,它们的输出差异性也会很大
那么通过哈希函数的性质,我们可以进一步发现:
如果现在有n个输入,通过哈希函数可以得到n个输出;
把着n个输出对m取余(模上m),把取余结果相同的看作一个集合,就可以把n个输入输出用m个桶分装起来;
如果能保证哈希函数的均匀性和离散型足够好,那么意味着分装桶的过程也是离散的、均匀的;
'''
# 现在有一个大文件,文件中有40亿个无符号的整数(范围是 0 ~ 2^32-1),如果只有1G的内存,请返回出现次数最多的数
#
# 首先我们先考虑用用哈希表去统计出现的次数,但是哈希表的存储方式是[key:value],至少需要4+4=8B(字节);
# 题目的最差情况是40亿个数全都不一样,那么内存是完全不够的(需要320亿字节将近30G)
#
# 可行的方法是,对文件中的数调用哈希函数,得到离散的40亿个哈希值,再对哈希值模上100(举例)得到0~99个不同的数,按照相同的为一组,分成100个文件;
# 每一哥小文件中有4千万个数,对每一个小文件使用哈希表统计词频,就只需要大概300M内存了
import random
'''
哈希表的实现和上面类似:
假设先设立一个初始大小为20的区域,现在添加一个记录key="abc" value="hello","abc"经过哈希函数得到一个哈希值,对哈希值模上20得到15;
在初始区域找到15这个位置,在15创建一个单链表,记录"abc"、"hello"
因为哈希函数的性质,可以认为每一条链是均匀变长的。
查找过程也是一样,假设需要查找"abc","abc"经过哈希函数得到一个哈希值,对哈希值模上20得到15;
在15这个位置进行遍历,如果找到了就返回他的值,没找到就返回没找到
问题来了,如果链子过长,哈希表的性能就不能做到O(1),怎样才能避免呢?
当发现某一条链的长度超过了一定数值(假设是8),根据哈希函数均匀离散的性质,其他链子长度也快到8了;
于是触发扩容,把原来20个区域内的值全部重新用之前同样的方法放到40(举例)的区域里面
计算哈希表的时间复杂度:
首先可以发现,得到哈希值、取余的时间复杂度都是O(1)的;
现在假设链表的长度为k,那么增改的时间复杂度就是O(K),如果k不会过长的话,可以近似于O(1);
接下来计算扩容的时间复杂度,假设需要添加n个记录且k<=2,从2开始需要扩容log(n)次,当k变大,实际扩容次数会变小,但是级别还是log(N)
每一次扩容都要对所有的数计算哈希值、取余、挂格子,时间复杂度为O(1)
所以扩容总代价为O(N*log(N)),所以单次代价理论上为O(log(N)),但是在实际使用过程中可以通过调整k的大小来做到O(1)
离线扩容技术:
当发现需要扩容的时候,继续使用老哈希表,在一个新的区域扩容,扩容完成后再接到新哈希表上,不占用用户在线的时间
'''
# 设计RandomPool结构
# 设计一种结构,在该结构中有如下三个功能,且时间复杂度都是O(1):
# 1.insert(key):将某个key加入到结构中,做到不重复加入
# 2.delete(key):将原本结构中的某个key移除
# 3.getRandom():等概率随机返回结构中的任何一个key
class RandomRool(object):
def __init__(self):
self.size = 0
self.value2index = {}
self.index2value = {}
def insert(self, value):
if value not in self.value2index:
self.size += 1
self.value2index[value] = self.size - 1
self.index2value[self.size - 1] = value
def getRandom(self):
randomindex = random.randint(0, self.size - 1)
return self.index2value[randomindex]
def delete(self, value):
# 用最后一条记录去堵删掉的那条记录的洞
if value in self.value2index:
self.size -= 1
index = self.value2index[value]
self.index2value[index] = self.index2value[self.size]
self.value2index[self.index2value[self.size]] = index
self.index2value.pop(self.size)
self.value2index.pop(value)
# 制作位图
# 如何用最少的空间表示是否出现过?
# 常规方法:用哈希表记录是否存在,需要8字节
# 位图:已知一格list占用8字节,可以用来表示1个信息,如果能让每个bit都表示一个信息,那么一格list就能表示64个信息,可以大大节省空间
class Bitmap(object):
def __init__(self, lenght):
# 64bit*lenght
self.bitmap = [0] * lenght # 这里不去考虑其他空间,只是一个理论演示
def status(self, value):
numindex = value / 64
bitindex = value % 64
numindex = int(numindex)
return (self.bitmap[numindex] >> bitindex) & 1 # 如果是1就返回1,如果是0就返回0
def change2One(self, value):
numindex = value / 64
bitindex = value % 64
numindex = int(numindex)
self.bitmap[numindex] = self.bitmap[numindex] | (
1 << (bitindex)) # 把1移到value对应的位置,只有value位置为1其他都为0,再做|运算,让value位置变成1
def change2Zero(self, value):
numindex = value / 64
bitindex = value % 64
numindex = int(numindex)
print(bin(self.bitmap[numindex]))
print(bin(~(1 << (bitindex))))
self.bitmap[numindex] = self.bitmap[numindex] & (
~(1 << (bitindex) & (0xffffffffffffffff))) # 把1左移到value对应的位置,在取反让value位置为0其他为1,再做&运算让value位置变成0
# python是有符号整数,所以要&上64个1才能得到预想的取反结果
'''
布隆过滤器
布隆过滤器就是一个大位图,0~m-1为位置,一共m个信息,占用m/8个字节数
假设现在需要制作一个100亿个url的黑名单,允许有极小的错误率(0.00001),怎样节省空间
把url通过哈希函数1得到哈希值out1,再对哈希值out1模上m得到index1,再长度为m的位图上把index1描黑;
再把url通过哈希函数2得到哈希值out2,再对哈希值out2模上m得到index2,再长度为m的位图上把index2描黑;
经过k个哈希函数,一个url可以描黑k个格子。
查询url时,同样经过k个哈希函数,得到k个index,再位图上查询这k个index的情况,如果全都被描黑过,则代表url再黑名单中;如果有一个index没有被描黑,则不在黑名单中
为什么会有失误率?
首先要理解失误率p、位图大小m和哈希函数个数k的意义,
当样本量和k一定,失误率p会随着m的增大而减少。
当样本量和m一定,失误率p会随着k的增大而先减少再增加。m越大越难被描黑完,失误率越小
如果k很小,一个样本的判断只根据很少的几个特征值就被判断了,所以k不能太小。
当k很大,m个格子很快就全部被描黑了,那么当一个不是黑名单的url查询的时候,因为位图全部被描黑了,所以也会误判
已知样本量N和预期失误率P,计算K、M值:
M=-(N*lnP)/(ln2)^2
上面这个100亿个样本的例子需要多少空间呢,-(100亿)*(ln0.00001)/(ln2)^2/8 ≈ 29953307429字节 ≈ 28G 。
K=ln2*M/N≈0.7*M/N,K单位bit,上面这个例子大概需要ln2* [-(100亿)*(ln0.00001)/(ln2)^2] /N ≈16.7,向上取整,K=17
实际失误率P=(1-e^[-(N*K)/M])^K
'''
'''
一致性哈希
选择哈希key时,尽量选择种类比较多,使高频中频低频都有数量的key,尽量使负载均衡
把数据哈希运算后模上服务器数可以较为均衡的分到每一个服务器,经典结构的问题是增加和减少服务器的代价都是全量的。
一致性哈希原理:
把服务器的某个特征值经过哈希函数的处理(ip地址、mac地址等等),依据哈希值把服务器放到一个想象的环上,因为哈希函数的离散性,大概率是均衡的。
对接收的请求数据取得哈希值,也放到这个环上,去顺时针最近的那个服务器中处理数据。
好处是增加或者减少服务器时,只需要分走或者释放最近的那个顺时针下一个服务器,其他服务器不需要动。
两个问题:
1.当机器很少时,不一定一开始就能把环均分
2.即便机器很少的时候均分了,但是再增加或者减少一台服务器的时候,又会变得不均分
解决方法:
虚拟节点:给每一个机器分配n个节点,让虚拟节点去抢环,实际上就是模拟有很多服务器,增大样本量,减少了不均分的概率。
好处是还可以根据每个服务器的具体性能分配不同的节点数
'''
python算法复习(五)----哈希函数及大数据问题
最新推荐文章于 2023-01-31 11:18:59 发布