01 算法之查找

一.查找/搜索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查找
  - 在前面的部分中,咱们经过利用关于元素在集合中相对于彼此存储的位置的信息,改进咱们的搜索算法。例如,经过知道集合是有序的,咱们可使用二分查找进行搜索。如今,咱们将尝试进一步创建一个能够在  O(1)时间内搜索的数据结构。这个概念被称为 Hash 查找
  - 分析:当咱们在集合中查找项时,咱们须要更多地了解被查找的元素可能在哪里。若是每一个元素都在应该在的地方,那么搜索可使用单个比较就能发现元素的存在。
  - 哈希表:是一种很是容易且便捷就能够定位到某一个具体元素的集合。哈希表的每一个位置,一般称为一个槽,每一个槽能够容纳一个元素,而且由从 0 开始的整数值命名。例如,咱们有一个名为 0 的槽,名为 1 的槽,名为 2 的槽...... 初始阶段,哈希表不包含元素,所以每一个槽都为空。咱们能够经过使用列表来实现一个哈希表,每一个元素初始化为 None 。下图展现了大小 m = 11 的哈希表。换句话说,在表中有 m 个槽,命名为 0 到 10。
  - hash函数:元素和元素在hash表中所属的槽之间的映射被称为hash函数。假设咱们有包含整数元素  54,26,93,17,77 和  31 的集合,hash 函数将接收集合中的任何元素,并在槽名范围内(0和 m-1之间)返回一个整数。
  - 哈希值:使用余除法计算哈希值。集合中的一个元素整余除以hash表大小,返回的整数值就是哈希值( h(item) = item%11 )。下图给出了咱们上述集合中全部元素的哈希值。注意,这种运算结果必须在槽名的范围内。
     
  - 计算了哈希值,咱们能够将每一个元素插入到指定位置的哈希表中,以下图所示。注意,11 个插槽中的 6 个如今已被占用。这被称为负载因子(λ),一般表示为  λ=项数/表大小, 在这个例子中, λ = 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 抽象数据类型定义以下:

  • Map() 建立一个新的 map 。它返回一个空的 map 集合。
  • put(key,val) 向 map 中添加一个新的键值对。若是键已经在 map 中,那么用新值替换旧值。
  • get(key) 给定一个键,返回存储在 map 中的值或 None。
  • del 使用 del map[key] 形式的语句从 map 中删除键值对。
  • len() 返回存储在 map 中的键值对的数量。
  • in 返回 True 对于 key in map 语句,若是给定的键在 map 中,不然为False。

  - 咱们使用两个列表来建立一个实现 Map 抽象数据类型的HashTable 类。一个名为 slots的列表将保存键项,一个称 data 的并行列表将保存数据值。当咱们查找一个键时,data 列表中的相应位置将保存相关的数据值。咱们将使用前面提出的想法将键列表视为哈希表。注意,哈希表的初始大小已经被选择为 11。尽管这是任意的,可是重要的是,大小是质数,使得冲突解决算法能够尽量高效。

class HashTable:
    def __init__(self):
        self.size = 11
        self.slots = [None] * self.size
        self.data = [None] * self.size

  - hash 函数实现简单的余数方法。冲突解决技术是 加1 rehash 函数的线性探测。 put 函数假定最终将有一个空槽,除非 key 已经存在于 self.slots 中。 它计算原始哈希值,若是该槽不为空,则迭代 rehash 函数,直到出现空槽。若是非空槽已经包含 key,则旧数据值将替换为新数据值。

  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  #replace
    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
      else:
        self.data[nextslot] = data #replace

def hashfunction(self,key,size):
     return key%size

def rehash(self,oldhash,size):
    return (oldhash+1)%size

  - 一样,get 函数从计算初始哈希值开始。若是值不在初始槽中,则 rehash 用于定位下一个可能的位置。注意,第 15 行保证搜索将经过检查以确保咱们没有返回到初始槽来终止。若是发生这种状况,咱们已用尽全部可能的槽,而且项不存在。HashTable 类提供了附加的字典功能。咱们重载 __getitem__ 和__setitem__ 方法以容许使用 [] 访问。 这意味着一旦建立了HashTable,索引操做符将可用。

 1 def get(self,key):
 2   startslot = self.hashfunction(key,len(self.slots))
 3 
 4   data = None
 5   stop = False
 6   found = False
 7   position = startslot
 8   while self.slots[position] != None and  \
 9                        not found and not stop:
10      if self.slots[position] == key:
11        found = True
12        data = self.data[position]
13      else:
14        position=self.rehash(position,len(self.slots))
15        if position == startslot:
16            stop = True
17   return data
18 
19 def __getitem__(self,key):
20     return self.get(key)
21 
22 def __setitem__(self,key,data):
23     self.put(key,data)

  - 下面展现了 HashTable 类的操做。首先,咱们将建立一个哈希表并存储一些带有整数键和字符串数据值的项。

>>> H=HashTable()
>>> H[54]="cat"
>>> H[26]="dog"
>>> H[93]="lion"
>>> H[17]="tiger"
>>> H[77]="bird"
>>> H[31]="cow"
>>> H[44]="goat"
>>> H[55]="pig"
>>> H[20]="chicken"
>>> H.slots
[77, 44, 55, 20, 26, 93, 17, None, None, 31, 54]
>>> H.data
['bird', 'goat', 'pig', 'chicken', 'dog', 'lion',
       'tiger', None, None, 'cow', 'cat']

  - 接下来,咱们将访问和修改哈希表中的一些项。注意,正替换键 20 的值。

>>> H[20]
'chicken'
>>> H[17]
'tiger'
>>> H[20]='duck'
>>> H[20]
'duck'
>>> H.data
['bird', 'goat', 'pig', 'duck', 'dog', 'lion',
       'tiger', None, None, 'cow', 'cat']
>> print(H[99])
None
相关文章
相关标签/搜索