在Python實踐中,我們往往遇到排序問題,比如在對搜索結果打分的排序(沒有排序就沒有Google等搜索引擎的存在),當然,這樣的例子數不勝數。《數據結構》也會花大量篇幅講解排序。之前一段時間,由於需要,我復習了一下排序算法,並用Python實現了各種排序算法,放在這里作為參考。
最簡單的排序有三種:插入排序,選擇排序和冒泡排序。這三種排序比較簡單,它們的平均時間復雜度均為O(n^2),在這里對原理就不加贅述了。貼出來源代碼。
插入排序:
1 def insertion_sort(sort_list): 2 iter_len = len(sort_list) 3 if iter_len < 2: 4 return sort_list 5 for i in range(1, iter_len): 6 key = sort_list[i] 7 j = i - 1 8 while j>=0 and sort_list[j]>key: 9 sort_list[j+1] = sort_list[j] 10 j -= 1 11 sort_list[j+1] = key 12 return sort_list
冒泡排序:
1 def bubble_sort(sort_list): 2 iter_len = len(sort_list) 3 if iter_len < 2: 4 return sort_list 5 for i in range(iter_len-1): 6 for j in range(iter_len-i-1): 7 if sort_list[j] > sort_list[j+1]: 8 sort_list[j], sort_list[j+1] = sort_list[j+1], sort_list[j] 9 return sort_list
選擇排序:
1 def selection_sort(sort_list): 2 iter_len = len(sort_list) 3 if iter_len < 2: 4 return sort_list 5 for i in range(iter_len-1): 6 smallest = sort_list[i] 7 location = i 8 for j in range(i, iter_len): 9 if sort_list[j] < smallest: 10 smallest = sort_list[j] 11 location = j 12 if i != location: 13 sort_list[i], sort_list[location] = sort_list[location], sort_list[i] 14 return sort_list
這里我們可以看到這樣的句子:
sort_list[i], sort_list[location] = sort_list[location], sort_list[i]
不了解Python的同學可能會覺得奇怪,沒錯,這是交換兩個數的做法,通常在其他語言中如果要交換a與b的值,常常需要一個中間變量temp,首先把a賦給temp,然后把b賦給a,最后再把temp賦給b。但是在python中你就可以這么寫:a, b = b, a,其實這是因為賦值符號的左右兩邊都是元組(這里需要強調的是,在python中,元組其實是由逗號“,”來界定的,而不是括號)。
平均時間復雜度為O(nlogn)的算法有:歸並排序,堆排序和快速排序。
歸並排序。對於一個子序列,分成兩份,比較兩份的第一個元素,小者彈出,然后重復這個過程。對於待排序列,以中間值分成左右兩個序列,然后對於各子序列再遞歸調用。源代碼如下,由於有工具函數,所以寫成了callable的類:
1 class merge_sort(object): 2 def _merge(self, alist, p, q, r): 3 left = alist[p:q+1] 4 right = alist[q+1:r+1] 5 for i in range(p, r+1): 6 if len(left)>0 and len(right)>0: 7 if left[0]<=right[0]: 8 alist[i] = left.pop(0) 9 else: 10 alist[i] = right.pop(0) 11 elif len(right)==0: 12 alist[i] = left.pop(0) 13 elif len(left)==0: 14 alist[i] = right.pop(0) 15 16 def _merge_sort(self, alist, p, r): 17 if p<r: 18 q = int((p+r)/2) 19 self._merge_sort(alist, p, q) 20 self._merge_sort(alist, q+1, r) 21 self._merge(alist, p, q, r) 22 23 def __call__(self, sort_list): 24 self._merge_sort(sort_list, 0, len(sort_list)-1) 25 return sort_list
堆排序,是建立在數據結構——堆上的。關於堆的基本概念、以及堆的存儲方式這里不作介紹。這里用一個列表來存儲堆(和用數組存儲類似),對於處在i位置的元素,2*i+1位置上的是其左孩子,2*i+2是其右孩子,類似得可以得出該元素的父元素。
首先我們寫一個函數,對於某個子樹,從根節點開始,如果其值小於子節點的值,就交換其值。用此方法來遞歸其子樹。接着,我們對於堆的所有非葉節點,自下而上調用先前所述的函數,得到一個樹,對於每個節點(非葉節點),它都大於其子節點。(其實這是建立最大堆的過程)在完成之后,將列表的頭元素和尾元素調換順序,這樣列表的最后一位就是最大的數,接着在對列表的0到n-1部分再調用以上建立最大堆的過程。最后得到堆排序完成的列表。以下是源代碼:
1 class heap_sort(object): 2 def _left(self, i): 3 return 2*i+1 4 def _right(self, i): 5 return 2*i+2 6 def _parent(self, i): 7 if i%2==1: 8 return int(i/2) 9 else: 10 return i/2-1 11 12 def _max_heapify(self, alist, i, heap_size=None): 13 length = len(alist) 14 15 if heap_size is None: 16 heap_size = length 17 18 l = self._left(i) 19 r = self._right(i) 20 21 if lalist[i]: 22 largest = l 23 else: 24 largest = i 25 if ralist[largest]: 26 largest = r 27 28 if largest!=i: 29 alist[i], alist[largest] = alist[largest], alist[i] 30 self._max_heapify(alist, largest, heap_size) 31 32 def _build_max_heap(self, alist): 33 roop_end = int(len(alist)/2) 34 for i in range(0, roop_end)[::-1]: 35 self._max_heapify(alist, i) 36 37 def __call__(self, sort_list): 38 self._build_max_heap(sort_list) 39 heap_size = len(sort_list) 40 for i in range(1, len(sort_list))[::-1]: 41 sort_list[0], sort_list[i] = sort_list[i], sort_list[0] 42 heap_size -= 1 43 self._max_heapify(sort_list, 0, heap_size) 44 45 return sort_list
最后一種要說明的交換排序算法(以上所有算法都為交換排序,原因是都需要通過兩兩比較交換順序)自然就是經典的快速排序。
先來講解一下原理。首先要用到的是分區工具函數(partition),對於給定的列表(數組),我們首先選擇基准元素(這里我選擇最后一個元素),通過比較,最后使得該元素的位置,使得這個運行結束的新列表(就地運行)所有在基准元素左邊的數都小於基准元素,而右邊的數都大於它。然后我們對於待排的列表,用分區函數求得位置,將列表分為左右兩個列表(理想情況下),然后對其遞歸調用分區函數,直到子序列的長度小於等於1。
下面是快速排序的源代碼:
1 class quick_sort(object): 2 def _partition(self, alist, p, r): 3 i = p-1 4 x = alist[r] 5 for j in range(p, r): 6 if alist[j]<=x: 7 i += 1 8 alist[i], alist[j] = alist[j], alist[i] 9 alist[i+1], alist[r] = alist[r], alist[i+1] 10 return i+1 11 12 def _quicksort(self, alist, p, r): 13 if p<r: 14 q = self._partition(alist, p, r) 15 self._quicksort(alist, p, q-1) 16 self._quicksort(alist, q+1, r) 17 18 def __call__(self, sort_list): 19 self._quicksort(sort_list, 0, len(sort_list)-1) 20 return sort_list
細心的朋友在這里可能會發現一個問題,如果待排序列正好是順序的時候,整個的遞歸將會達到最大遞歸深度(序列的長度)。而實際上在操作的時候,當列表長度大於1000(理論值)的時候,程序會中斷,報超出最大遞歸深度的錯誤(maximum recursion depth exceeded)。在查過資料后我們知道,Python在默認情況下,最大遞歸深度為1000(理論值,其實真實情況下,只有995左右,各個系統這個值的大小也不同)。這個問題有兩種解決方案,1)重新設置最大遞歸深度,采用以下方法設置:
import sys
sys.setrecursionlimit(99999)
2)第二種方法就是采用另外一個版本的分區函數,稱為隨機化分區函數。由於之前我們的選擇都是子序列的最后一個數,因此對於特殊情況的健壯性就差了許多。現在我們隨機從子序列選擇基准元素,這樣可以減少對特殊情況的差錯率。新的randomize partition函數如下:
1 def _randomized_partition(self, alist, p, r): 2 i = random.randint(p, r) 3 alist[i], alist[r] = alist[r], alist[i] 4 return self._partition(alist, p, r)
完整的randomize_quick_sort的代碼如下(這里我直接繼承之前的quick_sort類):
1 import random 2 class randomized_quick_sort(quick_sort): 3 def _randomized_partition(self, alist, p, r): 4 i = random.randint(p, r) 5 alist[i], alist[r] = alist[r], alist[i] 6 return self._partition(alist, p, r) 7 8 def _quicksort(self, alist, p, r): 9 if p<r: 10 q = self._randomized_partition(alist, p, r) 11 self._quicksort(alist, p, q-1) 12 self._quicksort(alist, q+1, r)
關於快速排序的討論還沒有結束。我們都知道,Python是一門很優雅的語言,而Python寫出來的代碼是相當簡潔而可讀性極強的。這里就介紹快排的另一種寫法,只需要三行就能夠搞定,但是又不失閱讀性。(當然,要看懂是需要一定的Python基礎的)代碼如下:
def quick_sort_2(sort_list):
if len(sort_list)<=1:
return sort_list
return quick_sort_2([lt for lt in sort_list[1:] if lt<sort_list[0]]) + \
sort_list[0:1] + \
quick_sort_2([ge for ge in sort_list[1:] if ge>=sort_list[0]])
怎么樣看懂了吧,這段代碼出自《Python cookbook 第二版》,這種寫法展示出了列表推導的強大表現力。
對於比較排序算法,我們知道,可以把所有可能出現的情況畫成二叉樹(決策樹模型),對於n個長度的列表,其決策樹的高度為h,葉子節點就是這個列表亂序的全部可能性為n!,而我們知道,這個二叉樹的葉子節點不會超過2^h,所以有2^h>=n!,取對數,可以知道,h>=logn!,這個是近似於O(nlogn)。也就是說比較排序算法的最好性能就是O(nlgn)。
那有沒有線性時間,也就是時間復雜度為O(n)的算法呢?答案是肯定的。不過由於排序在實際應用中算法其實是非常復雜的。這里只是討論在一些特殊情形下的線性排序算法。特殊情形下的線性排序算法主要有計數排序,桶排序和基數排序。這里只簡單說一下計數排序。
計數排序是建立在對待排序列這樣的假設下:假設待排序列都是正整數。首先,聲明一個新序列list2,序列的長度為待排序列中的最大數。遍歷待排序列,對每個數,設其大小為i,list2[i]++,這相當於計數大小為i的數出現的次數。然后,申請一個list,長度等於待排序列的長度(這個是輸出序列,由此可以看出計數排序不是就地排序算法),倒序遍歷待排序列(倒排的原因是為了保持排序的穩定性,及大小相同的兩個數在排完序后位置不會調換),假設當前數大小為i,list[list2[i]-1] = i,同時list2[i]自減1(這是因為這個大小的數已經輸出一個,所以大小要自減)。於是,計數排序的源代碼如下。
1 class counting_sort(object): 2 def _counting_sort(self, alist, k): 3 alist3 = [0 for i in range(k)] 4 alist2 = [0 for i in range(len(alist))] 5 for j in alist: 6 alist3[j] += 1 7 for i in range(1, k): 8 alist3[i] = alist3[i-1] + alist3[i] 9 for l in alist[::-1]: 10 alist2[alist3[l]-1] = l 11 alist3[l] -= 1 12 return alist2 13 14 def __call__(self, sort_list, k=None): 15 if k is None: 16 import heapq 17 k = heapq.nlargest(1, sort_list)[0] + 1 18 return self._counting_sort(sort_list, k)
各種排序算法介紹完(以上的代碼都通過了我寫的單元測試),我們再回到Python這個主題上來。其實Python從最早的版本開始,多次更換內置的排序算法。從開始使用C庫提供的qsort例程(這個方法有相當多的問題),到后來自己開始實現自己的算法,包括2.3版本以前的抽樣排序和折半插入排序的混合體,以及最新的適應性的排序算法,代碼也由C語言的800行到1200行,以至於更多。從這些我們可以知道,在實際生產環境中,使用經典的排序算法是不切實際的,它們僅僅能做學習研究之用。而在實踐中,更推薦的做法應該遵循以下兩點:
當需要排序的時候,盡量設法使用內建Python列表的sort方法。
當需要搜索的時候,盡量設法使用內建的字典。
我寫了測試函數,來比較內置的sort方法相比於以上方法的優越性。測試序列長度為5000,每個函數測試3次取平均值,可以得到以下的測試結果:
可以看出,Python內置函數是有很大的優勢的。因此在實際應用時,我們應該盡量使用內置的sort方法。
由此,我們引出另外一個問題。怎么樣判斷一個序列中是否有重復元素,如果有返回True,沒有返回False。有人會說,這不很簡單么,直接寫兩個嵌套的迭代,遍歷就是了。代碼寫下來應該是這樣。
1 def normal_find_same(alist): 2 length = len(alist) 3 for i in range(length): 4 for j in range(i+1, length): 5 if alist[i] == alist[j]: 6 return True 7 return False
這種方法的代價是非常大的(平均時間復雜度是O(n^2),當列表中沒有重復元素的時候會達到最壞情況),由之前的經驗,我們可以想到,利用內置sort方法極快的經驗,我們可以這么做:首先將列表排序,然后遍歷一遍,看是否有重復元素。包括完整的測試代碼如下:
1 import time 2 import random 3 4 def record_time(func, alist): 5 start = time.time() 6 func(alist) 7 end = time.time() 8 9 return end - start 10 11 def quick_find_same(alist): 12 alist.sort() 13 length = len(alist) 14 for i in range(length-1): 15 if alist[i] == alist[i+1]: 16 return True 17 return False 18 19 if __name__ == "__main__": 20 methods = (normal_find_same, quick_find_same) 21 alist = range(5000) 22 random.shuffle(alist) 23 24 for m in methods: 25 print 'The method %s spends %s' % (m.__name__, record_time(m, alist)) 26
運行以后我的數據是,對於5000長度,沒有重復元素的列表,普通方法需要花費大約1.205秒,而快速查找法花費只有0.003秒。這就是排序在實際應用中的一個例子。