本文参考于《python数据结构与算法分析》
顺序搜索:依次按照顺序进行访问。
#无序搜索
from datetime import datetime
def sequentialSearch(alist, item):
pos = 0
found = False
starttime = datetime.timestamp(datetime.now())
while pos < len(alist) and not found:
if alist[pos] == item:
found = True
else:
pos += 1
endtime = datetime.timestamp(datetime.now())
print(endtime - starttime)
return found
#测试
print(sequentialSearch([54, 26, 93, 17, 77, 31, 44, 55, 20, 65], 20))
#结果
True
1.5020370483398438e-05
#有序搜索
def orderSequentialSearch(alist, item):
pos = 0
found = False
stop = False
starttime = datetime.timestamp(datetime.now())
while pos < len(alist) and not stop and not found:
if alist[pos] == item:
found = True
else:
if alist[pos] > item:
stop = True
else:
pos += 1
endtime = datetime.timestamp(datetime.now())
print(endtime - starttime)
return found
#测试
print(orderSequentialSearch([17, 20, 26, 31, 44, 54,
55, 65, 77, 93], 20))
#结果
1.6927719116210938e-05
True
二分查找:首先这个表必须是有序列表,然后从数据中间的元素开始查起,如果小于这个数,重复此步骤在左边进行查找,如果大于这个数,重复此步骤在右边进行查找。
#有序二分查找
def binarySearch(alist, item):
first = 0
last = len(alist) - 1
found = False
while first <= last and not found:
midpoint = (first + last) // 2
if alist[midpoint] == item:
found = True
else:
if item < alist[midpoint]:
last = midpoint - 1
else:
first = midpoint + 1
return found
#递归版本
def binarySearchRec(alist, item):
if len(alist) == 0:
return False
else:
midpoint = len(alist) // 2
if alist[midpoint] == item:
return True
else:
if item < alist[midpoint]:
return binarySearchRec(alist[:midpoint], item)
else:
return binarySearchRec(alist[midpoint+1:],item)
散列表:是元素集合,其中的元素以一种便于查找的方法存储。散列表中的每个位置通常被称为槽,其中可以存储一个元素。
散列函数:将散列表中的元素与其所属位置对应起来。对散列表中的任一元素,散列函数返回 一个介于 0 和 m – 1 之间的整数。假设有一个由整数元素 54、26、93、17、77 和 31 构成的集 合。首先来看第一个散列函数,它有时被称作“取余函数”,即用一个元素除以表的大小,并将 得到的余数作为散列值(h(item) = item%11)。表 5-4 给出了所有示例元素的散列值。取余函 数是一个很常见的散列函数,这是因为结果必须在槽编号范围内。(散列表函数一定要简单高效,如果计算量比二分或者搜索还要久,就没有意义了)
只有当每个元素的散列值不同时,这个技巧才有用。如果集合中的下 一个元素是 44,它的散列值是 0(44%11==0),而 77 的散列值也是 0,这就有问题了。散列函 数会将两个元素都放入同一个槽,这种情况被称作冲突。给定一个元素集合,能将每个元素映射到不同的槽,这种散列函数称作完美散列函数。我们的目标是创建这样一个散列函数:冲突数最少,计算方便,元素均匀分布于散列表中。有多种常见的方法来扩展取余函数,下面介绍其中的几种。
折叠法:先将元素切成等长的部分(最后一部分的长度可能不同),然后将这些部分相加,得到 散列值。假设元素是电话号码 436-555-4601,以 2 位为一组进行切分,得到 43、65、55、46 和 01。将这些数字相加后,得到 210。假设散列表有 11 个槽,接着需要用 210 除以 11,并保留余数 1。 所以,电话号码 436-555-4601 被映射到散列表中的 1 号槽。有些折叠法更进一步,在加总前每 隔一个数反转一次。就本例而言,反转后的结果是:43+56+55+64+01=219,219%11=10。
平方取中法:先将元素取平方,然后提取中间几位数。如 果元素是 44,先计算 442=1936,然后提取中间两位 93,继续进行取余的步骤,得到 5(93%11)。
处理冲突:当两个元素被分到同一个槽中时,必须通过一种系统化方法在 散列表中安置第二个元素。这个过程被称为处理冲突。前文说过,如果散列函数是完美的,冲突 就永远不会发生。然而,这个前提往往不成立,因此处理冲突是散列计算的重点。
一种方法是在散列表中找到另一个空槽,用于放置引起冲突的元素。简单的做法是从起初的 散列值开始,顺序遍历散列表,直到找到一个空槽。注意,为了遍历散列表,可能需要往回检查 第一个槽。这个过程被称为开放定址法,它尝试在散列表中寻找下一个空槽或地址。由于是逐个 访问槽,因此这个做法被称作线性探测。
整数集合(54, 26, 93, 17, 77, 31, 44, 55, 20)。经过取余散列函数处理后的结果。当我们尝试把 44 放入 0 号槽时,就会产生冲突。采用线性探测,依次检查每个槽,直到找到一个空槽,在本例中 即为 1 号槽。
同理,55 应该被放入 0 号槽,但是为了避免冲突,必须被放入 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”探测策略处理冲突后的元素 分布情况。发生冲突时,为了找到空槽,该策略每次跳两个槽。
再散列泛指在发生冲突后寻找另一个槽的过程。采用线性探测时,再散列函数是 newhashvalue = rehash(oldhashvalue),并且 rehash(pos) = (pos + 1)%sizeoftable。 “加 3”探测策略的再散列函数可以定义为 rehash(pos) = (pos + 3)%sizeoftable。也就是说,可以将再散列函数定义为 rehash(pos) = (pos + skip)%sizeoftable。注意,“跨 步”(skip)的大小要能保证表中所有的槽最终都被访问到,否则就会浪费槽资源。要保证这一 点,常常建议散列表的大小为素数,这就是本例选用 11 的原因。
平方探测是线性探测的一个变体,它不采用固定的跨步大小,而是通过再散列函数递增散列 值。如果第一个散列值是 h,后续的散列值就是 h+1、h+4、h+9、h+16,等等。换句话说,平方 探测的跨步大小是一系列完全平方数。图 5-11 展示了采用平方探测处理后的结果。
另一种处理冲突的方法是让每个槽有一个指向元素集合(或链表)的引用。链接法允许散列 表中的同一个位置上存在多个元素。发生冲突时,元素仍然被插入其散列值对应的槽中。不过, 随着同一个位置上的元素越来越多,搜索变得越来越困难。采用链接法解决冲突 后的结果。
实现映射抽象数据类型:字典是最有用的 Python 集合之一。字典是存储键–值对的数据类型。键用来查找关联的值,这个概念常常被称作映射。
- Map()创建一个空的映射,它返回一个空的映射集合。
- put(key, val)往映射中加入一个新的键–值对。如果键已经存在,就用值替换旧值。
- get()根据key拿数据
class HashTable:
def __init__(self):
self.size = 11
self.slots = [None] * self.size #存储键
self.data = [None] * self.size #存储数据
def put(self, key, data):
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))
nextslot = self.rehash(nextslot, len(self.slots))
if self.slots[nextslot] == None:
self.slots[nextslot] = key
self.data[nextslot] = data
else:
self.data[nextslot] = data
def hashfunction(self, key, size):
return key % size
def rehash(self, oldhash, size):
return (oldhash+1)%size
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, data):
self.put(key, data)
#测试
testhash = HashTable()
testhash[54] = "cat"
testhash[26] = "dog"
#结果
[None, None, None, None, 26, None, None, None, None, None, 54]
[None, None, None, None, 'dog', None, None, None, None, None, 'cat']
#测试
testhash[54] = "new_cat"
#结果
[None, None, None, None, 26, None, None, None, None, None, 54]
[None, None, None, None, 'dog', None, None, None, None, None, 'new_cat']