# [資演] 排序演算法
###### tags: `資演`
在今天的課程中,我們將學習到什麼是排序演算法,以及它們是如何工作的。首先,讓我們來談談什麼是排序演算法。
排序演算法是一種演算法,它可以幫助你將一堆數字或其他資料排列成一個有序的列表,通常是用由小到大的順序。
大多數排序演算法有兩個基本步驟:比較和交換。首先,程式會比較陣列中的要素,以確定它們是否應該排列在一起以及用什麼方式排列。 然後是交換:一旦程式比較了一對要素,它會代表它們的位置對調,以使他們按照正確的排列順序排列。比較和交換步驟會繼續重複,直到陣列被完全排序為止。
排序演算法包括許多不同的演算法,其中包括選擇排序、氣泡排序、快速排序等。我們將探討每一種演算法,以及它們是如何將指定的資料排列成一個有秩序的列表。
## 選擇排序
選擇排序(selection sort)演算法是一種排序演算法,也被稱為直接選擇排序。它的原理是通過不斷地從未排序的array中找出最小的元素,並將其添加到有序的array中。
選擇排序的執行步驟如下:
1. 從array(通常是未排序的)中找出最小的元素,並將它放置在第一個位置。
2. 遍歷剩下的元素,找到最小的那個元素,並將其放置在下一個位置。
3. 繼續重複上述步驟,直到整個array排序完畢(array中所有元素都排序完成)。
選擇排序演算法是一種原地排序演算法,不需要額外的存儲空間,只需要根據比較的結果來變更原本的array排序。
該演算法的時間複雜度為 $O(n^2)$,是一種不穩定的排序演算法。
```python=
def selection_sort(nums):
# 遍歷整個列表
for i in range(len(nums)):
# 初始假設最小的是第一個元素
minimum = i
# 用此元素與列表中的其他元素進行比較,找出最小元素
for j in range(i + 1, len(nums)):
if nums[j] < nums[minimum]:
minimum = j
# 如果最小元素改變,替換
if minimum != i:
nums[i], nums[minimum] = nums[minimum], nums[i]
# 測試程式碼
arr = [19, 2, 31, 45, 30, 11, 121, 27]
selection_sort(arr)
print(arr)
```
## 氣泡排序
氣泡排序(bubble sort),也叫做交換排序,是一種簡單且眾所周知的排序演算法,其特點就是重複的比較和交換相鄰的元素,使得排序的元素慢慢的「冒」到最終的位置,所以也叫做「冒泡排序」。
冒泡排序主要由一個兩層的迴圈組成,外圈為 $n-1$ 次,其對應的是 $n$ 個數字的第一輪比較,使得最大的數字慢慢的“冒泡”到順序位置;而内圈為 $n-1-i$ 次,其對應的是 $n-1-i$ 個數字的比較,使得排序好的數字不會受到順序影響。
此外,冒泡排序也是一種原地排序演算法,僅需要 $O(1)$ 的空間複雜度,並且屬於一種穩定的演算法。
```python=
def bubbleSort(nums):
n = len(nums)
# 用一個迴圈執行到數組的倒數第二個元素
for i in range(n-1):
# 用另一個迴圈執行來自第一個元素到最後一個前一個元素
for j in range(0, n-i-1):
if nums[j] > nums[j+1] :
nums[j], nums[j+1] = nums[j+1], nums[j]
# 測試
arr = [5,2,7,1,3]
bubbleSort(arr)
print(arr) # 印出[1, 2, 3, 5, 7]
```
## 堆排序
堆排序(heap sort)是一種排序演算法,比較適合用在資料量大時,以空間換取時間做運算,所以使用時需要小心,以確保不影響系統資源太多。Heap Sort 演算法分為兩個部分,一個是建構 heap的步驟(heapify),另一個是排序的步驟。
建構 heap 的部分,會先將輸入的陣列轉成 heap(實際上是將原本陣列中的元素重新對應到 heap 中的節點),Heap 的特點之一是,它的父節點的值會大於或等於它的子節點(max-heap),運用這個特點,我們可以將原本陣列中的元素依序存到 heap 中,過程中確保堆積樹(heap)的特點。

接下來是排序的步驟,此步驟會先將 heap 的根節點(第一個最大的值)放置到堆積樹最後,接著將最後一個元素放置到堆積樹的根節點,並且重新產生 heap ,重複以上步驟,就可以依序排序出使用者輸入的陣列。
下面是一個 Heap Sort 的 python 程式碼,此範例排序將大的值放到尾端:
```python=
def heapify(arr, n, i): # 建構 heap
largest = i # 假設 root node
l = 2 * i + 1 # 左子樹
r = 2 * i + 2 # 右子樹
# root node 比子節點大時
if l < n and arr[i] < arr[l]:
largest = l
# root node 比子節點大時
if r < n and arr[largest] < arr[r]:
largest = r
# 將 root node 放大的一邊
if largest != i:
arr[i],arr[largest] = arr[largest],arr[i]
# heapify
heapify(arr, n, largest)
def heapSort(arr): # start from here
n = len(arr)
# build a maxheap
for i in range(n, -1, -1):
heapify(arr, n, i)
# One by one extract elements
for i in range(n-1, 0, -1):
arr[i], arr[0] = arr[0], arr[i]
heapify(arr, i, 0)
# Input Array
arr = [12, 11, 13, 5, 6, 7]
heapSort(arr)
n = len(arr)
print ("Sorted array is")
for i in range(n):
print ("%d" %arr[i]),
```
## 合併排序
合併排序(merge sort) 是一個演算法,藉由分割、排序、合併來將一個陣列(或區段)的值排序。
合併排序演算法的子步驟:
1. 將陣列分割(Divide):這是演算法的一個遞迴步驟,直到分割到只剩一個個體為止,以此來達成將給定的陣列分為更小的陣列(Unit Size)。
2. 排序(Conquer):對於每個個體做排序,直到只剩一個個體為止,以此來達成將小的個體排序完成。
3. 合併(Combine):將兩個子陣列利用比較技巧,合併成單一有序的陣列。
合併排序的優點:
- 時間複雜度為 $O(n\log n)$ 。
- 合併排序是一個穩定的排序,它保證相同值的順序不會改變。
- 合併排序比較容易實作,只需要兩個遞迴函式和兩個主循環即可完成。
合併排序的缺點:
- 它需要花費較多的空間來輔助我們做排序。
- 適合於大型數據的排序,如果數據的量小,則不是一個好的排序演算法。
以下是使用 Python 來實作合併排序的程式碼:
```python=
def mergesort(arr):
# 假如陣列只有一個個體,則返回
if len(arr) == 1:
return arr
else:
# 分割陣列
mid = len(arr)//2
left = arr[:mid]
right = arr[mid:]
# 遞迴呼叫自己,此時已經將陣列分割成一個個體
l1 = mergesort(left)
l2 = mergesort(right)
# 開始合併兩個陣列
# 兩個陣列各自都已排好序,使用比較技巧只需要 3 個步驟即可
i, j, k = 0, 0, 0
while i < len(l1) and j < len(l2):
if l1[i] < l2[j]:
arr[k] = l1[i]
i += 1
else:
arr[k] = l2[j]
j += 1
k += 1
# 如果任一陣列不全部被放入 arr ,則將另外一個陣列的個體全部放入 arr
while i < len(l1):
arr[k] = l1[i]
i += 1
k += 1
while j < len(l2):
arr[k] = l2[j]
j += 1
k += 1
return arr
```
## 快速排序
快速排序(quick sort)是一種遞迴排序演算法,它的特色是可以在 $O(n\log n)$ 的時間內完成排序。它的基本思路是:
1. 取出排序數列中的一個元素,作為基準值(pivot);
2. 把小於基準值的元素移到底部,把大於基準值的元素移到頂部;
3. 將這個子問題分成兩個子問題,對左右子問題分別進行同樣的程序(即快速排序)
快速排序確定一個基準值,然後將比基準值小的元素移到它的左邊,比基準值大的元素移到它的右邊,以此方法將數列分為兩組,接下來再對兩組數列分別進行同樣的操作,直到兩組中所有的數都按照基準值的大小分配到正確的位置為止,排序完成。
```python=
def quickSort(arr):
if len(arr) <= 1:
return arr # 基底結果,只有一個元素時直接返回
pivot = arr[len(arr) // 2] # 選基準值
left = [x for x in arr if x < pivot] # 小於基準值的元素置於左邊
middle = [x for x in arr if x == pivot] # 等於基準值的元素留在中間
right = [x for x in arr if x > pivot] # 大於基準值的元素置於右邊
return quickSort(left) + middle + quickSort(right) # 遞歸調用
# test
arr = [3,5,7,2,4,9,8,1]
print(quickSort(arr)) # [1, 2, 3, 4, 5, 7, 8, 9]
```
## 基數排序
基數排序(Radix Sort)其中一種特殊的排序演算法,它的原理是把數字的各位數字一個一個比較,並根據比較的結果來重新排序,此演算法特別適用在排序處理又要使用數字的狀況中。
Radix Sort 執行的步驟如下所述:
1. 從最低位數字開始判斷,並依照位數分到不同的桶子中;
2. 再從右到左開始比較,按照位數大小重新排序;
3. 並按順序取出排序好的數字後,開始比較下一個位數;
4. 重複上述比較、排序、取出等動作,直到比較完所有位數,至此完成Radix Sort的演算法。
使用Python來實作Radix Sort可參考以下程式碼:
```python=
def radixSort(arr):
# Determine maximum number of digits
max_digits = max([len(str(i)) for i in arr])
# Do counting sort for every digit. Note that instead
# of passing digit number, exp is passed.exp is 10^i
# where i is current digit number
exp = 1
while max_digits>=exp:
temp_arr = [0]*10
# Store count of occurrences in temp_arr[]
for i in range(0, len(arr)):
index = (arr[i]/exp)
temp_arr[ (index)%10 ] += 1
# Change count_arr[i] so that count_arr[i] now contains
# actual position of this digit in output[]
for i in range(1,10):
temp_arr[i] += temp_arr[i-1]
# Build the output array
output = [0]*len(arr)
for i in range(len(arr)-1, -1, -1):
index = (arr[i]/exp)
output[ temp_arr[ (index)%10 ] - 1] = arr[i]
temp_arr[ (index)%10 ] -= 1
# Copying the output array to arr[],
# so that arr now contains sorted numbers
for i in range(0,len(arr)):
arr[i] = output[i]
# Multiply exp by 10 to get next digit number
exp *= 10
原始資料= [2,1,5,7,4]
radixSort(原始資料)
排序後的資料 = 原始資料
print("排序後的資料為:", 排序後的資料)
排序後的資料為: [1, 2, 4, 5, 7]
```
## Bucket Sort
概念:
Bucket Sort是一種非比較型演算法,屬於排序演算法的一種。它將待排序的元素划分成不同的桶,按照一定的規則把對應的數據進行分組,在每個桶內利用某種快速排序進行排序後,再逐個桶合併,最後得到一個有序列表。
特點:
Bucket Sort 非常適合類似「已知取值范圍」的排序問題。它通過把值分布到不同桶中,比較簡單地完成排序。但它需要大量的預留空間和更多的計算時間,導致空間和時間複雜度增加。
空間複雜度: $O(n)$
時間複雜度:
- 最好情況: $O(n)$
- 最壞情況: $O(n^2)$
實作原理:
(1) 從數據集中取出最大/最小值,計算出取值範圍;
(2) 根據取值範圍,建立一個和取值符合的桶,比如$a$為最小值,$b$為最大值,創建一個包含 $(a,b)$ 為界的桶;
(3) 把每個元素分配到對應的桶中;
(4) 對每個不為空的桶內進行排序;
(5) 按照順序把各個桶中的元素合併起來;
Python實作示例:
```python=
#bucket sort for a list of numbers
def Bucket_Sort(arr):
#get the length/size of the array
n = len(arr)
#create an empty buckets
buckets = []
for i in range(n):
buckets.append([])
#iterate through the array, and add the elements to the respective lists
for j in arr:
index_b = int(n*j)
print(j, index_b)
buckets[index_b].append(j)
#sort the buckets
for i in range(n):
buckets[i] = sorted(buckets[i])
#append the sorted buckets
k = 0
for i in range(n):
for j in range(len(buckets[i])):
arr[k] = buckets[i][j]
k += 1
#sample list for testing
arr = [0.3, 0.2, 0.1, 0.4]
Bucket_Sort(arr)
print("Sorted array is:")
for i in range(len(arr)):
print(arr[i], end=" ")
#output:
Sorted array is:
0.1 0.2 0.3 0.4
```
## 插入排序
插入排序是一種簡單而有效的排序演算法。它的工作原理是:每次從待排序的序列中取出一個元素,然後把它插入到已經排好序的序列中,直到所有的元素都被插入完成。
插入排序的基本思想是:首先將第一個元素看作已經排好序的序列,然後從第二個元素開始,每次取出一個元素,然後把它插入到已經排好序的序列中。插入的過程中,需要比較新元素和已經排好序的序列中的元素,找到合適的插入位置。
下面是一個簡單的python實現:
```python=
def insertion_sort(arr):
for i in range(1, len(arr)):
temp = arr[i]
j = i
while j > 0 and arr[j - 1] > temp:
arr[j] = arr[j - 1]
j -= 1
arr[j] = temp
return arr
```
## Shell sort
Shell sort是一種比較高階的排序演算法,它是在插入排序的基礎上進行改進,得到的一種快速的排序方法。它的基本思想是:將待排序的序列進行分組,然後逐一進行插入排序。
一般來說,Shell sort的做法是將待排序的序列分成多個小的子序列,然後對這些子序列進行插入排序。分組的方式通常是按照一定的間隔進行分組,例如按照序列中相隔若干個元素的元素進行分組。每次插入排序完成後,都會將間隔減小,直到最後只剩一個子序列,這時再進行一次插入排序即可。
下面是一個簡單的python實現:
```python=
def shell_sort(arr):
# 計算出最大的間隔
gap = len(arr) // 2
while gap > 0:
# 間隔為gap的插入排序
for i in range(gap, len(arr)):
temp = arr[i]
j = i
while j >= gap and arr[j - gap] > temp:
arr[j] = arr[j - gap]
j -= gap
arr[j] = temp
gap //= 2
return arr
```
## Timsort
Timsort是一種比較高階的排序演算法,它是由Tim Peters在2002年發明的。Timsort是一種融合了插入排序和合併排序的演算法,它能夠從輸入序列中自動選擇最適合的排序方法,並且能夠高效地對序列進行排序。
Timsort的基本思想是:首先將輸入序列進行分組,每組的長度為一個有序的「塊」。然後對每個塊進行插入排序,得到若干個有序的塊。最後,對這些塊進行合併排序,得到最終的有序序列。
下面是一個簡單的python實現:
```python=
def timsort(arr):
# 將序列分為若干個塊
blocks = []
while len(arr) > 0:
block = []
for i in range(32):
if len(arr) == 0:
break
block.append(arr.pop(0))
block.sort()
blocks.append(block)
# 對塊進行合併排序
while len(blocks) > 1:
blocks = merge_blocks(blocks[0], blocks[1])
# 回傳排序結果
return blocks[0]
def merge_blocks(block1, block2):
# 合併兩個塊,並回傳合併後的塊
merged = []
while len(block1) > 0 and len(block2) > 0:
if block1[0] < block2[0]:
merged.append(block1.pop(0))
else:
merged.append(block2.pop(0))
while len(block1) > 0:
merged.append(block1.pop(0))
while len(block2) > 0:
merged.append(block2.pop(0))
return [merged]
```