查詢與排序演算法(Searching adn Sorting)
1,查詢演算法
常用的查詢演算法包括順序查詢,二分查詢和雜湊查詢。
1.1 順序查詢(Sequential search)
順序查詢: 依次遍歷列表中每一個元素,檢視是否為目標元素。python實現程式碼如下:
#無序列表 def sequentialSearch(alist,item): found = False pos=0 while not found and pos<len(alist): if alist[pos]==item: found=TrueView Codeelse: pos = pos+1 return found testlist = [1, 2, 32, 8, 17, 19, 42, 13, 0] print(sequentialSearch(testlist, 3)) print(sequentialSearch(testlist, 13)) #有序列表(升序) def orderedSequentialSearch(orderedList,item): found = False pos = 0 stop = False while not found and not stop andpos<len(orderedList): if orderedList[pos]==item: found=True else: if orderedList[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次比較運算,所以其複雜度為O(n)
查詢無序列表:
查詢有序列表:(僅僅在item不存在時,效能有所提高)
1.2 二分查詢(Binary search)
二分查詢:二分查詢又叫折半查詢,適用於有序列表。查詢的方法是找到列表正中間的值,我們假設是m,來跟v相比,如果m>v,說明我們要查詢的v在前列表的前半部,否則就在後半部。無論是在前半部還是後半部,將那部分再次折半查詢,重複這個過程,知道查詢到v值所在的地方。實現二分查詢可以用迴圈,也可以遞迴。
下圖為查詢54的過程:
使用迴圈,python實現程式碼如下:
#迴圈 def binarySearch(orderedList,item): found =False first = 0 last = len(orderedList)-1 while not found and first<=last: midpoint = (first+last)//2 if orderedList[midpoint]==item: found=True elif orderedList[midpoint]>item: 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))View Code
使用遞迴,python實現程式碼如下:
#遞迴 def binarySearch(orderedList,item): if len(orderedList)==0: return False else: midpoint = len(orderedList) // 2 if orderedList[midpoint] == item: return True elif orderedList[midpoint] > item: return binarySearch(orderedList[:midpoint],item) #注意得return else: return binarySearch(orderedList[midpoint+1:], item) #注意得return testlist = [0, 1, 2, 8, 13, 17, 19, 32, 42,] print(binarySearch(testlist, 3)) print(binarySearch(testlist, 17))View Code
二分查詢的時間複雜度分析:最好情況下為1,最壞情況下為log n,因此複雜度為O(log n)。但值得注意的是,使用遞迴時,進行列表的切片的複雜度為O(K),並不是依次操作,所以複雜度變大。(可以將切片改為傳入索引值: return binarySearch(orderedList,0,midpoint-1,item)和return binarySearch(orderedList,midpoint+1,len(orderedList),item))
1.3 雜湊查詢(Hash Search)
雜湊技術(Hashing):雜湊技術是在資料的儲存位置和資料的 key 之間建立一個確定的對映 f(),使得每個 key 對應一個儲存位置 f(key),其中f()稱作雜湊函式(hash function),記錄資料儲存位置的資料結構為雜湊表(hash table).如下圖中為一個空的雜湊表,key從0-10,共有11個儲存位置。
為儲存資料54, 26, 93, 17, 77, and 31,以除留餘數法(remainder method)為雜湊函式: f(n)=n%11,依次計算對應雜湊值,如下表所示:
根據計算的hash value,以其為雜湊表的key,依次將資料儲存在雜湊表中,結果如下圖所示,當我們需要查詢31時,只需利用雜湊函式就能找到該資料的儲存位置,比較值便能確定是否包含該資料,算符複雜度能達到O(1).
雜湊函式進行對映時,會出現兩個問題,一是部分儲存位置會空缺,造成雜湊表的浪費;二是會出現碰撞,如儲存44到上述雜湊表中,會和77所在的儲存位置碰撞。因此,需要有效的雜湊函式和碰撞解決途徑
1.3.1 其他雜湊函式
摺疊法(folding method):
如對於電話號碼436-555-4601,對其拆分求和 43+65+55+46+01=210,再以雜湊表的長度求餘數(210%11=1)
對於字串‘cat’,將其轉換為Ascill表中數值99,97,116,進行求和,再以雜湊表的長度求餘數。
平方取中法(mid-square method):
如對於資料44,對其進行平方44*44=1936,取中間兩位93,再以雜湊表的長度求餘數(93%11=5)
1.3.2 衝突解決途徑(collision resolution):
(為了更好的解決衝突,雜湊表的長度應該為質數)
開放定址法(open addressing):
該方法是一旦發生衝突,求從雜湊表的當前位置依次往後尋找下一個空的儲存位置,然後將其插入。。
二次探測(rehashing):上面方法會造成多個雜湊值集中在某一區域,爭奪同一個地址。可以利用雜湊值加上一個數值,再以雜湊表的長度求餘數。
隨機探測:雜湊值加上一個隨機數(1,3...)。
如上面提到的44插入雜湊表時雜湊值為0,會和77的雜湊值衝突,可以雜湊值加一個隨機數3,重新計算(0+3)%11=3,
由於3號位置空缺,因此將44插入。(若依舊衝突,繼續加3)
平方探測:雜湊值加上一個平方數(1,4,9,16)。第一次衝突加1,繼續衝突時加4,還是衝突時加9,如此重複下去直到找到資料
鏈地址法(chain addressing):發生衝突的資料依次放在一個連結串列中,雜湊表中存放連結串列地址,如下圖所示:
1.3.3 字典的實現(Map)
python的字典就是一種Map資料結構,利用雜湊演算法來實現鍵值對的儲存和查詢。
map常用操作如下:
Map() #建立字典 put(key,value) #加入鍵值對 get(key) #返回對應value len() # 返回字典長度 del #刪除字典鍵值對 in #查詢是否包含鍵
利用python 實現程式碼如下:(del如何實現?)
class Map(object): def __init__(self): self.size = 11 #雜湊表的長度 self.slots=[None]*self.size #存放key self.data = [None]*self.size #存放value def put(self,key,value): hashvalue = self._hashfunction(key,len(self.slots)) if self.slots[hashvalue]==None: self.slots[hashvalue]=key self.data[hashvalue]=value else: if self.slots[hashvalue]==key: self.data[hashvalue]=value else: nextslot = self._rehash(hashvalue,len(self.slots)) while self.slots[nextslot]!=None and self.slots[nextslot]!=key: #如果某個鍵值對刪除了,會不會出現key之前有None? nextslot = self._rehash(nextslot,len(self.slots)) if self.slots[nextslot] == None: self.slots[nextslot] = key self.data[nextslot] = value else: self.data[nextslot] = value #del 如何實現? 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)) found = False stop = False data = None position = startslot while self.slots[position]!=None and not found and not stop: if self.slots[position]==key: data = self.data[position] found = True else: position = self._rehash(position,len(self.slots)) if position==startslot: stop=True return data def __setitem__(self, key, value): self.put(key,value) def __getitem__(self, item): return self.get(item) m = Map() m[54] = 'cat' m[26] = 'dog' m[12] = 'snake' m[52] = 'bear' m[93]="lion" m[17]="tiger" m[77]="bird" m[31]="cow" m[44]="goat" m[55]="pig" m[20]="chicken" print m.slots,m.data # print m[12],m[20] #刪除m[77]會引起bug? # m.slots[0]=None m[44]= "not goat" print m.slots,m.dataView Code
由於衝突存在,雜湊查詢的複雜度並不完全是O(1),取決於雜湊表的負載因子(load factor)λ,λ=(numbers of key)/tablesize,即雜湊表中key的數量。
λ越大時,表明key越多,發生碰撞的可能性越高,查詢會變慢,λ越小時,碰撞可能性小,但空間資源浪費多。(python中字典的預設拉姆達為0.75?)
採用開放定址中隨機探測解決衝突,平均複雜度為 (1+1/(1-r))/2,最壞情況為(1+1/(1-r)2)/2
採用鏈地址法解決衝突,平均複雜度為 1+λ/2,最壞情況為λ
2,排序演算法
常用的排序演算法包括:氣泡排序(Bubble sort),選擇排序(Selection sort),插入排序(Insertion sort),希爾排序(Shell sort),歸併排序(Merge sort)和快速排序(Quick sort)
2.1 氣泡排序(Bubble sort)
氣泡排序:依次比較相鄰的兩個數,將小數放在前面,大數放在後面。
排序過程:在第一趟:首先比較第1個和第2個數,將小數放前,大數放後。然後比較第2個數和第3個數,將小數放前,大數放後,如此繼續,直至比較最後兩個數,將小數放前,大數放後,第一趟排序完成後最大的數被移動到了最後面。繼續重複第一趟步驟,直至全部排序完成。下圖為第一趟排序過程:
用python實現程式碼如下:
def bubbleSort(alist): for n in range(len(alist)-1,0,-1): for i in range(n): if alist[i]>alist[i+1]: alist[i],alist[i+1]=alist[i+1],alist[i] alist = [54,26,93,17,77,31,44,55,20] bubbleSort(alist) print(alist)View Code
無論列表是否有序,冒泡演算法都需要進行比較,共需要進行n*(n-1)/2 次比較,最好的情況下,列表有序時不需要交換,最壞的情況下需要n*(n-1)/2 次交換(每次比較都進行交換),因此氣泡排序的複雜度為O(n2)。對於氣泡排序可以進行改進,使其在列表有序時能停止比較,即短氣泡排序(short bubble),程式碼如下:
def shortBubbleSort(alist): n = len(alist)-1 exchange = True while n>0 and exchange: exchange = False for i in range(n): if alist[i]>alist[i+1]: exchange = True alist[i],alist[i+1]=alist[i+1],alist[i] n = n-1 alist=[20,30,40,90,50,60,70,80,100,110] shortBubbleSort(alist) print(alist)View Code
2.2 選擇排序(selection sort)
選擇排序:每一趟從待排序的記錄中選出最大的元素,放在已排好序的序列最後,直到全部記錄排序完畢。(相比氣泡排序,選擇排序每一趟只進行一次交換)
排序過程如下圖(前四趟):
用python實現程式碼如下:
def selectionSort(alist): for n in range(len(alist)-1,0,-1): pos = n for i in range(n): if alist[i]>alist[pos]: pos = i alist[n],alist[pos]=alist[pos],alist[n] alist = [54,26,93,107,77,31,44,55,28] selectionSort(alist) print(alist)View Code
選擇排序和冒泡一樣,也需要進行n*(n-1)/2 次比較,但交換列表中資料次數減少,最壞情況下需要n-1次交換,所以複雜度也為O(n2)。
2.3 插入排序(Insertion sort)
插入排序:已排序部分定義在左端,將未排序部分元的第一個元素插入到已排序部分合適的位置。排序過程示意如下:
用python實現程式碼如下:(插入到有序序列時也可以考慮二分查詢)
def insertionSort(alist): for i in range(1,len(alist)): pos = i currentvalue = alist[i] while alist[pos-1]>currentvalue and pos>0: alist[pos]=alist[pos-1] pos = pos-1 alist[pos]=currentvalue alist = [54,26,93,17,77,31,44,55,20] insertionSort(alist)View Code
插入排序的複雜度為為O(n2),其在最好的情況下(列表有序),複雜度為n,最壞的情況下,複雜度為n*(n-1)/2
2.4 希爾排序(Shell sort)
希爾排序:增量遞減排序(diminishing increment sort),是對插入排序的改進,減少資料移動操作。其主要是引入一個增量(gap),將原本的序列分成幾個子序列,分別對子序列採用插入排序。排序示意過程如下:
1,先以3為增量,將序列分為了三個子序列(54,17,44),(26,77,55),(93, 31, 20),分別對三個子序列進行插入排序
2,再以1為增量(即整個序列),然後再用插入排序,如下圖所示,可以發現shift操作明顯減少
對於希爾排序,增量的選擇十分重要,上面選擇了增量3,1。一般也可以選擇將增量n/2,n/4....,1(n為列表長度),上面序列若選擇4(n/2)為初始增量,子序列如下:
python實現希爾排序程式碼如下:(增量為n/2,n/4....,1)
def shellSort(alist): gap = len(alist)//2 while gap>0 : for i in range(gap): gapInsertionSort(i,alist,gap) #print "增量為%s,排序完成後alist為: %s"%(gap,alist) gap = gap//2 def gapInsertionSort(startPos,alist,gap): for i in range(startPos+gap,len(alist),gap): current = alist[i] pos = i while alist[pos-gap]>current and pos>=gap: alist[pos] = alist[pos-gap] pos = pos-gap alist[pos] = current alist = [54,26,93,17,77,31,44,55,20] shellSort(alist) print alistView Code
希爾排序的複雜度在O(n)—O(n2)之間,對於上面的gap(n/2,n/4..1)複雜度為O(n2),若gap為2k-1(1,3,5,7...),複雜度為O(n3/2)
2.5 歸併排序(Merge sort)
歸併排序:是一種分而治之的策略(divide and conquer)。採用遞迴演算法,不斷的將序列進平分成子序列,直到序列為空或只有一個元素,然後進行排序合併。其排序過程如下:
python實現歸併排序程式碼如下:
def mergeSort(alist): #print "平分序列,alist:%s"%alist if len(alist)>1: mid = len(alist)//2 lefthalf = alist[:mid] righthalf = alist[mid:] mergeSort(lefthalf) mergeSort(righthalf) i=0 j=0 k=0 while i<len(lefthalf) and j<len(righthalf): if lefthalf[i]>righthalf[j]: alist[k]=righthalf[j] j+=1 else: alist[k]=lefthalf[i] i+=1 k+=1 while i<len(lefthalf): alist[k]=lefthalf[i] i+=1 k+=1 while j<len(righthalf): alist[k]=righthalf[j] j+=1 k+=1 #print "開始合併,alist: %s"%alist alist = [54,26,93,17,77,31,44,55,20] mergeSort(alist) print alistView Code
歸併排序的複雜度為O(n logn),但上述程式碼中使用了切片,會使複雜度增加。(切片可以改為傳入index?)
2.6 快速排序(quick sort)
快速排序:也是一種分而治之的策略(divide and conquer)。但相比歸併排序,快速排序不需要額外的儲存空間(列表)。快速排序一般選擇列表中一個元素中間點(pivot value),然後將比其小的元素放在列表左邊,大的放在右邊,將列表進行劃分為左右兩個子列表,再分別對左右子列表遞迴的快速排序。排序過程如下(選取第一個元素為中間點):
python實現歸併排序程式碼如下:
def quickSort(alist): quickSortHelper(alist,0,len(alist)-1) def quickSortHelper(alist,first,last): if first<last: pivotvalue = alist[first] leftmark = first+1 rightmark = last done = False while not done: while leftmark<=rightmark and alist[leftmark]<=pivotvalue: #注意兩個判斷條件的前後順序不能換,否則leftmark=last-1時可能會報錯:list index out of range leftmark = leftmark+1 while alist[rightmark]>=pivotvalue and leftmark<=rightmark: rightmark = rightmark-1 if leftmark<rightmark: alist[leftmark],alist[rightmark]=alist[rightmark],alist[leftmark] else: done = True alist[first],alist[rightmark]=alist[rightmark],alist[first] print alist quickSortHelper(alist,first,rightmark-1) quickSortHelper(alist,rightmark+1,last) alist = [54,26,93,17,98,77,31,44,55,20] quickSort(alist) print alistView Code
快速排序的複雜度取決於pivot value的選擇,如果每次分割都為序列中間值,則需logn分割,複雜度為O(n logn),若每次分割pivot value都為最小或最大值,則需n次分割,複雜度為O(n2),複雜度沒有歸併排序穩定,但不需要額外的記憶體。 因此選擇pivot value值時,可以從序列的first, middle, last三個元素中選擇中間值(median of three),來避免總是選擇最小或最大值
3.總結
順序查詢:無論列表有序或無序,演算法複雜度為O(n)
二分查詢:適合有序列表的查詢,最壞情況下演算法複雜度為O(log n)
雜湊查詢:可以提供常量級別的演算法複雜度O(k) (k為常數1,2,3.....)
氣泡排序,選擇排序,插入排序:演算法複雜度都為O(n2)
希爾排序:希爾排序在選擇排序的基礎上引入增量來分割子序列,演算法複雜度在在O(n)—O(n2)之間,取決於增量的選擇
歸併排序:演算法複雜度為O(n log n),但排序過程中需要額外的儲存空間
快速排序:快速排序的演算法複雜度為O(n log n),但若選擇的分割點不是list的中間元素(取決於pivot value的選擇),也可能變壞為O(n2)
參考:http://interactivepython.org/runestone/static/pythonds/SortSearch/toctree.html