資料結構與演算法(2)排序演算法,用Python實現插入,選擇,堆排,冒泡,快排和歸併排序
前段時間鼓起勇氣和老闆說了一下以後想從事機器學習方向的工作,所以最好能有一份不錯的實習,希望如果我有好的機會他可以讓我去,沒想到老闆非常通情達理,說人還是要追尋自己感興趣的東西,忙完這陣你就去吧。所以最近開始瘋狂地投實習生簡歷,各家春招都去投了試試。那天第一次面試去了網易,面試官感覺很年輕,也挺有耐心的,前面機器學習部分基本都沒什麼問題,最後說那寫寫程式碼吧,先來個快排吧,當時感覺有點懵,說了一句我不是計算機專業的,我可以陳述這個演算法的原理但是快速地寫可能寫不出來,現在想想真是個敗筆……後來面試官為了給我個臺階下,讓我寫了個二分查詢,這個確實簡單,但自己還是寫糊了,再後來面試官出去轉了一圈回來說二輪面試官有事,今天就先到這兒吧,就這樣掛了第一家……
回來以後,痛定思痛,一些基本的排序演算法啊什麼的還是要隨手就能寫啊,所以今兒就把幾種最常見的排序演算法用Python實現以下,防止後面再出現上面的那種遺憾。
===========================================================================
1插入排序
第一個數自然有序不用動,從第二個位置開始,依次向前掃描,如果前面一個數大於該數則交換兩者位置,直到首尾置或者前面一個數小於該數,這樣有序陣列的長度不斷增長,直到最後一個位置排序就完成了。
對於插入排序,除了首尾置每個位置要掃描一次,每次掃描的平均長度n/2,所以整體演算法的平均複雜度O(n2
Python實現如下
def insert_sort(lst):
for i in range(1,len(lst)):
j=i
num=lst[j]
while j>0 and lst[j-1]>num:
lst[j]=lst[j-1]
j-=1
lst[j]=num
2 選擇排序
從第一個位置開始整體掃描陣列一遍,找到最小的數把它移動到首尾置,然後首尾置+1,再次從首尾置出發找到最小的數移動到當前的首尾置上,如此進行下去直到首尾置和末位置重合即完成了排序。
選擇排序時間複雜度不受陣列本來分佈的影響,穩定的是O(n2),因為每次從一個位置開始都要掃描到陣列的尾部。選擇排序把最小的數移動到當前首尾置,如果是陣列採用交換的話有可能會破壞演算法穩定性,因為在最小值之前可能存在於首元素相等的元素,但如果使用連結串列實現,也就是不交換兩者的位置,二是直接把最小的元素插到當前首尾置之前這樣演算法就又是穩定的了。
Python實現如下
def select_sort(lst):
for i in range(len(lst)):
index=i
for j in range(i,len(lst)):
if lst[j]<lst[index]:
index=j
lst[i],lst[index]=lst[index],lst[i]
3 堆排序
選擇排序的主要缺點就在於它每次都要從頭到尾整體比較依次,整個排序過程下來有很多重複的操作。這裡可以利用堆這種資料結構來實現更加高效的查詢。也就是說陣列來了,先建堆,從第一個非葉子節點開始直到根節點,將每個子樹都調節成堆,需要注意的是有的子樹調整完了會對其再下一層子樹產生影響。建堆完成之後,堆頂儲存的就是陣列中的最大值,將其交換到最後一個位置然後就忽略它,再調整當前堆,再把堆頂元素換下來,如此一直進行下去就完成了排序。
對於堆排而言,首先建堆的時間是O(n),然後每次選擇一個元素的時間不超過O(log(n)),整體的複雜度就是O(n*log(n))。堆排調整元素的時候其移動的路勁和自然順序交叉,所以它是明顯不穩定的。
Python實現如下
def adjust(lst,root,end):
left=2*root+1
right=2*root+2
larger=root
if left<end and lst[left]>lst[larger]:
larger=left
if right<end and lst[right]>lst[larger]:
larger=right
if larger!=root:
lst[root],lst[larger]=lst[larger],lst[root]
adjust(lst,larger,end)
def build_heap(lst):
loc=len(lst)//2-1
while loc>=0:
adjust(lst,loc,len(lst))
loc-=1
def heap_sort(lst):
build_heap(lst)
for i in range(len(lst)-1,-1,-1):
lst[0],lst[i]=lst[i],lst[0]
adjust(lst,0,i)
4氣泡排序
氣泡排序就是從第一個元素開始,如果其大於下一個元素則交換兩個元素的位置,這樣一遍掃描下來就將最大的元素移動到了陣列的尾部,第二趟忽略第一次找到的那個最大的再從頭開始掃描,再次找到最大的將其沉到當前的尾部,如此反覆進行下去就可以完成排序。
如果依照上面的演算法,氣泡排序每次都需要掃描n趟,每趟掃描的元素數量從n開始遞減。整體複雜度是O(n2)。當然還可以做一些優化,比如某一趟掃描中沒有發現任何一個逆序那就可以直接終止掃描,陣列已經有序了,比如一個已經排好序的陣列,只需要一趟掃描,n次比較就可以完成。
Python實現如下
def bubble_sort(lst):
for i in range(len(lst)):
flag=True
for j in range(1,len(lst)-i):
if lst[j-1]>lst[j]:
lst[j-1],lst[j]=lst[j],lst[j-1]
flag=False
if flag:
break
5 快速排序
終於到了我的心頭之痛……快排的原理其實挺簡單的,首先選擇一個元素作為標準,然後將陣列中比它小的元素移動到它的左邊去,比它大的則移動到右邊,然後再分別對左邊和右邊的陣列遞迴使用快排。這裡最主要的一個問題就是如何將陣列中的小於標準的移動到左邊,大於標準的移動到右邊。通常我們選取首元素作為標準,那麼相當於首尾置就空了,先末位置開始往前掃描找到第一個小於標準的數將其移動到首尾置,這下後面又剩下一個空位置,我們再從第二個元素開始往後掃描找到第一個大於標準的數將其移動到後面的空位置上,就這樣反覆進行,直到這兩個位置相交,再將標準移動到小於標準元素的最後一個位置。當然我們也可以採用另一種簡單一點的方式,分別用兩個座標表示小於標準元素和大於標準元素的最後一個位置,然後從標準後的元素開始掃描,如果該元素小於標準,則小於標準元素的最後位置座標+1,交換那個位置和當前位置的元素,掃描到陣列尾部,再交換標準元素和小於標準元素的最後一個位置。
快排的效率和元素劃分的關係很大,如果每次都是劃分成相等的兩段那麼複雜度大概在O(n*log(n)),如果每次劃分只是將標準劃分出去,那麼需要的複雜度就是O(n2)。但是抽象地看,快排這種劃分其實就是二叉樹,其平均高度應該是O(og(n)),那麼整體的平均複雜度應該是O(n*log(n))。快排明顯是不穩定的,因為在依據標準劃分成兩部分的時候很有可能產生新的逆序,所以對於快排而言即使原序列非常接近有序也不會使其變得更加高效。
Python實現如下
def quick_sort(lst,l=0,r=len(lst)-1):
if l>=r:
return
i=l
j=r
pivot=lst[i]
while i<j:
while i<j and lst[j]>=pivot:
j-=1
if i<j:
lst[i],lst[j]=lst[j],lst[i]
i+=1
while i<j and lst[i]<=pivot:
i+=1
if i<j:
lst[i],lst[j]=lst[j],lst[i]
j-=1
lst[i]=pivot
quick_sort(lst,l,i-1)
quick_sort(lst,i+1,r)
def quick_sort1(lst,start=0,end=len(lst)-1):
if start>=end:
return
pivot=lst[start]
i=start
for j in range(start+1,end+1):
if lst[j]<pivot:
i+=1
lst[i],lst[j]=lst[j],lst[i]
lst[start],lst[i]=lst[i],lst[start]
quick_sort1(lst,start,i-1)
quick_sort1(lst,i+1,end)
6 歸併排序
感覺比較快的排序都用了遞迴的思想,線性掃描的時間實在是太慢了。歸併排序的想法就是每次按照一定的長度對序列進行劃分,每個子序列是有序的,下一步子序列長度*2,兩兩合併成有序,依次進行下去。需要注意的部分就是序列的長度不一定是2的冪,所以最後剩下來的部分需要單獨考慮一下,如果其小於一個序列長度,直接新增在歸併完的序列後面,如果其大於一個序列長度,則將其劃分成一個序列和一個短序列進行合併。
歸併的複雜度很明顯是O(n*log(n))。對於穩定性而言,我們只要注意處理遇到相同值的時候將前面的值排在前面,那麼歸併排序演算法就是穩定的。
Python實現如下
def merge(lfrom,lto,low,mid,high):
i,j,k=low,mid,low
while i<mid and j<high:
if lfrom[i]<=lfrom[j]:
lto[k]=lfrom[i]
i+=1
else:
lto[k]=lfrom[j]
j+=1
k+=1
while i <mid:
lto[k]=lfrom[i]
i+=1
k+=1
while j <high:
lto[k]=lfrom[j]
k+=1
j+=1
def merge_pass(lfrom,lto,llen,slen):
i=0
while i+2*slen<llen:
merge(lfrom,lto,i,i+slen,i+2*slen)
i+=2*slen
if i+slen<llen:
merge(lfrom,lto,i,i+slen,llen)
else:
for j in range(i,llen):
lto[j]=lfrom[j]
def merge_sort(lst):
slen,llen=1,len(lst)
templst=[None]*llen
while slen<llen:
merge_pass(lst,templst,llen,slen)
slen*=2
merge_pass(templst,lst,llen,slen)
slen*=2
=====================================================
完成了這些演算法之後,我們用一個例子來看看這些演算法的執行時間分別是多少,看看是否和複雜度分析的一致。
隨機在1到10000生成10000個數,然後分別用Python自帶的sorted方法,插入排序,選擇排序,堆排序,兩種快排和歸併排序方法進行排序並統計排序時間,重複進行10次取平均值,程式碼如下
def test(sort,lst):
start=time.time()
sort(lst)
end=time.time()
return end-start
time_rec=[]
for i in range(10):
lst=[random.randint(0,10000) for x in range(10000)]
func=[sorted,insert_sort,select_sort,heap_sort,quick_sort,quick_sort1,merge_sort]
for j,fun in enumerate(func):
dt=test(fun,copy.deepcopy(lst))
print j,dt
time_rec.append(dt)
average_time=[]
for start in range(7):
average_time.append(sum(time_rec[start::7])/10)
最後的結果
Sorted:0.00280003547668 Insert_sort:3.81039996147
Select_sort:3.1276999712 Heap_sort:0.0629999876022
Quick_sort:0.0250000476837 Quick_sort1:0.0212999820709
Merge_sort:0.0320999860764
插入和選擇排序的確慢得令人髮指,然後兩種快排和歸併排序比其高兩個量級,系統自帶的sorted方法則又快了一個量級。算一算,10000/(log210000)差不多是752,這樣高兩個量級也是合情合理啊。但是還是系統自帶的厲害啊,它用的是蒂姆排序,它是一種混成式的方法,也就是結合了簡單排序和複雜排序各自的優點,這裡我就不去詳細討論了。
希望找實習可以順利一點啊~~~~