| 網(wǎng)站首頁 | 關(guān)于我們 | 開發(fā)優(yōu)勢 | 產(chǎn)品展示 |
| 合作企業(yè) | 新聞動態(tài) | 聯(lián)系我們 | 電話聯(lián)系 |
文章作者:濟(jì)南軟件開發(fā) 時間:2016年12月20日
排序算法可以說是一項基本功,解決實際問題中經(jīng)常遇到,針對實際數(shù)據(jù)的特點選擇合適的排序算法可以使程序獲得更高的效率,有時候排序的穩(wěn)定性還是實際問題中必須考慮的,這篇博客對常見的排序算法進(jìn)行整理,包括:插入排序、選擇排序、冒泡排序、快速排序、堆排序、歸并排序、希爾排序、二叉樹排序、計數(shù)排序、桶排序、基數(shù)排序。
代碼都經(jīng)過了CodeBlocks的調(diào)試,但是很可能有沒注意到的BUG,歡迎指出。
比較排序和非比較排序
常見的排序算法都是比較排序,非比較排序包括計數(shù)排序、桶排序和基數(shù)排序,非比較排序?qū)?shù)據(jù)有要求,因為數(shù)據(jù)本身包含了定位特征,所有才能不通過比較來確定元素的位置。
比較排序的時間復(fù)雜度通常為O(n2)或者O(nlogn),比較排序的時間復(fù)雜度下界就是O(nlogn),而非比較排序的時間復(fù)雜度可以達(dá)到O(n),但是都需要額外的空間開銷。
比較排序時間復(fù)雜度為O(nlogn)的證明:
a1,a2,a3……an序列的所有排序有n!種,所以滿足要求的排序a1',a2',a3'……an'(其中a1'<=a2'<=a3'……<=an')的概率為1/n!?;谳斎朐氐谋容^排序,每一次比較的返回不是0就是1,這恰好可以作為決策樹的一個決策將一個事件分成兩個分支。比如冒泡排序時通過比較a1和a2兩個數(shù)的大小可以把序列分成a1,a2……an與a2,a1……an(氣泡a2上升一個身位)兩種不同的結(jié)果,因此比較排序也可以構(gòu)造決策樹。根節(jié)點代表原始序列a1,a2,a3……an,所有葉子節(jié)點都是這個序列的重排(共有n!個,其中有一個就是我們排序的結(jié)果a1',a2',a3'……an')。如果每次比較的結(jié)果都是等概率的話(恰好劃分為概率空間相等的兩個事件),那么二叉樹就是高度平衡的,深度至少是log(n!)。
又因為 1. n! < nn ,兩邊取對數(shù)就得到log(n!)<nlog(n),所以log(n!) = O(nlogn).
2. n!=n(n-1)(n-2)(n-3)…1 > (n/2)^(n/2) 兩邊取對數(shù)得到 log(n!) > (n/2)log(n/2) = Ω(nlogn),所以 log(n!) = Ω(nlogn)。
因此log(n!)的增長速度與 nlogn 相同,即 log(n!)=Θ(nlogn),這就是通用排序算法的最低時間復(fù)雜度O(nlogn)的依據(jù)。
排序的穩(wěn)定性和復(fù)雜度
不穩(wěn)定:
選擇排序(selection sort)— O(n2)
快速排序(quicksort)— O(nlogn) 平均時間, O(n2) 最壞情況; 對于大的、亂序串列一般認(rèn)為是最快的已知排序
堆排序 (heapsort)— O(nlogn)
希爾排序 (shell sort)— O(nlogn)
基數(shù)排序(radix sort)— O(n·k); 需要 O(n) 額外存儲空間 (K為特征個數(shù))
穩(wěn)定:
插入排序(insertion sort)— O(n2)
冒泡排序(bubble sort) — O(n2)
歸并排序 (merge sort)— O(n log n); 需要 O(n) 額外存儲空間
二叉樹排序(Binary tree sort) — O(nlogn); 需要 O(n) 額外存儲空間
計數(shù)排序 (counting sort) — O(n+k); 需要 O(n+k) 額外存儲空間,k為序列中Max-Min+1
桶排序 (bucket sort)— O(n); 需要 O(k) 額外存儲空間
每種排序的原理和實現(xiàn)
插入排序
遍歷數(shù)組,遍歷到i時,a0,a1...ai-1是已經(jīng)排好序的,取出ai,從ai-1開始向前和每個比較大小,如果小于,則將此位置元素向后移動,繼續(xù)先前比較,如果不小于,則放到正在比較的元素之后??梢娤嗟仍乇容^是,原來靠后的還是拍在后邊,所以插入排序是穩(wěn)定的。
當(dāng)待排序的數(shù)據(jù)基本有序時,插入排序的效率比較高,只需要進(jìn)行很少的數(shù)據(jù)移動。
復(fù)制代碼
void insertion_sort (int a[], int n) {
int i,j,v;
for (i=1; i<n; i++) {
//如果第i個元素小于第j個,則第j個向后移動
for (v=a[i], j=i-1; j>=0&&v<a[j]; j--)
a[j+1]=a[j];
a[j+1]=v;
}
}
復(fù)制代碼
選擇排序
遍歷數(shù)組,遍歷到i時,a0,a1...ai-1是已經(jīng)排好序的,然后從i到n選擇出最小的,記錄下位置,如果不是第i個,則和第i個元素交換。此時第i個元素可能會排到相等元素之后,造成排序的不穩(wěn)定。
復(fù)制代碼
void selection_sort (int a[], int n) {
int i,j,pos,tmp;
for (i=0; i<n-1; i++) {
//尋找最小值的下標(biāo)
for (pos=i, j=i+1; j<n; j++)
if (a[pos]>a[j])
pos=j;
if (pos != i) {
tmp=a[i];
a[i]=a[pos];
a[pos]=tmp;
}
}
}
復(fù)制代碼
冒泡排序
冒泡排序的名字很形象,實際實現(xiàn)是相鄰兩節(jié)點進(jìn)行比較,大的向后移一個,經(jīng)過第一輪兩兩比較和移動,最大的元素移動到了最后,第二輪次大的位于倒數(shù)第二個,依次進(jìn)行。這是最基本的冒泡排序,還可以進(jìn)行一些優(yōu)化。
優(yōu)化一:如果某一輪兩兩比較中沒有任何元素交換,這說明已經(jīng)都排好序了,算法結(jié)束,可以使用一個Flag做標(biāo)記,默認(rèn)為false,如果發(fā)生交互則置為true,每輪結(jié)束時檢測Flag,如果為true則繼續(xù),如果為false則返回。
優(yōu)化二:某一輪結(jié)束位置為j,但是這一輪的最后一次交換發(fā)生在lastSwap的位置,則lastSwap到j(luò)之間是排好序的,下一輪的結(jié)束點就不必是j--了,而直接到lastSwap即可,代碼如下:
復(fù)制代碼
void bubble_sort (int a[], int n) {
int i, j, lastSwap, tmp;
for (j=n-1; j>0; j=lastSwap) {
for (i=0; i<j; i++) {
if (a[i] > a[i+1]) {
tmp=a[i];
a[i]=a[i+1];
a[i+1]=tmp;
//最后一次交換位置的坐標(biāo)
lastSwap = i;
}
}
}
}
復(fù)制代碼
快速排序
快速排序首先找到一個基準(zhǔn),下面程序以第一個元素作為基準(zhǔn)(pivot),然后先從右向左搜索,如果發(fā)現(xiàn)比pivot小,則和pivot交換,然后從左向右搜索,如果發(fā)現(xiàn)比pivot大,則和pivot交換,一直到左邊大于右邊,此時pivot左邊的都比它小,而右邊的都比它大,此時pivot的位置就是排好序后應(yīng)該在的位置,此時pivot將數(shù)組劃分為左右兩部分,可以遞歸采用該方法進(jìn)行??炫诺慕粨Q使排序成為不穩(wěn)定的。
復(fù)制代碼
int mpartition(int a[], int l, int r) {
int pivot = a[l];
while (l<r) {
while (l<r && pivot<=a[r]) r--;
if (l<r) a[l++]=a[r];
while (l<r && pivot>=a[l]) l++;
if (l<r) a[r--]=a[l];
}
a[l]=pivot;
return l;
}
void quick_sort (int a[], int l, int r) {
if (l < r) {
int q = mpartition(a, l, r);
msort(a, l, q-1);
msort(a, q+1, r);
}
}
復(fù)制代碼
堆排序
堆排序是把數(shù)組看作堆,第i個結(jié)點的孩子結(jié)點為第2*i+1和2*i+2個結(jié)點(不超出數(shù)組長度前提下),堆排序的第一步是建堆,然后是取堆頂元素然后調(diào)整堆。建堆的過程是自底向上不斷調(diào)整達(dá)成的,這樣當(dāng)調(diào)整某個結(jié)點時,其左節(jié)點和右結(jié)點已經(jīng)是滿足條件的,此時如果兩個子結(jié)點不需要動,則整個子樹不需要動,如果調(diào)整,則父結(jié)點交換到子結(jié)點位置,再以此結(jié)點繼續(xù)調(diào)整。
下述代碼使用的大頂堆,建立好堆后堆頂元素為最大值,此時取堆頂元素即使堆頂元素和最后一個元素交換,最大的元素處于數(shù)組最后,此時調(diào)整小了一個長度的堆,然后再取堆頂和倒數(shù)第二個元素交換,依次類推,完成數(shù)據(jù)的非遞減排序。
堆排序的主要時間花在初始建堆期間,建好堆后,堆這種數(shù)據(jù)結(jié)構(gòu)以及它奇妙的特征,使得找到數(shù)列中最大的數(shù)字這樣的操作只需要O(1)的時間復(fù)雜度,維護(hù)需要logn的時間復(fù)雜度。堆排序不適宜于記錄數(shù)較少的文件
復(fù)制代碼
void heapAdjust(int a[], int i, int nLength)
{
int nChild;
int nTemp;
for (nTemp = a[i]; 2 * i + 1 < nLength; i = nChild)
{
// 子結(jié)點的位置=2*(父結(jié)點位置)+ 1
nChild = 2 * i + 1;
// 得到子結(jié)點中較大的結(jié)點
if ( nChild < nLength-1 && a[nChild + 1] > a[nChild])
++nChild;
// 如果較大的子結(jié)點大于父結(jié)點那么把較大的子結(jié)點往上移動,替換它的父結(jié)點
if (nTemp < a[nChild])
{
a[i] = a[nChild];
a[nChild]= nTemp;
}
else
// 否則退出循環(huán)
break;
}
}
// 堆排序算法
void heap_sort(int a[],int length)
{
int tmp;
// 調(diào)整序列的前半部分元素,調(diào)整完之后第一個元素是序列的最大的元素
//length/2-1是第一個非葉節(jié)點,此處"/"為整除
for (int i = length / 2 - 1; i >= 0; --i)
heapAdjust(a, i, length);
// 從最后一個元素開始對序列進(jìn)行調(diào)整,不斷的縮小調(diào)整的范圍直到第一個元素
for (int i = length - 1; i > 0; --i)
{
// 把第一個元素和當(dāng)前的最后一個元素交換,
// 保證當(dāng)前的最后一個位置的元素都是在現(xiàn)在的這個序列之中最大的
/// Swap(&a[0], &a[i]);
tmp = a[i];
a[i] = a[0];
a[0] = tmp;
// 不斷縮小調(diào)整heap的范圍,每一次調(diào)整完畢保證第一個元素是當(dāng)前序列的最大值
heapAdjust(a, 0, i);
}
}
復(fù)制代碼
歸并排序
歸并排序是采用分治法(Divide and Conquer)的一個非常典型的應(yīng)用。首先考慮下如何將將二個有序數(shù)列合并。這個非常簡單,只要從比較二個數(shù)列的第一個數(shù),誰小就先取誰,取了后就在對應(yīng)數(shù)列中刪除這個數(shù)。然后再進(jìn)行比較,如果有數(shù)列為空,那直接將另一個數(shù)列的數(shù)據(jù)依次取出即可。這需要將待排序序列中的所有記錄掃描一遍,因此耗費O(n)時間,而由完全二叉樹的深度可知,整個歸并排序需要進(jìn)行.logn.次,因此,總的時間復(fù)雜度為O(nlogn)。
歸并排序在歸并過程中需 要與原始記錄序列同樣數(shù)量的存儲空間存放歸并結(jié)果,因此空間復(fù)雜度為O(n)。
歸并算法需要兩兩比較,不存在跳躍,因此歸并排序是一種穩(wěn)定的排序算法。
復(fù)制代碼
void mergearray(int a[], int first, int mid, int last, int temp[])
{
int i = first, j = mid + 1;
int m = mid, n = last;
int k = 0;
while (i <= m && j <= n)
{
if (a[i] <= a[j])
temp[k++] = a[i++];
else
temp[k++] = a[j++];
}
while (i <= m)
temp[k++] = a[i++];
while (j <= n)
temp[k++] = a[j++];
for (i = 0; i < k; i++)
a[first + i] = temp[i];
}
void merge_sort(int a[], int first, int last, int temp[])
{
if (first < last)
{
int mid = (first + last) / 2;
merge_sort(a, first, mid, temp); //左邊有序
merge_sort(a, mid + 1, last, temp); //右邊有序
mergearray(a, first, mid, last, temp); //再將二個有序數(shù)列合并
}
}
復(fù)制代碼
有的地方看到在mergearray()合并有序數(shù)列時分配臨時數(shù)組,即每一步mergearray的結(jié)果存放的一個新的臨時數(shù)組里,這樣會在遞歸中消耗大量的空間。因此做出小小的變化。只需要new一個臨時數(shù)組。后面的操作都共用這一個臨時數(shù)組。合并完后將臨時數(shù)組中排好序的部分寫回原數(shù)組。
歸并排序計算時間復(fù)雜度時可以很容易的列出遞歸方程,也是計算時間復(fù)雜度的一種方法。
希爾排序
希爾排序是對插入排序的優(yōu)化,基于以下兩個認(rèn)識:1. 數(shù)據(jù)量較小時插入排序速度較快,因為n和n2差距很??;2. 數(shù)據(jù)基本有序時插入排序效率很高,因為比較和移動的數(shù)據(jù)量少。
因此,希爾排序的基本思想是將需要排序的序列劃分成為若干個較小的子序列,對子序列進(jìn)行插入排序,通過則插入排序能夠使得原來序列成為基本有序。這樣通過對較小的序列進(jìn)行插入排序,然后對基本有序的數(shù)列進(jìn)行插入排序,能夠提高插入排序算法的效率。
希爾排序的劃分子序列不是像歸并排序那種的二分,而是采用的叫做增量的技術(shù),例如有十個元素的數(shù)組進(jìn)行希爾排序,首先選擇增量為10/2=5,此時第1個元素和第(1+5)個元素配對成子序列使用插入排序進(jìn)行排序,第2和(2+5)個元素組成子序列,完成后增量繼續(xù)減半為2,此時第1個元素、第(1+2)、第(1+4)、第(1+6)、第(1+8)個元素組成子序列進(jìn)行插入排序。這種增量選擇方法的好處是可以使數(shù)組整體均勻有序,盡可能的減少比較和移動的次數(shù),二分法中即使前一半數(shù)據(jù)有序,后一半中如果有比較小的數(shù)據(jù),還是會造成大量的比較和移動,因此這種增量的方法和插入排序的配合更佳。
希爾排序的時間復(fù)雜度和增量的選擇策略有關(guān),上述增量方法造成希爾排序的不穩(wěn)定性。
復(fù)制代碼
void shell_sort(int a[], int n)
{
int d, i, j, temp; //d為增量
for(d = n/2;d >= 1;d = d/2) //增量遞減到1使完成排序
{
for(i = d; i < n;i++) //插入排序的一輪
{
temp = a[i];
for(j = i - d;(j >= 0) && (a[j] > temp);j = j-d)
{
a[j + d] = a[j];
}
a[j + d] = temp;
}
}
}
復(fù)制代碼
二叉樹排序
二叉樹排序法借助了數(shù)據(jù)結(jié)構(gòu)二叉排序樹,二叉排序數(shù)滿足三個條件:(1)若左子樹不空,則左子樹上所有結(jié)點的值均小于它的根結(jié)點的值; (2)若右子樹不空,則右子樹上所有結(jié)點的值均大于它的根結(jié)點的值; (3)左、右子樹也分別為二叉排序樹。根據(jù)這三個特點,用中序遍歷二叉樹得到的結(jié)果就是排序的結(jié)果。
二叉樹排序法需要首先根據(jù)數(shù)據(jù)構(gòu)建二叉排序樹,然后中序遍歷,排序時間復(fù)雜度為O(nlogn),構(gòu)建二叉樹需要額外的O(n)的存儲空間,有相同的元素是可以設(shè)置排在后邊的放在右子樹,在中序變量的時候也會在后邊,所以二叉樹排序是穩(wěn)定的。
在實現(xiàn)此算法的時候遇到不小的困難,指針參數(shù)在函數(shù)中無法通過new賦值,后來采用取指針地址,然后函數(shù)設(shè)置BST** tree的方式解決。
復(fù)制代碼
int arr[] = {7, 8, 8, 9, 5, 16, 5, 3,56,21,34,15,42};
struct BST{
int number; //保存數(shù)組元素的值
struct BST* left;
struct BST* right;
};
void insertBST(BST** tree, int v) {
if (*tree == NULL) {
*tree = new BST;
(*tree)->left=(*tree)->right=NULL;
(*tree)->number=v;
return;
}
if (v < (*tree)->number)
insertBST(&((*tree)->left), v);
else
insertBST(&((*tree)->right), v);
}
void printResult(BST* tree) {
if (tree == NULL)
return;
if (tree->left != NULL)
printResult(tree->left);
cout << tree->number << " ";
if (tree->right != NULL)
printResult(tree->right);
}
void createBST(BST** tree, int a[], int n) {
*tree = NULL;
for (int i=0; i<n; i++)
insertBST(tree, a[i]);
}
int main()
{
int n = sizeof(arr)/sizeof(int);
BST* root;
createBST(&root, arr, n);
printResult(root);
}
復(fù)制代碼
計數(shù)排序
如果通過比較進(jìn)行排序,那么復(fù)雜度的下界是O(nlogn),但是如果數(shù)據(jù)本身有可以利用的特征,可以不通過比較進(jìn)行排序,就能使時間復(fù)雜度降低到O(n)。
計數(shù)排序要求待排序的數(shù)組元素都是 整數(shù),有很多地方都要去是0-K的正整數(shù),其實負(fù)整數(shù)也可以通過都加一個偏移量解決的。
計數(shù)排序的思想是,考慮待排序數(shù)組中的某一個元素a,如果數(shù)組中比a小的元素有s個,那么a在最終排好序的數(shù)組中的位置將會是s+1,如何知道比a小的元素有多少個,肯定不是通過比較去覺得,而是通過數(shù)字本身的屬性,即累加數(shù)組中最小值到a之間的每個數(shù)字出現(xiàn)的次數(shù)(未出現(xiàn)則為0),而每個數(shù)字出現(xiàn)的次數(shù)可以通過掃描一遍數(shù)組獲得。
計數(shù)排序的步驟:
找出待排序的數(shù)組中最大和最小的元素(計數(shù)數(shù)組C的長度為max-min+1,其中位置0存放min,依次填充到最后一個位置存放max)
統(tǒng)計數(shù)組中每個值為i的元素出現(xiàn)的次數(shù),存入數(shù)組C的第i項
對所有的計數(shù)累加(從C中的第一個元素開始,每一項和前一項相加)
反向填充目標(biāo)數(shù)組:將每個元素i放在新數(shù)組的第C(i)項,每放一個元素就將C(i)減去1(反向填充是為了保證穩(wěn)定性)
以下代碼中尋找最大和最小元素參考編程之美,比較次數(shù)為1.5n次。
計數(shù)排序適合數(shù)據(jù)分布集中的排序,如果數(shù)據(jù)太分散,會造成空間的大量浪費,假設(shè)數(shù)據(jù)為(1,2,3,1000000),這就需要1000000的額外空間,并且有大量的空間浪費和時間浪費。
復(fù)制代碼
void findArrMaxMin(int a[], int size, int *min, int *max)
{
if(size == 0) {
return;
}
if(size == 1) {
*min = *max = a[0];
return;
}
*min = a[0] > a[1] ? a[1] : a[0];
*max = a[0] <= a[1] ? a[1] : a[0];
int i, j;
for(i = 2, j = 3; i < size, j < size; i += 2, j += 2) {
int tempmax = a[i] >= a[j] ? a[i] : a[j];
int tempmin = a[i] < a[j] ? a[i] : a[j];
if(tempmax > *max)
*max = tempmax;
if(tempmin < *min)
*min = tempmin;
}
//如果數(shù)組元素是奇數(shù)個,那么最后一個元素在分組的過程中沒有包含其中,
//這里單獨比較
if(size % 2 != 0) {
if(a[size -1] > *max)
*max = a[size - 1];
else if(a[size -1] < *min)
*min = a[size -1];
}
}
void count_sort(int a[], int b[], int n) {
int max, min;
findArrMaxMin(a, n, &min, &max);
int numRange = max-min+1;
int* counter = new int[numRange];
int i, j, k;
for (k=0; k<numRange; k++)
counter[k]=0;
for (i=0; i<n; i++)
counter[a[i]-min]++;
for (k=1; k<numRange; k++)
counter[k] += counter[k-1];
for (j=n-1; j>=0; j--) {
int v = a[j];
int index = counter[v-min]-1;
b[index]=v;
counter[v-min]--;
}
}
復(fù)制代碼
桶排序
假設(shè)有一組長度為N的待排關(guān)鍵字序列K[1....n]。首先將這個序列劃分成M個的子區(qū)間(桶) 。然后基于某種映射函數(shù) ,將待排序列的關(guān)鍵字k映射到第i個桶中(即桶數(shù)組B的下標(biāo) i) ,那么該關(guān)鍵字k就作為B[i]中的元素(每個桶B[i]都是一組大小為N/M的序列)。接著對每個桶B[i]中的所有元素進(jìn)行比較排序(可以使用快排)。然后依次枚舉輸出B[0]....B[M]中的全部內(nèi)容即是一個有序序列。
桶排序利用函數(shù)的映射關(guān)系,減少了計劃所有的比較操作,是一種Hash的思想,可以用在海量數(shù)據(jù)處理中。
我覺得計數(shù)排序也可以看作是桶排序的特例,數(shù)組關(guān)鍵字范圍為N,劃分為N個桶。
基數(shù)排序
基數(shù)排序也可以看作一種桶排序,不斷的使用不同的標(biāo)準(zhǔn)對數(shù)據(jù)劃分到桶中,最終實現(xiàn)有序?;鶖?shù)排序的思想是對數(shù)據(jù)選擇多種基數(shù),對每一種基數(shù)依次使用桶排序。
基數(shù)排序的步驟:以整數(shù)為例,將整數(shù)按十進(jìn)制位劃分,從低位到高位執(zhí)行以下過程。
1. 從個位開始,根據(jù)0~9的值將數(shù)據(jù)分到10個桶桶,例如12會劃分到2號桶中。
2. 將0~9的10個桶中的數(shù)據(jù)順序放回到數(shù)組中。
重復(fù)上述過程,一直到最高位。
上述方法稱為LSD(Least significant digital),還可以從高位到低位,稱為MSD。
復(fù)制代碼
int getNumInPos(int num,int pos) //獲得某個數(shù)字的第pos位的值
{
int temp = 1;
for (int i = 0; i < pos - 1; i++)
temp *= 10;
return (num / temp) % 10;
}
#define RADIX_10 10 //十個桶,表示每一位的十個數(shù)字
#define KEYNUM 5 //整數(shù)位數(shù)
void radix_sort(int* pDataArray, int iDataNum)
{
int *radixArrays[RADIX_10]; //分別為0~9的序列空間
for (int i = 0; i < RADIX_10; i++)
{
radixArrays[i] = new int[iDataNum];
radixArrays[i][0] = 0; //index為0處記錄這組數(shù)據(jù)的個數(shù)
}
for (int pos = 1; pos <= KEYNUM; pos++) //從個位開始到31位
{
for (int i = 0; i < iDataNum; i++) //分配過程
{
int num = getNumInPos(pDataArray[i], pos);
int index = ++radixArrays[num][0];
radixArrays[num][index] = pDataArray[i];
}
for (int i = 0, j =0; i < RADIX_10; i++) //寫回到原數(shù)組中,復(fù)位radixArrays
{
for (int k = 1; k <= radixArrays[i][0]; k++)
pDataArray[j++] = radixArrays[i][k];
radixArrays[i][0] = 0;
}
}
}
想要了解更多詳情歡迎來電咨詢18678812288
登陸網(wǎng)址:m.h6244.cn。
聯(lián)系人:王經(jīng)理。