排序就是整理數據的序列,使其中元素按照特定的順序排列的操作。排序可以使數據的存儲方式更具有結構性。排序算法是算法的入門知識,每種算法都有其使用的場合,死記硬背很難記憶,理清算法的本質更有助於我們記憶。
對於每種排序方法,我們需要明白,每個算法的思想是什麼?算法的穩定性如何,時間複雜度是多少,在什麼情況下,算法出現最好(最壞)情況以及每種算法的具體實現。
插入排序:
顧名思義其基本操作是插入,不斷把一個個元素插入到一個序列中,最終得到排序序列。爲此只需維持好所構造序列的排序性質,最終就能自然地得到結果。
'''
當前需要排序的元素(data[i]),跟已經排序好的最後一個元素比較(data[i-1]),如果滿足條件繼續執行後面的程序,否則循環到下一個要排序的元素。
緩存當前要排序的元素的值,以便找到正確的位置進行插入。
排序的元素跟已經排序號的元素比較,比它大的向後移動(升序)。
要排序的元素,插入到正確的位置。
'''
def insert_sort(data):
for i in range(1,len(data)): #i控制遍歷所有的元素
j = i #j控制當前元素
while j>0 and data[j]<data[j-1]:
data[j],data[j-1] = data[j-1],data[j] #交換位置
j -= 1
data = raw_input() #通過IO讀入數據,注意讀入爲string類型
data=[int(k) for k in data.split(' ')]#將讀入的數據轉換成int型list
insert_sort(data)
print data
考慮算法時間複雜度:外層循環總是要做n-1次,內層循環的次數與實際比較的情況有關。因此,若原數列已排序,則只進行外層的循環,此時爲最好的情況,時間複雜度爲O(n)。若原數列爲逆序,則內層循環每一次都執行,此時爲最壞的情況,時間複雜度爲n*(n-1)/2,即爲O(n**2)。考慮平均時間,假設插入個位置的概率相等,內層循環每次迭代j/2次,求和都得到n*(n-1)/2 複雜度仍爲O(n**2),不難看出,算法是穩定的。
選擇排序
思想(從小到大選擇):
維護需要考慮的所有記錄中的最小的i個記錄的已排序序列。
每次從生育未排序的記錄中選取值最小的記錄,將其放在已排序序列記錄的後面,自以爲序列的第i+1個值,使已排序序列增長。
以空序列作爲排序工作的開始,做到尚未排序的序列裏只剩一個元素時,將其直接放在已排序序列的最後。
如果需要從大道小排序,只需要每次選擇最大記錄即可。
def SelectSort(data):
if(len(data)<=1):
return data
for i in range(len(data)-1): #外循環
mins = i
for j in range(i,len(data)): #尋找最小的元素
if data[j]<data[mins]:
mins = j
if mins!=i :
data[i],data[mins]=data[mins],data[i]
data = raw_input() #通過IO讀入數據,注意讀入爲string類型
data=[int(k) for k in data.split(' ')]#將讀入的數據轉換成int型list
SelectSort(data)
print data
直接選擇排序的時間複雜度是O(n**2)。
直接選擇排序不是穩定排序,舉個小例子:
例如:(7) 2 5 9 3 4 [7] 1…當我們利用直接選擇排序算法進行排序時候,(7)和1調換,(7)就跑到了[7]的後面了,原來的次序改變了,這樣就不穩定了。
堆排序
堆排序是一種高效的選擇排序算法,基於堆的概念,該算法高效的主要原因是在隊裏積累了前面所做的比較中得到的信息。由於堆的結構特徵,這種信息可以很自然地重複利用。另一方面,由於堆和順序表的關係,一個堆和一個排序序列可以很方便地嵌入同一個順序表,不需要任何輔助結構。
採用堆排序技術,初始建堆需要O(n)的時間,隨後每選擇一個元素不超過O(log n)的時間,所以堆排序的時間複雜度是O(nlog n).堆排序是原地排序算法,所以空間複雜度爲O(1).
堆排序不是穩定排序,在初始建堆和一系列選擇元素後的篩選中,元素都沿着堆中的路徑移動。這種移動路徑與連續表裏自然地順序相互交叉,在這種移動中,很可能出現相同關鍵碼記錄的順序被交換的情況。因此,對排序的特定決定了它的不穩定性,而且很難做出穩定的堆排序算法。
如果要得到從大到小的排序序列,每次選擇的關鍵碼應該是最小的記錄(即選擇小頂堆),反之選擇大頂堆。
堆及其性質:
採用樹形結構實現優先隊列的一種有效技術成爲堆。從結構上看,堆就是節點裏存儲數據的完全二叉樹,但堆中數據的存儲要滿足一種特殊的堆序:任一個節點裏所存的數據先於或等於其子節點裏面的數據。
根據堆的定義,不難看到:
1. 在一個堆中從樹根到任何一個葉結點的路徑上,各結點裏所存的數據按規定的優先關係(非嚴格)遞減。
2. 堆中最優先的元素必定位於二叉樹的根結點裏(堆頂),O(1)的時間就能得到。
3. 位於樹中不同路徑上的元素,不關心其順序關係。
基於上述理論,堆排序算法就分爲下面幾個步驟:
1. 把序列整理成大根堆
2. 把大根堆的根和序列最後一個數交換
3. 把除去最後一個數後剩下的子序列再變成大根堆
4. 重複2和3直到序列的第二個數爲止
那麼如何把一個序列變成大根堆,這是一個遞歸算法,大凡樹結構相關的算法,遞歸總是少不了的,因爲樹本身就是一個遞歸結構。假設一棵樹的左右子樹已經是大根堆,那麼要把這棵樹變成大根堆的過程就是:
1. 在根和他的左右子節點中找到最大值:
2. 如果根就是最大值,什麼都不做,這已經是大根堆
3. 如果根不是最大值,就把根和最大值交換位置。
4. 把交換後的那一棵樹執行1到4步。
堆排序代碼如下:
def heap_sort(data):
def siftdown(data,begin,end):
i,j=begin,begin*2+1
term =data[i]
while j <end:
if j+1 <end and data[j+1]< data[j]:
j+=1
if data[i]>data[j]:
break
data[i] = data[j]
i,j =j,j*2+1
data[i] = term
end = len(data)
for i in range(end/2,-1,-1):
siftdown(data,i,end)
for i in range((end-1),0,-1):
data[i],data[0] = data[0],data[i]
siftdown(data,0,i)
data = [1,10,5,0,9]
heap_sort(data)
print data
冒泡排序
冒泡排序是一種最簡單的排序方法,其通過交換元素消除逆序實現排序。其時間複雜度爲O(n**2),是一種穩定的排序算法。
其代碼如下:
def BubbleSort(data):
for i in range(len(data)):
for j in range(1,len(data)-i):
if data[j-1]>data[j]:
data[j-1],data[j]=data[j],data[j-1]
data = [5,9,2,8,4,1]
BubbleSort(data)
print data
冒泡算法的改進:
冒泡算法是基於交換的策略的排序算法,如果某一次交換中,發現沒有遇到逆序,則說明排序工作已經完成,可以提前結束,基於此策略,改進算法的代碼如下:
def SuperBubbleSort(data):
for i in range(len(data)) :
judge = 0 #設置一個警示位,處置
for j in range(1,len(data)-i):
if data[j-1]>data[j]:
data[j-1],data[j]=data[j],data[j-1]
judge = 1 #如果在本輪中發生了交換,則將其置爲1
if(judge ==0): #如果在本輪中沒交換,結束冒泡算法
break
data = [5,9,2,8,4,1,100]
SuperBubbleSort(data)
print data
快速排序
快速排序是一種基於分治思想的算法,其思想是按某種標準把考慮的記錄劃分爲“小記錄”和“大記錄”,並通過遞歸不斷劃分,最終得到一個排序的序列。其基本過程是:
1.選擇一種標準,把被排序序列中的記錄按這種標準分爲大小兩組。
2.採用同樣的方式,遞歸地分別劃分得到的這兩組記錄,並繼續遞歸地劃分下去
3.劃分總是得到越來越小的數組,如此工作下去直到每個記錄組中最多包含一個記錄時,整個序列的排序完成。
# 切分過程,先在數組中選擇一個數字,接下來把數組中的數字分成兩部分,比選擇小的數字移到
#數組的左邊,比選擇大的數字移到右邊
import random
#切分的第一種實現:固定基準円,標準實現
def partition(data,start,end):
# index = random.randint(start,end)
frist =start
last = end
index = data[start]
#data[start],data[index]=data[index],data[start]
while(frist<last):
while(frist<last and data[last]>=index):
last-=1
data[frist]=data[last]
while(frist<last and data[frist]<=index):
frist+=1
data[last]=data[frist]
data[frist]=index
print frist
print data
return frist
#切分的第一種實現,通過定義一個小數的區間,原地排序
def partition2(data,start,end):
index = random.randint(start,end)
#print data[index]
small = start -1
data[index],data[end]=data[end],data[index]
for i in range(start,end):
if(data[i]<data[end]):
small+=1
if(small!=i):
data[small],data[i]=data[i],data[small]
small+=1
data[small],data[end]=data[end],data[small]
return small
def quicksort(data,start,end):
if(start>=end):
return
index = partition(data,start,end)
print index
if(index>start):
quicksort(data,start,index-1)
if(index<end):
quicksort(data,index+1,end)
if __name__ == '__main__':
data=raw_input()
data=map(int,data.split(' '))
quicksort(data,0,len(data)-1)
其中:對於基準index的選擇,可以使用隨機選擇,取第一個或最後一個元素,或者三數取中值的方法進行選擇。隨機選擇和三數取中值策略能夠避免因待排序列有序而造成的快速排序算法退化到冒泡的問題。
關於快排基準元選擇的問題,如果讀者更深入的去了解,請訪問http://www.codeceo.com/article/3-sort-quick-sort-improve.html,這篇文章比較系統和詳細的說明了這一問題。
快速排序時間複雜度爲O(nlog(n)),最壞情況爲O(n**2)。
歸併排序
歸併是一種典型的序列操作,其工作是把兩個或更多有序序列合併成爲一個有序序列。歸併排序(二路)的基本方法如下:
初始時,把待排序序列中的n個記錄看成n個有序子序列,每個子序列的長度爲1.
把當時序列組裏的有序子序列兩兩歸併,完成一遍後序列組裏的排序序列個數減半,每個子序列的長度加倍。
對加長的有序子序列重複上面的操作,最終得到一個長度爲n的有序序列。
算法時間複雜度:最好的情況下:一趟歸併需要n次,總共需要logN次,因此爲O(N*logN)。最壞的情況下,接近於平均情況下,爲O(N*logN)
說明:對長度爲n的文件,需進行logN 趟二路歸併,每趟歸併的時間爲O(n),故其時間複雜度無論是在最好情況下還是在最壞情況下均是O(nlgn)。
歸併排序最大的特色就是它是一種穩定的排序算法。歸併過程中是不會改變元素的相對位置的。缺點是,它需要O(n)的額外空間。但是很適合於多鏈表排序。
一種代碼實現方式如下:
import sys
##sys.setrecursionlimit(1000000) #例如這裏設置爲一百萬
def merge(nums,first,middle,last):
lnums = nums[first:middle+1]
rnums = nums[middle+1:last+1]
lnums.append(sys.maxint) #保證兩個數都能被歸併到NUM中去
rnums.append(sys.maxint)
l = 0
r = 0
for i in range(first,last+1):
if lnums[l]<rnums[r]:
nums[i] = lnums[l]
l+=1
else:
nums[i]=rnums[r]
r+=1
def merge_sort(nums,frist,last):
if frist<last:
middle = (frist + last)/2
merge_sort(nums,frist,middle)
merge_sort(nums,middle+1,last)
merge(nums,frist,middle,last)
if __name__=='__main__':
nums =[10,2,5,7,8,4,6,8,9,3,1,0,-5,-3]
print nums
merge_sort(nums,0,13)
print nums