天天看點

淺談常見的Java中的排序

排序是我們接觸數學以後就一直圍繞我們的問題,國小數學課上有給數字排序,在生活中,給長輩們排序,在coding的世界中,也離不開排序,而在程式人生中,我們接觸最早的排序就屬于冒泡排序了。下面我就來在coding的世界中淺聊排序。

排序從總體來說分為内部排序和外部排序。

其中内部排序一般使用計算機記憶體進行排序(記憶體:又稱記憶體儲器,它是與CPU進行溝通的橋梁。計算機中所有程式的運作都是在記憶體中進行的,是以記憶體的性能對計算機的影響非常大。記憶體(Memory)也被稱為記憶體儲器,其作用是用于暫時存放CPU中的運算資料,以及與硬碟等外部存儲器交換的資料。隻要計算機在運作中,CPU就會把需要運算的資料調到記憶體中進行運算,當運算完成後CPU再将結果傳送出來,記憶體的運作也決定了計算機的穩定運作。 記憶體是由記憶體晶片、電路闆、金手指等部分組成的)。

其中外部排序一般是記憶體和外存結合使用。外部排序指的是大檔案的排序,即待排序的記錄存儲在外存儲器上,待排序的檔案無法一次裝入記憶體,需要在記憶體和外部存儲器之間進行多次資料交換,以達到排序整個檔案的目的。

内部排序有:插入排序(直接插入排序、希爾排序);選擇排序(簡單選擇排序、堆排序);交換排序(冒泡排序、快速排序);歸并排序;基數排序。

外部排序有:外部快排,多路歸并排序等等,一般研究較多的是内部排序。是以這篇部落格會有重點性的介紹内部的排序。又簡稱内部排序大亂鬥,即窩裡鬥。

下面的兩張圖檔詳細的劃分了排序分為内、外排序,内部排序都有哪些排序,外部排序又有哪些。其中内部排序又列出了時間複雜度,最壞時間複雜度,空間複雜度。穩定性等。下面你先仔細閱讀這兩張圖檔,接下來會詳細說說内部排序大亂鬥,又稱窩裡鬥。

淺談常見的Java中的排序

圖一:排序分類圖

淺談常見的Java中的排序

圖二:排序穩定性、複雜度圖

=====================================================================

冒泡排序:

冒泡排序算法的起名貼切于算法本身的名字,即每周遊循環一次,都會把無序中的最大數放到最後,就像水中的氣泡一樣,在水底時最小,越往上升就越大,冒泡排序也類似于這樣的過程,故稱為冒泡排序。

概念:該算法就是重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。

 算法原理:

1:比較相鄰的元素,如果第一個比第二個大,就交換它們兩個。

2:對每一對相鄰元素作同樣的工作,從開始第一對到結尾最後一對。在這一點,最後的元素應該會是最大的數。

3:針對所有的元素重複以上的步驟,出了最後一個。

4:持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。

算法複雜度:

冒泡排序:時間複雜度O(n^2);最壞的時間複雜度O(n^2);空間複雜度:O(1);穩定性:穩定;

Coding:

void bubbleSort(int []a){
	for(int i = 0; i < a.length-1; i++){
		for(int j = 0; j < a.length-i-1; j++){
			if(a[j] > a[j+1]){
				int temp = a[j];
				a[j] = a[j+1];
				a[j+1] = temp;
			}
		}
	}
}
           

=====================================================================

直接插入排序:

直接插入排序我認為概括其最簡單的一句話是:第一次就插入合适的位置(忽略其位置的移動)  ,使其插入後原來有序的部分還是有序,無序的部分減少一個元素。直到所有元素都有序,直接插入排序結束。

概念:每次将一個待排序的記錄,按其關鍵字大小插入到前面已經排好序的子序列中的适當位置,直到全部記錄插入完成為止。

算法原理:

1:在初始時,n個無序序列,a[0]自己形成一個有序區,a[1,.....n-1]為無序區。令i=1;

2:将a[i]并入目前的有序區a[0,...,i-1]中形成a[0, ..., i]的有序區間。

3:i++并重複第2步直到i==n-1。排序完成。

算法複雜度:

直接插入排序:時間複雜度:O(n^2);最壞時間複雜度:O(n^2);空間複雜度:O(1);

Coding1:

void straightInsertSort(int []a){
	for(int i = 1; i < a.length; i++){
		if(a[i] < a[i-1]){
			int temp = a[i];
			int k = i-1;
			for(int j = k; j >=0 && a[j] > temp; j--){
				a[j+1] = a[j];
				k--;
			}
			a[k+1] = temp;
		}
	}
}
           

Coding2:

public void directlyArray(int []arrs){
	for(int i = 1; i < arrs.length; i++){
		int ins = arrs[i];
		int j = 0;
		for(j = i; j > 0 && arrs[j-1] > ins; j--){
			arrs[j] = arrs[j-1];
		}
		arrs[j] = ins;
	}
}
           

=====================================================================

簡單選擇排序:

簡單選擇排序我覺得關鍵字在選擇,就是每次都在無序的元素中找到其中最小的一個元素。

概念:掃描所有的元素,得到最小的元素,并将最小的元素與左邊第一個元素進行交換。再次掃描除第一位置的所有元素,得到最小的元素,與左邊第二個元素進行交換。以此類推。

算法原理:

即通過n-i次元素的比較,從n-i+1個記錄中選出最小的元素,并和第i個元素進行交換位置。

算法複雜度:

簡單選擇排序:時間複雜度:O(n^2);最壞時間複雜度:O(n^2);空間複雜度:O(1);

Coding:

//選擇排序
public void selectArray(int arrs[]){
	for(int i = 0; i < arrs.length-1; i++){
		int min = i;
		for(int j = i+1; j < arrs.length; j++){
			if(arrs[j] < arrs[min]){
				min = j;
			}
		}
		int temp = arrs[min];
		arrs[min] = arrs[i];
		arrs[i] = temp;
	}
}
           

=====================================================================

歸并排序:

歸并排序概括為一句話就是采用了經典的分治政策。分開,治(2路合并排序)

概念:歸并排序(Merging Sort)就是利用歸并的思想實作的排序方法。它的原理是假設初始序列含有n個記錄,則可以看成是n個有序的子序列,每個子序列的長度為1,然後兩兩歸并,得到n/2取上整數個長度為2或1的有序子序列;再兩兩歸并,......,如此重複,直至得到一個長度為n的有序序列為止,這種排序方法稱為2路歸并排序。

算法原理:

把原始無序數組成2路分開,再在每次子數組序列成2路分開,知道形成了每個元素成為一個子數組,然後再形成2路合并,每次合并都是有序的合并。這就是我認為的歸并排序原理。

時間複雜度:

歸并排序:時間複雜度為:O(nlogn)(最壞最好的時間複雜度都為它);空間複雜度:O(n);穩定的排序算法。

圖解:

淺談常見的Java中的排序

Coding:

/**
 * 歸并排序
 * @author Peter
 *
 */
public class Main {

	//分
	public int[] sort(int arrays[], int low, int high){
		int mid = (low + high) / 2;
		if(low < high){
			// 左邊
			sort(arrays, low, mid);
			// 右邊
			sort(arrays, mid + 1, high);
			// 左右歸并
			merge(arrays, low, mid, high);
		}
		return arrays;
	}
	//治
	public static void merge(int []arrays, int low, int mid,int high){
		int temp[] = new int[high - low + 1];
		int i = low; //左指針
		int j = mid + 1; //右指針
		int k = 0;
		//把較小的數先移到新數組
		while(i <= mid && j <= high){
			if(arrays[i] < arrays[j]){
				temp[k++] = arrays[i++];	
			}else{
				temp[k++] = arrays[j++];
			}
		}
		
		//把左邊剩餘的移入新數組
		while(i <= mid){
			temp[k++] = arrays[i++];
		}
		
		//把右邊剩餘的移入新數組
		while(j <= high){
			temp[k++] = arrays[j++];
		}
		
		//新數組中的數覆寫arrays數組
		for (int k2 = 0; k2 < temp.length; k2++) {
			arrays[k2+low] = temp[k2];
		}
	}
	
	//測試
	public static void main(String[] args) {  
		  
        int[] nums = { 12, 17, 18, 13, 11, 16, 19, 10, 15, 14 };  
        
        Main m1 = new Main();
        m1.sort(nums, 0, nums.length-1);
        System.out.println(Arrays.toString(nums));
    }  
}
           

=====================================================================

快速排序:

快速排序一句話概括為大而化小,小而化無,每次都分成兩部分,其中一部分的最大元素小于另一部的最小元素。

算法原理:

通過一趟排序将待排記錄分割成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小,則可分别對這兩部分記錄繼續進行排序,以達到整個序列有序的目的。

時間複雜度分析:

快速排序:平均時間複雜度為O(nlogn);最壞的時間複雜度為O(n^2);最優的時間複雜度為O(nlogn);

平均空間複雜度為O(logn);最壞的空間複雜度為O(n);最優的空間複雜度為O(logn)。

public void quickSort(int []nums){
	qSort(nums, 0, nums.length-1);
}
           
private void qSort(int[] nums, int low, int high) {
	int pivot;
	if(low < high){
		pivot = partition(nums, low, high); //算出樞軸值
		qSort(nums, low, pivot - 1); //對低子表遞歸排序
		qSort(nums, pivot + 1, high); //對高子表遞歸排序
	}
}
           
private int partition(int[] nums, int low, int high) {
	int pivotkey;
	pivotkey = nums[low]; //用子表的第一個記錄作為樞軸記錄
	while(low < high){ //從表的兩端交替向中間掃描
		while(low < high && nums[high] >= pivotkey){ //從數組後面向前掃描,将比pivotkey值小的交換到低端
			--high;
		}
		swap(nums, low, high);	
		while(low < high && nums[low] <= pivotkey){ //從數組前面向後掃描,将比pivotkey值大的交換到高端
			++low;
		}	
		swap(nums, low, high);
	}
	return low;
}
           
//數組交換值
private void swap(int[] nums, int low, int high) {
	int temp = nums[low]; //比pivotkey小的值交換到低端
	nums[low] = nums[high];
	nums[high] = temp;
}
           
public static void main(String[] args) { //測試
	Main m = new Main();
	int nums[] = {50, 10, 90, 30, 70, 40, 80, 60, 20};
	m.quickSort(nums);
	for(int i : nums){
		System.out.println(i);
	}
}
           

優化快速排序:

1:在partition( )中優化選取樞軸pivotkey

2:在partition( )中優化不必要的交換

注:優化不必要的交換,就必須是數組中有一個元素的位置為備援的,這裡選取第一個位置

void quickSort(int nums[]){
	qSort(nums, 1, nums.length-1);
}
           
private void qSort(int[] nums, int low, int high) {
	int pivot;
	if(low <high){
		pivot = partition(nums, low, high); //算出樞軸值
		qSort(nums, low, pivot - 1); //對低子表遞歸排序
		qSort(nums, pivot + 1, high); //對高子表遞歸排序
	}
}
           
private int partition(int[] nums, int low, int high) {
	int middle = low + (high - low) / 2; //數組中間的元素的下标
	if(nums[low] > nums[high]){ //交換左端與右端資料,保證左端較小
		swap(nums, low, high);
	}
	if(nums[middle] > nums[high]){ //交換中間和右端資料,保證中間較小
		swap(nums, middle, high);
	}
	if(nums[middle] > nums[low]){ //交換左端與中間資料,保證左端為左、中、右端的次小元素
		swap(nums, middle, low);
	}
	int pivotkey = nums[low];
	nums[0] = pivotkey;
		
	while(low < high){
		while(low < high && nums[high] >= pivotkey){
			--high;
		}
		nums[low] = nums[high];
		while(low < high && nums[low] <= pivotkey){
			++low;
		}
		nums[high] = nums[low];
	}
	nums[low] = nums[0];
	return low;
}
           
//交換數組中的兩個值
private void swap(int[] nums, int low, int high) {
	int temp = nums[low];
	nums[low] = nums[high];
	nums[high] = temp;
}
           
public static void main(String[] args) { //測試
	Main1 m1 = new Main1();
	int nums[] = {0, 10, 90, 30, 70, 40, 80, 60, 20};
	m1.quickSort(nums);
	for (int i = 1; i < nums.length; i++) {
		System.out.println(nums[i]);
	}
}
           

3:優化小數組的排序方案,在qSort方法中規定(high - low)小于某一值時用小數組排序方式(直接插入排序)。

if((high - low) > MaxLen){
	pivot = partition(nums, low, high); //算出樞軸值
	qSort(nums, low, pivot - 1); //對低子表遞歸排序
	qSort(nums, pivot + 1, high); //對高子表遞歸排序
}else{
	//直接插入排序
			
}
           

4:優化遞歸操作。對于高子表遞歸進行優化。減少耗費的空間。

if((high - low) > MaxLen){
	while(low < high){
		pivot = partition(nums, low, high); //算出樞軸值
		qSort(nums, low, pivot - 1); //對低子表遞歸排序
		low = pivot + 1; //尾遞歸
	}
}else{
	//直接插入排序
			
}
           

堆排序:時間複雜度O(nlogn);不穩定的排序算法

//堆排序
public class HeapSort {
	
	//數組交換
	public static void swap(int nums[], int i, int j){
		int tmp = nums[i];
		nums[i] = nums[j];
		nums[j] = tmp;
	}
	
	//堆排序先建立大根堆或者小根堆,再進行排序
	public static void heapSort(int []arr){
		if(arr == null || arr.length < 2){
			return;
		}
		for(int i = 0; i < arr.length; i++){
			heapInsert(arr, i);//調整為大根堆,從下往上調
		}
		int size = arr.length;
		swap(arr, 0, --size);//把root和最後一個交換
		while(size > 0){
			//重新調整為大根堆
			heapify(arr, 0, size);
			swap(arr, 0, --size);//根結點最大,是以減掉
		}
	}
	
	//重新調整為大根堆,從上往下調
	private static void heapify(int[] arr, int i, int size) {
		int left = 2 * i + 1; //取最小的左結點
		while(left < size){//保證數組不越界
			//取得最大孩子結點的下标
			int largest = left+1<size &&arr[left+1]>arr[left]?left+1:left;
			//比較最大孩子結點和父結點的值
			largest = arr[largest]>arr[i]?largest:i;
			if(largest==i){
				break;//如果最大下标是父結點,則不要交換,跳出while循環
			}
			//否則需要交換
			swap(arr, i, largest);
			i = largest;
			left = 2*i+1;
		}
		
	}

	//調整大根堆,不斷向上循環,調整大根堆
	private static void heapInsert(int[] arr, int i) {
		while(arr[i] > arr[i-1]/2){//循環結束,調整為大根堆
			swap(arr, i, (i-1)/2);
			i = (i-1)/2;
		}
	}
}