一、排序簡介
我們通常所說的排序算法往往指的是內部排序算法,即數據記錄在內存中進行排序。
排序算法大體可分為兩種:
一種是比較排序,時間復雜度O(nlogn) ~ O(n^2),主要有:冒泡排序,選擇排序,插入排序,歸并排序,堆排序,快速排序等。
另一種是非比較排序,時間復雜度可以達到O(n),主要有:計數排序,基數排序,桶排序等。
穩定性:冒泡直接歸并。
二、冒泡排序
參考冒泡排序
冒泡排序算法的運作如下:
- 比較相鄰的元素,如果前一個比后一個大,就把它們兩個調換位置。
- 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最后一對。這步做完后,最后的元素會是最大的數。
- 針對所有的元素重復以上的步驟,除了最后一個。
- 持續每次對越來越少的元素重復上面的步驟,直到沒有任何一對數字需要比較。
對于圖片上的直觀理解,建議看冒泡排序中代碼后面的圖片。
需要注意的地方:
- 1、因為是需要比較兩個元素,所以在索引時用到了alist[i+1]的情況,因此在為了不讓index超出范圍,應該讓for循環中的passnum-1。
- 2、每一次循環,最大的元素都已經排到列表最后了,下一次循環就可以減短列表長度了,因此在循環結束以后需要加上passnum -= 1。
代碼實現:
def bubbleSort(alist):
#構造一個倒序列表,從而限制了每次遍歷一次以后的循環長度
passnum = len(alist)
while passnum > 0:
for i in range(passnum-1):
if alist[i]>alist[i+1]:
temp = alist[i]
alist[i] = alist[i+1]
alist[i+1] = temp
passnum -= 1
alist = [54,26,93,17,77,31,44,55,20]
bubbleSort(alist)
print(alist)
三、選擇排序
選擇排序也是一種簡單直觀的排序算法。(以從小到大的排序為例。)
1、初始時在序列中找到最小元素,放到序列的起始位置作為已排序序列;
2、然后,再從剩余未排序元素中繼續尋找最小元素,放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。
具體操作圖片見 選擇排序
選擇排序與冒泡排序的區別:
- 冒泡排序通過依次交換相鄰兩個順序不合法的元素位置,從而將當前最小元素放到合適的位置
- 選擇排序每遍歷一次都記住了當前最?。ù螅┰氐奈恢?/strong>,最后僅需一次交換操作即可將其放到合適的位置。
由于選擇排序需要記住的是當前最大元素的位置,因此需要設置一個index_max的變量并令初始值為0 ,接下來就要比較每個元素,循環一次,就能得到最大值的index了。
接下來就是交換元素的操作。
另外,這里的for循環中passnum應該保持不變。
代碼實現:
def selectionSort(alist):
#構造一個倒序列表,從而限制了每次遍歷一次以后的循環長度
passnum = len(alist)
while passnum > 0:
index_max = 0
for i in range(passnum):
if alist[i]>alist[index_max]:
index_max = i
temp = alist[passnum-1]
alist[passnum-1] = alist[index_max]
alist[index_max] = temp
passnum -= 1
alist = [54,26,93,17,77,31,44,55,20]
selectSort(alist)
print(alist)
四、插入排序
插入排序是一種簡單直觀的排序算法。它的工作原理非常類似于我們抓撲克牌。
如下圖:
對于一個列表[5,4,2,10,7],在用插入排序到達[2,4,5,10,7]的結果時,對于最后一個元素7,從后向前進行比較,比10小,就與10交換位置,直到前面一位的元素比7小為止。
注意,在從后向前掃描的過程中,需要不斷地交換兩個元素的位置。
從圖像上直觀理解插入排序
偽代碼:
- 從第一個元素開始,該元素可以認為已經被排序
- 取出下一個元素,在已經排序的元素序列中從后向前掃描
- 如果該元素(已排序)大于新元素,將該元素移到下一位置
- 重復步驟3,直到找到已排序的元素小于或者等于新元素的位置
- 將新元素插入到該位置后
- 重復步驟2~5
代碼:
按照偽代碼,此處需要加兩個循環:
- 1、一個是for循環,按照插入排序的思想,需要對列表中每個元素執行一次插入操作,從左到右。
- 2、一個是while循環,執行比較與換位操作。
若是前一個元素nums[cur-1]大于當前元素nums[cur],則二者互換位置
1、while循環結束條件:
當cur指向的元素為0即列表開頭時,停止循環
當cur元素小于等于第cur-1的元素時,目的已經達到,執行break語句。
2、while循環改變語句:
每循環一次,cur指針就要減1,與前面的前面的元素進行比較
代碼實現:
def Insertionsort(nums):
for i in range(len(nums)):
cur = i
while cur >= 1:
if nums[cur-1] > nums[cur]:
temp = nums[cur - 1]
nums[cur - 1] = nums[cur]
nums[cur] = temp
cur -= 1
else:
break
return nums
print(Insertionsort([6, 5, 4, 1, 8, 7, 2, 4 ]))
五、希爾排序
希爾排序,也叫遞減增量排序,是插入排序的一種更高效的改進版本。
希爾排序是基于插入排序的以下兩點性質而提出改進方法的:
- 插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率
- 但插入排序一般來說是低效的,因為插入排序每次只能將數據移動一位
于是,我們提出一個想法,(針對第二點)對于每個元素,每次不再只移動一位,而是“大踏步”的移動。(針對第一點)經過“大踏步”的移動以后,順序相對好很多,此時再進行插入排序,效果會好很多。
針對上面你的想法,我們再具體的提出一些改進。
比如,對于一個有9個元素的list,我們取gap為3,先對index為0,3,6的元素進行插入排序,再對1,4,7的子列表插入排序,再對2,5,8的子列表進行插入排序。
經過對這三個子列表進行排序以后,我們發現此時的列表相對好了很多:
此時再進行插入排序,只需要再經過三次的替換就行了:
另外我們注意到,gap == 子列表數。比如,我們設定gap為3,那么子列表的數目也就是3。怎么理解呢,也很好理解:從第0位元素出發,3為gap向右走到頭是sublist1,同理從第1位出發, 第2位出發,可以得到sublist2,sublist3。這三個子列表就已經把整個list給遍歷完了。
也就是說,對于一個gap,我們會得到gap個子列表,而這gap個子列表的起始元素列表是
start_list = [x for i in range(gap)]
我們可以通過上面的理解,得到要進行插入排序的每個子列表的起始元素。
我們再定義一個插入排序函數,就是上一節的插入排序代碼,只不過gap不再是1了。傳入列表nums,子列表的起始元素start,gap,我們便可以對這個子列表進行插入排序。
而這里的希爾排序,我們不是固定了gap,我們先采用最大的gap(即len(nums)//2),然后依次將gap除以2,直到gap為1進行最后的插入排序。比如上面例子中,我們采用的是長度為9 的list,那么我們先用gap為4,此時就有四個子列表,我們對這四個子列表調用插入排序的函數。下次gap為2,就有2個子列表,再調用插入排序的函數。。。
代碼思路
- 所以需要一個while循環,循環對不同的gap進行分列表以及排序操作,循環結束條件為gap等于0.
- while里再需要一個for循環,按照上面所說對gap子列表調用插入排序函數。
- 定義一個傳入start,gap,nums,能進行插入排序的函數。
代碼實現:
def shellSort(nums):
#gap初始化為最大的gap,然后在while循環中不斷整除2,減小gap
gap = len(nums)//2
while gap > 0:
#對gap個子列表調用插入排序函數
for startposition in range(gap):
gapInsertionSort(nums,startposition,gap)
print("After increments of size",gap,
"The list is",nums)
gap = gap // 2
#輸入nums,起始位置,gap,就可以選出子列表,并且進行插入排序
def gapInsertionSort(nums,start,gap):
for i in range(start,len(nums),gap):
cur = i
while cur >= gap:
if nums[cur - gap] > nums[cur]:
temp = nums[cur - gap]
nums[cur - gap] = nums[cur]
nums[cur] = temp
cur -= gap
else:
break
nums = [54,26,93,17,77,31,44,55,20]
shellSort(nums)
print(nums)
六、歸并排序
參考歸并排序
最易于理解的白話:首先考慮下如何將將二個有序數列合并
- 1、這個非常簡單,只要從比較二個數列的第一個數,誰小就先取誰,取了后就在對應數列中刪除這個數。
- 2、然后再進行比較,如果有數列為空,那直接將另一個數列的數據依次取出即可。
比如,13跟24678合并。
1跟2比較,1小于2,那么list.append(1)。
3跟2比較,2小于3,那么list.append(2)
3跟4比較,3小于4,那么list.append(3)
left數列已經為空,那么就把right的數列都append到list中。
那么歸并排序呢,一樣的道理。但是不一樣的地方就是,一開始不是兩個有序list,而是是一整個無序list,為了滿足“兩個,有序”的要求,我們分兩步:
- 1、先把list分成left和right兩個無序的部分
- 2、把這兩個無序的部分,調用函數排成兩個有序的部分
這里的兩個無序的部分怎么排序成有序的部分呢。就是遞歸調用歸并排序函數了。 - 3、對兩個有序的部分,進行上面的merge操作即可。
其中遞歸的地方就是,上面的第二步中,上面的
代碼思路
- 寫一個對兩個有序list進行歸并的函數merge。傳入兩個有序list,返回一個合并好的list。
- 寫一個遞歸調用自身的歸并排序函數mergesort,傳入一個無序list,調用merge,返回一個有序list。內容如下
1、把無序列表分成兩個無序列表
2、對上面兩個無序列表遞歸調用歸并排序函數mergesort,能夠返回兩個有序的子列表。
3、對上一步返回的兩個有序子列表,調用merge函數。
把一個list一路遞歸到每個list長度為1的時候,返回兩個長度為2的有序list,合并后再返回兩個長度為4的有序list,再合并。。一直到返回len(nums)的list。
具體細節圖片參考歸并排序
代碼實現:
def MergeSort(lists):
#遞歸結束條件,list小于等于1
if len(lists) <= 1:
return lists
#把無序list分成兩個部分
num = int( len(lists)/2 )
#對這兩個無序list,遞歸調用歸并排序函數
left = MergeSort(lists[:num])
right = MergeSort(lists[num:])
#對于返回的兩個有序list,調用merge函數
return Merge(left, right)
#按照合并兩個有序list的論述,定義一個合并函數
def Merge(left,right):
r, l=0, 0
result=[]
while l<len(left) and r<len(right):
if left[l] < right[r]:
result.append(left[l])
l += 1
else:
result.append(right[r])
r += 1
result += right[r:]
result += left[l:]
return result
print(MergeSort([1, 2, 3, 4, 5, 6, 7, 90, 21, 23, 45]))
七、堆排序
對于堆的理解,參考樹4,二叉樹的特例——堆
三、快速排序
3.1、大致理解快速排序的方針
3.1.1、以中間數為基準,先把一個list分成兩個大小兩個區域
對于一個無序list,取index為0的元素作為中間數,然后執行分區函數,讓比中間數小的點都在中間數左邊,比中間數大的點都在中間數右邊。此時得到了一個看似有序其實無序的list:
- 有序體現在此時list分成了兩個區域,左邊的都比中間數小,右邊的都比中間數大。
- 無序體現在,而這兩個區域,又都是無序的list
3.1.2、對上述的兩個區域,遞歸調用快速排序
我們對這兩個無序的list再次調用分區函數,此時會得到四個看似有序而又無序的list,接著調用分區函數,直到最終list長度為2時,再調用一次分區函數,那么一定是左邊有序并且小于右邊,因此此時所有的子列表都是有序的,那么此時一整個list也就是一個有序的list了。
3.2、快速排序的細節問題
3.2.1、分區函數怎么分區
我們取index為0的元素為中間數,left指針代表其指向的元素應該在中間數左邊,即小于中間數;right代表其指向的元素應該在中間數右邊,大于中間數。而實際情況不會這么完美,于是我們進行下面這樣的操作,如下圖:
無序列表為nums,對于我們要執行分區函數的列表調用分區函數:
- 一開始我們選54作為中間節點,定義left指針指向index為1的元素,right指向列表的最后一個元素。左右指針開始匯合;
- 一開始左指針指向元素為26,小于54,說明這個元素的位置是正確的,則左指針加1向右移動,指向96時,這個元素位置是錯誤的,暫時停止left的移動
- 同理,我們移動右指針,直到找到錯誤的元素,為20
- 對這兩個位置錯誤的元素,調換位置,然后再接著移動左右指針。
移動過程中發現位置錯誤的元素,繼續調換位置。
直到最后這種情況:
此時左指針指向77停止,右指針指向31停止,但是此時左指針大于右指針了,說明他倆指向了兩人已經工作過的區域了,此時不用再調換位置了。直接替換右指針指向元素與中間數,就能得到一個,已經分好區域的list了。左右兩個區域。
3.2.2、怎么調用遞歸函數使得能夠繼續分區
上面我們其實會得到兩個小的無序的區域,以什么劃分這兩個區域呢(注意中間數不用再摻和進去了),就是中間元素的index啊。于是:
- 左邊的無序區域為nums[first:index-1]
- 右邊的無序區域為nums[index+1,last]
于是對這兩個區域再調用遞歸函數即可。
3.3、代碼思路:
3.3.1、定義快排函數quickSort()
根據上面所說,遞歸調用的函數,需要傳入三個參數,一個是列表nums,還有兩個是用來劃分需要快排區域的參數first和last。
而一開始我們只能傳入一個參數就是列表nums。
因此我們定義一個輔助函數quickSortHelper(),既能用來遞歸調用快排操作,又能傳入三個參數。
初始化中,傳入的三個參數分別是nums,first = 0,last = len(nums) - 1
3.3.2、定義遞歸調用函數partition()
此函數傳入三個參數,列表nums,以及用來劃分要對列表進行快排操作的區域指針,first和last
執行此函數后,函數會將要分區的列表區域進行分區。
- 1、遞歸結束條件是first小于last
- 2、函數改變條件,每次傳入的需要排序的區域指針都會不斷改變,直到first大于last截止
函數里面調用一次分區函數,返回中間數的index,用來改變下次調用遞歸函數的first與last。
其中左半部分區域為first,index-1.右半部分為index+1,last - 遞歸調用,上一步,得到遞歸調用的區域,直接遞歸調用兩次快排函數即可。
3.3.3、定義分區函數
此函數傳入三個參數,列表nums,以及用來劃分要對列表進行快排操作的區域指針,first和last
1、用一個“大”while循環來執行錯誤元素換位的情況,循環結束的條件為左指針大于右指針。
2、里面再用“小”while循環來執行移動指針的情況,當指針指向元素相對中間數“正確”時,就繼續執行循環移動指針。
需要用到兩個while,一個控制左指針,一個控制右指針。3、當上述兩個“小”while循環結束時,說明左右指針都指向了相對錯誤的元素,此時分為兩種情況:
1、左指針小于右指針:說明此時分區工作還沒結束,對兩個元素進行換位(上面的圖1跟圖2),繼續執行元素換位的“大”while。
2、左指針大于右指針:說明此時的分區工作已經結束了,對右指針指向的元素與中間數進行換位(上面的圖3),并且結束最外面的while循環。4、循環結束,可以返回中間數的index了,留著下一次遞歸調用函數的時候使用。
圖片理解參考 快速排序
代碼實現:
def quickSort(alist):
quickSortHelper(alist, 0, len(alist) - 1)
def quickSortHelper(alist, first, last):
if first < last:
splitpoint = partition(alist, first, last)
# 得到中點正確的位置,限制下面兩個遞歸的邊界
quickSortHelper(alist, first, splitpoint - 1)
quickSortHelper(alist, splitpoint + 1, last)
def partition(alist,first,last):
pivotvalue = alist[first]
leftmark = first+1
rightmark = last
done = False
while not done:
#如果左指針小于右指針,并且左指針指向的元素小于等于中間數,那么左指針就繼續走
while leftmark <= rightmark and alist[leftmark] <= pivotvalue:
leftmark = leftmark + 1
#如果左指針小于等于右指針,并且右指針指向元素大于等于中間數,右指針就繼續走
while rightmark >= leftmark and alist[rightmark] >= pivotvalue:
rightmark = rightmark -1
#左指針大于右指針時,done為True,結束循環
if rightmark < leftmark:
done = True
#經過上面的指針操作以后,如果左指針還是小于右指針,則替換二者指向元素
else:
temp = alist[leftmark]
alist[leftmark] = alist[rightmark]
alist[rightmark] = temp
#done為True,結束循環,替換中間元素與右指針指向元素的位置
temp = alist[first]
alist[first] = alist[rightmark]
alist[rightmark] = temp
##返回此時中點的位置,用于下次遞歸分類劃分左右部分
return rightmark
alist = [54,26,93,17,77,31,44,55,20]
quickSort(alist)
print(alist)