算法與數據結構之美—排序(上)


本篇博客將會總結一下冒泡、插入、選擇排序,思考一個問題就是,插入排序和冒泡排序的時間複雜度都是O(n^2),但是爲何在實際軟件開發中更傾向於選擇插入排序?

如何分析一個排序算法?

分析排序算法的執行效率,主要是從以下幾個方面:

算法的執行效率

  1. 最好情況、最壞情況、平均情況複雜度;
  2. 時間複雜度的係數、常數、低階;
  3. 比較次數和交換次數;

排序算法的內存消耗

對於排序算法的空間複雜度,引入一個原地排序(Sorted in place)的概念,原地排序算法就是空間複雜度爲O(1)的排序算法;

排序算法的穩定性

穩定性,是指待排序的序列中存在值相等的元素,經過排序之後,想等元素之間原有的先後順序不變;

冒泡排序(Bubble Sort)

冒泡排序每次只會操作相鄰的兩個數據,看是否滿足大小關係,不滿足就呼喚,一次冒泡排序就會至少讓一個元素移動到它應該在的位置上,重複n次,就完成了n個數據的排序工作。
冒泡排序
上圖爲一次冒泡排序的過程,其實冒泡排序還是可以優化的。當某次冒泡操作沒有數據可以交換時,說明已經達到完全有序,不用在執行後序的冒泡操作。
代碼如下:

public void bubbleSort(int[ ] a,int n){
	if(n<=1) return;
	for(int i=0;i<n;i++){
		//提前退出冒泡排序的標誌flag
		boolean flag = false;
		for(int j = 0;j<n-i-1;j++){
			if(a[j]>a[j+1]){
				int tmp = a[j];
				a[j] = a[j+1];
				a[j+1] = tmp;
				flag = true; //有數據進行交換
			}
		}
		if(!flag) break;  //沒有數據交換,提前退出
	}
}
  • 冒泡排序算法是原地排序算法,冒泡的過程只涉及相鄰數據的交換,空間複雜度是O(1);
  • 冒泡排序算法是穩定排序算法,當相鄰元素相等時,不做交換;
  • 冒泡排序最好的時間複雜度是O(n),平均和最壞時間複雜度都是O(n^2);

插入排序(Insertion Sort)

首先將數組中的數據分爲兩個區間,已排序區間和未排序區間。初始已排序區間只有一個元素,就是數組的第一個元素;插入排序算法的核心是從未排序區間中選取元素,在已排序區間中找到合適的插入位置將其插入,並保證已排序區間中的數據一直有序,直到未排序的區間中的元素爲空;下圖是插入排序的過程;

插入排序過程

//插入排序,a表示數組,n表示數組大小
public static void insertionSort(int[ ] a ,int n){
		if(n<=1) return;
		for(int i =1;i<n;i++){
			int value = a[i];
			int j = i-1;
			//查找要插入的位置並移動數據
			for(;j>=0;--j){
				if(a[j]>value){
 					a[j+1]=a[j];
			}else{
				break;
			}
		}
		a[j+1] = value;
	}
}
  • 插入排序是原地排序算法;
  • 插入排序是穩定的排序算法;
  • 插入排序的最好時間複雜度是O(n),每次在數組的第一個位置插入新的數據,就需要移動大量的數據,所以最壞的時間複雜度是O(n^2);

選擇排序(Selection Sort)

選擇排序算法類似插入排序,將數組分爲已排序區間和未排序區間,但是每次是從未排序區間中找到最小的元素,將其放到已排序的區間的末尾;
選擇排序

//選擇排序,a表示數組,n表示數組大小
public static void selectionSort(int[ ] a,int n){
	if(n<=1) return;
	for(int i =0;i< n-1;i++){
		//查找最小值
		int minIndex = i;
		for(int j = i+!;j<n;j++){
			if(a[j]<a[minIndex]){
				minIndex = j;
			}
		}
		//交換
		int tmp = a[i];
		a[i] = a[minIndex];
		a[minIndex] = tmp;
	}
}
  • 選擇排序是一種原地排序算法,空間複雜度是O(1);
  • 最壞和平均時間複雜度都是O(n^2);
  • 選擇排序是一種不穩定的排序算法,每次都是從未排序元素中取出最小值和前面的元素交換位置,破壞了穩定性;

解答開篇

從代碼實現上來看,冒泡排序的數據交換要比插入排序的數據移動要複雜,冒泡排序需要3個賦值操作,而插入排序只需要1個。我們來看這段操作:
冒泡排序中數據的交換操作:

if (a[j] > a[j+1]) { // 交換
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true;
}
插入排序中數據的移動操作:
if (a[j] > value) {
a[j+1] = a[j]; // 數據移動
} else {
break;
}

我們把執行一個賦值語句的時間粗略地計爲單位時間(unit_time),然後分別用冒泡排序和插入排序對同一個逆序度是K的數組進行排序。用冒泡排序,需要K次交換操作,每次需要3個賦值語句,所以交換操作總耗時就是3*K單位時間。而插入排序中數據移動操作只需要K個單位時間雖然冒泡排序和插入排序在時間複雜度上是一樣的,都是O(n2),但是如果我們希望把性能優化做到極致,那肯定首選插入排序。
排序算法小結

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