一.查找/搜索html
- 咱们如今把注意力转向计算中常常出现的一些问题,即搜索或查找的问题。搜索是在元素集合中查找特定元素的算法过程。搜索一般对于元素是否存在返回 True 或 False。有时它可能返回元素被找到的地方。咱们在这里将仅关注成员是否存在这个问题。算法
- 在 Python 中,有一个很是简单的方法来询问一个元素是否在一个元素列表中。咱们使用 in 运算符。数据结构
>>> 15 in [3,5,2,4,1] False >>> 3 in [3,5,2,4,1] True >>>
- 这很容易写,一个底层的操做替咱们完成这个工做。事实证实,有不少不一样的方法来搜索。咱们在这里感兴趣的是这些算法如何工做以及它们如何相互比较。函数
二.顺序查找spa
- 当数据存储在诸如列表的集合中时,咱们说这些数据具备线性或顺序关系。 每一个数据元素都存储在相对于其余数据元素的位置。 在 Python 列表中,这些相对位置是单个元素的索引值。因为这些索引值是有序的,咱们能够按顺序访问它们。 这个过程产实现的搜索即为顺序查找
。设计
- 顺序查找原理剖析:从列表中的第一个元素开始,咱们按照基本的顺序排序,简单地从一个元素移动到另外一个元素,直到找到咱们正在寻找的元素或遍历完整个列表。若是咱们遍历完整个列表,则说明正在搜索的元素不存在。code
- 代码实现:该函数须要一个列表和咱们正在寻找的元素做为参数,并返回一个是否存在的布尔值。found
布尔变量初始化为 False,若是咱们发现列表中的元素,则赋值为 True。htm
def sequentialSearch(alist, item): pos = 0 found = False while pos < len(alist) and not found: if alist[pos] == item: found = True else: pos = pos+1 return found testlist = [1, 2, 32, 8, 17, 19, 42, 13, 0] print(sequentialSearch(testlist, 3)) print(sequentialSearch(testlist, 13))
- 顺序查找分析:为了分析搜索算法,咱们能够分析一下上述案例中搜索算法的时间复杂度,即统计为了找到搜索目标耗费的运算步骤。实际上有三种不一样的状况可能发生。在最好的状况下,咱们在列表的开头找到所需的项,只须要一个比较。在最坏的状况下,咱们直到最后的比较才找到项,第 n 个比较。平均状况怎么样?平均来讲,咱们会在列表的一半找到该项; 也就是说,咱们将比较 n/2 项。然而,回想一下,当 n 变大时,系数,不管它们是什么,在咱们的近似中变得不重要,所以顺序查找的复杂度是 O(n)
blog
- 有序列表:以前咱们列表中的元素是随机放置的,所以在元素之间没有相对顺序。若是元素以某种方式排序,顺序查找会发生什么?咱们可以在搜索技术中取得更好的效率吗?
排序
- 设计:假设元素的列表按升序排列。若是咱们正在寻找的元素存在此列表中,则目标元素在列表的 n 个位置中存在的几率是相同。咱们仍然会有相同数量的比较来找到该元素。然而,若是该元素不存在,则有一些优势。下图展现了这个过程,在列表中寻找元素 50。注意,元素仍然按顺序进行比较直到 54,由于列表是有序的。在这种状况下,算法没必要继续查看全部项。它能够当即中止。
def orderedSequentialSearch(alist, item): pos = 0 found = False stop = False while pos < len(alist) and not found and not stop: if alist[pos] == item: found = True else: if alist[pos] > item: stop = True else: pos = pos+1 return found testlist = [0, 1, 2, 8, 13, 17, 19, 32, 42,] print(orderedSequentialSearch(testlist, 3)) print(orderedSequentialSearch(testlist, 13))
该排序模式在最好的状况下,咱们经过只查看一项会发现该项不在列表中。 平均来讲,咱们将只了解 n/2 项就知道。然而,这种复杂度仍然是 O(n)。 可是在咱们没有找到目标元素的状况下,才经过对列表排序来改进顺序查找。
三. 二分查找:
- 有序列表对于咱们的实现搜索是颇有用的。在顺序查找中,当咱们与第一个元素进行比较时,若是第一个元素不是咱们要查找的,则最多还有 n-1
个元素须要进行比较。 二分查找则是从中间元素开始,而不是按顺序查找列表。 若是该元素是咱们正在寻找的元素,咱们就完成了查找。 若是它不是,咱们可使用列表的有序性质来消除剩余元素的一半。若是咱们正在查找的元素大于中间元素,就能够消除中间元素以及比中间元素小的一半元素。若是该元素在列表中,确定在大的那半部分。而后咱们能够用大的半部分重复该过程,继续从中间元素开始,将其与咱们正在寻找的内容进行比较。下图展现了该算法如何快速找到值 54 。
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 testlist = [0, 1, 2, 8, 13, 17, 19, 32, 42,] print(binarySearch(testlist, 3)) print(binarySearch(testlist, 13))
- 二分查找分析:为了分析二分查找算法,咱们须要记住,每一个比较消除了大约一半的剩余元素。该算法检查整个列表的最大比较数是多少?若是咱们从 n 项开始,大约 n/2 项将在第一次比较后留下。第二次比较后,会有约 n/4。 而后 n/8,n/16,等等。 咱们能够拆分列表多少次?
当咱们切分列表足够屡次时,咱们最终获得只有一个元素的列表。 要么是咱们正在寻找的元素,要么不是。达到这一点所需的比较数是 i,当 n / i^2=1时。 求解 i 得出 i = logn。 最大比较数相对于列表中的项是对数的。 所以,二分查找是 O(log n)。
Hash 查找
。None
。下图展现了大小 m = 11 的哈希表。换句话说,在表中有 m 个槽,命名为 0 到 10。54,26,93,17,77
和31
的集合,hash 函数将接收集合中的任何元素,并在槽名范围内(0和 m-1之间)返回一个整数。λ=项数/表大小
, 在这个例子中,λ = 6/11
。- 结论:当咱们要搜索一个元素时,咱们只需使用哈希函数来计算该元素的槽名称,而后检查哈希表以查看它是否存在。该搜索操做是 O(1)。
- 注意:只有每一个元素映射到哈希表中的位置是惟一的,这种技术才会起做用。 例如,元素77是咱们集合中的某一个元素,则它的哈希值为
0(77%11 == 0)
。 那么若是集合中还有一个元素是44,则44的hash值也是 0,咱们会有一个问题。根据hash函数,两个或更多元素将须要在同一槽中。这种现象被称为碰撞(它也能够被称为“冲突”)。显然,冲突使散列技术产生了问题。咱们将在后面详细讨论。- 其余计算hash值的方法:
- 分组求和法:若是咱们的元素是电话号码
436-555-4601
,咱们将取出数字,并将它们分红2位数(43,65,55,46,01)
。43 + 65 + 55 + 46 + 01
,咱们获得 210。咱们假设哈希表有 11 个槽,那么咱们须要除以 11 。在这种状况下,210%11
为 1,所以电话号码436-555-4601
放置到槽 1 。- 平方取中法:咱们首先对该元素进行平方,而后提取一部分数字结果。例如,若是元素是 44,咱们将首先计算
44^2 = 1,936
。经过提取中间两个数字93
,咱们获得93%11=5,所以元素44放置到槽5.
- 注意:还能够思考一些其余方法来计算集合中元素的哈希值。重要的是要记住,哈希函数必须是高效的,以便它不会成为存储和搜索过程的主要部分。若是哈希函数太复杂,则计算槽名称的程序要比以前所述的简单地进行基本的顺序或二分搜索更耗时。 这将打破哈希的目的。
- 冲突解决:若是有两个元素经过调用hash函数返回两个一样的槽名,咱们就必须有一种方法可使得这两个元素能够散落在hash表的不一样槽中!
- 解决方案:解决冲突的一种方法是查找哈希表,尝试查找到另外一个空槽以保存致使冲突的元素。一个简单的方法是从原始哈希值位置开始,而后以顺序方式移动槽,直到遇到第一个空槽。这种冲突解决过程被称为开放寻址,由于它试图在哈希表中找到下一个空槽或地址。经过系统的依次访问每一个槽。当咱们尝试将
44
放入槽 0 时,发生冲突。在线性探测下,咱们逐个顺序观察,直到找到位置。在这种状况下,咱们找到槽 1。再次,55
应该在槽 0 中,可是必须放置在槽 2 中,由于它是下一个开放位置。值 20 散列到槽 9 。因为槽 9 已满,咱们进行线性探测。咱们访问槽10,0,1
和2
,最后在位置 3 找到一个空槽。一旦咱们使用开放寻址创建了哈希表,咱们就必须使用相同的方法来搜索项。假设咱们想查找项
93
。当咱们计算哈希值时,咱们获得5
。查看槽 5 获得93
,返回 True。若是咱们正在寻找20
, 如今哈希值为9
,而槽9
当前项为31
。咱们不能简单地返回 False,由于咱们知道可能存在冲突。咱们如今被迫作一个顺序搜索,从位置10
开始寻找,直到咱们找到项20
或咱们找到一个空槽。- 代码实现
- 实现 map 抽象数据类型:最有用的 Python 集合之一是字典。回想一下,字典是一种关联数据类型,你能够在其中存储键-值对。该键用于查找关联的值。咱们常常将这个想法称为
map
。map 抽象数据类型定义以下:del map[key]
形式的语句从 map 中删除键值对。key in map
语句,若是给定的键在 map 中,不然为False。- 咱们使用两个列表来建立一个实现 Map 抽象数据类型的HashTable 类。一个名为
slots
的列表将保存键项,一个称data
的并行列表将保存数据值。当咱们查找一个键时,data
列表中的相应位置将保存相关的数据值。咱们将使用前面提出的想法将键列表视为哈希表。注意,哈希表的初始大小已经被选择为 11。尽管这是任意的,可是重要的是,大小是质数,使得冲突解决算法能够尽量高效。- hash 函数实现简单的余数方法。冲突解决技术是
加1
rehash 函数的线性探测。 put 函数假定最终将有一个空槽,除非 key 已经存在于self.slots
中。 它计算原始哈希值,若是该槽不为空,则迭代 rehash 函数,直到出现空槽。若是非空槽已经包含 key,则旧数据值将替换为新数据值。- 一样,get 函数从计算初始哈希值开始。若是值不在初始槽中,则 rehash 用于定位下一个可能的位置。注意,第 15 行保证搜索将经过检查以确保咱们没有返回到初始槽来终止。若是发生这种状况,咱们已用尽全部可能的槽,而且项不存在。HashTable 类提供了附加的字典功能。咱们重载
__getitem__
和__setitem__
方法以容许使用[]
访问。 这意味着一旦建立了HashTable,索引操做符将可用。- 下面展现了 HashTable 类的操做。首先,咱们将建立一个哈希表并存储一些带有整数键和字符串数据值的项。
- 接下来,咱们将访问和修改哈希表中的一些项。注意,正替换键 20 的值。