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
相關文章
相關標籤/搜索