各種排序算法小結

各種排序算法小結
 
 
排序算法是一種基本並且常用的算法。由於實際工作中處理的數量巨大,所以排序算法 
對算法本身的速度要求很高。而一般我們所謂的算法的性能主要是指算法的複雜度,一般用O方法來表示。在後面我將給出詳細的說明。 
   
對於排序的算法我想先做一點簡單的介紹,也是給這篇文章理一個提綱。我將按照算法的複雜度,從簡單到難來分析算法。 
    第一部分是簡單排序算法,後面你將看到他們的共同點是算法複雜度爲O(N*N)(因爲沒有使用word,所以無法打出上標和下標)。 
    第二部分是高級排序算法,複雜度爲O(Log2(N))。這裏我們只介紹一種算法。另外還有幾種算法因爲涉及樹與堆的概念,所以這裏不於討論。 
    第三部分類似動腦筋。這裏的兩種算法並不是最好的(甚至有最慢的),但是算法本身比較奇特,值得參考(編程的角度)。同時也可以讓我們從另外的角度來認識這個問題。 
    第四部分是我送給大家的一個餐後的甜點——一個基於模板的通用快速排序。由於是模板函數,可以對任何數據類型排序。 

     現在,讓我們開始吧: 

一、簡單排序算法 
    由於程序比較簡單,所以沒有加什麼註釋。所有的程序都給出了完整的運行代碼,並在我的VC環境下運行通過。因爲沒有涉及MFC和WINDOWS的內容,所以在BORLAND C++的平臺上應該也不會有什麼問題的。在代碼的後面給出了運行過程示意,希望對理解有幫助。 

1.冒泡法: 
    這是最原始,也是衆所周知的最慢的算法了。他的名字的由來因爲它的工作看來象是冒泡: 
#include <iostream.h> 

void BubbleSort(int* pData,int Count) 

int iTemp; 
for(int i=1;i<Count;i++) 

for(int j=Count-1;j>=i;j--) 

if(pData[j]<pData[j-1]) 

iTemp = pData[j-1]; 
pData[j-1] = pData[j]; 
pData[j] = iTemp; 





void main() 

int data[] = {10,9,8,7,6,5,4}; 
BubbleSort(data,7); 
for (int i=0;i<7;i++) 
cout<<data[i]<<" "; 
cout<<"/n"; 


倒序(最糟情況) 
第一輪:10,9,8,7->10,9,7,8->10,7,9,8->7,10,9,8(交換3次) 
第二輪:7,10,9,8->7,10,8,9->7,8,10,9(交換2次) 
第一輪:7,8,10,9->7,8,9,10(交換1次) 
循環次數:6次 
交換次數:6次 

其他: 
第一輪:8,10,7,9->8,10,7,9->8,7,10,9->7,8,10,9(交換2次) 
第二輪:7,8,10,9->7,8,10,9->7,8,10,9(交換0次) 
第一輪:7,8,10,9->7,8,9,10(交換1次) 
循環次數:6次 
交換次數:3次 

    上面我們給出了程序段,現在我們分析它:這裏,影響我們算法性能的主要部分是循環和交換,顯然,次數越多,性能就越差。從上面的程序我們可以看出循環的次數是固定的,爲1+2+...+n-1。寫成公式就是1/2*(n-1)*n。現在注意,我們給出O方法的定義: 
    若存在一常量K和起點n0,使當n>=n0時,有f(n)<=K*g(n),則f(n) = O(g(n))。
現在我們來看1/2*(n-1)*n,當K=1/2,n0=1,g(n)=n*n時,1/2*(n-1)*n<=1/2*n*n=K*g(n)。所以f(n) =O(g(n))=O(n*n)。所以我們程序循環的複雜度爲O(n*n)。再看交換。從程序後面所跟的表可以看到,兩種情況的循環相同,交換不同。其實交換本身同數據源的有序程度有極大的關係,當數據處於倒序的情況時,交換次數同循環一樣(每次循環判斷都會交換),複雜度爲O(n*n)。當數據爲正序,將不會有交換。複雜度爲O(0)。亂序時處於中間狀態。正是由於這樣的原因,我們通常都是通過循環次數來對比算法。 


2.交換法: 
   交換法的程序最清晰簡單,每次用當前的元素一一的同其後的元素比較並交換。 
#include <iostream.h> 
void ExchangeSort(int* pData,int Count) 

int iTemp; 
for(int i=0;i<Count-1;i++) 

for(int j=i+1;j<Count;j++) 

if(pData[j]<pData[i]) 

iTemp = pData[i]; 
pData[i] = pData[j]; 
pData[j] = iTemp; 





void main() 

int data[] = {10,9,8,7,6,5,4}; 
ExchangeSort(data,7); 
for (int i=0;i<7;i++) 
cout<<data[i]<<" "; 
cout<<"/n"; 

倒序(最糟情況) 
第一輪:10,9,8,7->9,10,8,7->8,10,9,7->7,10,9,8(交換3次) 
第二輪:7,10,9,8->7,9,10,8->7,8,10,9(交換2次) 
第一輪:7,8,10,9->7,8,9,10(交換1次) 
循環次數:6次 
交換次數:6次 

其他: 
第一輪:8,10,7,9->8,10,7,9->7,10,8,9->7,10,8,9(交換1次) 
第二輪:7,10,8,9->7,8,10,9->7,8,10,9(交換1次) 
第一輪:7,8,10,9->7,8,9,10(交換1次) 
循環次數:6次 
交換次數:3次 

    從運行的表格來看,交換幾乎和冒泡一樣糟。事實確實如此。循環次數和冒泡一樣也是1/2*(n-1)*n,所以算法的複雜度仍然是O(n*n)。由於我們無法給出所有的情況,所以只能直接告訴大家他們在交換上面也是一樣的糟糕(在某些情況下稍好,在某些情況下稍差)。 

3.選擇法: 
    現在我們終於可以看到一點希望:選擇法,這種方法提高了一點性能(某些情況下)這種方法類似我們人爲的排序習慣:從數據中選擇最小的同第一個值交換,在從剩下的部分中 選擇最小的與第二個交換,這樣往復下去。 
#include <iostream.h> 
void SelectSort(int* pData,int Count) 

int iTemp; 
int iPos; 
for(int i=0;i<Count-1;i++) 

iTemp = pData[i]; 
iPos = i; 
for(int j=i+1;j<Count;j++) 

if(pData[j]<iTemp) 

iTemp = pData[j]; 
iPos = j; 


pData[iPos] = pData[i]; 
pData[i] = iTemp; 
}
 


void main() 

int data[] = {10,9,8,7,6,5,4}; 
SelectSort(data,7); 
for (int i=0;i<7;i++) 
cout<<data[i]<<" "; 
cout<<"/n"; 

倒序(最糟情況) 
第一輪:10,9,8,7->(iTemp=9)10,9,8,7->(iTemp=8)10,9,8,7->(iTemp=7)7,9,8,10(交換1次) 
第二輪:7,9,8,10->7,9,8,10(iTemp=8)->(iTemp=8)7,8,9,10(交換1次) 
第一輪:7,8,9,10->(iTemp=9)7,8,9,10(交換0次) 
循環次數:6次 
交換次數:2次 

其他: 
第一輪:8,10,7,9->(iTemp=8)8,10,7,9->(iTemp=7)8,10,7,9->(iTemp=7)7,10,8,9(交換1次) 
第二輪:7,10,8,9->(iTemp=8)7,10,8,9->(iTemp=8)7,8,10,9(交換1次) 
第一輪:7,8,10,9->(iTemp=9)7,8,9,10(交換1次) 
循環次數:6次 
交換次數:3次 
    遺憾的是算法需要的循環次數依然是1/2*(n-1)*n。所以算法複雜度爲O(n*n)。我們來看他的交換。由於每次外層循環只產生一次交換(只有一個最小值)。所以f(n)<=n所以我們有f(n)=O(n)。所以,在數據較亂的時候,可以減少一定的交換次數。 


4.插入法: 
    插入法較爲複雜,它的基本工作原理是抽出牌,在前面的牌中尋找相應的位置插入,然後繼續下一張 
#include <iostream.h> 
void InsertSort(int* pData,int Count) 

int iTemp; 
int iPos; 
for(int i=1;i<Count;i++) 

iTemp = pData[i]; 
iPos = i-1; 
while((iPos>=0) && (iTemp<pData[iPos])) 

pData[iPos+1] = pData[iPos]; 
iPos--; 

pData[iPos+1] = iTemp; 



void main() 

int data[] = {10,9,8,7,6,5,4}; 
InsertSort(data,7); 
for (int i=0;i<7;i++) 
cout<<data[i]<<" "; 
cout<<"/n"; 


倒序(最糟情況) 
第一輪:10,9,8,7->9,10,8,7(交換1次)(循環1次) 
第二輪:9,10,8,7->8,9,10,7(交換1次)(循環2次) 
第一輪:8,9,10,7->7,8,9,10(交換1次)(循環3次) 
循環次數:6次 
交換次數:3次 

其他: 
第一輪:8,10,7,9->8,10,7,9(交換0次)(循環1次) 
第二輪:8,10,7,9->7,8,10,9(交換1次)(循環2次) 
第一輪:7,8,10,9->7,8,9,10(交換1次)(循環1次) 
循環次數:4次 
交換次數:2次 

    上面結尾的行爲分析事實上造成了一種假象,讓我們認爲這種算法是簡單算法中最好的,其實不是,因爲其循環次數雖然並不固定,我們仍可以使用O方法。從上面的結果可以看出,循環的次數f(n)<= 1/2*n*(n-1)<=1/2*n*n。所以其複雜度仍爲O(n*n)(這裏說明一下,其實如果不是爲了展示這些簡單排序的不同,交換次數仍然可以這樣推導)。現在看交換,從外觀上看,交換次數是O(n)(推導類似選擇法),但我們每次要進行與內層循環相同次數的‘='操作。正常的一次交換我們需要三次‘=' 而這裏顯然多了一些,所以我們浪費了時間。 

    最終,我個人認爲,在簡單排序算法中,選擇法是最好的。 


二、高級排序算法: 
    高級排序算法中我們將只介紹這一種,同時也是目前我所知道(我看過的資料中)的最快的。它的工作看起來仍然象一個二叉樹。首先我們選擇一箇中間值middle程序中我們使用數組中間值,然後把比它小的放在左邊,大的放在右邊(具體的實現是從兩邊找,找到一對後交換)。然後對兩邊分別使用這個過程(最容易的方法——遞歸)。 

1.快速排序: 
#include <iostream.h> 

void run(int* pData,int left,int right) 

int i,j; 
int middle,iTemp; 
i = left; 
j = right; 
middle = pData[(left+right)/2]; //求中間值 
do{ 
while((pData[i]<middle) && (i<right))//從左掃描大於中值的數 
i++; 
while((pData[j]>middle) && (j>left))//從右掃描大於中值的數 
j--; 
if(i<=j)//找到了一對值 

//交換 
iTemp = pData[i]; 
pData[i] = pData[j]; 
pData[j] = iTemp; 
i++; 
j--; 

}while(i<=j);//如果兩邊掃描的下標交錯,就停止(完成一次) 

//當左邊部分有值(left<j),遞歸左半邊 
if(left<j) 
run(pData,left,j); 
//當右邊部分有值(right>i),遞歸右半邊 
if(right>i) 
run(pData,i,right); 


void QuickSort(int* pData,int Count) 

run(pData,0,Count-1); 


void main() 

int data[] = {10,9,8,7,6,5,4}; 
QuickSort(data,7); 
for (int i=0;i<7;i++) 
cout<<data[i]<<" "; 
cout<<"/n"; 


    這裏我沒有給出行爲的分析,因爲這個很簡單,我們直接來分析算法:首先我們考慮最理想的情況:
1.數組的大小是2的冪,這樣分下去始終可以被2整除。假設爲2的k次方,即k=log2(n)。 
2.每次我們選擇的值剛好是中間值,這樣,數組纔可以被等分。
第一層遞歸,循環n次,第二層循環2*(n/2)......所以共有n+2(n/2)+4(n/4)+...+n*(n/n) = n+n+n+...+n=k*n=log2(n)*n所以算法複雜度爲O(log2(n)*n)其他的情況只會比這種情況差,最差的情況是每次選擇到的middle都是最小值或最大值,那麼他將變成交換法(由於使用了遞歸,情況更糟)。但是你認爲這種情況發生的機率有多大?你完全不必擔心這個問題。實踐證明,大多數的情況,快速排序總是最好的。如果你擔心這個問題,你可以使用堆排序,這是一種穩定的O(log2(n)*n)算法,但是通常情況下速度要慢於快速排序(因爲要重組堆)。 

三、其他排序 
1.雙向冒泡:
 
通常的冒泡是單向的,而這裏是雙向的,也就是說還要進行反向的工作。代碼看起來複雜,仔細理一下就明白了,是一個來回震盪的方式。寫這段代碼的作者認爲這樣可以在冒泡的基礎上減少一些交換。反正我認爲這是一段有趣的代碼,值得一看。 
#include <iostream.h> 
void Bubble2Sort(int* pData,int Count) 

int iTemp; 
int left = 1; 
int right =Count -1; 
int t; 
do 

//正向的部分 
for(int i=right;i>=left;i--) 

if(pData[i]<pData[i-1]) 

iTemp = pData[i]; 
pData[i] = pData[i-1]; 
pData[i-1] = iTemp; 
t = i; 


left = t+1; 

//反向的部分 
for(i=left;i<right+1;i++) 

if(pData[i]<pData[i-1]) 

iTemp = pData[i]; 
pData[i] = pData[i-1]; 
pData[i-1] = iTemp; 
t = i; 


right = t-1; 
}while(left<=right); 


void main() 

int data[] = {10,9,8,7,6,5,4}; 
Bubble2Sort(data,7); 
for (int i=0;i<7;i++) 
cout<<data[i]<<" "; 
cout<<"/n"; 



2.SHELL排序 
    這個排序非常複雜,看了程序就知道了。 
    首先需要一個遞減的步長,這裏我們使用的是9、5、3、1(最後的步長必須是1)。工作原理是首先對相隔9-1個元素的所有內容排序,然後再使用同樣的方法對相隔5-1個元素的排序以次類推。 
#include <iostream.h> 
void ShellSort(int* pData,int Count) 

int step[4]; 
step[0] = 9; 
step[1] = 5; 
step[2] = 3; 
step[3] = 1; 

int iTemp; 
int k,s,w; 
for(int i=0;i<4;i++) 

k = step[i]; 
s = -k; 
for(int j=k;j<Count;j++) 

iTemp = pData[j]; 
w = j-k;//求上step個元素的下標 
if(s ==0) 

s = -k; 
s++; 
pData[s] = iTemp; 

while((iTemp<pData[w]) && (w>=0) && (w<=Count)) 

pData[w+k] = pData[w]; 
w = w-k; 

pData[w+k] = iTemp; 




void main() 

int data[] = {10,9,8,7,6,5,4,3,2,1,-10,-1}; 
ShellSort(data,12); 
for (int i=0;i<12;i++) 
cout<<data[i]<<" "; 
cout<<"/n"; 

    程序看起來有些頭疼。不過也不是很難,把s==0的塊去掉就輕鬆多了,這裏是避免使用0步長造成程序異常而寫的代碼。這個代碼我認爲很值得一看。這個算法的得名是因爲其發明者的名字D.L.SHELL。依照參考資料上的說法:“由於複雜的數學原因避免使用2的冪次步長,它能降低算法效率。”另外算法的複雜度爲n的1.2次冪。同樣因爲非常複雜並“超出本書討論範圍”的原因(我也不知道過程),我們只有結果了。 


四、基於模板的通用排序: 
    這個程序我想就沒有分析的必要了,大家看一下就可以了。不明白可以在論壇上問。 
MyData.h文件 
/////////////////////////////////////////////////////// 
class CMyData 

public: 
CMyData(int Index,char* strData); 
CMyData(); 
virtual ~CMyData(); 

int m_iIndex; 
int GetDataSize(){ return m_iDataSize; }; 
const char* GetData(){ return m_strDatamember; }; 
//這裏重載了操作符: 
CMyData& operator =(CMyData &SrcData); 
bool operator <(CMyData& data ); 
bool operator >(CMyData& data ); 

private: 
char* m_strDatamember; 
int m_iDataSize; 
}; 
//////////////////////////////////////////////////////// 

MyData.cpp文件 
//////////////////////////////////////////////////////// 
CMyData::CMyData(): 
m_iIndex(0), 
m_iDataSize(0), 
m_strDatamember(NULL) 



CMyData::~CMyData() 

if(m_strDatamember != NULL) 
delete[] m_strDatamember; 
m_strDatamember = NULL; 


CMyData::CMyData(int Index,char* strData): 
m_iIndex(Index), 
m_iDataSize(0), 
m_strDatamember(NULL) 

m_iDataSize = strlen(strData); 
m_strDatamember = new char[m_iDataSize+1]; 
strcpy(m_strDatamember,strData); 


CMyData& CMyData::operator =(CMyData &SrcData) 

m_iIndex = SrcData.m_iIndex; 
m_iDataSize = SrcData.GetDataSize(); 
m_strDatamember = new char[m_iDataSize+1]; 
strcpy(m_strDatamember,SrcData.GetData()); 
return *this; 


bool CMyData::operator <(CMyData& data ) 

return m_iIndex<data.m_iIndex;
 


bool CMyData::operator >(CMyData& data ) 

return m_iIndex>data.m_iIndex; 

/////////////////////////////////////////////////////////// 

////////////////////////////////////////////////////////// 
//主程序部分 
#include <iostream.h> 
#include "MyData.h" 

template <class T> 
void run(T* pData,int left,int right) 

int i,j; 
T middle,iTemp; 
i = left; 
j = right; 
//下面的比較都調用我們重載的操作符函數 
middle = pData[(left+right)/2]; //求中間值 
do{ 
while((pData[i]<middle) && (i<right))//從左掃描大於中值的數 
i++; 
while((pData[j]>middle) && (j>left))//從右掃描大於中值的數 
j--; 
if(i<=j)//找到了一對值 

//交換 
iTemp = pData[i]; 
pData[i] = pData[j]; 
pData[j] = iTemp; 
i++; 
j--; 

}while(i<=j);//如果兩邊掃描的下標交錯,就停止(完成一次) 

//當左邊部分有值(left<j),遞歸左半邊 
if(left<j) 
run(pData,left,j); 
//當右邊部分有值(right>i),遞歸右半邊 
if(right>i) 
run(pData,i,right); 


template <class T> 
void QuickSort(T* pData,int Count) 

run(pData,0,Count-1); 


void main() 

CMyData data[] = { 
CMyData(8,"xulion"), 
CMyData(7,"sanzoo"), 
CMyData(6,"wangjun"), 
CMyData(5,"VCKBASE"), 
CMyData(4,"jacky2000"), 
CMyData(3,"cwally"), 
CMyData(2,"VCUSER"), 
CMyData(1,"isdong") 
}; 
QuickSort(data,8); 
for (int i=0;i<8;i++) 
cout<<data[i].m_iIndex<<" "<<data[i].GetData()<<"/n"; 
cout<<"/n";
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章