各種排序算法的穩(wěn)定性和時間復(fù)雜度小結(jié)
冒泡法:
這是最原始,也是眾所周知的最慢的算法了。他的名字的由來因?yàn)樗墓ぷ骺磥硐笫敲芭荩?nbsp; 復(fù)雜度為O(n*n)。當(dāng)數(shù)據(jù)為正序,將不會有交換。復(fù)雜度為O(0)。
直接插入排序:O(n*n)
選擇排序:O(n*n)
快速排序:平均時間復(fù)雜度log2(n)*n,所有內(nèi)部排序方法中最高好的,大多數(shù)情況下總是最好的。
歸并排序:log2(n)*n
堆排序:log2(n)*n
希爾排序:算法的復(fù)雜度為n的1.2次冪
這里我沒有給出行為的分析,因?yàn)檫@個很簡單,我們直接來分析算法:
首先我們考慮最理想的情況
1.數(shù)組的大小是2的冪,這樣分下去始終可以被2整除。假設(shè)為2的k次方,即k=log2(n)。
2.每次我們選擇的值剛好是中間值,這樣,數(shù)組才可以被等分。
第一層遞歸,循環(huán)n次,第二層循環(huán)2*(n/2)......
所以共有n+2(n/2)+4(n/4)+...+n*(n/n) = n+n+n+...+n=k*n=log2(n)*n
所以算法復(fù)雜度為O(log2(n)*n)
其他的情況只會比這種情況差,最差的情況是每次選擇到的middle都是最小值或最大值,那么他將變成交換法(由于使用了遞歸,情況更糟)。但是你認(rèn)為這種情況發(fā)生的幾率有多大??呵呵,你完全不必?fù)?dān)心這個問題。實(shí)踐證明,大多數(shù)的情況,快速排序總是最好的。
如果你擔(dān)心這個問題,你可以使用堆排序,這是一種穩(wěn)定的O(log2(n)*n)算法,但是通常情況下速度要慢 于快速排序(因?yàn)橐亟M堆)。
這幾天筆試了好幾次了,連續(xù)碰到一個關(guān)于常見排序算法穩(wěn)定性判別的問題,往往還是多選,對于我以及和我一樣拿不準(zhǔn)的同學(xué)可不是一個能輕易下結(jié)論的題目,當(dāng)然如果你筆試之前已經(jīng)記住了數(shù)據(jù)結(jié)構(gòu)書上哪些是穩(wěn)定的,哪些不是穩(wěn)定的,做起來應(yīng)該可以輕松搞定。
本文是針對老是記不住這個或者想真正明白到底為什么是穩(wěn)定或者不穩(wěn)定的人準(zhǔn)備的。
首先,排序算法的穩(wěn)定性大家應(yīng)該都知道,通俗地講就是能保證排序前2個相等的數(shù)其在序列的前后位置順序和排序后它們兩個的前后位置順序相同。在簡單形式化一下,如果Ai = Aj, Ai原來在位置前,排序后Ai還是要在Aj位置前。
其次,說一下穩(wěn)定性的好處。排序算法如果是穩(wěn)定的,那么從一個鍵上排序,然后再從另一個鍵上排序,第一個鍵排序的結(jié)果可以為第二個鍵排序所用。基數(shù)排序就是這樣,先按低位排序,逐次按高位排序,低位相同的元素其順序再高位也相同時是不會改變的。另外,如果排序算法穩(wěn)定,對基于比較的排序算法而言,元素交換的次數(shù)可能會少一些(個人感覺,沒有證實(shí))。
回到主題,現(xiàn)在分析一下常見的排序算法的穩(wěn)定性,每個都給出簡單的理由。
(1)冒泡排序
冒泡排序就是把小的元素往前調(diào)或者把大的元素往后調(diào)。比較是相鄰的兩個元素比較,交換也發(fā)生在這兩個元素之間。所以,如果兩個元素相等,我想你是不會再無聊地把他們倆交換一下的;如果兩個相等的元素沒有相鄰,那么即使通過前面的兩兩交換把兩個相鄰起來,這時候也不會交換,所以相同元素的前后順序并沒有改變,所以冒泡排序是一種穩(wěn)定排序算法。
(2)選擇排序
選擇排序是給每個位置選擇當(dāng)前元素最小的,比如給第一個位置選擇最小的,在剩余元素里面給第二個元素選擇第二小的,依次類推,直到第n-1個元素,第n個元素不用選擇了,因?yàn)橹皇O滤粋€最大的元素了。那么,在一趟選擇,如果當(dāng)前元素比一個元素小,而該小的元素又出現(xiàn)在一個和當(dāng)前元素相等的元素后面,那么交換后穩(wěn)定性就被破壞了。比較拗口,舉個例子,序列5 8 5 2 9, 我們知道第一遍選擇第1個元素5會和2交換,那么原序列中2個5的相對前后順序就被破壞了,所以選擇排序不是一個穩(wěn)定的排序算法。
(3)插入排序
插入排序是在一個已經(jīng)有序的小序列的基礎(chǔ)上,一次插入一個元素。當(dāng)然,剛開始這個有序的小序列只有1個元素,就是第一個元素。比較是從有序序列的末尾開始,也就是想要插入的元素和已經(jīng)有序的最大者開始比起,如果比它大則直接插入在其后面,否則一直往前找直到找到它該插入的位置。如果碰見一個和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后順序沒有改變,從原無序序列出去的順序就是排好序后的順序,所以插入排序是穩(wěn)定的。
(4)快速排序
快速排序有兩個方向,左邊的i下標(biāo)一直往右走,當(dāng)a[i] <= a[center_index],其中center_index是中樞元素的數(shù)組下標(biāo),一般取為數(shù)組第0個元素。而右邊的j下標(biāo)一直往左走,當(dāng)a[j] > a[center_index]。如果i和j都走不動了,i <= j, 交換a[i]和a[j],重復(fù)上面的過程,直到i>j。 交換a[j]和a[center_index],完成一趟快速排序。在中樞元素和a[j]交換的時候,很有可能把前面的元素的穩(wěn)定性打亂,比如序列為 5 3 3 4 3 8 9 10 11, 現(xiàn)在中樞元素5和3(第5個元素,下標(biāo)從1開始計(jì))交換就會把元素3的穩(wěn)定性打亂,所以快速排序是一個不穩(wěn)定的排序算法,不穩(wěn)定發(fā)生在中樞元素和a[j]交換的時刻。
(5)歸并排序
歸并排序是把序列遞歸地分成短序列,遞歸出口是短序列只有1個元素(認(rèn)為直接有序)或者2個序列(1次比較和交換),然后把各個有序的段序列合并成一個有序的長序列,不斷合并直到原序列全部排好序。可以發(fā)現(xiàn),在1個或2個元素時,1個元素不會交換,2個元素如果大小相等也沒有人故意交換,這不會破壞穩(wěn)定性。那么,在短的有序序列合并的過程中,穩(wěn)定是是否受到破壞?沒有,合并過程中我們可以保證如果兩個當(dāng)前元素相等時,我們把處在前面的序列的元素保存在結(jié)果序列的前面,這樣就保證了穩(wěn)定性。所以,歸并排序也是穩(wěn)定的排序算法。
(6)基數(shù)排序
基數(shù)排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次類推,直到最高位。有時候有些屬性是有優(yōu)先級順序的,先按低優(yōu)先級排序,再按高優(yōu)先級排序,最后的次序就是高優(yōu)先級高的在前,高優(yōu)先級相同的低優(yōu)先級高的在前。基數(shù)排序基于分別排序,分別收集,所以其是穩(wěn)定的排序算法。
(7)希爾排序(shell)
希爾排序是按照不同步長對元素進(jìn)行插入排序,當(dāng)剛開始元素很無序的時候,步長最大,所以插入排序的元素個數(shù)很少,速度很快;當(dāng)元素基本有序了,步長很小,插入排序?qū)τ谟行虻男蛄行屎芨摺K裕柵判虻臅r間復(fù)雜度會比o(n^2)好一些。由于多次插入排序,我們知道一次插入排序是穩(wěn)定的,不會改變相同元素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最后其穩(wěn)定性就會被打亂,所以shell排序是不穩(wěn)定的。
(8)堆排序
我們知道堆的結(jié)構(gòu)是節(jié)點(diǎn)i的孩子為2*i和2*i+1節(jié)點(diǎn),大頂堆要求父節(jié)點(diǎn)大于等于其2個子節(jié)點(diǎn),小頂堆要求父節(jié)點(diǎn)小于等于其2個子節(jié)點(diǎn)。在一個長為n的序列,堆排序的過程是從第n/2開始和其子節(jié)點(diǎn)共3個值選擇最大(大頂堆)或者最小(小頂堆),這3個元素之間的選擇當(dāng)然不會破壞穩(wěn)定性。但當(dāng)為n/2-1, n/2-2, ...1這些個父節(jié)點(diǎn)選擇元素時,就會破壞穩(wěn)定性。有可能第n/2個父節(jié)點(diǎn)交換把后面一個元素交換過去了,而第n/2-1個父節(jié)點(diǎn)把后面一個相同的元素沒有交換,那么這2個相同的元素之間的穩(wěn)定性就被破壞了。所以,堆排序不是穩(wěn)定的排序算法
1 快速排序(QuickSort)
快速排序是一個就地排序,分而治之,大規(guī)模遞歸的算法。從本質(zhì)上來說,它是歸并排序的就地版本。快速排序可以由下面四步組成。
(1) 如果不多于1個數(shù)據(jù),直接返回。
(2) 一般選擇序列最左邊的值作為支點(diǎn)數(shù)據(jù)。
(3) 將序列分成2部分,一部分都大于支點(diǎn)數(shù)據(jù),另外一部分都小于支點(diǎn)數(shù)據(jù)。
(4) 對兩邊利用遞歸排序數(shù)列。
快速排序比大部分排序算法都要快。盡管我們可以在某些特殊的情況下寫出比快速排序快的算法,但是就通常情況而言,沒有比它更快的了。快速排序是遞歸的,對于內(nèi)存非常有限的機(jī)器來說,它不是一個好的選擇。
2 歸并排序(MergeSort)
歸并排序先分解要排序的序列,從1分成2,2分成4,依次分解,當(dāng)分解到只有1個一組的時候,就可以排序這些分組,然后依次合并回原來的序列中,這樣就可以排序所有數(shù)據(jù)。合并排序比堆排序稍微快一點(diǎn),但是需要比堆排序多一倍的內(nèi)存空間,因?yàn)樗枰粋€額外的數(shù)組。
3 堆排序(HeapSort)
堆排序適合于數(shù)據(jù)量非常大的場合(百萬數(shù)據(jù))。
堆排序不需要大量的遞歸或者多維的暫存數(shù)組。這對于數(shù)據(jù)量非常巨大的序列是合適的。比如超過數(shù)百萬條記錄,因?yàn)榭焖倥判颍瑲w并排序都使用遞歸來設(shè)計(jì)算法,在數(shù)據(jù)量非常大的時候,可能會發(fā)生堆棧溢出錯誤。
堆排序會將所有的數(shù)據(jù)建成一個堆,最大的數(shù)據(jù)在堆頂,然后將堆頂數(shù)據(jù)和序列的最后一個數(shù)據(jù)交換。接下來再次重建堆,交換數(shù)據(jù),依次下去,就可以排序所有的數(shù)據(jù)。
4 Shell排序(ShellSort)
Shell排序通過將數(shù)據(jù)分成不同的組,先對每一組進(jìn)行排序,然后再對所有的元素進(jìn)行一次插入排序,以減少數(shù)據(jù)交換和移動的次數(shù)。平均效率是O(nlogn)。其中分組的合理性會對算法產(chǎn)生重要的影響。現(xiàn)在多用D.E.Knuth的分組方法。
Shell排序比冒泡排序快5倍,比插入排序大致快2倍。Shell排序比起QuickSort,MergeSort,HeapSort慢很多。但是它相對比較簡單,它適合于數(shù)據(jù)量在5000以下并且速度并不是特別重要的場合。它對于數(shù)據(jù)量較小的數(shù)列重復(fù)排序是非常好的。
5 插入排序(InsertSort)
插入排序通過把序列中的值插入一個已經(jīng)排序好的序列中,直到該序列的結(jié)束。插入排序是對冒泡排序的改進(jìn)。它比冒泡排序快2倍。一般不用在數(shù)據(jù)大于1000的場合下使用插入排序,或者重復(fù)排序超過200數(shù)據(jù)項(xiàng)的序列。
6 冒泡排序(BubbleSort)
冒泡排序是最慢的排序算法。在實(shí)際運(yùn)用中它是效率最低的算法。它通過一趟又一趟地比較數(shù)組中的每一個元素,使較大的數(shù)據(jù)下沉,較小的數(shù)據(jù)上升。它是O(n^2)的算法。
7 交換排序(ExchangeSort)和選擇排序(SelectSort)
這兩種排序方法都是交換方法的排序算法,效率都是 O(n2)。在實(shí)際應(yīng)用中處于和冒泡排序基本相同的地位。它們只是排序算法發(fā)展的初級階段,在實(shí)際中使用較少。
8 基數(shù)排序(RadixSort)
基數(shù)排序和通常的排序算法并不走同樣的路線。它是一種比較新穎的算法,但是它只能用于整數(shù)的排序,如果我們要把同樣的辦法運(yùn)用到浮點(diǎn)數(shù)上,我們必須了解浮點(diǎn)數(shù)的存儲格式,并通過特殊的方式將浮點(diǎn)數(shù)映射到整數(shù)上,然后再映射回去,這是非常麻煩的事情,因此,它的使用同樣也不多。而且,最重要的是,這樣算法也需要較多的存儲空間。
9 總結(jié)
下面是一個總的表格,大致總結(jié)了我們常見的所有的排序算法的特點(diǎn)。
| 排序法 | 平均時間 | 最差情形 | 穩(wěn)定度 | 額外空間 | 備注 |
| 冒泡 | O(n2) | O(n2) | 穩(wěn)定 | O(1) | n小時較好 |
| 交換 | O(n2) | O(n2) | 不穩(wěn)定 | O(1) | n小時較好 |
| 選擇 | O(n2) | O(n2) | 不穩(wěn)定 | O(1) | n小時較好 |
| 插入 | O(n2) | O(n2) | 穩(wěn)定 | O(1) | 大部分已排序時較好 |
| 基數(shù) | O(logRB) | O(logRB) | 穩(wěn)定 | O(n) |
B是真數(shù)(0-9), R是基數(shù)(個十百) |
| Shell | O(nlogn) | O(ns) 1<s<2 | 不穩(wěn)定 | O(1) | s是所選分組 |
| 快速 | O(nlogn) | O(n2) | 不穩(wěn)定 | O(nlogn) | n大時較好 |
| 歸并 | O(nlogn) | O(nlogn) | 穩(wěn)定 | O(1) | n大時較好 |
| 堆 | O(nlogn) | O(nlogn) | 不穩(wěn)定 | O(1) | n大時較好 |
以下是一個基于模板的通用排序:
這個程序我想就沒有分析的必要了,大家看一下就可以了。不明白可以在論壇上問。
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;
//下面的比較都調(diào)用我們重載的操作符函數(shù)
middle = pData[(left+right)/2]; //求中間值
do{
while((pData[i]<middle) && (i<right))//從左掃描大于中值的數(shù)
i++;
while((pData[j]>middle) && (j>left))//從右掃描大于中值的數(shù)
j--;
if(i<=j)//找到了一對值
{
//交換
iTemp = pData[i];
pData[i] = pData[j];
pData[j] = iTemp;
i++;
j--;
}
}while(i<=j);//如果兩邊掃描的下標(biāo)交錯,就停止(完成一次)
//當(dāng)左邊部分有值(left<j),遞歸左半邊
if(left<j)
run(pData,left,j);
//當(dāng)右邊部分有值(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";
}

浙公網(wǎng)安備 33010602011771號