1)分類:
1)插入排序(直接插入排序、希爾排序)
2)交換排序(冒泡排序、快速排序)
3)選擇排序(直接選擇排序、堆排序)
4)歸併排序
5)分配排序(箱排序、基數排序)
所需輔助空間最多:歸併排序
所需輔助空間最少:堆排序
平均速度最快:快速排序
不穩定:快速排序,希爾排序,堆排序。
2)選擇排序算法的時候
1.數據的規模 ; 2.數據的類型 ; 3.數據已有的順序
一般來說,當數據規模較小時,應選擇直接插入排序或冒泡排序。任何排序算法在數據量小時基本體現不出來差距。
考慮數據的類型,比如如果全部是正整數,那麼考慮使用桶排序爲最優。
考慮數據已有順序,快速排序是一種不穩定的排序(當然可以改進),對於大部分排好的數據,快速排序會浪費大量不必要的步驟。數據量極小,而起已經基本排好序,冒泡是最佳選擇。我們說快速排序好,是指大量隨機數據下,快排效果最理想。而不是所有情況。
3)總結:
排序方法 平均時間 最壞時間 輔助存儲 簡單排序 O(n^2) O(n^2) O(1) 快速排序 O(n log n) O(n^2) O(log n) 堆排序 O(n log n) O(n log n) O(1) 歸併排序 O(n log n) O(n log n) O(n) 基數排序 O(d(n+rd)) O(d(n+rd)) O(rd)
——按平均的時間性能來分:
1)時間複雜度爲O(nlogn)的方法有:快速排序、堆排序和歸併排序,其中以快速排序爲最好;
2)時間複雜度爲O(n2)的有:直接插入排序、起泡排序和簡單選擇排序,其中以直接插入爲最好,特別是對那些對關鍵字近似有序的記錄序列尤爲如此;
3)時間複雜度爲O(n)的排序方法只有,基數排序。
當待排記錄序列按關鍵字順序有序時,直接插入排序和起泡排序能達到O(n)的時間複雜度;而對於快速排序而言,這是最不好的情況,此時的時間性能蛻化爲O(n2),因此是應該儘量避免的情況。簡單選擇排序、堆排序和歸併排序的時間性能不隨記錄序列中關鍵字的分佈而改變。
——按平均的空間性能來分(指的是排序過程中所需的輔助空間大小):
1) 所有的簡單排序方法(包括:直接插入、起泡和簡單選擇)和堆排序的空間複雜度爲O(1);
2) 快速排序爲O(logn ),爲棧所需的輔助空間;
3) 歸併排序所需輔助空間最多,其空間複雜度爲O(n );
4)鏈式基數排序需附設隊列首尾指針,則空間複雜度爲O(rd )。
——排序方法的穩定性能:
1) 穩定的排序方法指的是,對於兩個關鍵字相等的記錄,它們在序列中的相對位置,在排序之前和 經過排序之後,沒有改變。
2) 當對多關鍵字的記錄序列進行LSD方法排序時,必須採用穩定的排序方法。
3) 對於不穩定的排序方法,只要能舉出一個實例說明即可。
4) 快速排序,希爾排序和堆排序是不穩定的排序方法。
4)各排序算法分析
(下面的算法除了主程序外,其他方法可以用final 修飾,在java編譯時期,可以直接插入到主程序中,提高效率,下面的方法沒有聲明,只是在這裏說明下)
共用方法:
/**
* 交換元素
* @param <AnyType>
* @param a
* @param fromIndex
* @param toIndex
*/
private static<AnyType extends Comparable<? super AnyType>>
void swapReferences(AnyType[]a,int fromIndex,int toIndex){
AnyType tmp=a[fromIndex];
a[fromIndex]=a[toIndex];
a[toIndex]=tmp;
}
1、插入排序
該算法在數據規模小的時候十分高效,該算法每次插入第K+1到前K個有序數組中一個合適位置,K從0開始到N-1,從而完成排序。
/**
* 插入排序
* @param <AnyType>
* @param a
*/
public static<AnyType extends Comparable<? super AnyType>>
void insertionSort(AnyType[] a){
int j;
for(int p=1;p<a.length;p++){
//保存需插入的值
AnyType tmp=a[p];
//將p位置前的元素(已排序)向左移動,直到它在前p+1個元素前被找到
for(j=p;j>0&&tmp.compareTo(a[j-1])<0;j--){
a[j]=a[j-1];
}
a[j]=tmp;
}
}
2、希爾排序
Shell排序可以理解爲插入排序的變種,它充分利用了插入排序的兩個特點:
1)當數據規模小的時候非常高效
2)當給定數據已經有序時的時間代價爲O(N)
所以,Shell排序每次把數據分成若個小塊,來使用插入排序,而且之後在這若個小塊排好序的情況下把它們合成大一點的小塊,繼續使用插入排序,不停的合併小塊,知道最後成一個塊,並使用插入排序。
這裏每次分成若干小塊是通過“增量” 來控制的,開始時增量交大,接近N/2,從而使得分割出來接近N/2個小塊,逐漸的減小“增量“最終到減小到1。
一直較好的增量序列是2^k-1,2^(k-1)-1,.....7,3,1,這樣可使Shell排序時間複雜度達到O(N^1.5),所以我在實現Shell排序的時候採用該增量序列。
/**
* 希爾排序(也叫縮減增量排序)
* 選取增量序列有關
* 它通過比較相距一定間隔的元素(用插入排序進行排序)來工作,各趟比較所用的距離隨算法的進行而減小,
* 直到比較間隔爲1的元素爲止。
* 其實希爾排序是多次的插入排序,只是在移動位置的次數上的區別。
* @param <AnyType>
* @param a
*/
public static<AnyType extends Comparable<? super AnyType>>
void shellSort(AnyType[] a){
int j;
//gap 爲增量,這裏只是選取數組長度的一半,在應用中可適當調整
for(int gap=a.length/2;gap>0;gap/=2){
for(int i=gap;i<a.length;i++){
AnyType tmp=a[i];
//和插入排序的不同,進入這個循環的次數會減少。因爲希爾排序是進行多次插入排序後的部分相對有序
for(j=i;j>=gap&&tmp.compareTo(a[j-gap])<0;j-=gap){
a[j]=a[j-gap];
}
a[j]=tmp;
}
}
}
3、歸併排序
算法思想是每次把待排序列分成兩部分,分別對這兩部分遞歸地用歸併排序,完成後把這兩個子部分合併成一個序列。
歸併排序藉助一個全局性臨時數組來方便對子序列的歸併,該算法核心在於歸併。
/**
* 歸併排序
* 算法的最壞時間是:O(N log N) 佔用空間:排序數組容量的兩倍
* 思想:採用分治法,遞歸的把數組從中間分成兩個數組,最後爲兩個長度爲1的數組,然後進行合併。
* 歸併排序的運行時間嚴重依賴於比較元素和在數組(以及臨時數組)中移動元素的相對開銷,這些開銷是和語言有關的。
* 在java 中,當執行一次泛型排序(使用Comparator時),進行一次元素比較可能是昂貴的(因爲比較可能
* 不容易內嵌,從而動態調度的開銷可能會減慢執行的速度),但是移動元素則是省時的(因爲他們是引用的賦值,
* 而不是龐大對象的拷貝(C++ 是對象的拷貝)),歸併排序使用所有流行的排序算法中最少的比較次數,
* 因此是使用java 的通用排序算法中的上好的選擇。事實上,它就是標準java 類庫中泛型排序所使用的算法。
* @param <AnyType>
* @param a
*/
public static<AnyType extends Comparable<? super AnyType>>
void mergeSort(AnyType[] a){
//合併過程的臨時數組(減少了排序消耗的空間)
AnyType[] tmpArray=(AnyType[])new Comparable[a.length];
//正式的歸併排序開始
mergeSort(a,tmpArray,0,a.length-1);
}
/* ------------------ 歸併排序私有方法----------------------*/
/**
* 正式的歸併排序方法
*/
private static<AnyType extends Comparable<? super AnyType>>
void mergeSort(AnyType[] a,AnyType[] tmpArray,int left,int right){
//遞歸的開始
if(left<right){
int center=(left+right)/2;
mergeSort(a,tmpArray,left,center);//歸併左邊的數組
mergeSort(a,tmpArray,center+1,right);//歸併右邊的數組
merge(a,tmpArray,left,center+1,right);//合併兩個數組
}
}
/**
* 合併兩個數組
* @param <AnyType>
* @param a 排序數組
* @param tmpArray 臨時數組
* @param leftPos 排序數組的左子數組開始下標(歸併的左數組)
* @param rightPos 排序數組的右子數組開始下標(歸併的右數組)
* @param rightEnd 排序數組的右子數組結束下標
*/
private static<AnyType extends Comparable<? super AnyType>>
void merge(AnyType[] a,AnyType[] tmpArray,int leftPos,int rightPos,int rightEnd){
int leftEnd=rightPos-1;
int tmpPos=leftPos;
int numElements=rightEnd-leftPos+1;
//合併比較的開始
while(leftPos<=leftEnd&&rightPos<=rightEnd){
if(a[leftPos].compareTo(a[rightPos])<=0)
tmpArray[tmpPos++]=a[leftPos++];
else
tmpArray[tmpPos++]=a[rightPos++];
}
//複製剩餘的左子數組
while(leftPos<=leftEnd)
tmpArray[tmpPos++]=a[leftPos++];
//複製剩餘的右子數組
while(rightPos<=rightEnd)
tmpArray[tmpPos++]=a[rightPos++];
//把臨時數組的值重新複製到排序數組
for(int i=0;i<numElements;i++,rightEnd--)
a[rightEnd]=tmpArray[rightEnd];
}
4、堆排序
堆是一種完全二叉樹,一般使用數組來實現。
堆主要有兩種核心操作,
1)從指定節點向上調整(上濾)(percUp)
2)從指定節點向下調整(下濾)(percDown)
建堆,以及刪除堆定節點使用shiftDwon,而在插入節點時一般結合兩種操作一起使用。
堆排序藉助最大值堆來實現,第i次從堆頂移除最大值放到數組的倒數第i個位置,然後shiftDown到倒數第i+1個位置,一共執行N此調整,即完成排序。
顯然,堆排序也是一種選擇性的排序,每次選擇第i大的元素。
/**
* 堆排序
* 思想:第一步以線性時間建立一個堆,然後通過每次將堆中的最後元素與第一個元素交換,
* 執行N-1次deleteMax 操作,每次將堆的大小縮減1並進行下濾,
* 當算法終止時,數組則以排好的順序包含這些元素。
* @param <AnyType>
* @param a
*/
public static<AnyType extends Comparable<? super AnyType>>
void heapSort(AnyType[] a){
//創建堆
for(int i=a.length/2;i>=0;i--)
percDown(a,i,a.length);
//刪除最大的元素
for(int i=a.length-1;i>0;i--){
swapReferences(a,0,i);
percDown(a,0,i);
}
}
/* ------------------ 堆排序私有方法----------------------*/
/**
* 返回左孩子
* @param i
* @return
*/
private static int leftChild(int i){
return 2*i+1;
}
/**
* 堆的下濾操作
* @param <AnyType>
* @param a
* @param i
* @param n
*/
private static<AnyType extends Comparable<? super AnyType>>
void percDown(AnyType[] a,int i,int n){
int child;
AnyType tmp;//保留父節點值
//目的是比較它和它兩個孩子,找到三個中最大的替換成相應兩個的父節點
for(tmp=a[i];leftChild(i)<n;i=child){
child=leftChild(i);
//如果左孩子<右孩子 ,則把child指向右孩子
if(child!=n-1&&a[child].compareTo(a[child+1])<0)
child++;
//如果孩子比父節點大,則替換之
if(tmp.compareTo(a[child])<0)
a[i]=a[child];
else
break;
}
a[i]=tmp;
}
5、快速排序
快速排序是目前使用可能最廣泛的排序算法了。
一般分如下步驟:
1)如果子數組中元素的個數爲0或者1,則返回。
2)選擇一個樞紐元素(有很多選法,好的樞紐元素,能有效提高排序效率,這裏的實現裏採用三數中值分割法)。
3)使用該樞紐元素分割數組,使得比該元素小的元素在它的左邊,比它大的在右邊。並把樞紐元素放在合適的位置。
4)根據樞紐元素最後確定的位置,把數組分成三部分,左邊的,右邊的,樞紐元素自己,對左邊的,右邊的分別遞歸調用快速排序算法即可。
快速排序的核心在於分割算法,也可以說是最有技巧的部分。
三數中值分割法:選取相應數組最左邊、中間和最右邊值,選取三個中的中間值作爲樞紐元素。且在這裏用了個取巧的辦法,把最大的放在最右邊,最小的放在最左邊,中間值放在right-1的位置,以後分割的時候就可以不去管這三個值了。
分割算法:由於三數中值法已經把right和right-1的 位置排好,i ong 一個元素開始,j right-1 位置開始,分別和樞紐元素比較,i>樞紐元素 停止,j<樞紐元素 停止,然後交換兩個值。i>=j 時候結束。
/**
* 快速排序
* 關鍵在於選取好樞紐元
* @param <AnyType>
* @param a
*/
public static<AnyType extends Comparable<? super AnyType>>
void quickSort(AnyType[] a){
quickSort(a,0,a.length-1);
}
/* ------------------ 快速排序私有方法----------------------*/
/**
* 真正快速排序方法
* @param <AnyType>
* @param a
* @param left
* @param right
*/
public static<AnyType extends Comparable<? super AnyType>>
void quickSort(AnyType[] a,int left,int right){
if(left<right){
AnyType pivot=median3(a,left,right);//選取樞紐元素,好的樞紐元素能有效提高算法的效率
int i=left,j=right-1;
for(;;){
while(a[++i].compareTo(pivot)<0){}//i索引移動到大於樞紐元的位置停止
while(a[--j].compareTo(pivot)>0){}//j索引移動到小於樞紐元的位置停止
if(i<j)
swapReferences(a,i,j);//交換停止後的i、j 索引上的值
else
break;
}
if(i!=right)
swapReferences(a,i,right-1);//恢復樞紐元素的值到正確的位置
quickSort(a,left,i-1);//遞歸左數組
quickSort(a,i+1,right);//遞歸右數組
}else{
return;
}
}
/**
* 三數中值分割法,選取三個值中的中間值,並排序
* @param <AnyType>
* @param a
* @param left
* @param right
* @return
*/
public static<AnyType extends Comparable<? super AnyType>>
AnyType median3(AnyType[] a,int left,int right){
int center=(left+right)/2;
//下面是把最左邊、中間位置、最右邊的元素進行排序
if(a[center].compareTo(a[left])<0)
swapReferences(a,left,center);
if(a[right].compareTo(a[left])<0)
swapReferences(a,left,right);
if(a[right].compareTo(a[center])<0)
swapReferences(a,center,right);
//把樞紐元素放在最右邊的倒數第二個
swapReferences(a,center,right-1);
return a[right-1];
}
6、冒泡排序
這可能是最簡單的排序算法了,算法思想是每次從數組末端開始比較相鄰兩元素,把第i小的冒泡到數組的第i個位置。i從0一直到N-1從而完成排序。(當然也可以從數組開始端開始比較相鄰兩元素,把第i大的冒泡到數組的第N-i個位置。i從0一直到N-1從而完成排序。)
/**
* 冒泡排序
* @param <AnyType>
* @param a
*/
public static<AnyType extends Comparable<? super AnyType>>
void bubbelSort(AnyType[] a){
for(int i=0;i<a.length;i++){
for(int j=i;j<a.length;j++){
if(a[i].compareTo(a[j])>0){
swapReferences(a, i, j);
}
}
}
}
7、選擇排序
選擇排序相對於冒泡來說,它不是每次發現逆序都交換,而是在找到全局第i小的時候記下該元素位置,最後跟第i個元素交換,從而保證數組最終的有序。
相對與插入排序來說,選擇排序每次選出的都是全局第i小的,不會調整前i個元素了。
/**
* 選擇排序
* @param <AnyType>
* @param a
*/
public static<AnyType extends Comparable<? super AnyType>>
void selectSort(AnyType[] a){
for(int i=0;i<a.length;i++){
int smallest=i;//記錄最小的索引
for(int j=i;j<a.length;j++){
//如果比最小的索引的值還小,則記錄在smallest中
if(a[smallest].compareTo(a[j])>0)
smallest=j;
}
//替換
swapReferences(a, smallest,i);
}
}
(下面兩個算法的程序由於未能理解所以尚未給出,等過幾天補上)
8、桶式排序
桶式排序不再是基於比較的了,它和基數排序同屬於分配類的排序,這類排序的特點是事先要知道待排序列的一些特徵。
桶式排序事先要知道待排序列在一個範圍內,而且這個範圍應該不是很大的。
比如知道待排序列在[0,M)內,那麼可以分配M個桶,第I個桶記錄I的出現情況,最後根據每個桶收到的位置信息把數據輸出成有序的形式。
這裏我們用兩個臨時性數組,一個用於記錄位置信息,一個用於方便輸出數據成有序方式,另外我們假設數據落在0到MAX,如果所給數據不是從0開始,你可以把每個數減去最小的數。
下面的程序只是簡單的用了int 數組來寫,如果用其他的Comparable 子類也可以,不過麻煩了點,但原理是一樣的 。
還有下面計算重複的值的時候用了個取巧的辦法,程序也可以用鏈式桶式排序來寫,即每個桶位爲一個鏈表,當遇到重複的時候,排序桶上面的鏈表,再輸出就行了。
public static void bucketSort(int[] keys,int max){
int[] temp=new int[keys.length];//創建臨時數組
int[] count=new int[max];//創建桶
//進桶,如果有重複的,那桶裏的值爲重複的次數
for(int i=0;i<keys.length;i++){
count[keys[i]]++;
}
//計算keys 數組的索引值,keys數組索引值和前面count索引相加的值是相同的,重複的除外,但下面的方法也計算了重複的值
for(int i=1;i<max;i++){
count[i]=count[i]+count[i-1];
}
//複製數組
System.arraycopy(keys, 0, temp, 0, keys.length);
//根據上一個for 循環計算的索引值進行計算,當遇到重複值的時候,--count[temp[k]]起了作用。
for(int k=keys.length-1;k>=0;k--){
keys[--count[temp[k]]]=temp[k];
}
}
9、基數排序
基數排序可以說是擴展了的桶式排序,比如當待排序列在一個很大的範圍內,比如0到999999內,那麼用桶式排序是很浪費空間的。而基數排序把每個排序碼拆成由d個排序碼,比如任何一個6位數(不滿六位前面補0)拆成6個排序碼,分別是個位的,十位的,百位的。。。。
排序時,分6次完成,每次按第i個排序碼來排。
一般有兩種方式:
1) 高位優先(MSD): 從高位到低位依次對序列排序
2)低位優先(LSD): 從低位到高位依次對序列排序
計算機一般採用低位優先法(人類一般使用高位優先),但是採用低位優先時要確保排序算法的穩定性。
基數排序藉助桶式排序,每次按第N位排序時,採用桶式排序。對於如何安排每次落入同一個桶中的數據有兩種安排方法:
1)順序存儲:每次使用桶式排序,放入r個桶中,,相同時增加計數。
2)鏈式存儲:每個桶通過一個靜態隊列來跟蹤。