七、排序(1)
一、排序的概述
1、最經典、最常用的排序方法:
- 冒泡排序、插入排序、選擇排序、歸并排序、快速排序、計數排序、基數排序、桶排序。
2、分類:
| 1 | 冒泡、插入、選擇 | O(n2) | √ | 適合小規模數據排序 |
| 2 | 快排、歸并 | O(nlogn) | √ | 適合大規模的數據排序 |
| 3 | 桶、計數、基數 | O(n) | × |
思考: 插入排序和冒泡排序具有相同的時間復雜度,但在實際開發中,更傾向于使用插入排序?
解:冒泡程序中的交換操作:需要3個賦值操作,而插入排序的移動操作:只需一個賦值操作。
3、分析“排序算法”
(1)執行效率
- 最好情況、最壞情況、平均情況時間復雜度
- 原始數據的形態——有序度
- 時間復雜度的系數、常數、低階
- 對同一階時間復雜度的排序算法性能對比時,需考慮系數、常數、低階
- 比較 “次數和交換(或移動)” 次數
(2)內存消耗——空間復雜度
原地排序(sorted in place)——特指空間復雜度為O(1)的排序算法
(3)穩定性
穩定性:若待排序的序列中存在值相等的元素,經過排序之后,相等元素之間原有的先后順序不變
穩定性重要性:可針對對象的多種屬性進行有優先級的排序。
示例:電商交易系統中的“訂單”排序。
- 目標:訂單有兩個屬性,一個是下單時間,另一個是訂單金額。希望按照金額從小到大對訂單數據排序。對于金額相同的訂單,希望按照金額從小到大對訂單數據排序。
- 實現:借助穩定排序算法。先按照下單時間給訂單排序,注意是按照下單時間,不是金額。排序完成之后,我們用穩定排序算法,按照訂單金額重新排序。兩遍排序之后,我們得到的訂單數據就是按照金額從小到大排序,金額相同的訂單按照下單時間從早到晚排序的。
二、冒泡算法(Bubble Sort)
1、基本思想
兩兩比較相鄰記錄的關鍵字,若反序則交換,直到沒有反序的記錄為止。
2、示例
目標:數組{4,5,6,3,2,1}——從小到大排序
過程:
- 第一次冒泡操作的詳細過程:
- 完成排序的所有操作:
3、優化
當某次冒泡操作已經沒有數據交換時,說明已經達到完全有序,不需要再繼續執行后面的冒泡操作。
4、實現
(1)基于數組的實現
#include <iostream> using namespace std;/** 冒泡排序——從小到大排序** 參數說明:* a -- 待排序的數組* n -- 數組的長度*/ void bubbleSort(int *a, int n) {int i,j,tmp;for(i = 0; i < n-1; i++){for(j = 0; j < n-1-i;j++){if(a[j] > a[j+1]){// 交換 a[j] 與 a[j+1]tmp = a[j];a[j] = a[j+1];a[j+1] = tmp;}}} }/** 冒泡排序(改進版)——從小到大排序** 參數說明:* a -- 待排序的數組* n -- 數組的長度*/ void bubbleSort1(int *a, int n) {int i,j,tmp;int flag; // 標記for(i = 0; i < n-1; i++){flag = 0;for(j = 0; j < n-1-i;j++){if(a[j] > a[j+1]){tmp = a[j];a[j] = a[j+1];a[j+1] = tmp;flag = 1;}}// 若沒發生交換,則說明數列已有序。if (flag == 0)break;} }int main() {int i;int a[] = {2,4,3,1,6,5};int len = (sizeof(a)/sizeof(a[0]));cout<<"Before Sort:";for(i = 0; i < len; i++)cout<< a[i]<<" ";cout<<endl;// 傳統方法bubbleSort(a, len); // 傳統方法// 改進方法:即若內循環中不發生交換,則說明數列已有序// bubbleSort1(a,len);cout<<"After Sort:";for(i = 0; i < len; i++)cout<< a[i]<<" ";cout<<endl;return 0; }(2)基于C++模板實現冒泡排序
/** 基于C++模板實現:冒泡排序——從小到大排序** 參數說明:* a -- 待排序的數組* n -- 數組的長度*/ #include <iostream> using namespace std; template<typename T> //整數或浮點數皆可使用 void bubble_sort(T arr[], int len) {int i, j; T temp;for (i = 0; i < len - 1; i++)for (j = 0; j < len - 1 - i; j++)if (arr[j] > arr[j + 1]){temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;} }int main() {int arr[] = { 61, 17, 29, 22, 34, 60, 72, 21, 50, 1, 62 };int len = (int) sizeof(arr) / sizeof(*arr);bubble_sort(arr, len);for (int i = 0; i < len; i++)cout << arr[i] << ' ';cout << endl;float arrf[] = { 17.5, 19.1, 0.6, 1.9, 10.5, 12.4, 3.8, 19.7, 1.5, 25.4, 28.6, 4.4, 23.8, 5.4 };len = (int) sizeof(arrf) / sizeof(*arrf);bubble_sort(arrf, len);for (int i = 0; i < len; i++)cout << arrf[i] << ' ';return 0; }5、性能分析
(1)原地排序算法
- 冒泡過程中只涉及相鄰數據的交換操作 ==》需要常量級的臨時空間,即空間復雜度為 O(1)
(2)穩定排序算法
- 為保證冒泡排序算法的穩定性 ==》當有相鄰的兩個元素大小相等的時候,不做交換 ==》穩定
(3)時間復雜度
- 最好情況時間復雜度(有序時):O(n)
- 最壞情況時間復雜度(逆序時):O(n2)
- 平均情況時間復雜度:O(n2)
- 最壞情況下,初始狀態的有序度是 0,所以要進行 n*(n-1)/2 次交換。最好情況下,初始狀態的有序度是 n*(n-1)/2,就不需要進行交換。
- 平均情況下,需要 n*(n-1)/4 次交換操作。
平均情況時間復雜度(加權平均期望時間復雜度)——通過“有序度” 和 “逆序度” 兩個概念進行分析
有序元素對用數學表達式表示:a[i] <= a[j], 如果 i < j
示例:
{2,4,3,1,5,6} =》有序度為11,分別為: (2,4)、(2,3)、(2,5)、(2,6)、(4,5)、(4,6)、(3,5)、(3,6)、(1,5)、(1,6)、(5,6)
{1,2,3,4,5,6}=》有序度為n*(n-1)/2,稱為滿有序度
三、插入排序(Insertion Sort)
1、基本思想
將一個記錄插入到已經排好序的有序表中,從而得到一個新的、記錄數增1的有序表。
(1)過程概述
插入排序在實現上,通常采用in-place排序(即只需用到O(1)的額外空間的排序),因而在從后向前掃描過程中,需要反復把已排序元素逐步向后挪位,為最新元素提供插入空間。
(2)具體算法描述:
2、示例
原數據:{4,5,6,1,3,2}
排序過程:其中左側是已排序區間,右側是未排序區間。
分析:滿有序度是 n*(n-1)/2=15,初始序列的有序度是 5,所以逆序度是 10。插入排序中,數據移動的個數總和也等于10 = 3+3+4。
3、實現
void insertSort(int a[], int len) { int tmp,j;for(int i = 1; i < len; i++){tmp = a[i]int j = i -1;while(tmp < a[j]){a[j+1] = a[j];j--;} a[j+1] = tmp;} }4、分析
(1)原地排序算法
- 空間復雜度為 O(1) ==》原地排序算法
(2)穩定排序算法
- 對于值相同的元素,可以選擇將后面出現的元素,插入到前面出現元素的后面。==》保持原有前后順序,也就是穩定的排序算法
(3)時間復雜度
- 最好情況時間復雜度(數據已經有序):O(n)
- 最壞情況時間復雜度:O(n2)
- 平均時間復雜度:O(n2)
- 對于插入排序來說,每次插入操作(時間復雜度為O(n))都相當于在數組中插入一個數據,循環執行 n 次插入操作
四、選擇排序(Selection Sort)
1、基本思想
從未排序區間中找到最小的元素,將其放在已排序區間的末尾,也就是與未排序區間的第一個元素進行交換。
分析:
(1)原地排序算法 《= 空間復雜度:O(1)
(2)最好情況時間復雜度、最壞情況和平均情況時間復雜度都為 O(n2)
(3)非穩定排序:選擇排序每次都要找剩余未排序元素中的最小值,并和前面的元素交換位置,這樣破壞了穩定性。
2、實現
void selectSort(int a[], int len) { int min,tmp;for(int i = 0; i < len - 1;i++){min = i; // 假設最小元素的下標for(int j = i + 1; j < n; j++){if(a[j] < a[min])min = j;}// 若數組中真的存在比假設的元素還要小的元素,則交換if(i != min){tmp = a[i];a[i] = a[min];a[min] = tmp;}} }歸并排序和快速排序都用到了分治思想,非常巧妙。
==》可以借鑒這個思想,來解決非排序的問題,
比如:如何在 O(n) 的時間復雜度內查找一個無序數組中的第 K 大元素?
總結
- 上一篇: 六、递归(Recursion)
- 下一篇: 七、排序 (2)