Python實(shí)現(xiàn)各種排序算法的代碼示例總結(jié)
在Python實(shí)踐中,我們往往遇到排序問題,比如在對搜索結(jié)果打分的排序(沒有排序就沒有Google等搜索引擎的存在),當(dāng)然,這樣的例子數(shù)不勝數(shù)?!稊?shù)據(jù)結(jié)構(gòu)》也會花大量篇幅講解排序。之前一段時間,由于需要,我復(fù)習(xí)了一下排序算法,并用Python實(shí)現(xiàn)了各種排序算法,放在這里作為參考。
最簡單的排序有三種:插入排序,選擇排序和冒泡排序。這三種排序比較簡單,它們的平均時間復(fù)雜度均為O(n^2),在這里對原理就不加贅述了。貼出來源代碼。
插入排序:
def insertion_sort(sort_list):
iter_len = len(sort_list)
if iter_len < 2:
return sort_list
for i in range(1, iter_len):
key = sort_list[i]
j = i - 1
while j >= 0 and sort_list[j] > key:
sort_list[j+1] = sort_list[j]
j -= 1
sort_list[j+1] = key
return sort_list
冒泡排序:
def bubble_sort(sort_list):
iter_len = len(sort_list)
if iter_len < 2:
return sort_list
for i in range(iter_len-1):
for j in range(iter_len-i-1):
if sort_list[j] > sort_list[j+1]:
sort_list[j], sort_list[j+1] = sort_list[j+1], sort_list[j]
return sort_list
選擇排序:
def selection_sort(sort_list):
iter_len = len(sort_list)
if iter_len < 2:
return sort_list
for i in range(iter_len-1):
smallest = sort_list[i]
location = i
for j in range(i, iter_len):
if sort_list[j] < smallest:
smallest = sort_list[j]
location = j
if i != location:
sort_list[i], sort_list[location] = sort_list[location], sort_list[i]
return sort_list
這里我們可以看到這樣的句子:
sort_list[i], sort_list[location] = sort_list[location], sort_list[i]
不了解Python的同學(xué)可能會覺得奇怪,沒錯,這是交換兩個數(shù)的做法,通常在其他語言中如果要交換a與b的值,常常需要一個中間變量temp,首先把a(bǔ)賦給temp,然后把b賦給a,最后再把temp賦給b。但是在python中你就可以這么寫:a, b = b, a,其實(shí)這是因?yàn)橘x值符號的左右兩邊都是元組(這里需要強(qiáng)調(diào)的是,在python中,元組其實(shí)是由逗號“,”來界定的,而不是括號)。
平均時間復(fù)雜度為O(nlogn)的算法有:歸并排序,堆排序和快速排序。
歸并排序。對于一個子序列,分成兩份,比較兩份的第一個元素,小者彈出,然后重復(fù)這個過程。對于待排序列,以中間值分成左右兩個序列,然后對于各子序列再遞歸調(diào)用。源代碼如下,由于有工具函數(shù),所以寫成了callable的類:
class merge_sort(object):
def _merge(self, alist, p, q, r):
left = alist[p:q+1]
right = alist[q+1:r+1]
for i in range(p, r+1):
if len(left) > 0 and len(right) > 0:
if left[0] <= right[0]:
alist[i] = left.pop(0)
else:
alist[i] = right.pop(0)
elif len(right) == 0:
alist[i] = left.pop(0)
elif len(left) == 0:
alist[i] = right.pop(0)
def _merge_sort(self, alist, p, r):
if p<r:
q = int((p+r)/2)
self._merge_sort(alist, p, q)
self._merge_sort(alist, q+1, r)
self._merge(alist, p, q, r)
def __call__(self, sort_list):
self._merge_sort(sort_list, 0, len(sort_list)-1)
return sort_list
堆排序,是建立在數(shù)據(jù)結(jié)構(gòu)——堆上的。關(guān)于堆的基本概念、以及堆的存儲方式這里不作介紹。這里用一個列表來存儲堆(和用數(shù)組存儲類似),對于處在i位置的元素,2i+1位置上的是其左孩子,2i+2是其右孩子,類似得可以得出該元素的父元素。
首先我們寫一個函數(shù),對于某個子樹,從根節(jié)點(diǎn)開始,如果其值小于子節(jié)點(diǎn)的值,就交換其值。用此方法來遞歸其子樹。接著,我們對于堆的所有非葉節(jié)點(diǎn),自下而上調(diào)用先前所述的函數(shù),得到一個樹,對于每個節(jié)點(diǎn)(非葉節(jié)點(diǎn)),它都大于其子節(jié)點(diǎn)。(其實(shí)這是建立最大堆的過程)在完成之后,將列表的頭元素和尾元素調(diào)換順序,這樣列表的最后一位就是最大的數(shù),接著在對列表的0到n-1部分再調(diào)用以上建立最大堆的過程。最后得到堆排序完成的列表。以下是源代碼:
class heap_sort(object):
def _left(self, i):
return 2*i+1
def _right(self, i):
return 2*i+2
def _parent(self, i):
if i%2==1:
return int(i/2)
else:
return i/2-1
def _max_heapify(self, alist, i, heap_size=None):
length = len(alist)
if heap_size is None:
heap_size = length
l = self._left(i)
r = self._right(i)
if l < heap_size and alist[l] > alist[i]:
largest = l
else:
largest = i
if r < heap_size and alist[r] > alist[largest]:
largest = r
if largest!=i:
alist[i], alist[largest] = alist[largest], alist[i]
self._max_heapify(alist, largest, heap_size)
def _build_max_heap(self, alist):
roop_end = int(len(alist)/2)
for i in range(0, roop_end)[::-1]:
self._max_heapify(alist, i)
def __call__(self, sort_list):
self._build_max_heap(sort_list)
heap_size = len(sort_list)
for i in range(1, len(sort_list))[::-1]:
sort_list[0], sort_list[i] = sort_list[i], sort_list[0]
heap_size -= 1
self._max_heapify(sort_list, 0, heap_size)
return sort_list
最后一種要說明的交換排序算法(以上所有算法都為交換排序,原因是都需要通過兩兩比較交換順序)自然就是經(jīng)典的快速排序。
先來講解一下原理。首先要用到的是分區(qū)工具函數(shù)(partition),對于給定的列表(數(shù)組),我們首先選擇基準(zhǔn)元素(這里我選擇最后一個元素),通過比較,最后使得該元素的位置,使得這個運(yùn)行結(jié)束的新列表(就地運(yùn)行)所有在基準(zhǔn)元素左邊的數(shù)都小于基準(zhǔn)元素,而右邊的數(shù)都大于它。然后我們對于待排的列表,用分區(qū)函數(shù)求得位置,將列表分為左右兩個列表(理想情況下),然后對其遞歸調(diào)用分區(qū)函數(shù),直到子序列的長度小于等于1。
下面是快速排序的源代碼:
class quick_sort(object):
def _partition(self, alist, p, r):
i = p-1
x = alist[r]
for j in range(p, r):
if alist[j] <= x:
i += 1
alist[i], alist[j] = alist[j], alist[i]
alist[i+1], alist[r] = alist[r], alist[i+1]
return i+1
def _quicksort(self, alist, p, r):
if p < r:
q = self._partition(alist, p, r)
self._quicksort(alist, p, q-1)
self._quicksort(alist, q+1, r)
def __call__(self, sort_list):
self._quicksort(sort_list, 0, len(sort_list)-1)
return sort_list
細(xì)心的朋友在這里可能會發(fā)現(xiàn)一個問題,如果待排序列正好是順序的時候,整個的遞歸將會達(dá)到最大遞歸深度(序列的長度)。而實(shí)際上在操作的時候,當(dāng)列表長度大于1000(理論值)的時候,程序會中斷,報超出最大遞歸深度的錯誤(maximum recursion depth exceeded)。在查過資料后我們知道,Python在默認(rèn)情況下,最大遞歸深度為1000(理論值,其實(shí)真實(shí)情況下,只有995左右,各個系統(tǒng)這個值的大小也不同)。這個問題有兩種解決方案,1)重新設(shè)置最大遞歸深度,采用以下方法設(shè)置:
import sys sys.setrecursionlimit(99999)
2)第二種方法就是采用另外一個版本的分區(qū)函數(shù),稱為隨機(jī)化分區(qū)函數(shù)。由于之前我們的選擇都是子序列的最后一個數(shù),因此對于特殊情況的健壯性就差了許多。現(xiàn)在我們隨機(jī)從子序列選擇基準(zhǔn)元素,這樣可以減少對特殊情況的差錯率。新的randomize partition函數(shù)如下:
def _randomized_partition(self, alist, p, r): i = random.randint(p, r) alist[i], alist[r] = alist[r], alist[i] return self._partition(alist, p, r)
完整的randomize_quick_sort的代碼如下(這里我直接繼承之前的quick_sort類):
import random
class randomized_quick_sort(quick_sort):
def _randomized_partition(self, alist, p, r):
i = random.randint(p, r)
alist[i], alist[r] = alist[r], alist[i]
return self._partition(alist, p, r)
def _quicksort(self, alist, p, r):
if p<r:
q = self._randomized_partition(alist, p, r)
self._quicksort(alist, p, q-1)
self._quicksort(alist, q+1, r)
關(guān)于快速排序的討論還沒有結(jié)束。我們都知道,Python是一門很優(yōu)雅的語言,而Python寫出來的代碼是相當(dāng)簡潔而可讀性極強(qiáng)的。這里就介紹快排的另一種寫法,只需要三行就能夠搞定,但是又不失閱讀性。(當(dāng)然,要看懂是需要一定的Python基礎(chǔ)的)代碼如下:
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 第二版》,這種寫法展示出了列表推導(dǎo)的強(qiáng)大表現(xiàn)力。
對于比較排序算法,我們知道,可以把所有可能出現(xiàn)的情況畫成二叉樹(決策樹模型),對于n個長度的列表,其決策樹的高度為h,葉子節(jié)點(diǎn)就是這個列表亂序的全部可能性為n!,而我們知道,這個二叉樹的葉子節(jié)點(diǎn)不會超過2^h,所以有2^h>=n!,取對數(shù),可以知道,h>=logn!,這個是近似于O(nlogn)。也就是說比較排序算法的最好性能就是O(nlgn)。
那有沒有線性時間,也就是時間復(fù)雜度為O(n)的算法呢?答案是肯定的。不過由于排序在實(shí)際應(yīng)用中算法其實(shí)是非常復(fù)雜的。這里只是討論在一些特殊情形下的線性排序算法。特殊情形下的線性排序算法主要有計數(shù)排序,桶排序和基數(shù)排序。這里只簡單說一下計數(shù)排序。
計數(shù)排序是建立在對待排序列這樣的假設(shè)下:假設(shè)待排序列都是正整數(shù)。首先,聲明一個新序列l(wèi)ist2,序列的長度為待排序列中的最大數(shù)。遍歷待排序列,對每個數(shù),設(shè)其大小為i,list2[i]++,這相當(dāng)于計數(shù)大小為i的數(shù)出現(xiàn)的次數(shù)。然后,申請一個list,長度等于待排序列的長度(這個是輸出序列,由此可以看出計數(shù)排序不是就地排序算法),倒序遍歷待排序列(倒排的原因是為了保持排序的穩(wěn)定性,及大小相同的兩個數(shù)在排完序后位置不會調(diào)換),假設(shè)當(dāng)前數(shù)大小為i,list[list2[i]-1] = i,同時list2[i]自減1(這是因?yàn)檫@個大小的數(shù)已經(jīng)輸出一個,所以大小要自減)。于是,計數(shù)排序的源代碼如下。
class counting_sort(object):
def _counting_sort(self, alist, k):
alist3 = [0 for i in range(k)]
alist2 = [0 for i in range(len(alist))]
for j in alist:
alist3[j] += 1
for i in range(1, k):
alist3[i] = alist3[i-1] + alist3[i]
for l in alist[::-1]:
alist2[alist3[l]-1] = l
alist3[l] -= 1
return alist2
def __call__(self, sort_list, k=None):
if k is None:
import heapq
k = heapq.nlargest(1, sort_list)[0] + 1
return self._counting_sort(sort_list, k)
各種排序算法介紹完(以上的代碼都通過了我寫的單元測試),我們再回到Python這個主題上來。其實(shí)Python從最早的版本開始,多次更換內(nèi)置的排序算法。從開始使用C庫提供的qsort例程(這個方法有相當(dāng)多的問題),到后來自己開始實(shí)現(xiàn)自己的算法,包括2.3版本以前的抽樣排序和折半插入排序的混合體,以及最新的適應(yīng)性的排序算法,代碼也由C語言的800行到1200行,以至于更多。從這些我們可以知道,在實(shí)際生產(chǎn)環(huán)境中,使用經(jīng)典的排序算法是不切實(shí)際的,它們僅僅能做學(xué)習(xí)研究之用。而在實(shí)踐中,更推薦的做法應(yīng)該遵循以下兩點(diǎn):
當(dāng)需要排序的時候,盡量設(shè)法使用內(nèi)建Python列表的sort方法。
當(dāng)需要搜索的時候,盡量設(shè)法使用內(nèi)建的字典。
我寫了測試函數(shù),來比較內(nèi)置的sort方法相比于以上方法的優(yōu)越性。測試序列長度為5000,每個函數(shù)測試3次取平均值,可以得到以下的測試結(jié)果:

可以看出,Python內(nèi)置函數(shù)是有很大的優(yōu)勢的。因此在實(shí)際應(yīng)用時,我們應(yīng)該盡量使用內(nèi)置的sort方法。
由此,我們引出另外一個問題。怎么樣判斷一個序列中是否有重復(fù)元素,如果有返回True,沒有返回False。有人會說,這不很簡單么,直接寫兩個嵌套的迭代,遍歷就是了。代碼寫下來應(yīng)該是這樣。
def normal_find_same(alist):
length = len(alist)
for i in range(length):
for j in range(i+1, length):
if alist[i] == alist[j]:
return True
return False
這種方法的代價是非常大的(平均時間復(fù)雜度是O(n^2),當(dāng)列表中沒有重復(fù)元素的時候會達(dá)到最壞情況),由之前的經(jīng)驗(yàn),我們可以想到,利用內(nèi)置sort方法極快的經(jīng)驗(yàn),我們可以這么做:首先將列表排序,然后遍歷一遍,看是否有重復(fù)元素。包括完整的測試代碼如下:
import time
import random
def record_time(func, alist):
start = time.time()
func(alist)
end = time.time()
return end - start
def quick_find_same(alist):
alist.sort()
length = len(alist)
for i in range(length-1):
if alist[i] == alist[i+1]:
return True
return False
if __name__ == "__main__":
methods = (normal_find_same, quick_find_same)
alist = range(5000)
random.shuffle(alist)
for m in methods:
print 'The method %s spends %s' % (m.__name__, record_time(m, alist))
運(yùn)行以后我的數(shù)據(jù)是,對于5000長度,沒有重復(fù)元素的列表,普通方法需要花費(fèi)大約1.205秒,而快速查找法花費(fèi)只有0.003秒。這就是排序在實(shí)際應(yīng)用中的一個例子。
- python實(shí)現(xiàn)冒泡排序算法的兩種方法
- python 實(shí)現(xiàn)插入排序算法
- python選擇排序算法的實(shí)現(xiàn)代碼
- python 實(shí)現(xiàn)歸并排序算法
- Python實(shí)現(xiàn)的直接插入排序算法示例
- 八大排序算法的Python實(shí)現(xiàn)
- python 實(shí)現(xiàn)堆排序算法代碼
- python算法學(xué)習(xí)之桶排序算法實(shí)例(分塊排序)
- Python實(shí)現(xiàn)的幾個常用排序算法實(shí)例
- python實(shí)現(xiàn)歸并排序算法
- python常用的各種排序算法原理與實(shí)現(xiàn)方法小結(jié)
相關(guān)文章
Flask SQLAlchemy一對一,一對多的使用方法實(shí)踐
Flask-SQLAlchemy一對一,一對多的使用方法實(shí)踐,需要的朋友可以參考下2013-02-02
DJANGO-ALLAUTH社交用戶系統(tǒng)的安裝配置
django-allauth是集成了local用戶系統(tǒng)和social用戶系統(tǒng),其social用戶系統(tǒng)可以掛載多個賬戶。也是一個流行度非常高的Django user系統(tǒng),我們這里簡單介紹下,分享下個人的使用經(jīng)驗(yàn)2014-11-11
一篇文章帶你了解python標(biāo)準(zhǔn)庫--datetime模塊
這篇文章主要為大家介紹了python中的datetime模塊,datetime模塊的接口則更直觀、更容易調(diào)用,想要了解datetime模塊的朋友可以參考一下2021-08-08

