〖Leetcode-1〗排序算法總結! |
文章目錄
本文參考了以下作者的文章,這裏表示感謝!
一. 前言
1.1. 術語說明
- 穩定: 簡單的理解就是能保證排序前2個相等的數所在序列的前後相對位置順序和排序後它們兩個的前後位置順序相同。如果 , 原來在位置前面,排序後 還是保持在 位置前;
- 不穩定: 排序前後在序列中的相對位置發生變化;
- 內排序: 所有排序操作都在內存中完成;
- 外排序: 由於數據太大,因此把數據放在磁盤中,而排序通過磁盤和內存的數據傳輸才能進行;
- 時間複雜度: 一個算法執行所耗費的時間;
- 空間複雜度: 運行完一個程序所需內存的大小;
1.2. 算法總結
圖片名詞解釋:
- :表示數據規模;
- :表示桶的個數;
- -:佔用常數內存,不佔用額外內存;
- -:佔用額外內存;
1.3. 算法分類
1.4. 比較和非比較的區別
- 常見的快速排序、歸併排序、堆排序、冒泡排序等屬於比較排序。在排序的最終結果裏,元素之間的次序依賴於它們之間的比較。每個數都必須和其他數進行比較,才能確定自己的位置。
- 在冒泡排序之類的排序中,問題規模爲 ,又因爲需要比較 次,所以平均時間複雜度爲 。在歸併排序、快速排序之類的排序中,問題規模通過分治法消減爲 次,所以時間複雜度平均 。
- 計數排序、基數排序、桶排序則屬於非比較排序。非比較排序是通過確定每個元素之前,應該有多少個元素來排序。針對數組 ,計算 之前有多少個元素,則唯一確定了 在排序後數組中的位置。
- 非比較排序只要確定每個元素之前的已有的元素個數即可,所有一次遍歷即可解決。算法時間複雜度 。
- 非比較排序時間複雜度底,但由於非比較排序需要佔用空間來確定唯一位置。所以對數據規模和數據分佈有一定的要求。
二. 冒泡排序(Bubble Sort)
- 冒泡排序是一種簡單的排序算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果它們的順序錯誤就把它們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個算法的名字由來是因爲越小的元素會經由交換慢慢“浮”到數列的頂端。
2.1. 算法描述
- 比較相鄰的元素。如果第一個比第二個大,就交換它們兩個;
- 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對,這樣在最後的元素應該會是最大的數;
- 針對所有的元素重複以上的步驟,除了最後一個;
- 重複步驟1~3,直到排序完成。
2.2. 動圖演示
2.3. 代碼實現
#include<iostream>
#include<vector>
using namespace std;
vector<int> sortArr(vector<int> &arr){
for(int i=0;i<arr.size()-1;i++) //這裏不減1也是一樣的
for(int j=0;j<arr.size()-1-i;j++)
if(arr[j]>arr[j+1])
swap(arr[j], arr[j+1]);
return arr;
}
int main(){
vector<int> arr; //初始化一個動態數組arr
int n;
for(int i=0;i<8;i++){
cin>>n;
arr.push_back(n);
}
arr = sortArr(arr);
for(int i=0;i<arr.size();i++)
cout<<arr[i]<<" ";
return 0;
}
33 22 11 44 55 6 7 8
6 7 8 11 22 33 44 55
--------------------------------
Process exited after 14.68 seconds with return value 0
請按任意鍵繼續. . .
2.4. 算法時間分析
- 最佳情況:
- 最差情況:
- 平均情況:
- 穩定性: 在相鄰元素相等時,它們並不會交換位置,所以,冒泡排序是穩定排序。
- 應用場景: 冒泡排序思路簡單,代碼也簡單,特別適合小數據的排序。但是,由於算法複雜度較高,在數據量大的時候不適合使用。
2.5. 代碼優化
- 在數據完全有序的時候展現出最優時間複雜度,爲 。其他情況下,幾乎總是 。因此,算法在數據基本有序的情況下,性能最好。要使算法在最佳情況下有 複雜度,需要做一些改進,增加一個flag的標誌,當前一輪沒有進行交換時,說明數組已經有序,沒有必要再進行下一輪的循環了,直接退出。
vector<int> sortArr(vector<int> &arr){
bool flag;
for(int i=0;i<arr.size()-1;i++) //這裏不減1也是一樣的
for(int j=0;j<arr.size()-1-i;j++)
if(arr[j]>arr[j+1]){
swap(arr[j], arr[j+1]);
flag=true;
}
if(flag==false)
break;
return arr;
}
三. 選擇排序(Selection Sort)
- 表現最穩定的排序算法之一,因爲無論什麼數據進去都是 的時間複雜度,所以用到它的時候,數據規模越小越好。唯一的好處可能就是不佔用額外的內存空間了吧。理論上講,選擇排序可能也是平時排序一般人想到的最多的排序方法了吧。
- 選擇排序(Selection-sort)是一種簡單直觀的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。
3.1. 算法描述
個記錄的直接選擇排序可經過 趟直接選擇排序得到有序結果。具體算法描述如下:
- 初始狀態:無序區爲 ,有序區爲空;
- 第 趟排序 開始時,當前有序區和無序區分別爲 和。該趟排序從當前無序區中-選出關鍵字最小的記錄 ,將它與無序區的第1個記錄 交換,使 和 分別變爲記錄個數增加1個的新有序區和記錄個數減少1個的新無序區;
- 趟結束,數組有序化了。
3.2. 動圖演示
3.3. 代碼實現
- 方法1:有序表位於數組的右側;
#include<iostream>
#include<vector>
using namespace std;
vector<int> sortArr(vector<int> &arr){
for(int i=0;i<arr.size()-1;i++){
int max_index=0; //假設起始位置爲最大值元素位置
for(int j=0;j<arr.size()-i;j++)
if(arr[j]>arr[max_index])
max_index=j;
swap(arr[max_index], arr[arr.size()-1-i]);
}
return arr;
}
int main(){
vector<int> arr; //初始化一個動態數組arr
int n;
for(int i=0;i<8;i++){
cin>>n;
arr.push_back(n);
}
arr = sortArr(arr);
for(int i=0;i<arr.size();i++)
cout<<arr[i]<<" ";
return 0;
}
- 方法2:有序表位於數組的左側;
#include<iostream>
#include<vector>
using namespace std;
vector<int> sortArr(vector<int> &arr){
int j=0, k;
for(int i=0;i<arr.size()-1;i++){
k=i;
for(j=i+1;j<arr.size();j++)
if(arr[k]>arr[j])
k=j;
if(i!=k){ //說明最小元素位置不是i位置,需要交換。
swap(arr[i], arr[k]);
}
}
return arr;
}
int main(){
vector<int> arr; //初始化一個動態數組arr
int n;
for(int i=0;i<8;i++){
cin>>n;
arr.push_back(n);
}
arr = sortArr(arr);
for(int i=0;i<arr.size();i++)
cout<<arr[i]<<" ";
return 0;
}
31 22 2 4 8 333 8 99
2 4 8 8 22 31 99 333
--------------------------------
Process exited after 15.36 seconds with return value 0
請按任意鍵繼續. . .
3.4. 算法時間分析
- 最佳情況:
- 最差情況:
- 平均情況:
- 穩定性: 選擇排序不穩定,舉個例子,序列5 8 5 2 9,我們知道第一遍選擇第1個元素5會和2交換,那麼原序列中2個5的相對前後順序就被破壞了,所以選擇排序不是一個穩定的排序算法(注意:用數組實現的選擇排序是不穩定的,用鏈表實現的選擇排序是穩定的。不過,一般提到排序算法時,大家往往會默認是數組實現,所以選擇排序是不穩定的)。
- 應用場景: 選擇排序實現也比較簡單,並且由於在各種情況下複雜度波動小,因此一般是優於冒泡排序的。在所有的完全交換排序中,選擇排序也是比較不錯的一種算法。但是,由於固有的O(n2)複雜度,選擇排序在海量數據面前顯得力不從心。因此,它適用於簡單數據排序。
四. 插入排序(Insertion Sort)
- 插入排序(Insertion-Sort)的算法描述是一種簡單直觀的排序算法。它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,通常採用in-place排序(即只需用到 的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,爲最新元素提供插入空間。
4.1. 算法描述
一般來說,插入排序都採用in-place在數組上實現。具體算法描述如下:
- 從第一個元素開始,該元素可以認爲已經被排序;
- 取出下一個元素,在已經排序的元素序列中從後向前掃描;
- 如果該元素(已排序)大於新元素,將該元素移到下一位置;
- 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置;
- 將新元素插入到該位置後;
- 重複步驟2~5。
4.2. 動圖演示
4.3. 代碼實現
#include<iostream>
#include<vector>
using namespace std;
vector<int> sortArr(vector<int> &arr){
for(int i=1;i<arr.size();i++){ //從二個元素開始遍歷
int current=arr[i]; //當前元素
int j=i-1; //有序表末尾
while(j>=0 && current<arr[j]){
arr[j+1]=arr[j]; //元素右移
j--;
}
arr[j+1] = current; //插入元素
}
return arr;
}
int main(){
vector<int> arr; //初始化一個動態數組arr
int n;
for(int i=0;i<8;i++){
cin>>n;
arr.push_back(n);
}
arr = sortArr(arr);
for(int i=0;i<arr.size();i++)
cout<<arr[i]<<" ";
return 0;
}
31 22 2 4 8 333 8 99
2 4 8 8 22 31 99 333
--------------------------------
Process exited after 16.78 seconds with return value 0
請按任意鍵繼續. . .
4.4. 算法時間分析
- 最佳情況:
- 最差情況:
- 平均情況:
- 穩定性: 由於只需要找到不大於當前數的位置而並不需要交換,因此,直接插入排序是穩定的排序方法。
- 應用場景: 插入排序由於的複雜度,在數組較大的時候不適用。但是,在數據比較少的時候,是一個不錯的選擇,一般做爲快速排序的擴充。例如,在STL的sort算法和stdlib的qsort算法中,都將插入排序作爲快速排序的補充,用於少量元素的排序。又如,在JDK 7 java.util.Arrays所用的sort方法的實現中,當待排數組長度小於47時,會使用插入排序。
五. 希爾排序(插入排序的改良版)
- 希爾排序是希爾(Donald Shell)於1959年提出的一種排序算法。希爾排序也是一種插入排序,它是簡單插入排序經過改進之後的一個更高效的版本,也稱爲縮小增量排序,同時該算法是衝破O(n2)的第一批算法之一。它與插入排序的不同之處在於,它會優先比較距離較遠的元素。希爾排序又叫縮小增量排序。
- 希爾排序是把記錄按下表的一定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個文件恰被分成一組,算法便終止。
5.1. 算法描述
我們來看下希爾排序的基本步驟,在此我們選擇增量 ,縮小增量繼續以 的方式,這種增量選擇我們可以用一個序列來表示,,稱爲增量序列。希爾排序的增量序列的選擇與證明是個數學難題,我們選擇的這個增量序列是比較常用的,也是希爾建議的增量,稱爲希爾增量,但其實這個增量序列不是最優的。此處我們做示例使用希爾增量。
先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,具體算法描述:
- 選擇一個增量序列 ,其中 ;
- 按增量序列個數 ,對序列進行 趟排序;
- 每趟排序,根據對應的增量 ,將待排序列分割成若干長度爲 的子序列,分別對各子表進行直接插入排序。僅增量因子爲1 時,整個序列作爲一個表來處理,表長度即爲整個序列的長度。