[排序算法] 各類經典排序算法(動態圖,Java實現)

寫在前面

寫作本文的目的,是想對各種經典排序算法做一個自己的歸納總結,與大家一起分享。

先來看一張圖,對各類排序算法有個大致的瞭解。圖是從網上借鑑的:
各類排序算法比較
穩定性是指這麼一種情況:

對於元素a和b,排序前:a等於b,並且a在b的前面。排序後:
如果a還在b的前面,該算法就是穩定的;
如果b有可能在a的前面,該算法就是不穩定的。

我個人認爲算法穩定性不具有普遍意義,與算法的具體實現有密切的關係,不好統一歸納。

下面來看各算法的具體分析吧。

基於比較的排序算法

給定一個亂序元素集合,通過元素間的各種比較策略,最終得到有序的元素序列,這樣的算法就稱之爲“基於比較的排序算法”。

選擇排序

這是最簡單的排序算法,也是最好理解的算法,就是一遍遍的從亂序元素集合中挑出最值元素,依次排好即可。

算法實際效率比較低,一般不常用,時間複雜度是O(N2)。

算法描述

  1. 遍歷N個元素集合,找出最值元素,與第一個元素交換位置
  2. 遍歷剩下N-1個元素,找出最值元素,與數組第二個元素交換位置
  3. 重複上述過程N-1次,完成排序
  4. 優化點:如果某次遍歷過後,發現最值元素的位置就是交換位置,則無需交換

動圖演示

選擇排序 動圖演示

代碼實現

public static void sort(int[] arr) {
	int len = arr.length;
	for (int i = 0; i < len - 1; i++) {
		int minIndex = i;
		for (int j = i + 1; j < len; j++) {
			if (arr[j] < arr[minIndex]) minIndex = j;
		}
		if (minIndex == i) continue;
		int temp = arr[i];
		arr[i] = arr[minIndex];
		arr[minIndex] = temp;
	}
}

冒泡排序

這種排序算法簡單,也不難理解,可以聯想體育老師根據學生身高調整隊列的場景。

算法實際效率比較低,不常用,時間複雜度是O(N2)。

算法描述

  1. 從左到右,依次比較相鄰兩元素,如果左元素比右元素大,就交換位置,然後繼續向後比較。一輪比較下來,最大的元素就會被移動到最右端
  2. 最大元素位置確定後,在前N-1個元素中重複上述步驟
  3. 經過N-1輪比較調整後,完成排序

動圖演示

冒泡排序 動圖演示

代碼實現

public static void sort(int[] arr) {
	int len = arr.length;
	for (int i = 0; i < len - 1; i++) {
		for (int j = 0; j < len - i - 1; j++) {
			if (arr[j] > arr[j + 1]) {
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
}

插入排序

也是很簡單很好理解的排序算法,可以聯想玩撲克牌的場景。對於新來的元素,要與已經排好序的元素隊列挨個進行比較,找到自己的位置後插入隊列。

最好時間複雜度O(N),最壞時間複雜度O(N2)。

算法描述

  1. 假設第一個元素是已經排好序的元素
  2. 取出下一個元素,空出一個位置,向前依次與有序元素進行比較,如果有序元素大於當前元素,則有序元素後移一位,直到有序元素不大於當前元素時,將當前元素插入空位
  3. 重複上述過程,完成排序

動圖演示

插入排序 動圖演示

代碼實現

public static void sort(int[] arr) {
	int len = arr.length;
	for (int i = 1; i < len; i++) {
		int temp = arr[i];
		int j = i - 1;
		for (; j >= 0 && arr[j] > temp; j--) {
			arr[j + 1] = arr[j];
		}
		arr[j + 1] = temp;
	}
}

希爾排序

希爾排序是對插入排序的一種改進優化,是第一個突破O(n2)的排序算法。

希爾排序的解題思想是把序列按一定的間隔分組,對每組使用插入排序,然後不斷的減小間隔,直到間隔值等於0的時候,整個序列就是有序的。

語言說不太清楚,看圖更直觀,結合動態圖去理解吧。

算法描述

  1. 初始化gap間隔值,通常取集合長度length的一半,也就是gap=length/2,gap值的含義既是分組數量,也是每組元素的間隔長度
  2. 對所有分組進行插入排序
  3. gap=gap/2,得到新的分組,再對新分組進行插入排序,重複該過程,直到gap等於0
  4. gap等於0時,排序也就完成了

動圖演示

希爾排序 動圖演示

代碼實現

public static void sort(int[] arr) {
	for (int gap = arr.length / 2; gap > 0; gap /= 2) { // gap 步長
		// 代碼實現是多個分組交替執行,動態圖是按組依次執行,這就是 i++ 的原因
		for (int i = gap; i < arr.length; i++) {
			if (arr[i] >= arr[i - gap]) continue;// 本次沒有調整的必要
			// 當前元素排序不正確,對該分組進行插入排序
			int temp = arr[i];
			int j = i - gap;
			while (j >= 0 && arr[j] > temp) {
				arr[j + gap] = arr[j];
				j -= gap;
			}
			arr[j + gap] = temp;
		}
	}
}

歸併排序 (非遞歸實現)

歸併排序的思路是分治思想,將大問題分解了多個相似的小問題,通過求解多個小問題,最終合併出大問題的解。

歸併排序的時間複雜度是O(nlogn)。

算法描述

  1. 將N個元素分散爲N個區間,每個區間只有1個元素,所以每個區間的元素是有序的,這裏稱之爲N個“1元素區間”
  2. 對兩個相鄰的“1元素區間”進行合併排序,得到N/2個有序的“2元素區間”
  3. 對兩個相鄰的“2元素區間”進行合併排序,得到N/4個有序的“4元素區間”
  4. 繼續合併相鄰區間,直到N個元素處於同一區間,完成排序

動圖演示

歸併排序 動圖演示

代碼實現

網上很多人的代碼都是採用遞歸實現,數據量大的時候,遞歸就會有棧溢出的問題。

我這裏不寫遞歸了,採用非遞歸思路來實現歸併排序算法:

public static void sort(int[] arr) {
	int len = arr.length, space = 1;
	while (space < len) {
		space *= 2; // 2,4,8,16 ... 每次歸併區間的數組長度
		for (int low = 0; low < len; low += space) {
			int high = low + space - 1, mid = (low + high) / 2;
			if (mid + 1 >= len) continue;
			merge(arr, low, mid, high < len ? high : len - 1);
		}
	}
}

public static void merge(int arr[], int low, int mid, int high) {
	// 數組 arr[low..mid], arr[mid+1..high] 都是排好序的
	int len = high - low + 1;
	int[] temp = new int[len];// 用了一個額外數組空間,可優化掉
	int left = low, right = mid + 1, index = 0;
	while (left <= mid && right <= high) // 歸併
		temp[index++] = (arr[left] <= arr[right]) ? arr[left++] : arr[right++];
	while (left <= mid) temp[index++] = arr[left++];
	while (right <= high) temp[index++] = arr[right++];
	for (int k = 0; k < len; k++) arr[low + k] = temp[k];
}

快速排序 (遞歸實現)

快速排序算法也是分治思想的一種實踐,將大問題化解爲小問題求解。它的思路是將亂序元素集合一分爲二,兩個子集合整體有序,再將子集合一份爲二,保證更小的子集合也是整體有序的,就這麼一直分下去,直到所有的子集合的元素只有一個的時候,整體就有序了。一分爲二的代碼實現就可以藉助遞歸思路了。

快速排序、歸併排序,這兩種算法都是分治思想典型的實踐。但是它們的實現思路卻是截然相反的:快速排序是先對整體排序再對局部排序,從大到小的解決問題,歸併排序是先對局部排序再對整體排序,從小到大的解決問題。

快速排序是個很常用的算法,在實際應用中,爲了達到更好的效果,在各種細節上的會有進一步優化。

這裏只講快速排序的算法理論模型。

算法描述

  1. 挑選一個元素,作爲劃分兩個子區間的基準元素,通常取第一個元素
  2. 將基準元素作爲左區間唯一的元素,也當作是左區間最大的元素,剩下的元素先暫時歸入右區間
  3. 遍歷右區間元素,與基準元素比較,將小於基準值的元素交換到右區間的左側,同時在概念上將它們歸入左區間
  4. 右區間遍歷完成以後,將基準元素與左區間最右端元素交換位置,也就是左區間兩端元素互換
  5. 此時,基準元素就位於左區間最右側,所以,基準元素左側都是比它小的元素,右側都是比它大的元素
  6. 以基準元素作爲分界線,對其左右兩個區間的元素分別重複上述過程,直到每個區間只含有一個元素的時候,排序完成

動圖演示

快速排序 動圖演示

代碼實現

public static void sort(int[] arr) {
	quickSort(arr, 0, arr.length - 1);
}

public static void quickSort(int[] arr, int left, int right) {
	if (left >= right) {
		return;
	}
	int key = arr[left], empty = left;
	int[] index = new int[]{left, right};
	while (left < right) {
		if (empty == left) {
			if (arr[right] < key) {
				arr[left++] = arr[right];
				empty = right;
			} else right--;
		} else {
			if (arr[left] > key) {
				arr[right--] = arr[left];
				empty = left;
			} else left++;
		}
	}
	arr[empty] = key;
	quickSort(arr, index[0], empty - 1);
	quickSort(arr, empty + 1, index[1]);
}

不基於比較的排序算法

從這裏開始,接下來的排序思想就不再是基於元素的比較了,而是藉助於數學上的關係映射思想。

實現這些算法前,需要找到某種映射關係,可以將亂序元素映射到另一種狀態,並且,另一種狀態當中隱藏着有序的關係,藉助這種有序的關係,可以反推出亂序元素的排序。

計數排序

計數排序算法,是將亂序元素映射成鍵值關係,鍵是元素本身,值是元素出現的次數。把鍵當成數組下標,它就是有序的,也就是說,亂序元素可以存儲在下標有序的數組裏面,通過數組下標的有序性可以推導出亂序元素的有序性。

當亂序元素取值範圍比較集中,且左右差值不大的時候,該算法纔會有比較良好的表現。

算法描述

  1. 遍歷亂序元素,找到最小值元素和最大值元素,確定亂序元素取值區間
  2. 根據取值區間,構建出合適的數組存儲結構
  3. 將亂序元素映射成數組下標,存入數組空間
  4. 根據排序規則,順序訪問數組並取出元素,完成排序

動圖演示

計數排序 動圖演示

代碼實現

public static void sort(int[] arr) {
	int len = arr.length, min = arr[0], max = arr[0];
	// 確定取值範圍
	for (int i = 1; i < len; i++) {
		if (arr[i] < min) min = arr[i];
		if (arr[i] > max) max = arr[i];
	}
	// 用數組構建哈希存儲結構,數組下標 index + min 就是取值範圍,數組值就是排序元素出現的次數
	int[] temp = new int[max - min + 1];
	for (int i = 0; i < len; i++) temp[arr[i] - min]++;
	int index = 0;
	for (int i = 0; i < temp.length; i++) {
		while (temp[i]-- > 0) arr[index++] = min + i;
	}
}

桶排序

很形象的算法名稱,就是準備多個裝元素的桶,並且,桶要有順序性,然後根據某種映射關係,將亂序元素裝入桶中,此時,每個桶中的元素是亂序的,但是桶區間整體是有序的,最後,對每個桶中的元素再進行排序,從而達到整體的有序性。

對單個桶的元素再排序,可以選擇繼續用桶排序算法,但是通常會選擇其它更簡單的算法排序,因爲映射關係通常是不好找的,並且也沒有一種放眼四海而皆準的映射關係。

仔細想想,其實桶排序和快速排序很像,都是將大區間分爲小區間,在小區間裏對元素進行排序,都有點分治思想。但是它們有本質區別:快速排序是一種原地排序算法,亂序元素之間存在相互作用關係(元素比較),桶排序不是原地排序算法,亂序元素在映射的過程中也不存在相互作用關係。

好的映射關係能夠有效提高算法效率。

算法描述

桶是一種解決問題的思路,桶排序算法沒有一個固定的編程範式,爲了理解,這裏假設一種排序場景:對3位數以內的非負整數集合進行排序,映射關係是相同位數的數字映射到一個桶內。

  1. 準備三個桶,分別接收1位數字、2位數字、3位數字
  2. 根據映射關係,將集合元素映射到三個桶中
  3. 對每個桶內的元素子集合進行排序
  4. 按順序從桶中取出元素,完成排序

動圖演示

桶排序 動圖演示

代碼實現

代碼不具備通用性,只針對上述假設的排序場景:

public static void sort(int[] arr) {
	// 1位數桶,2位數桶,3位數桶
	List<Integer>[] buckets = new ArrayList[3];
	// 初始化桶
	for (int i = 0; i < buckets.length; i++) buckets[i] = new ArrayList<>();
	// 元素根據映射關係入桶
	for (int i = 0; i < arr.length; i++) {
		int temp = arr[i];
		if (temp / 10 == 0) buckets[0].add(temp);
		else if (temp / 100 == 0) buckets[1].add(temp);
		else buckets[2].add(temp);
	}
	// 桶排序
	for (int i = 0; i < buckets.length; i++) Collections.sort(buckets[i]);
	// 元素歸位
	int index = 0;
	for (int i = 0; i < buckets.length; i++) {
		List<Integer> bucket = buckets[i];
		for (Integer integer : bucket) arr[index++] = integer;
	}
}

基數排序

基數排序算法依賴於元素自身的特性,適用於非負整數這一類的元素排序,用非負整數來理解該算法也更容易一些。

每個整數都可以拆分爲個位、十位、百位等等,對非負整數排序,可以先根據個位數字排序一次,再根據十位數字排序一次,再根據百位數字排序一次,以此類推,從低位到高位都排經過一次排序後,整體元素就是有序的。

算法描述

  1. 找出所有元素中最大的數,也就是位數最多的元素,有幾位就需要進行幾次基數排序
  2. 從最低位(個位)開始進行基數排序,這個過程中,所有元素會進入基數桶,然後按排序規則從桶中取出元素順序排列
  3. 從低位到高位,重複第二個過程,完成排序

動圖演示

基數排序 動圖演示

代碼實現

public static void sort(int[] arr) {
	// 找出最大數
	int max = -1;
	for (int num : arr) if (num > max) max = num;

	int count = String.valueOf(max).length();// 需要進行幾次基數排序
	int auxiliary = 1;// 輔助數,可用它求解每一位的數字
	List<Integer>[] buckets = new ArrayList[10];// 0-10的基數桶
	for (int i = 0; i < buckets.length; i++) buckets[i] = new ArrayList<>();// 初始化桶元素
	while (count-- > 0) {
		// 這個for循環是讓排序元素進入基數桶
		auxiliary *= 10;
		for (int i = 0; i < arr.length; i++) {
			int temp = arr[i];
			int num = (temp % auxiliary) / (auxiliary / 10);// 個位、十位、百位...數字
			buckets[num].add(temp);
		}
		// 這個for循環是按排序規則,把基數桶中的元素放回原始數組
		int index = 0;
		for (int i = 0; i < buckets.length; i++) {
			List<Integer> bucket = buckets[i];
			for (Integer integer : bucket) arr[index++] = integer;
			bucket.clear();
		}
	}
}

總結

常用經典排序算法應該總結的差不多了,還有一個堆排序沒寫,因爲沒有找到非常合適的動態圖,以後有空再補上。

代碼實現都是自己寫的,細節之處還有改進的地方。

感謝VisuAlgo網站提供的部分動態圖,希爾排序和桶排序的動態圖是我自己畫的。

文章較長,寫作不易,看完點個讚唄。

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