一.查找/搜索
- 我們現在把注意力轉向計算中經常出現的一些問題,即搜索或查找的問題。搜索是在元素集合中查找特定元素的算法過程。搜索通常對於元素是否存在返回 True 或 False。有時它可能返回元素被找到的地方。我們在這里將僅關注成員是否存在這個問題。
- 在 Python 中,有一個非常簡單的方法來詢問一個元素是否在一個元素列表中。我們使用 in 運算符。
>>> 15 in [3,5,2,4,1] False >>> 3 in [3,5,2,4,1] True >>>
- 這很容易寫,一個底層的操作替我們完成這個工作。事實證明,有很多不同的方法來搜索。我們在這里感興趣的是這些算法如何工作以及它們如何相互比較。
二.順序查找
- 當數據存儲在諸如列表的集合中時,我們說這些數據具有線性或順序關系。 每個數據元素都存儲在相對於其他數據元素的位置。 在 Python 列表中,這些相對位置是單個元素的索引值。由於這些索引值是有序的,我們可以按順序訪問它們。 這個過程產實現的搜索即為順序查找
。
- 順序查找原理剖析:從列表中的第一個元素開始,我們按照基本的順序排序,簡單地從一個元素移動到另一個元素,直到找到我們正在尋找的元素或遍歷完整個列表。如果我們遍歷完整個列表,則說明正在搜索的元素不存在。
- 代碼實現:該函數需要一個列表和我們正在尋找的元素作為參數,並返回一個是否存在的布爾值。found
布爾變量初始化為 False,如果我們發現列表中的元素,則賦值為 True。
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)
- 有序列表:之前我們列表中的元素是隨機放置的,因此在元素之間沒有相對順序。如果元素以某種方式排序,順序查找會發生什么?我們能夠在搜索技術中取得更好的效率嗎?
- 設計:假設元素的列表按升序排列。如果我們正在尋找的元素存在此列表中,則目標元素在列表的 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 。
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 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 的值。