前言
1 、排序的概念
排序是計算機內(nèi)經(jīng)常進(jìn)行的一種操作,其目的是將一組“無序”的記錄序列調(diào)整為“有序”的記錄序列。
排序分為內(nèi)部排序和外部排序。
若整個排序過程不需要訪問外存便能完成,則稱此類排序問題為內(nèi)部排序。
反之,若參加排序的記錄數(shù)量很大,整個序列的排序過程不可能在內(nèi)存中完成,則稱此類排序問題為外部排序。
2 、排序分類
八大排序算法均屬于內(nèi)部排序。如果按照策略來分類,大致可分為:交換排序、插入排序、選擇排序、歸并排序和基數(shù)排序。如下圖所示:
3 、算法分析
下表給出各種排序的基本性能,具體分析請參看各排序的詳解:
一、插入排序
插入排序分為兩種:直接插入排序和希爾排序
直接插入排序
基本思想:
直接插入排序的核心思想就是:將數(shù)組中的所有元素依次跟前面已經(jīng)排好的元素相比較,如果選擇的元素比已排序的元素小,則交換,直到全部元素都比較過。
因此,從上面的描述中我們可以發(fā)現(xiàn),直接插入排序可以用兩個循環(huán)完成:
- 第一層循環(huán):遍歷待比較的所有數(shù)組元素
- 第二層循環(huán):將本輪選擇的元素與已經(jīng)排好序的元素相比較。
如果:selected > ordered,那么將二者交換
def insert_sort(array):
for i in range(len(array)):
for j in range(i):
if array[i] < array[j]:
array.insert(j, array.pop(i))
break
return array
希爾排序
基本思想:
將數(shù)組列在一個表中并對列分別進(jìn)行插入排序,重復(fù)這過程,不過每次用更長的列(步長更長了,列數(shù)更少了)來進(jìn)行。最后整個表就只有一列了。將數(shù)組轉(zhuǎn)換至表是為了更好地理解這算法,算法本身還是使用數(shù)組進(jìn)行排序。
希爾排序(Shell Sort)是插入排序的一種。也稱縮小增量排序,是直接插入排序算法的一種更高效的改進(jìn)版本。希爾排序是非穩(wěn)定排序算法。該方法因DL.Shell于1959年提出而得名。 希爾排序是把記錄按下標(biāo)的一定增量分組,對每組使用直接插入排序算法排序;隨著增量逐漸減少,每組包含的關(guān)鍵詞越來越多,當(dāng)增量減至1時,整個文件恰被分成一組,算法便終止。
def shell_sort(alist):
n = len(alist)
# 初始步長
gap = n // 2
while gap > 0:
# 按步長進(jìn)行插入排序
for i in range(gap,n):
j = i
# 原地插入排序,之前的插入排序另辟空間存儲的
while j>=gap and alist[j-gap] > alist[j]:
alist[j-gap], alist[j] = alist[j], alist[j-gap]
j -= gap
# 得到新的步長
gap = gap //2
return alist
最壞情況O(n^2) ,平均O(nlog(n))~O(n2),最好O(n1.3) ,不穩(wěn)定。
二、選擇排序
選擇排序分為簡單選擇排序和堆排序
簡單選擇排序
基本思想:
比較+交換。
- 從待排序序列中,找到關(guān)鍵字最小的元素;
- 如果最小元素不是待排序序列的第一個元素,將其和第一個元素互換;
- 從余下的 N - 1 個元素中,找出關(guān)鍵字最小的元素,重復(fù)(1)、(2)步,直到排序結(jié)束。
因此我們可以發(fā)現(xiàn),簡單選擇排序也是通過兩層循環(huán)實現(xiàn)。
- 第一層循環(huán):依次遍歷序列當(dāng)中的每一個元素
- 第二層循環(huán):將遍歷得到的當(dāng)前元素依次與余下的元素進(jìn)行比較,符合最小元素的條件,則交換。
def select_sort(array):
for i in range(len(array)):
x = i # min index
for j in range(i, len(array)):
if array[j] < array[x]:
x = j
array[i], array[x] = array[x], array[i]
return array
堆排序
基本思想:
堆排序可以按照以下步驟來完成:
- 首先將序列構(gòu)建成為大頂堆;
(這樣滿足了大頂堆那條性質(zhì):位于根節(jié)點的元素一定是當(dāng)前序列的最大值) - 取出當(dāng)前大頂堆的根節(jié)點,將其與序列末尾元素進(jìn)行交換;
(此時:序列末尾的元素為已排序的最大值;由于交換了元素,當(dāng)前位于根節(jié)點的堆并不一定滿足大頂堆的性質(zhì)) - 對交換后的n-1個序列元素進(jìn)行調(diào)整,使其滿足大頂堆的性質(zhì);
- 重復(fù)2.3步驟,直至堆中只有1個元素為止
堆排序的時間復(fù)雜度分為兩個部分一個是建堆的時候所耗費的時間,一個是進(jìn)行堆調(diào)整的時候所耗費的時間。而堆排序則是調(diào)用了建堆和堆調(diào)整。
建堆是一個線性過程,從len/2-0一直調(diào)用堆調(diào)整的過程,相當(dāng)于o(h1)+o(h2)+…+o(hlen/2)這里的h表示節(jié)點深度,len/2表示節(jié)點深度,對于求和過程,結(jié)果為線性的O(n) 堆調(diào)整為一個遞歸的過程,調(diào)整堆的過程時間復(fù)雜度與堆的深度有關(guān)系,相當(dāng)于lgn的操作。 因為建堆的時間復(fù)雜度是O(n),調(diào)整堆的時間復(fù)雜度是lgn,所以堆排序的時間復(fù)雜度是O(nlgn)。
def heap_sort(array):
def heap_adjust(parent):
child = 2 * parent + 1 # left child
while child < len(heap):
if child + 1 < len(heap):
if heap[child + 1] > heap[child]:
child += 1 # right child
if heap[parent] >= heap[child]:
break
heap[parent], heap[child] = \
heap[child], heap[parent]
parent, child = child, 2 * child + 1
heap, array = array.copy(), []
for i in range(len(heap) // 2, -1, -1):
heap_adjust(i)
while len(heap) != 0:
heap[0], heap[-1] = heap[-1], heap[0]
array.insert(0, heap.pop())
heap_adjust(0)
return array
三、歸并排序
基本思想:
歸并排序是建立在歸并操作上的一種有效的排序算法,該算法是采用分治法的一個典型的應(yīng)用。它的基本操作是:將已有的子序列合并,達(dá)到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。
歸并排序其實要做兩件事:
分解----將序列每次折半拆分
合并----將劃分后的序列段兩兩排序合并
因此,歸并排序?qū)嶋H上就是兩個操作,拆分+合并
合并:
L[first...mid]為第一段,L[mid+1...last]為第二段,并且兩端已經(jīng)有序,現(xiàn)在我們要將兩端合成達(dá)到L[first...last]并且也有序。
首先依次從第一段與第二段中取出元素比較,將較小的元素賦值給temp[]
重復(fù)執(zhí)行上一步,當(dāng)某一段賦值結(jié)束,則將另一段剩下的元素賦值給temp[]
此時將temp[]中的元素復(fù)制給L[],則得到的L[first...last]有序
分解:
在這里,我們采用遞歸的方法,首先將待排序列分成A,B兩組;然后重復(fù)對A、B序列
分組;直到分組后組內(nèi)只有一個元素,此時我們認(rèn)為組內(nèi)所有元素有序,則分組結(jié)束。
def merge_sort(nums): # 變成二次樹
if len(nums) == 1:
return nums
mid = len(nums) // 2
left = nums[:mid]
right = nums[mid:]
a = merge_sort(left)
b = merge_sort(right)
return merge(a, b)
def merge(left, right): # 整合排序
c = []
j = k = 0
while len(left) > j and len(right) > k:
if left[j] <= right[k]:
c.append(left[j])
j += 1
else:
c.append(right[k])
k += 1
if j == len(left):
for i in right[k:]:
c.append(i)
else:
for i in left[j:]:
c.append(i)
return c
四、基數(shù)排序
基本思想:
通過序列中各個元素的值,對排序的N個元素進(jìn)行若干趟的“分配”與“收集”來實現(xiàn)排序。
分配:我們將L[i]中的元素取出,首先確定其個位上的數(shù)字,根據(jù)該數(shù)字分配到與之序號相同的桶中
收集:當(dāng)序列中所有的元素都分配到對應(yīng)的桶中,再按照順序依次將桶中的元素收集形成新的一個待排序列L[ ]
對新形成的序列L[]重復(fù)執(zhí)行分配和收集元素中的十位、百位...直到分配完該序列中的最高位,則排序結(jié)束
根據(jù)上述“基數(shù)排序”的展示,我們可以清楚的看到整個實現(xiàn)的過程
def radix_sort(array):
bucket, digit = [[]], 0
while len(bucket[0]) != len(array):
bucket = [[], [], [], [], [], [], [], [], [], []]
for i in range(len(array)):
num = (array[i] // 10 ** digit) % 10
bucket[num].append(array[i])
array.clear()
for i in range(len(bucket)):
array += bucket[i]
digit += 1
return array
五、交換排序
交換排序分為兩種:冒泡排序和快速排序
冒泡排序
基本思想:
- 將序列當(dāng)中的左右元素,依次比較,保證右邊的元素始終大于左邊的元素;
( 第一輪結(jié)束后,序列最后一個元素一定是當(dāng)前序列的最大值;) - 對序列當(dāng)中剩下的n-1個元素再次執(zhí)行步驟1。
- 對于長度為n的序列,一共需要執(zhí)行n-1輪比較
(利用while循環(huán)可以減少執(zhí)行次數(shù))
def bubble_sort(array):
for i in range(len(array)):
for j in range(i, len(array)):
if array[i] > array[j]:
array[i], array[j] = array[j], array[i]
return array
快速排序
基本思想:挖坑填數(shù)+分治法
從序列當(dāng)中選擇一個基準(zhǔn)數(shù)(pivot)
在這里我們選擇序列當(dāng)中第一個數(shù)最為基準(zhǔn)數(shù)
將序列當(dāng)中的所有數(shù)依次遍歷,比基準(zhǔn)數(shù)大的位于其右側(cè),比基準(zhǔn)數(shù)小的位于其左側(cè)
重復(fù)步驟1.2,直到所有子集當(dāng)中只有一個元素為止。
用偽代碼描述如下:
1.i =L; j = R; 將基準(zhǔn)數(shù)挖出形成第一個坑a[i]。
2.j--由后向前找比它小的數(shù),找到后挖出此數(shù)填前一個坑a[i]中。
3.i++由前向后找比它大的數(shù),找到后也挖出此數(shù)填到前一個坑a[j]中。
4.再重復(fù)執(zhí)行2,3二步,直到i==j,將基準(zhǔn)數(shù)填入a[i]中
def quick_sort(array):
sys.setrecursionlimit(10 ** 8)
def recursive(begin, end):
if begin > end:
return
l, r = begin, end
pivot = array[l]
while l < r:
while l < r and array[r] > pivot:
r -= 1
while l < r and array[l] <= pivot:
l += 1
array[l], array[r] = array[r], array[l]
array[l], array[begin] = pivot, array[l]
recursive(begin, l - 1)
recursive(r + 1, end)
recursive(0, len(array) - 1)
return array
六、總結(jié)
- (1)若n較小(如n≤50),可采用直接插入或直接選擇排序。
當(dāng)記錄規(guī)模較小時,直接插入排序較好;否則如果直接選擇移動的記錄數(shù)少于直接插人,應(yīng)選直接選擇排序為宜。 - (2)若文件初始狀態(tài)基本有序(指正序),則應(yīng)選用直接插入、冒泡或隨機的快速排序為宜;
- (3)若n較大,則應(yīng)采用時間復(fù)雜度為O(nlgn)的排序方法:快速排序、堆排序或歸并排序。
快速排序是目前基于比較的內(nèi)部排序中被認(rèn)為是最好的方法,當(dāng)待排序的關(guān)鍵字是隨機分布時,快速排序的平均時間最短;
堆排序所需的輔助空間少于快速排序,并且不會出現(xiàn)快速排序可能出現(xiàn)的最壞情況。這兩種排序都是不穩(wěn)定的。
若要求排序穩(wěn)定,則可選用歸并排序。但本章介紹的從單個記錄起進(jìn)行兩兩歸并的排序算法并不值得提倡,通常可以將它和直接插入排序結(jié)合在一起使用。先利用直接插入排序求得較長的有序子文件,然后再兩兩歸并之。因為直接插入排序是穩(wěn)定的,所以改進(jìn)后的歸并排序仍是穩(wěn)定的。
七、參考文獻(xiàn)
[1] 李春葆. 數(shù)據(jù)結(jié)構(gòu)教程[M].北京市:清華大學(xué)出版社,2013.
[2] li563868273.各種排序的比較和使用場景分析.CSDN,2016.
[3] LeeLom. 數(shù)據(jù)結(jié)構(gòu)常見的八大排序算法.簡書,2016.
[4] woider. Python 八大排序算法速度比較.博客園,2016.