排序算法分析(都以從小到大爲例):
1、Insertsort:(原地排序)
最壞情況:從大到小排列(逆序),6,5,4,3,2,1, 比較(移動次數要乘以3)次數=n*(n - 1) / 2;
最好情況:從小到大排列(正序),1,2,3,4,5,6,比較次數 = n;
平均情況:與最壞情況一樣,爲Θ(n^2).
算法穩定。
void insertsort(unsigned long *a,int num,unsigned long *res)
{
unsigned long temp[num];
temp[0] = a[0];
//每次取一個數 插入temp數組中
for(int i = 1; i < num; i++)
for(int j = i - 1; j >= 0; j--)
{
if(a[i] < temp[j])
{
temp[j + 1] = temp[j];
if(j == 0)
temp[j] = a[i];
}
else
{
temp[j + 1] = a[i];
break;
}
}
for(int i = 0; i < num; i++)
res[i] = temp[i];
}
//原地排序版本
void insertsort1(unsigned long *a,int num)
{
unsigned long key;
//每次取一個數 插入temp數組中
for(int i = 1; i < num; i++)
{
key = a[i];
for(int j = i - 1; j >= 0; j--)
{
if(key < a[j])
{
a[j + 1] = a[j];
if(j == 0)
a[j] = key;
}
else
{
a[j + 1] = key;
break;
}
}
}
}
2、Shellsort:(分組的插入排序)
開始無序的時候每組元素較少,最後差不多有序的時候元素纔多起來,所以效果比插入排序要好。
算法不穩定 14336,步長開始取2,則33會換。
void shellsort(unsigned long *a,int num)
{
int step = num / 3;
unsigned long temp;
//每次插入排序的間隔步長
for(int i = step; i >= 1; i /= 2)
//插入排序
for(int j = 0; j < i; j++)
//將每隔i的數分爲一組,並進行每組的插入排序
for(int k = j + i; k <= num - 1; k += i)
{
temp = a[k];
for(int m = k - i; m >= j; m -= i)
{
if(temp < a[m])
{
a[m + i] = a[m];
if(m == j)
a[m] = temp;
}
else
{
a[m + i] = temp;
break;
}
}
}
}
3、Bubblesort:
每次將最小的元素移到(冒泡到)最前面(i)的位置。
最壞情況:逆序,6,5,4,3,2,1,比較次數:n*(n - 1) / 2; 移動次數:3 * n*(n - 1) / 2
最好情況:正序,123456,比較次數n
平均情況:
算法穩定
void bubblesort(unsigned long *a,int num)
{
for(int i = 0; i < num; i++)
for(int j = num - 1; j >= i + 1; j--)
{
unsigned long temp;
if(a[j] < a[j - 1])
{
temp = a[j - 1];
a[j - 1] = a[j];
a[j] = temp;
}
}
}
4、selectsort:
和冒泡排序不同的是每次都只找到後面s[i+1,end]中最小的,僅進行下標的記錄,而不進行相鄰位置的交換。
最好最壞情況和冒泡一樣,不過移動次數減少,比較次數一樣。
void selectsort(unsigned long *a,int num)
{
int min_flag;
for(int i = 0; i < num; i++)
{
unsigned long min = a[i];
min_flag = i;
for(int j = i; j < num; j++)
{
if(a[j] < min)
{
min = a[j];
min_flag = j;
}
}
unsigned long temp = a[min_flag];
a[min_flag] = a[i];
a[i] = temp;
}
}
5、Qsort(挖坑、填數)
最壞情況:每次partition都取的是當前組最大或最小,62345,每次分治的時候都是一個數組中有一個,另外一個有m-1個,故需進行n次劃分,而每次進行的比較次數爲m-1,故複雜度爲n^2
最好情況:每次flag取中間大小的數,分治2邊數組大小差不多,則會劃分logn次,而進行比較次數就是每組的元素數,故爲nlogn。
平均情況:假設規模爲N的問題分爲一個規模爲9/10N的問題和規模爲1/10N的問題,即T(n) = T(9n/10) + T(n/10) + Θ(n),用遞歸樹分析可得T(n) = O(nlogn),而且比分區9:1要更平均(也就是情況更好)的概率爲80%,所以在絕大部分情況下快速排序算法的運行時間爲O(nlogn)。
//i從左往右,j從右往左,flag爲第一個元素,將左邊(i所指)比flag大的放到右邊,把右邊(j所指)的放到左邊,i和j交替進行,挖坑、填數
int partition(unsigned long *a,int start,int end)
{
unsigned long flag = a[start];
int i = start;
int j = end;
//每次先跳過不需要動的,然後對需要動的進行移動
while(i < j)
{
//跳過右邊比flag大的 ,這裏有(i< j)是爲了防止數組a後面值沒有初始化會是亂值!
while((a[j] >= flag) && (i < j)) j--;
if(i < j)
a[i++] = a[j];
//跳過左邊比flag小的
while((a[i] <= flag) && (i < j))i++;
if(i < j)
a[j--] = a[i];
}
a[i] = flag;
return i;
}
//不斷進行左右分治的partition,直到每個左右數組"僅有一個元素 "
void qsort(unsigned long *a,int start,int end)
{
//start < end,保證僅有一個元素的時候停止
if(start < end)
{
int mid = partition(a,start,end);
qsort(a,start,mid - 1);
qsort(a,mid + 1,end);
}
}
6、mergesort
算法裏面有一種分治策略:Divide(分解子問題的步驟) 、 Conquer(遞歸解決子問題的步驟)、 Combine(子問題解求出來後合併成原問題解的步驟)
使得複雜度爲nlogn,就是將問題分成2個同樣的子問題,然後再找一個O(n)複雜度的方法將2個子問題的結果合併成原問題的結果,複雜度計算式爲:
T(n)=2T(n/2)+O(n);
這樣的T(n) = nlogn,mergesort就是這種思路。
Merge的最壞情況,需要進行n次比較與n次移動。
最好情況,進行n次移動+比較。
最好情況:已排好序,不用移動,僅比較 nlogn
最壞情況:一直都是merge的最壞情況,分組每次都是平均分的,故分logn次,所以也是nlogn
//將2個已排好序的數組合併成一個有序數組
//[start:mid]是array1,[mid+1:end]是array2
void merge(unsigned long *a,int start,int mid,int end,unsigned long *res)
{
int frsize = mid - start;
unsigned long temp[1000];
int j = mid + 1;
int i = start;
int k = 0;
while((i <= mid) && (j <= end))
{
if(a[i] > a[j])
temp[k++] = a[j++];
else
temp[k++] = a[i++];
}
if(i == mid + 1)//first array finishes first
for(int m = j; m <= end; m++)
temp[k++] = a[m];
else//second finishes first
for(int m = i; m <= mid; m++)
temp[k++] = a[m];
for(int i = 0; i <= end; i++)
res[i] = temp[i];
}
void mergesort(unsigned long *a ,int start,int end)
{
if(start < end)
{
int i = (start + end) / 2;
mergesort(a,start,i);
mergesort(a,i + 1,end);
merge(a,start,i,end,a);
}
}
歸併排序與快速排序比較:歸併的merge要開額外的空間,而快排的partition可以原地進行!
7.堆排序
堆排序主要需要建立堆,通過heapify(int *a,int start,int num)操作將堆建立,然後每次取出根節點並將其移除後再將餘下num-1個數用heapify操作建立堆。heapify操作一次複雜度爲logn,一共n次heapify操作,複雜度O(nlogn)。其爲不穩定排序算法。是就地排序,只需要O(1)的空間。
#include "heap.h"
using namespace std;
//a數組有效元素爲a[1]-a[num].
void heap::min_heapify(int *a, int p,int num)
{
int lchild = 2 * p, rchild = 2 * p + 1;
int temp;
if (lchild > num)//已經到葉子節點
return;
else if (rchild > num)
{
if (a[lchild] < a[p])
{
temp = a[p];
a[p] = a[lchild];
a[lchild] = temp;
}
return;
}
min_heapify(a, 2 * p, num);
min_heapify(a, 2 * p + 1, num);
int min;
if (a[lchild] < a[rchild])
min = lchild;
else
min = rchild;
if (a[min] < a[p])
{
temp = a[p];
a[p] = a[min];
a[min] = temp;
}
}
void heap::min_heapsort(int *a, int num)
{
for (int i = num; i >= 1; i--)
{
min_heapify(a, 1, i);
int temp = a[i];
a[i] = a[1];
a[1] = temp;
}
}
8、桶排序
此排序是爲了優化計數排序的空間太大問題,即要排序的數很多(如500萬個數),但是數的範圍不大(如0-900),通過將數組的數據全部歸一化到[0,1)之間,然後建立m個區間分別爲[0,1/m)、[1/m,2/m)、……[m-1/m,1)的bucket,每個桶都是一個鏈表,將歸一化的數據放入到對應的桶中,然後對每個桶進行排序(用qsort、insertsort.etc都行),然後按順序輸出即可。
void bucketsort(unsigned long *a,int num,int m)
{
double data[1000];
unsigned long max = 0;
vector<double> bucket[20];
float seg = 1.0 / m;
for(int i = 0; i < num; i++)
{
if(max < a[i])
max = a[i];
}
max = max + 100;
for(int i = 0; i < num; i++)
{
data[i] = (double)a[i] / max;
for(int j = 0; j < m; j++)//放桶裏
{
if(data[i] >= seg * j && data[i] <= seg * (j + 1))
{
bucket[j].push_back(data[i]);
break;
}
}
}
for(int i = 0; i < m; i++)
sort(bucket[i].begin(),bucket[i].end());
int base = 0;
for(int i = 0; i < m; i++)
{
for(int j = 0; j < bucket[i].size(); j++)
a[base++] = (bucket[i])[j] * max;
}
}
複雜度分析:
在做出數據落入每個bucket都是等可能的情況下,我們可以看到T() = O(n) + 每個桶排序時間總和,兩邊同時取期望,然後可以證明,排序時間的期望(用insertsort)爲:n * (2 - 1/n).最終桶排序複雜度爲O(n + c),其中C=N*(logN-logm),空間複雜度爲O(N+M)。
eg:
在一個文件中有10G個整數,亂序排列,要求找出中位數。內存限制爲2G。只寫出思路即可(內存限制爲2G意思是可以使用2G空間來運行程序,而不考慮本機上其他軟件內存佔用情況。)
分析:既然要找中位數,很簡單就是排序的想法。那麼基於字節的桶排序是一個可行的方法。
思想:將整型的每1byte作爲一個關鍵字,也就是說一個整形可以拆成4個keys,而且最高位的keys越大,整數越大。如果高位keys相同,則比較次高位的keys。整個比較過程類似於字符串的字典序。
第一步: 把10G整數每2G讀入一次內存,然後一次遍歷這536,870,912即(1024*1024*1024)*2 /4個數據。每個數據用位運算">>"取出最高8位(31-24)。這8bits(0-255)最多表示256個桶,那麼可以根據8bit的值來確定丟入第幾個桶。最後把每個桶寫入一個磁盤文件中,同時在內存中統計每個桶內數據的數量NUM[256]。
代價:(1) 10G數據依次讀入內存的IO代價(這個是無法避免的,CPU不能直接在磁盤上運算)。(2)在內存中遍歷536,870,912個數據,這是一個O(n)的線性時間複雜度。(3)把256個桶寫回到256個磁盤文件空間中,這個代價是額外的,也就是多付出一倍的10G數據轉移的時間。
第二步:根據內存中256個桶內的數量NUM[256],計算中位數在第幾個桶中。很顯然,2,684,354,560個數中位數是第1,342,177,280個。假設前127個桶的數據量相加,發現少於1,342,177,280,把第128個桶數據量加上,大於1,342,177,280。說明,中位數必在磁盤的第128個桶中。而且在這個桶的第1,342,177,280-N(0-127)個數位上。N(0-127)表示前127個桶的數據量之和。然後把第128個文件中的整數讀入內存。(若數據大致是均勻分佈的,每個文件的大小估計在10G/256=40M左右,當然也不一定,但是超過2G的可能性很小)。注意,變態的情況下,這個需要讀入的第128號文件仍然大於2G,那麼整個讀入仍然可以按照第一步分批來進行讀取。
代價:(1)循環計算255個桶中的數據量累加,需要O(M)的代價,其中m<255。(2)讀入一個大概80M左右文件大小的IO代價。
第三步:繼續以內存中的某個桶內整數的次高8bit(他們的最高8bit是一樣的)進行桶排序(23-16)。過程和第一步相同,也是256個桶。
第四步:一直下去,直到最低字節(7-0bit)的桶排序結束。我相信這個時候完全可以在內存中使用一次快排就可以了。
整個過程的時間複雜度在O(n)的線性級別上(沒有任何循環嵌套)。但主要時間消耗在第一步的第二次內存-磁盤數據交換上,即10G數據分255個文件寫回磁盤上。一般而言,如果第二步過後,內存可以容納下存在中位數的某一個文件的話,直接快排就可以了(修改者注:我想,繼續桶排序但不寫回磁盤,效率會更高?).
9、基數排序
void radixsort(unsigned long *a,int num)
{
vector<int> radix[10];//裝每次排序後的序列
int allzero = 0;
unsigned long temp[1000];
int step = 1;
while(!allzero)
{
allzero = 1;
for(int i = 0; i < num; i++)
{
int digit = a[i] % (int)pow(10,step);
digit /= (int)pow(10,step - 1);
radix[digit].push_back(i);
if(digit > 0)
allzero = 0;
}
int k = 0;
for(int i = 0; i < 10; i++)//按照序列順序輸出即可
{
for(int j = 0; j < radix[i].size(); j++)
temp[k++] = a[(radix[i])[j]];
radix[i].clear();
}
for(int i = 0; i < num; i++)
a[i] = temp[i];
step++;
}
}