Java 排序算法總結

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)鏈式存儲:每個桶通過一個靜態隊列來跟蹤。

 

 



  

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章