前言
常用的有十種排序算法,包含了插入、選擇、交換、分治、線性五種類別,本篇博客將對這十種排序算法做一個總結,並附帶C++代碼
總體表格
分別來看
插入排序
在要排序的一組數中,假定前n-1個數已經排好序,現在將第n個數插到前面的有序數列中,使得這n個數也是排好順序的。如此反覆循環,直到全部排好順序。
void InserttSort(int array[], int length)
{
for(int i = 0; i < length - 1; ++ i)
{
for(int j = i + 1; j > 0; --j)
{
if(array[j] < array[j - 1])
MySwap(array, j, j - 1);
else
break;
}
}
}
希爾排序
插入排序一種高效率的實現,也叫縮小增量排序
簡單插入排序中,如果序列是基本有序的,使用直接插入排序效率就非常高
希爾排序利用了這個特點:先將整個序列分割成若干個子序列進行直接插入排序,待整個序列中的記錄基本有序時再對全體記錄進行一次直接插入排序
注意:分割子序列的時候不是逐段分割,而是將某個相隔增量的元素組成一個子序列
較小的元素跳躍式往前挪動,比直接插入排序效率高
時間複雜度需要複雜的數學推算
void ShellSort(int array[], int length)
{
int incre = length;
while (true)
{
incre = incre / 2;
cout << "incre: " << incre << endl;
for(int k = 0; k < incre; ++k)//根據增量分爲若干子列
{
for(int i = k + incre; i < length ; i += incre)
{
for(int j = i; j > k; j -= incre)
{
if(array[j] < array[j - incre])
MySwap(array, j, j - incre);
else
break;
}
}
}
if(incre == 1)
break;
}
}
選擇排序
依次選擇最小、第二小。。。的數放在第一位、第二位。。。
第一次遍歷n-1個數,第二次n-2個數
void SelectSort(int array[], int length)
{
for(int i = 0; i < length -1; ++i)
{
int minIndex = i;
for(int j = i + 1; j < length; ++j)
{
if(array[j] < array[minIndex])
minIndex = j;
}
if(minIndex != i)
MySwap(array, minIndex, i);
}
}
堆排序
利用堆這種數據結構而設計的一種排序算法,是一種選擇排序
最壞、最好、平均時間複雜度均爲O(nlogn)
堆
是具有以下性質的完全二叉樹:
每個結點的值都大於或等於其左右孩子結點的值,稱爲大頂堆;或者每個結點的值都小於或等於其左右孩子結點的值,稱爲小頂堆。
映射到數組中
所以大頂堆:arr[i] >= arr[2i + 1] && arr[i] >= arr[2i + 2]
小頂堆:arr[i] <= arr[2i + 1] && arr[i] <= arr[2i + 2]
堆排序思路:將待排序序列構造成一個大頂堆,此時,整個序列的最大值就是堆頂的根節點。將其與末尾元素進行交換,此時末尾就爲最大值。然後將剩餘n-1個元素重新構造成一個堆,這樣會得到n個元素的次小值。如此反覆執行,便能得到一個有序序列了
步驟
- 構建初始堆(一般升序採用大頂堆,降序採用小頂堆)
- 從最後一個非葉子節點開始,從左到右,從下至上進行調整
- 繼續調整構成一個大頂堆
- 將堆頂元素與末尾元素進行交換,使末尾元素最大,然後繼續調整堆,再將棧頂元素與末尾元素進行交換,得到第二大的元素,如此反覆的重建、交換、重建、交換
總結基本思路:
- 將無需序列構建成一個堆,根據升序降序需求選擇大頂堆或小頂堆;
- 將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端;
- 重新調整結構,使其滿足堆定義,然後繼續交換堆頂元素與當前末尾元素,反覆執行調整+交換步驟,直到整個序列有序。
void AdjustHeapNode(int array[], int i, int length)//調整大頂堆
{
int k = i * 2 + 1;
while(k < length)
{
if(k + 1 < length && array[k] < array[k + 1])//如果左子節點小於右子節點,k指向右子節點
++k;
if(array[k] > array[i])//如果子節點大於父節點,將子節點賦值給父節點(不用進行交換)
{
MySwap(array, i, k);
}
else
break;
i = k; //檢查更換的節點是否滿足最大堆的特性
k = 2 * i + 1;
}
}
void HeapSort(int array[], int length)
{
//構建大頂堆
for(int i = (length - 1) / 2; i >= 0; i--) //最後一個非葉子節點開始
{
cout << i << " ";
AdjustHeapNode(array, i, length);
}
cout << endl;
//調整堆結構+交換堆頂元素與末尾元素
for(int j = length - 1; j > 0; j--)
{
MySwap(array, 0, j);
AdjustHeapNode(array, 0, j);
}
}
冒泡排序
兩個數比較大小,較大的下沉,較小的數冒起來
從最後開始冒,總共走length-1趟,每次排好一個數
void BubbleSort(int array[], int length)
{
if(array == nullptr || length <= 0)
return;
for(int i = 0; i < length - 1; ++i)
{
for(int j = length - 1; j > i; --j)
if(array[j] < array[j - 1])
MySwap(array, j, j-1);
}
}
優化
因爲冒泡排序可能出現已經排好序但是依然要走滿length-1趟的情況
添加一個標誌,每趟開始前置零,當發生交換置1
每趟跑完檢測是否爲0,如果爲0,說明本次沒有發生交換,已經排好序了
void BubbleSort_Better(int array[], int length)
{
if(array == nullptr || length <= 0)
return;
for(int i = 0; i < length - 1; ++i)
{
bool flag = false;
for(int j = length - 1; j > i; --j)
if(array[j] < array[j - 1])
{
MySwap(array, j, j-1);
flag = true;
}
if(!flag) break;
}
}
快速排序
基本思路(分治)
- 先從數組中取出一個數作爲key
- 將比這個數小的數全部放在它的左邊,比這個數大的全放在它右邊
- 對左右兩個小數列重複執行第二步,直至各區間只有一個數
key值的選取有多種形式,例如中間數或者隨機數
快排的時間性能取決於快排遞歸的深度
最優情況就是每次都恰好把數組分成兩半,最優時間複雜度爲O(nlogn)
最壞情況就是正序或者逆序(恰好與所要求的的排序相反),最差時間負責度是O(n^2)
時間複雜度和空間複雜度推導:可以看看這篇文章 文章地址
void QuickSort(int array[], int left, int right)
{
if(left >= right)
return;
int i = left, j = right, key = array[left];
while(i < j)
{
while(i < j && array[j] >= key)//從右開始找第一個小於等於key的值
j--;
if(i < j)
{
array[i] = array[j];
i++;
}
while(i < j && array[i] < key)
i++;
if(i < j)
{
array[j] = array[i];
j--;
}
}
array[i] = key;
QuickSort(array, left, right - 1);
QuickSort(array, left + 1, right);
}
歸併排序
建立在歸併操作上的一種有效的排序算法
分治法的典型應用
首先要考慮如何將兩個有序數列合併(有序數列合併問題,三個指針)
歸併排序的效率是比較高的,設數列長爲N,將數列分開成小數列一共要logN步,每步都是一個合併有序數列的過程,時間複雜度可以記爲O(N),故一共爲O(N*logN)。
空間複雜度爲O(n)
void mergeArray(int array[], int first, int middle, int last)
{
int temp[last - first +1];
int i = first;
int m = middle;
int j = middle + 1;
int n = last;
int k = 0;
while(i <= m && j <= n)
{
if(array[i] <= array[j])
{
temp[k] = array[i];
k++;
i++;
}
else
{
temp[k] = array[j];
k++;
j++;
}
}
while(i <= m)
{
temp[k] = array[i];
k++;
i++;
}
while(j <= n)
{
temp[k] = array[j];
k++;
j++;
}
for(int ii = 0; ii < k; ii++)
array[first+ii] = temp[ii];
}
void MergeSort(int array[], int first, int last)
{
if(first < last)
{
int middle = (first + last) / 2;
MergeSort(array, first, middle);
MergeSort(array, middle + 1, last);
mergeArray(array, first, middle, last);
}
}
計數排序
時間複雜度爲O(n)的排序算法
適用於待排序數有範圍,需要較多的輔助空間,空間大小與待排序數範圍的大小有關
設定一個計數數組(大小爲待排序數最大值加1)
將待排序數讀入並給對應值自增
輸出到待排序數組完成排序
int get_max(int array[], int length)//獲取數組中最大值的函數
{
if(array == nullptr || length <= 0)
return -1;
int max = array[0];
for(int i = 1; i < length; i++)
{
if(array[i] > max)
max = array[i];
}
return max;
}
void CountSort(int array[], int length)
{
if(array == nullptr || length <= 0)
return;
int max = get_max(array, length) + 1;//獲取數組中的最大值
cout << max << endl;
int *count= new int[max];//分配空間
for(int i = 0; i < max; i++)//初始化計數數組各位爲0
count[i] = 0;
// for(int i = 0; i < max; i++)
// cout << count[i] << " ";
// cout <<endl;
for(int i = 0; i < length; i++)//計數
{
count[array[i]]++;
}
for(int i = 0; i < max; i++)
cout << count[i] << " ";
cout <<endl;
for(int i = 0, j = 0; i < max; i++)//輸出到源數組,完成排序
{
for(int k = count[i]; k > 0; k--)
{
array[j] = i;
j++;
}
}
}
桶排序
是對計數排序的一種改進和推廣
全依賴“比較”操作的排序算法時間複雜度的一個下界O(N*logN)
這些算法並不是不用“比較”操作,也不是想辦法將比較操作的次數減少到 logN。而是利用對待排數據的某些限定性假設 ,來避免絕大多數的“比較”操作。
基本思想
- 將待排序列以某種映射關係映射到多個桶中,然後對每個桶中的元素進行排序,然後依次枚舉輸出所有桶中的元素,這樣就得到一個有序序列
映射函數
- 如果關鍵字k1<k2,那麼f(k1)<=f(k2)。也就是說B(i)中的最小數據都要大於B(i-1)中最大數據
- 數據分段
使用映射函數,減少了幾乎所有的比較工作
對N個關鍵字進行桶排序的時間複雜度分爲兩個部分:
- (1)循環計算每個關鍵字的桶映射函數,這個時間複雜度是O(N)。
- (2)利用先進的比較排序算法對每個桶內的所有數據進行排序,其時間複雜度爲 ∑ O(Ni*logNi) 。其中Ni 爲第i個桶的數據量。
很顯然,第(2)部分是桶排序性能好壞的決定因素。儘量減少桶內數據的數量是提高效率的唯一辦法(因爲基於比較排序的最好平均時間複雜度只能達到**O(N*logN)**了)。因此,我們需要儘量做到下面兩點:
- (1) 映射函數f(k)能夠將N個數據平均的分配到M個桶中,這樣每個桶就有[N/M]個數據量。
- (2) 儘量的增大桶的數量。極限情況下每個桶只能得到一個數據,這樣就完全避開了桶內數據的“比較”排序操作。 當然,做到這一點很不容易,數據量巨大的情況下,f(k)函數會使得桶集合的數量巨大,空間浪費嚴重。這就是一個時間代價和空間代價的權衡問題了。
對於N個待排數據,M個桶,平均每個桶[N/M]個數據的桶排序平均時間複雜度爲:
O(N)+O(M*(N/M)log(N/M))=O(N+N(logN-logM))=O(N+NlogN-NlogM)
當N=M時,即極限情況下每個桶只有一個數據時。桶排序的最好效率能夠達到O(N)。
總結
- 桶排序的平均時間複雜度爲線性的O(N+C),其中C=N*(logN-logM)。如果相對於同樣的N,桶數量M越大,其效率越高,最好的時間複雜度達到O(N)。 當然桶排序的空間複雜度 爲O(N+M),如果輸入數據非常龐大,而桶的數量也非常多,則空間代價無疑是昂貴的。此外,桶排序是穩定的。
- 適用範圍類似於計數排序
int DataMap(int num) //桶排序映射函數
{
return num / 10;
}
void BucketSort(int array[], int length)
{
if(array == nullptr || length <= 0)
return;
//vector<list<int>> bucket;//使用雙向鏈表來存儲桶內元素,同vector來組織桶
list<int> bucket[10];//使用雙向鏈表來存儲桶內元素,用數組來組織桶
for(int i = 0; i < length; i++)
{
bucket[DataMap(array[i])].push_back(array[i]);//給對應的桶中插入,插入操作O(1)時間複雜度
}
for(int i = 0; i < 10; i++)//分別對每個桶中的元素進行排序
{
bucket[i].sort();
}
for(int i = 0, k = 0; i < 10; i++)//輸出桶中的元素到序列中,完成排序
{
for(auto j : bucket[i])
{
if(k < length)
{
array[k] = j;
k++;
}
}
}
}
基數排序
BinSort想法非常簡單,首先創建數組A[MaxValue];然後將每個數放到相應的位置上(例如17放在下標17的數組位置);最後遍歷數組,即爲排序後的結果。
問題: 當序列中存在較大值時,BinSort 的排序方法會浪費大量的空間開銷。
基數排序是一種藉助多關鍵字排序思想對單邏輯關鍵字進行排序的方法。
所謂的多關鍵字排序就是有多個優先級不同的關鍵字。
如果對數字進行排序,那麼個位、十位、百位就是不同優先級的關鍵字,如果要進行升序排序,那麼個位、十位、百位優先級一次增加。基數排序是通過多次的收分配和收集來實現的,關鍵字優先級低的先進行分配和收集。
void BitCountSort(int array[], int length, int exp)//按位計數排序函數
{
int range[10];
int temparr[length];
for(int i = 0; i <10; i++)
range[i] = 0;
for(int i = 0; i < length; i++)
{
range[(array[i]/exp)%10]++;
}
cout << "range :" << endl;
for(int i = 0; i < 10; i++)
{
cout << range[i] << " ";
}
cout << endl;
for(int i = 1; i < 10; i++)
{
range[i] += range[i-1];//統計本應出現的位置
}
cout << "range :" << endl;
for(int i = 0; i < 10; i++)
{
cout << range[i] << " ";
}
cout << endl;
for(int i = length - 1; i >=0; i--)
{
temparr[range[(array[i]/exp)%10] - 1] = array[i];
range[(array[i]/exp)%10]--;
}
for(int i = 0; i < length; i++)
{
array[i] = temparr[i];
}
}
void RadixSort(int array[], int length)
{
int max = -1;
//提取最大值
for(int i = 0; i < length; i++)
{
if(array[i] > max)
max = array[i];
}
//提取每一位進行比較位數不足的高位補0
for(int exp = 1; max/exp > 0; exp *= 10)
BitCountSort(array, length, exp);
}
總結
簡單排序:冒泡排序、選擇排序、插入排序
簡單排序的變種:快速排序、堆排序、希爾排序
基於分治遞歸的:歸併排序
線性排序:計數排序、桶排序、基數排序
穩定性討論
我們希望如果關鍵值相等的時候,先輸入的數據應該還是排在前面,而不是隨便排
- 穩定的排序算法有: 插入排序,冒泡排序,合併排序,計數排序,基數排序,桶排序
- 不穩定的排序算法有:堆排序,快速排序,選擇排序,希爾排序。
內存佔用討論
- In-place sort(不佔用額外內存或佔用常數的內存):插入排序、選擇排序、冒泡排序、堆排序、快速排序、希爾排序。
- Out-place sort:歸併排序、計數排序、基數排序、桶排序。
當需要對大量數據進行排序時,In-place sort就顯示出優點,因爲只需要佔用常數的內存。