第四节–查找与哈希算法
0x01 常见查找算法
影响查找时间长短的主要因素有算法,数据存储和方式及结构。如果是以查找过程中被查找的表格或数据是否变动来分类,则可以分为静态查找(Static Search)和动态查找(Dynamic Search)
静态查找是指数据在查找过程中,该查找数据不会有添加,删除或更新等操作。动态查找则是指所查找的数据,在查找过程中会经常性添加,删除或更新
一.顺序查找法
1原理简介
顺序查找法又称线性查找法,是一种最简单的查找法。它的方法是将数据一项一项地按顺序逐个查找,所以不管数据顺序如何,都得从头到尾遍历一次。此法的优点是文件在查找前不需要进行任何的处理与排序,缺点为查找速度较慢
2.例子说明
3.程序说明
3.1功能要求
以随机数生成1–150之间的80个整数,然后实现顺序查找法的过程
3.2源程序
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# Author LQ6H
import random
val = 0
data = [0]*80
for i in range(80):
data[i] = random.randint(1,150)
while val!=-1:
find = 0
val = int(input("请输入查找键值(1-150),输入-1离开:"))
for i in range(80):
if data[i] == val:
print("在第%3d个位置找到键值[%3d]" %(i+1,data[i]))
find +=1
if find==0 and val!=-1:
print("-----没有找到[%3d]-----" %val)
print("数据内容为:")
for i in range(10):
for j in range(8):
print("%2d[%3d]" %(i*8+j+1,data[i*8+j]),end=" ")
print("")
3.3运行说明
请输入查找键值(1-150),输入-1离开:36
-----没有找到[ 36]-----
请输入查找键值(1-150),输入-1离开:12
-----没有找到[ 12]-----
请输入查找键值(1-150),输入-1离开:58
-----没有找到[ 58]-----
请输入查找键值(1-150),输入-1离开:69
-----没有找到[ 69]-----
请输入查找键值(1-150),输入-1离开:1
在第 70个位置找到键值[ 1]
请输入查找键值(1-150),输入-1离开:-1
数据内容为:
1[138] 2[100] 3[ 54] 4[ 89] 5[ 70] 6[ 79] 7[121] 8[120]
9[ 50] 10[135] 11[ 87] 12[143] 13[128] 14[ 74] 15[122] 16[150]
17[ 25] 18[100] 19[141] 20[ 72] 21[ 5] 22[123] 23[150] 24[ 64]
25[130] 26[141] 27[ 27] 28[ 71] 29[ 87] 30[ 60] 31[121] 32[143]
33[ 23] 34[148] 35[ 2] 36[ 91] 37[105] 38[ 90] 39[144] 40[132]
41[ 68] 42[ 29] 43[ 30] 44[ 51] 45[104] 46[ 6] 47[111] 48[ 75]
49[ 50] 50[ 95] 51[ 85] 52[ 72] 53[ 94] 54[ 41] 55[ 72] 56[112]
57[ 38] 58[ 16] 59[102] 60[ 97] 61[134] 62[ 48] 63[ 56] 64[110]
65[ 6] 66[116] 67[105] 68[147] 69[ 17] 70[ 1] 71[ 60] 72[ 56]
73[ 97] 74[ 82] 75[135] 76[125] 77[117] 78[ 61] 79[ 47] 80[ 38]
二.二分查找法
1.原理简介
如果要查找的数据已经事先排好序了,则可使用二分查找法来进行查找。二分查找是将数据分割成两等份,再比较键值与中间值的大小,如果键值小于中间值,可确定要查找的数据在前半段,否则在后半部分。如此分割数次直到找到或确定不存在为止
2.例子说明
3.程序说明
3.1功能要求
随机数生成1–150间的80个整数,然后实现二分查找法的过程与步骤
3.2源程序
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# Author LQ6H
import random
def bin_search(data,val):
low = 0
high = 49
while low<=high and val !=-1:
mid = int((low+high)/2)
if val<data[mid]:
print("%d介于位置%d[%3d]和中间值%d[%3d]之间,找左半边" %(val,low+1,data[low],mid+1,data[mid]))
high = mid -1
elif val>data[mid]:
print("%d介于位置%d[%3d]和中间值%d[%3d]之间,找右半边" % (val, mid + 1, data[mid], high + 1, data[high]))
low = mid+1
else:
return mid
return -1
val =1
data = [0]*50
for i in range(50):
data[i]=val
val = val+random.randint(1,5)
while True:
num = 0
val = int(input("请输入查找键值(1-150),输入-1结束:"))
if val == -1:
break
num = bin_search(data,val)
if num==-1:
print("------没有找到[%3d]" %val)
else:
print("在第%2d个位置找到[%3d]" %(num+1,data[num]))
print("数据内容为:")
for i in range(5):
for j in range(10):
print("%3d-%-3d" %(i*10+j+1,data[i*10+j]),end=" ")
print()
3.3运行结果
请输入查找键值(1-150),输入-1结束:58
58介于位置1[ 1]和中间值25[ 76]之间,找左半边
58介于位置12[ 37]和中间值24[ 73]之间,找右半边
58介于位置18[ 56]和中间值24[ 73]之间,找右半边
58介于位置19[ 58]和中间值21[ 62]之间,找左半边
在第19个位置找到[ 58]
请输入查找键值(1-150),输入-1结束:69
69介于位置1[ 1]和中间值25[ 76]之间,找左半边
69介于位置12[ 37]和中间值24[ 73]之间,找右半边
69介于位置18[ 56]和中间值24[ 73]之间,找右半边
69介于位置21[ 62]和中间值24[ 73]之间,找右半边
69介于位置22[ 65]和中间值23[ 70]之间,找左半边
69介于位置22[ 65]和中间值22[ 65]之间,找右半边
------没有找到[ 69]
请输入查找键值(1-150),输入-1结束:-1
数据内容为:
1-1 2-2 3-7 4-8 5-13 6-17 7-22 8-25 9-26 10-29
11-33 12-37 13-42 14-43 15-45 16-50 17-54 18-56 19-58 20-61
21-62 22-65 23-70 24-73 25-76 26-78 27-79 28-80 29-82 30-84
31-87 32-92 33-94 34-95 35-96 36-101 37-104 38-106 39-110 40-114
41-115 42-120 43-124 44-128 45-129 46-132 47-134 48-138 49-140 50-145
三.插值查找法
1.原理简介
插值查找法(Interpolation Search)又叫插补查找法,是二分查找法的改进版。它是按照数据位置的分布,利用公式预测数据所在的位置,再以二分法的方式渐渐逼近。使用插值法是假设数据平均分布在数组中,而每一项数据的差距相当接近或有一定距离比例。插值法的公式为:Mid=low+((key-data[low])/(data[high]-data[low]))*(high-low)
其中key是要查找的键,data[high]和data[low]是剩余待查找记录中的最大值和最小值
2.例子说明
3.程序说明
3.1功能要求
随机数生成1–150间的50个整数,然后实现插值查找法的过程与步骤
3.2源程序
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# Author LQ6H
import random
def interpolation_search(data,val):
low=0
high=49
print('查找过程中......')
while low<= high and val !=-1:
mid=low+int((val-data[low])*(high-low)/(data[high]-data[low])) #插值查找法公式
if val==data[mid]:
return mid
elif val < data[mid]:
print('%d 介于位置 %d[%3d] 和中间值 %d[%3d] 之间,找左半边'
%(val,low+1,data[low],mid+1,data[mid]))
high=mid-1
elif val > data[mid]:
print('%d 介于中间值位置 %d[%3d] 和 %d[%3d] 之间,找右半边'
%(val,mid+1,data[mid],high+1,data[high]))
low=mid+1
return -1
val=1
data=[0]*50
for i in range(50):
data[i]=val
val=val+random.randint(1,5)
while True:
num=0
val=int(input('请输入查找键值(1-150),输入-1结束:'))
if val==-1:
break
num=interpolation_search(data,val)
if num==-1:
print('##### 没有找到[%3d] #####' %val)
else:
print('在第 %2d个位置找到 [%3d]' %(num+1,data[num]))
print('数据内容为:')
for i in range(5):
for j in range(10):
print('%3d-%-3d' %(i*10+j+1,data[i*10+j]),end='')
print()
3.3运行结果
请输入查找键值(1-150),输入-1结束:76
查找过程中......
76 介于中间值位置 27[ 64] 和 50[141] 之间,找右半边
76 介于位置 28[ 69] 和中间值 30[ 77] 之间,找左半边
76 介于中间值位置 29[ 73] 和 29[ 73] 之间,找右半边
##### 没有找到[ 76] #####
请输入查找键值(1-150),输入-1结束:87
查找过程中......
87 介于中间值位置 31[ 78] 和 50[141] 之间,找右半边
在第 33个位置找到 [ 87]
请输入查找键值(1-150),输入-1结束:-1
数据内容为:
1-1 2-2 3-3 4-6 5-10 6-14 7-19 8-22 9-24 10-26
11-27 12-29 13-30 14-33 15-34 16-39 17-41 18-42 19-43 20-46
21-49 22-53 23-54 24-55 25-60 26-63 27-64 28-69 29-73 30-77
31-78 32-82 33-87 34-92 35-97 36-101 37-105 38-108 39-113 40-114
41-116 42-117 43-120 44-122 45-126 46-131 47-133 48-134 49-137 50-141
0x02 常见哈希法
哈希法是使用哈希函数来计算一个键值所对应的地址,进而建立哈希表格,然后依靠哈希函数来查找到各个键值存放在表格中的地址,查找速度与数据多少无关,在没有碰撞和溢出的情况下,一次读取即可完成
常见的哈希算法有除留余数法,平方取中法,折叠法及数字分析法
一.除留余数法
1.原理简介
最简单的哈希函数是将数据除以某一个常数后,取余数来当索引
2.例子说明
在一个有13个位置的数组中,只使用到7个地址,值分别是12,65,70,99,33,67,48。我们可以把数组内的值除以13,并以其余数来当数组的下标(作为索引),可以用以下式子表示:h(key)=key mod B
索引 | 数据 |
---|---|
0 | 65 |
1 | |
2 | 67 |
3 | |
4 | |
5 | 70 |
6 | |
7 | 33 |
8 | 99 |
9 | 48 |
10 | |
11 | |
12 | 12 |
二.平方取中法
1.原理简介
先计算数据的平方,之后再取中间的某段数字作为索引
2.例子说明
将数据存放在100个地址空间中,其操作步骤如下:
第一步:将12,65,70,99,33,67,51平方后如下144,4225,4900,9801,1089,4489,2601
第二步:取百位数和十位数作为键值,分别为14,22,90,80,08,48,60
第三步:存放数据
f(14) = 12
f(22) = 65
f(90) = 70
f(80) = 99
f(8) = 33
f(48) = 67
f(60) = 51
三.折叠法
1.原理简介
折叠法是将数据转换成一串数字后,先将这串数字拆成几个部分,再把它们加起来,就可计算出这个键值的Bucket Address(桶地址)
2.例子说明
有一个数据,转换成数字后为2365479125443,若以每4个数字为一个部分则可拆为2365,4791,2544,3。将这4组数字加起来后即为索引值:2365+4791+2544+3=9703—>桶地址
在折叠法中有两种做法,如上例直接将每一部分相加所得的值作为其bucket address,这种方法称为"移动折叠法"。哈希法的设计原则之一就是降低碰撞,如果希望降低碰撞的机会,就可以将上述每一部分数字中的奇数或偶数反转,再相加来取得其bucket address,这种改进的做法称为"边界折叠法"(folding at the boundaries)
0x03 碰撞与溢出问题的处理
在哈希法中,当标识符要放入某个通(Bucket,哈希表中存储数据的位置)时,若该桶已经满了,就会发生溢出(Overflow);另一方面哈希法的理想情况是所有数据经过哈希函数运算后都得到不同的值,但现实情况是即使所有关键字段的值都不相同,还是可能得到相同的地址,于是就发生了碰撞(Collision)问题
常见的处理算法有线性探测性,平方探测性,再哈希法
一.线性探测法
1.原理简介
线性探测法是当发生碰撞情况时,若该索引对应的存储位置已有数据,则以线性的方式往后寻找空的存储位置,一旦找到位置就把数据放进去。线性探测法通常把哈希的位置视为环形结构,如此一来若后面的位置已被填满而前面还有位置时,可以将数据放到前面
2.线性探测算法
def create_table(num,index):
temp = num % INDEXBOX
while True:
if index[temp]==-1:
index[temp]=num
else:
temp=(temp+1)%INDEXBOX
3.程序说明
3.1功能要求
以除留余数法的哈希函数取得索引值,再以·线性探测法来存储数据
3.2源程序
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# Author LQ6H
import random
INDEXBOX = 10
MAXNUM = 7
def print_data(data,max_number):
print(" ",end="")
for i in range(max_number):
print("[%2d]" %data[i],end="")
print()
def create_table(num,index):
temp = num%INDEXBOX
while True:
if index[temp]==-1:
index[temp]=num
break
else:
temp=(temp+1)%INDEXBOX
index = [None]*INDEXBOX
data = [None]*MAXNUM
print("原始数组值:")
for i in range(MAXNUM):
data[i]=random.randint(1,20)
for i in range(INDEXBOX):
index[i]=-1
print_data(data,MAXNUM)
print("哈希表内容:")
for i in range(MAXNUM):
create_table(data[i],index)
print("%2d =>" %data[i],end="")
print_data(index,INDEXBOX)
print("完成哈希表:")
print_data(index,INDEXBOX)
3.3运行结果
原始数组值:
[ 2][20][ 7][ 7][19][13][18]
哈希表内容:
2 => [-1][-1][ 2][-1][-1][-1][-1][-1][-1][-1]
20 => [20][-1][ 2][-1][-1][-1][-1][-1][-1][-1]
7 => [20][-1][ 2][-1][-1][-1][-1][ 7][-1][-1]
7 => [20][-1][ 2][-1][-1][-1][-1][ 7][ 7][-1]
19 => [20][-1][ 2][-1][-1][-1][-1][ 7][ 7][19]
13 => [20][-1][ 2][13][-1][-1][-1][ 7][ 7][19]
18 => [20][18][ 2][13][-1][-1][-1][ 7][ 7][19]
完成哈希表:
[20][18][ 2][13][-1][-1][-1][ 7][ 7][19]
二.平方探测法
1.原理简介
线性探测法有一个缺点,就是相类似的键值经常会聚集在一起,因此可以考虑以平方探测法来加以改进。在平方探测中,当溢出发生时,下一次查找的地址是(f(x)+i)mod B与(f(x)-i)mod B,即让数据值加或减i的平方
2.例子说明
数据值key,哈希函数f
第一次查找:f(key)
第二次查找:(f(key)+1^2)%B
第三次查找:(f(key)-1^2)%B
第四次查找:(f(key)+2^2)%B
第五次查找:(f(key)-2^2)%B
......
......
......
第n次查找:(f(key)+-((B-1)/2)^2)%B,其中,B必须为4j+3型的质数,且1<=i<=(B-1)/2
三.再哈希法
1.原理简介
再哈希法就是一开始就先设置一系列的哈希函数,如果使用第一种哈希函数出现溢出时就改用第二种,如果第二种也出现溢出就用第三种,一直到没有发生溢出为止
2.例子说明
数据:681,467,633,511,100,164,472,438,445,366,118
其中哈希函数为(此处的m=13)
f1 = h(key) = key MOD m
f2 = h(key) = (key+2) MOD m
f3 = h(key) = (key+4) MOD m
说明如下:
第一次:使用第一种哈希函数f1 = h(key) = key MOD m,得到的哈希地址如下:
681 ---> 5
467 ---> 12
633 ---> 9
511 ---> 4
100 ---> 9
164 ---> 8
472 ---> 4
438 ---> 9
445 ---> 3
366 ---> 2
118 ---> 1
第二步:其中100,472,438都发生碰撞,再使用第二种哈希函数f2 = h(key) = (key+2) MOD m,进行数据的地址
100 ---> h(100+2) = 102 mod 13 = 11
472 ---> h(472+2) = 474 mod 13 = 6
438 ---> h(438+2) = 440 mod 13 = 11
第三步:438仍发生碰撞问题,使用第三种哈希函数f3 = h(key) = (key+4) MOD m,重新进行438地址的安排
438 ---> h(438+4) = 442 mod 13 = 0
经过三次再哈希后,数据的地址安排如下:
位置 | 数据 | 位置 | 数据 |
---|---|---|---|
0 | 438 | 7 | null |
1 | 118 | 8 | 164 |
2 | 366 | 9 | 633 |
3 | 445 | 10 | null |
4 | 511 | 11 | 100 |
5 | 681 | 12 | 467 |
6 | 472 | 7 | null |