02 余数-哈希函数
1. 余数的思想
所谓余数,就是程序中取模运算中的“模”。
余数具有一个非常重要的特性:可以将无限的数据归一到有限的范围(余数总是小于除数)
你知道,整数是没有边界的,它可能是正无穷,也可能是负无穷。但是余数却可以通过某一种关系,让整数处于一个确定的边界内。我想这也是人类发明星期或者礼拜的初衷吧,任你时光变迁,我都是以 7 天为一个周期,“周”而复始地过着确定的生活。因为从星期的角度看,不管你是哪一天,都会落到星期一到星期日的某一天里。
2. 同余定理
在上边的例子中,第一天与第八天都是周一,第二天与第九天都是周二,即
1%7=8%7=1
2%7=9%7=2
这就引出了余数的另一个重要概念:同余定理
口语化表述:
两个整数 a 和 b,如果它们除以正整数 m 得到的余数相等,我们就可以说 a 和 b 对于模 m 同余
其实,奇数与偶数的确定就是同余定理的应用。将一个数字模2,得0为偶数,得1为奇数
复杂算法拆解后的原理并不一定复杂,同余定理也可以作为有用的应用,就是哈希函数
3. 哈希函数(散列函数)
将任意长度的输入,通过哈希算法,压缩为某一固定长度的输出,所得存储位置为散列地址
散列过程
(1)存储记录时,通过散列函数记录散列地址,按地址存储记录
(2)查找记录时,通过同样的散列函数计算记录的散列地址,按散列地址访问记录
散列技术通过散列函数建立了从记录的关键码集合到散列表的地址集合的一个映射,显然,会出现两个不同记录存放在同一位置的情况,这种现象称为冲突,此时相同位置的记录称为同义词
散列函数中最常采用的方案是除留余数法,其基本思想:
选择适当的正整数P,以关键码除以P的余数作为散列地址
通常P为小于或等于表长(最好接近)的最小质数或不包含小于 20 质因子的合数
散列函数的例子
假如你想要快速读写 100 万条数据记录,要达到高速地存取,最理想的情况当然是开辟一个连续的空间存放这些数据,这样就可以减少寻址的时间。但是由于条件的限制,我们并没有能够容纳100 万条记录的连续地址空间,这个时候该怎么办呢?
我们就可以使用余数和同余定理来设计一个散列函数,并实现哈希表的结构,如下图
在这个公式中,x 表示等待被转换的数值,而 size 表示有限存储空间的大小,mod 表示取余操作。通过余数,你就能将任何数值,转换为有限范围内的一个数值,然后根据这个新的数值,来确定将数据存放在何处。
具体来说,我们可以通过记录标号模 100 的余数,指定某条记录存放在哪个空间。这个时候,我们的公式就变成了这样:
假设有两条记录,它们的记录标号分别是 1 和 101。我们把这些模 100 之后余数都是 1 的,存放到第 1 个可用空间里。以此类推,将余数为 2 的 2、102、202 等,存放到第 2 个可用空间,将 100、200、300 等存放到第 100 个可用空间里。
这样,我们就可以根据求余的快速数字变化,对数据进行分组,并把它们存放到不同的地址空间里。而求余操作本身非常简单,因此几乎不会增加寻址时间。
除此之外,为了增加数据散列的随机程度,我们还可以在公式中加入一个较大的随机数 MAX,于是,上面的公式就可以写成这样
随机数MAX在解码的时候还要使用,如果使用Random函数会很麻烦,所以可以直接设定一个
DIVIDEND = 7
RAND = 590127
def encrypt(num):
if not isinstance(num, int):
raise TypeError("num is not 'int' object")
# int转为list
num = map(int, str(num))
# 对每位加上随机数
num = map(lambda i:i+RAND, num)
# 保存商和求余
quotient, num = zip(*[(i//DIVIDEND, i%DIVIDEND) for i in list(num)])
# 反转
num = list(num)[::-1]
print(list(num))
# list 转回 int
num = map(str, num)
# num = int(''.join(list(num)))
num = ''.join(list(num)) # 首位余数0则会去除,所以用str
# 返回加密数据和商
return (num, quotient)
def decrypt(num, quotient):
#if not isinstance(num, int):
# raise TypeError("num is not 'int' object")
# int转为list
num = map(int, str(num))
# 反转
num = list(num)[::-1]
# 商和余求值
for i,v in enumerate(num):
num[i] = v + quotient[i] * DIVIDEND
# 对每位减去随机数
num = map(lambda i:i-RAND, num)
# list 转回 int
num = map(str, num)
num = int(''.join(list(num)))
return num
if __name__ == '__main__':
num = 8251
print('加密', num)
en_num, q = encrypt(num)
print(f"加密后{en_num}, 商为{q} \n解密...")
de_num = decrypt(en_num, q)
print(de_num)