前言
說起排序啊,我心裏想到的第一個例子就是給定三個數a,b,c按照從小到大的順序排序輸出,這應該算是我們接觸編程遇到的最簡單的排序問題了叭。不知道大家當時是怎麼解決這個問題的?別告訴我你直接sort()哈,其實我當初遇到這個問題的時候是想了一會兒的,包括當時遇到比較三個數大小的問題時,在我們眼裏比較三個數的大小或者排序應該是一眼睛就看穿的問題了,但是放進程序語言裏可能沒有你想象的那麼簡單。
用C語言寫的話,我們之前像這樣一定試過,中間的三個if語句寫的真是腦殼大。
# include<stdio.h>
int main(){
int a,b,c,temp;
//讀入三個數
scanf("%d %d %d",&a,&b,&c);
//三個if 後續有列表解析過程
if(a>b) temp=a,a=b,b=temp;
if(b>c) temp=b,b=c,c=temp;
if(a>b) temp=a,a=b,b=temp;
//打印排序後的三個數
printf("%d %d %d",a,b,c);
return 0;
或許三個數我們還能像這樣的完成,要是30個300個數呢?if語句肯定是不行的,這時候就到了我們排序算法發揮他真正實力的時候了!
排序算法
冒泡排序
相信大多數的同學包括我自己接觸到的第一個排序算法應該就是冒泡排序。
冒泡排序(英語:Bubble Sort)是一種簡單的排序算法。它重複地遍歷要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。遍歷數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個算法的名字由來是因爲越小的元素會經由交換慢慢“浮”到數列的頂端。
上面是冒泡排序的一個GIF演示圖:從第一個元素開始,相鄰的兩個元素比較大小,然後小的元素放在左邊,大的元素就往右邊排,這樣一趟的話就能找出序列裏最大的元素,然後再走n-1趟,倒數第二大的,倒數第三大的…就依次被找出來,直到這個序列有序爲止。概括來說:
冒泡排序算法的運作如下:
- 比較相鄰的元素。如果第一個比第二個大(升序),就交換他們兩個。
- 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數。
- 針對所有的元素重複以上的步驟,除了最後一個。
- 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
def buble_sort(alist):
"""冒泡排序"""
n = len(alist)
for j in range(n-1):
# 要走多少趟
count = 0
for i in range(0, n-1-j):
# 從頭走到尾要走多少次
if alist[i] > alist[i+1]:
alist[i], alist[i+1] = alist[i+1], alist[i]
count += 1
if count == 0:
return
這個冒泡排序是經過改進的,在於我們解決了一個已經排好序的列表的冒泡排序問題,降低了該算法的時間複雜度。
選擇排序
選擇排序(Selection
sort)是一種簡單直觀的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。
選擇排序的主要優點與數據移動有關。如果某個元素位於正確的最終位置上,則它不會被移動。選擇排序每次交換一對元素,它們當中至少有一個將被移到其最終位置上,因此對n個元素的表進行排序總共進行至多n-1次交換。在所有的完全依靠交換去移動元素的排序方法中,選擇排序屬於非常好的一種。
def select_sort(alist):
"""選擇排序"""
n = len(alist)
for j in range(0, n-1):
min_index = j
for i in range(j+1, n):
if alist[min_index] > alist[i]:
min_index = i
alist[j], alist[min_index] = alist[min_index], alist[j]
這些代碼是個啥意思呢?先看裏面的那個循環,假設我第一個數是最小的,然後我依次遍歷後面的n-1個數,如果我找到了一個比這個數更小的數,我把最小值的下標記住,然後直到找到最小的,第一層遍歷結束,再把這個數與第一個數交換,那麼最小的數就排在第一個了。按照這個規律,我再遍歷n-2遍直到整個序列有序爲止。
插入排序
插入排序(英語:Insertion Sort)是一種簡單直觀的排序算法。它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,爲最新元素提供插入空間。
這個圖片的演示就很直觀了:從第二個元素開始,如果第二個元素小於第一個元素,我就把第二個元素放在最左邊,然後是第三個元素,如果小於第二個,再與第一個比較,如果小於第一個,那第三個元素就放在第一個元素的位置,以此類推。
def insert_sort(alist):
"""插入排序"""
n = len(alist)
# 從右邊的無序序列中取出多少個元素執行這樣的過程
for j in range(1, n):
# j = [1, 2, 3, n-1]
# i 代表內層循環起始值
i = j
# 執行從右邊的無序序列中取出第一個元素,即i位置的元素,然後將其插入到前面的正確位置中
while i > 0:
if alist[i] < alist[i-1]:
alist[i], alist[i-1] = alist[i-1], alist[i]
i -= 1
else:
break
先看while循環,如果第i個元素小於他前面的一個元素,我們就將兩個元素交換位置,也就是較小的元素前移一位,然後這個元素再與他前面的元素比較,直到有個元素比他小了,循環結束,這個元素的位置也就確定了。再看外層for循環,i表示是第幾個元素,我有幾個元素就要執行幾次,每次找到一個元素的正確位置,知道最後序列有序爲止。
希爾排序
希爾排序(Shell Sort)是插入排序的一種。也稱縮小增量排序,是直接插入排序算法的一種更高效的改進版本。希爾排序是非穩定排序算法。該方法因DL.Shell於1959年提出而得名。 希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個文件恰被分成一組,算法便終止。
希爾排序的基本思想是:將數組列在一個表中並對列分別進行插入排序,重複這過程,不過每次用更長的列(步長更長了,列數更少了)來進行。最後整個表就只有一列了。將數組轉換至表是爲了更好地理解這算法,算法本身還是使用數組進行排序。
def shell_sort(alist):
"""希爾排序"""
# n=9
n = len(alist)
# gap =4
gap = n // 2
# i = gap
# for i in range(gap, n):
# # i = [gap, gap+1, gap+2, gap+3... n-1]
# while:
# if alist[i] < alist[i-gap]:
# alist[i], alist[i-gap] = alist[i-gap], alist[i]
# gap變化到0之前,插入算法執行的次數
while gap > 0:
# 插入算法,與普通的插入算法的區別就是gap步長
for j in range(gap, n):
# j = [gap, gap+1, gap+2, gap+3, ..., n-1]
i = j
while i > 0:
if alist[i] < alist[i-gap]:
alist[i], alist[i-gap] = alist[i-gap], alist[i]
i -= gap
else:
break
# 縮短gap步長
gap //= 2
希爾排序是一種改進的特殊的插入排序算法,所以裏面很多都是插入排序裏面的步驟,只是每次比較的元素位置不同,希爾排序每次比較步長爲指定數字的兩個元素,然而插入排序步長爲1。
歸併排序
歸併排序是採用分治法的一個非常典型的應用。歸併排序的思想就是先遞歸分解數組,再合併數組。
將數組分解最小之後,然後合併兩個有序數組,基本思路是比較兩個數組的最前面的數,誰小就先取誰,取了後相應的指針就往後移一位。然後再比較,直至一個數組爲空,最後把另一個數組的剩餘部分複製過來即可。
def merge_sort(alist):
"""歸併排序"""
n = len(alist)
if n <= 1:
return alist
mid = n//2
# left 採用歸併排序後形成的有序的新的列表
left_li = merge_sort(alist[:mid])
# right 採用歸併排序後形成的有序的新的列表
right_li = merge_sort(alist[mid:])
# 將兩個有序的子序列合併爲一個新的整體
# merge(left, right)
left_pointer, right_pointer = 0, 0
result = []
while left_pointer < len(left_li) and right_pointer < len(right_li):
if left_li[left_pointer] <= right_li[right_pointer]:
result.append(left_li[left_pointer])
left_pointer += 1
else:
result.append(right_li[right_pointer])
right_pointer += 1
result += left_li[left_pointer:]
result += right_li[right_pointer:]
return result
歸併排序是一個遞歸的過程,爲什麼這麼說呢?首先我們將這個序列2分,然後再把子序列 二分,直到每一個子序列都只含有兩個元素,我們將這兩個元素排序(這應該是很簡單的),然後在將每兩個子序列排序合併成一個更大的序列,兩個更大的序列再排序合併成更更大的序列。中間的排序過程是怎樣的呢?首先每一個要合併的子序列都有一個指向第一個元素的遊標,兩個子序列遊標所指的數中找出最小的數,該數所指的遊標+1,再比較,找出小的數跟原來那個數構成一個序列,以此類推,知道序列有序爲止。
快速排序
快速排序(英語:Quicksort),又稱劃分交換排序(partition-exchange sort),通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。
步驟爲:
- 從數列中挑出一個元素,稱爲"基準"(pivot)。
- 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分區結束之後,該基準就處於數列的中間位置。這個稱爲分區(partition)操作。
- 遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。
遞歸的最底部情形,是數列的大小是零或一,也就是永遠都已經被排序好了。雖然一直遞歸下去,但是這個算法總會結束,因爲在每次的迭代(iteration)中,它至少會把一個元素擺到它最後的位置去。
def quick_sort(alist, first, last):
"""快速排序"""
if first >= last:
return
mid_value = alist[first]
low = first
high = last
while low < high:
# high 左移
while low < high and alist[high] >= mid_value:
high -= 1
alist[low] = alist[high]
while low <high and alist[low] < mid_value:
low += 1
alist[high] = alist[low]
# 從循環退出時,low==high
alist[low] = mid_value
# 對low左邊的列表執行快速排序
quick_sort(alist, first, low-1)
# 對low右邊的列表排序
quick_sort(alist, low+1, last)
快排跟歸併排序有那麼一點小像,都用到了遞歸的方法,並且都有將序列分組來處理。他是這樣分組的:首先將第一個元素拿出來,我們將low遊標指向第二個元素,high遊標指向最後一個元素,每次low+1,high-1,然後把大於第一個元素的數字放在第一個元素的左邊,大於他的放在右邊,然後左右就分別有一個序列,每個子序列用同樣的方法,直到整個序列有序爲止。
Python中sort()函數的本質
當然我在這之前是不知道的,經過一番查詢,我瞭解到Python中的sort()函數是歸併排序和插入排序的結合版本。
還可以有這種騷操作?那我要是把所有的排序算法結合一遍我不是要吊炸天?(當然也只是想想)畢竟是人人都喜歡用的sort函數嘛,還是要整的快一點,牛批一點的,不然就失去了他作爲一個內置函數的意義了。
排序算法的時間複雜度
搜索算法
搜索是在一個項目集合中找到一個特定項目的算法過程。搜索通常的答案是真的或假的,因爲該項目是否存在。 搜索的幾種常見方法:順序查找、二分法查找、二叉樹查找、哈希查找
二分查找
二分查找又稱折半查找,優點是比較次數少,查找速度快,平均性能好;其缺點是要求待查表爲有序表,且插入刪除困難。因此,折半查找方法適用於不經常變動而查找頻繁的有序列表。首先,假設表中元素是按升序排列,將表中間位置記錄的關鍵字與查找關鍵字比較,如果兩者相等,則查找成功;否則利用中間位置記錄將表分成前、後兩個子表,如果中間位置記錄的關鍵字大於查找關鍵字,則進一步查找前一子表,否則進一步查找後一子表。重複以上過程,直到找到滿足條件的記錄,使查找成功,或直到子表不存在爲止,此時查找不成功。
非遞歸實現:
def binary_search_2(alist, item):
"""二分查找, 非遞歸"""
n = len(alist)
first = 0
last = n-1
while first <= last:
mid = (first + last)//2
if alist[mid] == item:
return True
elif item < alist[mid]:
last = mid - 1
else:
first = mid + 1
return False
遞歸實現:
def binary_search(alist, item):
"""二分查找,遞歸"""
n = len(alist)
if n > 0:
mid = n//2
if alist[mid] == item:
return True
elif item < alist[mid]:
return binary_search(alist[:mid], item)
else:
return binary_search(alist[mid+1:], item)
return False
二分查找的最優時間複雜度:O(1),最壞時間複雜度:O(logn)。
後記
部分圖片代碼源於我學習的資料
還是那句話:天生我才必有用。雖然sort函數很好用,用着也方便,但是這並不妨礙我們去學習各種各樣的排序算法,當我們真正的理解了當中的道道,等到實際使用的時候正確的去選擇該用的算法,或許這比你一味的調用sort函數要好吧!加油!!!