1. 程式人生 > >查詢與排序演算法(Searching adn Sorting)

查詢與排序演算法(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=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)) #有序列表(升序) def orderedSequentialSearch(orderedList,item): found = False pos = 0 stop = False while not found and not stop and
pos<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))
View Code

    順序查詢的複雜度分析如下,無論對於有序列表或無序列表,在最差的情況下,其都需要進行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.data
View 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 alist
View 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 alist
View 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 alist
View 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