5.2 搜索
5.2.1 顺序搜索
def sequetialSearch(alist,item):
index=0
found=False
while index<len(alist) and not found:
if alist[index]==item:
found=True
else:
index=index+1
return found
对于无序列表,是比较次数为:
假设列表中的元素按升序排列。如果存在目标元素,那么它出现在n个位置中任意一个位置的可能性仍然一样大,因此比较次数与在无序列表中相同。
不过,如果不存在目标元素,那么搜索效率就会提高。
def orderedSequetialSearch(alist,item):
index=0
found=False
stop=False
while pos<len(alist) and not found and not stop:
if alist[index]==item:
found=True
else:
if alist[pos]>item:
stop=True
else:
pos=pos+1
return found
5.2.2 二分搜索
还可以进一步利用列表有序这个有利条件
进行二分搜索,这是一种分治的思想
两种情况下,都是针对一个更小的列表递归调用二分搜索函数,所以可写成下面的递归版本
alist=[1,4,5,6,7,8,9,10]
item=11
[8,9,10]
def binarySearch(alist,item):
if len(alist)==0:
return False
else:
minpoint=len(alist)//2
if alist[minpoint]==item:
return True
else:
if item<alist[minpoint]:
return binarySearch(alist[:minpoint],item)
else:
return binarySearch(alist[minpoint+1:],item)
注意python列表的这个特性:
a=[10]
print(a[1:])
返回值不报错,而是返回一个[]空列表
print(a[1])会报错
时间复杂度分析:
比较到就剩一个元素时,程序结束
此时
2的i次方等于n,所以,i=logn,二分搜索算法的时间复杂度是O(logn)
注意:
实际上在Python中,切片操作的时间复杂度是O(k)。这意味着若采用切片操作,那么二分搜索算法的时间复杂度不是严格的对数阶。所幸,通过在传入列表时带上头和尾的下标,可以弥补这一点。
实际使用中,如果先排序,在二分搜索也是有时间开销的,所以实际中要根据实际情况,综合考虑是使用顺序搜索还是先排序在使用二分搜索.
5.2.3 散列
通过散列构建一个查找时间复杂度为O(1)的数据结构。
散列表是元素集合,其中的元素以一种便于查找的方式存储。散列表中的每个位置通常被称为槽,其中可以存储一个元素。
用列表实现一个散列表:
散列函数将散列表中的元素与其所属位置对应起来
对散列表中的任一元素,散列函数返回一个介于0和m -1之间的整数
设计一个散列函数(取余函数):
h(item) = item%11
用散列函数计算出每个元素的散列值
将元素按照散列值放入散列表:
为了统计目前散列表的占用率,使用载荷因子
搜索目标元素时,仅需使用散列函数计算出该元素的槽编号,并查看对应的槽中是否有值。
因为计算散列值并找到相应位置所需的时间是固定的,所以搜索操作的时间复杂度是O(1)。
不过,还要考虑一种情况,散列冲突,也叫散列碰撞,就是两个元素有相同的散列值的时候该怎么办?
再次详细的定义一下散列表的相关概念!
1.散列函数
完美散列函数:可以确保将不同元素映射到不同的槽,但是,给定任意一个元素集合,没有系统化方法来保证散列函数是完美的
我们的目标是创建这样一个散列函数:冲突数最少,计算方便,元素均匀分布于散列表中.
常见散列函数方法:
取余函数法
折叠法(取余函数法的优化):
折叠法先将元素切成等长的部分(最后一部分的长度可能不同),然后将这些部分相加,用这个和得到散列值.
平方取中法((取余函数法的优化)):
先将元素取平方,然后提取中间几位数。如果元素是44,先计算442=1936,然后提取中间两位93,继续进行取余的步骤,得到5(93%11)
计算字符串的散列值:
def hash(astring,tablesize):
sum=0
for pos in range(len(astring)):
sum=sum+ord(astring[pos])
return sum%tablesize
这种算法,异序词得到的散列值是完全相同的.
可以用位置权重的方法来重写这个算法:
def hash(astring,tablesize):
sum=0
for pos in range(len(astring)):
sum=sum+ord(astring[pos])*(pos+1)
return sum%tablesize
2.处理冲突
冲突总是会发生的,合理的处理冲突是散列计算的重点.
解决方法:
开放定址法,它尝试在散列表中为冲突元素寻找下一个空槽或地址。由于是逐个访问槽,因此这个做法被称作线性探测
比如要把20放入散列表:
它的散列值对应9号槽。因为9号槽中已有元素,所以开始线性探测,依次访问10号槽、0号槽、1号槽和2号槽,最后找到空的3号槽。
一旦利用开放定址法和线性探测构建出散列表,即可使用同样的方法来搜索元素。假设要查找元素93,它的散列值是5。查看5号槽,发现槽中的元素就是93,因此返回True。如果要查找的是20,又会如何呢?20的散列值是9,而9号槽中的元素是31。因为可能有冲突,所以不能直接返回False,而是应该从10号槽开始进行顺序搜索,直到找到元素20或者遇到空槽。
线性探测有个缺点,那就是会使散列表中的元素出现聚集现象。也就是说,如果一个槽发生太多冲突,线性探测会填满其附近的槽,而这会影响到后续插入的元素。在尝试插入元素20时,要越过数个散列值为0的元素才能找到一个空槽。图5-9展示了这种聚集现象。
要避免元素聚集,一种方法是扩展线性探测,不再依次顺序查找空槽,而是跳过一些槽,例如加3策略
再散列(rehash)泛指在发生冲突后寻找另一个槽的过程
开放定址法及扩展线性探测的方式可以用公式表达:
rehash(pos) = (pos +skip)%sizeoftable
(skip)的大小要能保证表中所有的槽最终都访问到,常常建议散列表的大小为素数
平方探测是线性探测的一个变体
如果第一个散列值是h,后续的散列值就是h+1、h+4、h+9、h+16,…
链接法
允许散列表中的同一个位置上存在多个元素。发生冲突时,元素仍然被插入其散列值对应的槽中。不过,随着同一个位置上的元素越来越多,搜索变得越来越困.
链接法的优点是,平均算来,每个槽的元素不多,因此搜索可能更高效。
3.实现映射抽象数据类型
字典是存储键-值对的数据类型。键用来查找关联的值,这个概念常常被称作映射。
映射抽象数据类型定义如下:
- 它是将键和值关联起来的无序集合
- 键是不重复的
- 键和值之间是一一对应的关系
实现一个散列表数据结构:
class HashTable:
def __init__(self):
self.size=11
#散列表大小
#但选用一个素数很重要,这样做可以尽可能地提高冲突处理算法的效率。
self.slots=[None]*self.size
#保存键的列表
self.data=[None]*self.size
#保存值的列表
def put(self,key,data):
"""put(key, val)往映射中加入一个新的键-值对。如果键已经存在,就用新值替换旧值。"""
hashvalue=self.hashfunction(key,len(self.slots))
if self.slots[hashvalue]==None:
self.slots[hashvalue]=key
self.data[hashvalue]=data
else:
if self.slots[hashvalue]==key:
self.data[hashvalue]=data#替换
else:
nextslot=self.rehash(hashvalue,len(self.slots))
while self.slots[nextslot]!=None and self.slots[nextslot]!=key:
nextslot=self.rehash(nextslot,len(self.slots))
if self.slots[nextslot]==None:
self.slots[nextslot]=key
self.data[nextslot]=data
def get(self,key):
startslot=self.hashfunction(key,len(self,slots))
data=None
stop=False
found=False
position=startslot
while self.slots[position]!=None and not found and not stop:
if self.slots[position]==key:
found=True
data=self.data[position]
else:
position=self.rehash(position,len(self.slots))
if position==startslot:
stop=True
return data
def __getitem__(self,key):
return self.get(key)
def __setitem__(self,key):
self.put(key.data)
4.分析散列搜索算法
在分析散列表的使用情况时,最重要的信息就是载荷因子λ。从概念上来说,如果λ很小,那么发生冲突的概率就很小,元素也就很有可能各就各位。如果λ很大,则意味着散列表很拥挤,发生冲突的概率也就很大。因此,冲突解决起来会更难,找到空槽所需的比较次数会更多。若采用链接法,冲突越多,每条链上的元素也越多。