以下代碼是個人學習排序算法的一些實踐,實現了大部分排序算法的升序版本,並且對每一種算法進行了簡要的介紹和複雜度分析。
涉及的算法如下:
- 插入排序:直接插入排序、折半插入排序、希爾排序
- 交換排序:冒泡排序、快速排序
- 選擇排序:簡單選擇排序、堆排序
- 其他類型:歸併排序、基數排序
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// 以下排序均爲升序算法
void printArray(vector<int> A){
for(int i = 0; i < A.size(); i++){
cout << A[i] << " ";
}
cout << endl << endl;
}
/************************************
插入排序
1. 直接插入排序
2. 折半插入排序
3. 希爾排序(shell)
*************************************/
vector<int> simpleInsertSort(vector<int> A){
/**
-- 簡單插入排序的思想
從數組的第二個元素開始,將其選爲目標待排序
認定元素左邊的序列爲有序,而右邊的序列爲無序
將目標元素和左邊序列從後向前進行比較,找到插入有序的位置t之後,
先將該位置之後的元素往後移動1位,然後將目標元素插入到位置t
空間效率:O(1)
時間效率:O(n^2)
最好情況 順序,O(n)
最壞情況:逆序,比較次數達到最大,移動次數達到最大
穩定的排序算法:不會改變相同關鍵字元素的相對位置。
適用於順序存儲和鏈式存儲的線性表,適合基本有序和數據量不大的情況
**/
if(A.size() <= 1) return A;
for(int i = 1; i < A.size(); i++){
if(A[i] < A[i - 1]){
int tmp = A[i];
int j = 0;
for(j = i - 1; j >= 0 && tmp < A[j]; j--)
A[j + 1] = A[j];
A[j + 1] = tmp;
}
}
return A;
}
vector<int> binaryInsertSort(vector<int> A){
/**
折半插入在簡單插入排序的基礎上進行了改進
在向前查找插入位置時使用了二分查找的方法,減少了比較的次數
折半插入減少了比較的次數,約爲 O(nlog2), 元素移動次數沒有發生改變O(n^2)
**/
int i, j, low, high, mid;
for(i = 1; i < A.size(); i++){
int tmp = A[i];
low = 0;
high = i - 1;
while(low <= high){
mid = (low + high) / 2;
if(A[mid] > tmp) high = mid-1;
else low = mid + 1;
}
for(j = i - 1; j >= high + 1; j--)
A[j + 1] = A[j];
A[high + 1] = tmp;
}
return A;
}
vector<int> shellSort(vector<int> A){
/****
希爾排序,又稱縮小增量排序,由於簡單插入排序很適合基本有序的序列,
那麼可以對一些較亂的序列進行處理,使之變爲基本有序的序列,
然後在進行一次直接插入排序。
思想如下:
先將數組分割成若干子數組,如[i, i+d, i+2d, i+kd]。
d爲步長,小於,數組長度。這樣整個數組就可以分成d個子數組。
分別對這d個子數組進行直接插入排序。
進行下一輪,取更小的步長e,重複上述動作,知道步長等於1,在進行一次直接插入排序完成。
希爾排序是不穩定的,僅適用於順序存儲的線性表
時間複雜度爲O(n^2),空間複雜度爲O(1)
*****/
for(int d = A.size() / 2; d > 0; d /= 2){
for(int i = d; i < A.size(); i++){
int tmp = A[i];
int j = 0;
for(j = i; j >= d && A[j - d] > tmp; j -= d)
A[j] = A[j-d];
A[j] = tmp;
}
}
return A;
}
/*****************************
交換排序
1. 冒泡排序
2. 快速排序
*****************************/
vector<int> bubbleSort(vector<int> A){
/***
冒泡排序是一種簡單的通過比較關鍵字來交換元素的排序方法
助記碼:
i∈[0,N-1) //循環N-1遍
j∈[0,N-1-i) //每遍循環要處理的無序部分
swap(j,j+1) //兩兩排序(升序/降序)
空間複雜度分析 常數個輔助單元 O(1)
最優時間複雜度 O(n)
最壞時間複雜度 O(n^2)
平均時間複雜度 O(n^2)
最壞情況下
比較次數爲: n(n-1)/ 2
移動次數爲:3n(n-1)/2
屬於穩定排序
***/
int i, j;
for(i = 0; i < A.size() - 1; i++){ // 進行N-1趟比較
bool flag = false;
for(j = 0; j < A.size() - 1 - i; j++){
// 在該趟比較中,0~N-1-i的序列中最大的元素移動到N-2-i的位置
// 所以下一趟待冒泡的序列不需要考慮已經確定好位置的元素了
// 冒泡序列需要減1.
if(A[j] > A[j + 1]){
int tmp = A[j];
A[j] = A[j+1];
A[j+1] = tmp;
flag = true;
}
}
if(!flag) return A; // 若未發生交換,則說明排序已經完成
}
/* 上面的版本是從前向後比較的版本,這裏寫一下從後向前比較的版本,依舊是升序
但不同的是,每趟冒泡總是將冒泡序列最小的元素移動到最前
for(int i = 0; i < A.size() - 1; i++){
for(int j = A.size() - 1; j > i; j--){
if(A[j - 1] > A[j]){
int tmp = A[j - 1];
A[j-1] = A[j];
A[j] = tmp;
}
}
}
*/
return A;
}
/**********************************************************************
快速排序 from wiki:
快速排序使用分治法(Divide and conquer)策略來把一個序列(list)
分爲較小和較大的2個子序列,然後遞歸地排序兩個子序列。
步驟爲:
1. 挑選基準值:從數列中挑出一個元素,稱爲“基準”(pivot),
2. 分割:重新排序數列,所有比基準值小的元素擺放在基準前面,
所有比基準值大的元素擺在基準後面(與基準值相等的數可以到任何一邊)。
在這個分割結束之後,對基準值的排序就已經完成,
3. 遞歸排序子序列:遞歸地將小於基準值元素的子序列和
大於基準值元素的子序列排序。
遞歸到最底部的判斷條件是數列的大小是零或一,此時該數列顯然已經有序。
時間複雜度分析!!!
最壞的情況就是每次遞歸劃分時,pivot選取的位置位於子序列的第一個或者最後一個。
T (n) = O(n) + T(1) + T(n-1)
最壞時間複雜度爲 O(n^2) 選取的pivot後劃分的兩個區域大小爲n-1,0此時會發生較多的交換操作
平均時間複雜度爲 O(nlogn)
最好的情況就是每次劃分pivot,恰好位於序列的中間
T(n) = O(n) + 2 T(n/2)
最好時間複雜度爲 O(nlogn)選取的pivot恰好滿足左邊區域小於,右邊區域恰好大於。
空間效率分析!!!
由於快排是遞歸進行的,需要一個遞歸工作棧來保存每層遞歸調用的必要信息
其容量應該與遞歸調用的最大深度一直。
最好情況下棧深度爲 log2(n+1)向上取整
最壞情況下棧深度爲 n - 1
故最好空間複雜度爲 O(logn)
最壞情況下,空間複雜度爲O(n)
快排的問題是:
使用遞歸,空間代價較高
對於小規模數據,還不如插入排序
解決方案:
當遞歸數據規模充分小,則停止遞歸,調用一些簡單排序如插入排序
這裏實現的版本是以每個子表的第一個元素作爲pivot。
每趟快速排序,都會將pivot放置在最終合適的位置上
快排是一種不穩定的排序方法,例如,在pivot左邊有兩個關鍵字大於pivot且二者相等,
當其進行交換之後,相對位置發生了改變
***************************************************************************/
int partition(vector<int> &A, int low, int high){
// 劃分操作,選定low位置的元素作爲pivot,對low~high中的元素進行處理
// 使得pivot左邊元素小於pivot,右邊元素小於pivot
int pivot = A[low];
while(low < high){
while(low < high && A[high] >= pivot) high--; //從後往前數,找到第一個小於pivot的數
A[low] = A[high]; // 將該數移動到pivot的左邊
while(low < high && A[low] <= pivot) low ++; //從前往後數,找到第一個大於pivot的數
A[high] = A[low]; // 將該數移動到pivot的右邊
}
A[low] = pivot;
return low;
}
void recursiveQuickSort(vector<int> &A, int low, int high){
if(low < high){
int pivotPos = partition(A, low, high);
recursiveQuickSort(A, low, pivotPos - 1);
recursiveQuickSort(A, pivotPos + 1, high);
}
}
vector<int> quickSort(vector<int> A){
recursiveQuickSort(A, 0, A.size() -1);
return A;
}
/**** 快排的一個改進 (僞代碼)
void QuickSort(vector<int> A, int left, int right){
cutoff = 50; // 閾值,當進行快排的區間大於該值時,進行快排
if(cutoff <= right - left){
pivot = partition(A, left, right);
quickSort(A, left, pivot-1);
quickSort(A, pivot, right);
}
else{
insertSort(A, left, right); // 區間較小使用簡單排序
}
}
*******/
/**************************
選擇排序
1. 簡單選擇排序
2. 堆排序
***************************/
vector<int> simpleSelectionSort(vector<int> A){
/**
簡單選擇排序
簡單選擇算法思想如下:
假設數組大小爲n
第一趟,選擇一個最小的數,選擇交換將其放置到位置0,
第二趟,從位置1開始考慮,從剩下的位置中繼續找一個最小的元素,放置到位置1
如此反覆,直到第n-1趟結束。
時間複雜度爲 O(n^2)
比較次數和序列的初始狀態無關,次數爲 n(n-1)/2
不穩定排序
**/
int min = 0;
for(int i = 0; i < A.size() - 1; i++){
min = i;
for(int j = i; j < A.size(); j++){
if(A[j] < A[min]) min = j;
}
if(min !=i){
int tmp = A[i];
A[i] = A[min];
A[min] = tmp;
}
}
return A;
}
/***
堆排序
在簡單選擇算法的基礎上進行改進,我們可以考慮使用堆來快速查找最小元素
在排序過程中,我們將數組視爲一個完全二叉樹順序存儲結構
利用雙親和孩子結點的關係,快速查找最小或最大元素
堆的定義如下:
n個關鍵字L[1...n]序列稱爲堆,當且僅當序列滿足
L(i) <= L(2i) && L(i) <= L(2i+1) (小根堆)
or L(i) >= L(2i) && L(i) >= L(2i+1) (大根堆)
算法的過程就是:
- 將數組建成大根堆
- 將根頭結點和堆底結點交換(將最大值放置到當前數組的末尾)
- 重新調整剩下的結點,變成大根堆。
時間複雜度分析
建立堆時間複雜度爲 O(n),之後有n-1次的向下調整的操作
平均,最好,最壞的時間複雜度都爲O(nlogn)
這裏需要了解一下堆的插入和刪除操作。
堆的插入,我們先將元素放置在數組的末尾,對該結點執行向上調整操作
向上調整的操作就是和父結點比較,如果大於父結點,那麼就和父結點對調,繼續和新的父結點比較
刪除堆頂元素,那麼就讓最後一個元素和堆頂交換,刪除最後一個結點,然後重新從堆頂向下調整
**/
/***自底向上調整爲根大堆***/
vector<int> AdjustDown(vector<int> A, int start, int end){
int dad = start;
int son = 2 * dad + 1;
while(son <= end){
if(son + 1 <= end && A[son] < A[son+1])
son++;
if(A[dad] > A[son]) break;
else{
int tmp = A[dad];
A[dad] = A[son];
A[son] = tmp;
dad = son;
son = 2 * dad + 1;
}
}
return A;
}
/*** 插入堆底元素,需要向上調整 **/
vector<int> AdjustUp(vector<int> A){
int son = A.size() - 1;
int dad = son / 2;
while(son > 0 && A[0] > A[son]){
if(A[son] > A[dad]){
int tmp = A[son];
A[son] = A[dad];
A[dad] = tmp;
son = dad;
dad = son / 2;
}
}
return A;
}
vector<int> heapSort(vector<int> A){
// 建立根大堆
for(int i = A.size() / 2; i >= 0; i--)
A = AdjustDown(A, i, A.size() - 1);
// 循環地將根大堆的頂點和堆底進行交換
for(int i = A.size() - 1; i > 0; i--){
int tmp = A[0];
A[0] = A[i];
A[i] = tmp;
A = AdjustDown(A, 0, i - 1);
}
return A;
}
/********************************
歸併排序(2路歸併排序)
歸併的思想如下:
一開始把數組看成是n個有序的子表,每個表長度爲1
然後將兩個或兩個以上的有序表進行組合成一個新的有序表
得到新的有序表的集合(n/2個),重新兩兩進行組合排序
直到最終有序表的個數爲1個。
時間複雜度爲 O(nlogn)進行歸併需要O(logn)趟,每趟的合併兩個有序表的時間複雜度爲O(n)
由於merge操作不會修改兩個表中相同關鍵字的記錄次序,所以歸併排序是穩定的排序算法
但是空間複雜度卻爲O(n)
*********************************/
void Merge(vector<int> &A, vector<int> &B, int low, int mid, int high){
// 將A中數據全部賦值給B
for(int k = low; k <= high; k++){
B[k] = A[k];
}
int i = low, j = mid+1, k = i;
// 合併兩個有序表,簡單來說就是用兩個指針,比較兩個指針的大小,將較小的存入A中
// 然後移動指針
for(; i <= mid && j <= high; k++){
if(B[i] < B[j]) A[k] = B[i++];
else A[k] = B[j++];
}
while(i<=mid) A[k++] = B[i++];
while(j<=high) A[k++] = B[j++];
}
void recursiveMergeSort(vector<int> & A, vector<int> & B,int low, int high){
if(low < high){
int mid = (low + high) / 2;
recursiveMergeSort(A, B, low, mid);
recursiveMergeSort(A, B, mid + 1, high);
Merge(A, B, low, mid, high);
}
}
vector<int> mergeSort(vector<int> A){
vector<int> B; // 輔助數組
for(int i = 0; i < A.size(); i ++){
B.push_back(0);
}
recursiveMergeSort(A, B, 0, A.size() - 1);
return A;
}
/******************************************************************************
基數排序
不是基於比較的排序,而是基於多關鍵字的排序思想。
藉助分配和收集兩種操作對單邏輯關鍵字完成排序。
在開始談基數排序之前,我們得先了解一下桶排序
假設現在有 N 個整數,其值位於0~100之間,我們要在線性的時間內對其排序
使用空間換時間的思路,我們可以申請一個長度爲M=101的數組A,初始化爲0
讀N個整數,記爲i,並將其A[i]+= 1.
然後從頭開始遍歷數組,讀取不爲0的A[i]的i,如果A[i]=2就需要讀兩次,
這樣就完成了線性時間內的排序過程
但是如果M>>N的話,而且數據不是整數,那麼要怎麼辦呢。
由此就引申出了基數排序算法。算法分爲高位優先MSD和低位優先順序
這裏只介紹低位優先順序的實現。
首先明確基數是什麼,此處記爲r,簡單來說就是要滿足進位的數就是基數,如逢十進一,十就是基數
對於一組數來說,我們可以得到其最大的數所包含的位數K,所有未到達K位的數,我們默認補0
我們申請 r 個隊列來作爲桶保存數據(0~r-1),從低位第一位開始考慮,
分配:如果該位所在的數是 1,那麼我們就將其存入隊列 1。當所有數都存入隊列之後
收集:將隊列中的數從0~1-r中依次首尾連接,構成新的數組,重新上述操作,直到達到最高位,排序完成。
空間複雜度爲 O(r) r個隊列
時間複雜度O(d(n+r)) 需要d趟收集和分配,一趟分配需要O(n),一趟收集需要O(r)
*******************************************************************************/
int maxBit(vector<int> A){
// 獲取數組中最大數的位數
if(!A.size()) return 0;
int maxVal = A[0];
for(int i = 1; i < A.size(); i++){
if(A[i] > maxVal){
maxVal = A[i];
}
}
// 這裏假設數都是十位數
int d = 1;
int p = 10;
while(maxVal>=p){
maxVal /= p;
++d;
}
return d;
}
vector<int> radixSort(vector<int> A){
int d = maxBit(A); //獲取最大位數
int len = A.size();
int *tmp = new int[len]; // 存取臨時數組元素
int *count = new int[10]; // 計數器
int radix = 1;
for(int i = 1; i <= d; i++){
// 清空計數器
for(int j = 0; j < len; j++)
count[j] = 0;
//按位計入桶
for(int j = 0; j < len; j++){
int k = (A[j] / radix) % 10;
count[k]++;
}
// 按位累加計數器,將數轉換成排序好數組的位置信息
for(int j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j];
// 按照計數器的位置信息,從後向前將數存入tmp,
for(int j = len - 1; j >= 0; j--){
int k = (A[j] / radix) % 10;
tmp[count[k] - 1] = A[j];
count[k]--;
}
for(int j = 0; j < len; j++){
A[j] = tmp[j];
}
radix *= 10;
}
delete []tmp;
delete []count;
return A;
}
int main()
{
int num = 0;
vector<int> A;
int a[10] = {18, 22, 31, 15, 13, 10, 11, 59, 67, 13};
for(int i = 0; i < 10; i++){
A.push_back(a[i]);
}
cout << "原序列: ";
printArray(A);
cout << "簡單插入排序: ";
printArray(simpleInsertSort(A));
cout << "二分插入排序:";
printArray(binaryInsertSort(A));
cout << "希爾排序:";
printArray(shellSort(A));
cout << "冒泡排序:";
printArray(bubbleSort(A));
cout << "快速排序:";
printArray(quickSort(A));
cout << "簡單選擇排序:";
printArray(simpleSelectionSort(A));
cout << "堆排序:";
printArray(heapSort(A));
cout << "歸併排序:";
printArray(mergeSort(A));
cout << "基數排序:";
printArray(radixSort(A));
cout <<"原序列:";
printArray(A);
return 0;
}
一個簡單的測試運行結果:
這裏給出一個總結排序算法的速記表,供各位參考。
本博客代碼來自於個人的一個數據結構與算法的總結項目,彙總了常見的數據結構和算法的實現。
歡迎點此訪問!
歡迎大家指正!