Java數據結構與算法 day05 排序算法

第六章 排序算法

本章源碼:https://github.com/name365/Java-Data-structure

排序算法介紹和分類

排序也稱排序算法 (Sort Algorithm),排序是將一組數據,依指定的順序進行排列的過程。

  • 排序的分類:

    • 內部排序:指將需要處理的所有數據都加載
      到內部存儲器中進行排序。
    • 外部排序:數據量過大,無法全部加載到內
      存中,需要藉助外部存儲進行排序。
  • 常見的排序算法分類(如圖):

在這裏插入圖片描述

算法的時間複雜度與空間複雜度

時間複雜度

度量一個程序(算法)執行時間的兩種方法

  1. 事後統計的方法
    這種方法可行, 但是有兩個問題:一是要想對設計的算法的運行性能進行評測,需要實際運行該程序;二是所得時間的統計量依賴於計算機的硬件、軟件等環境因素, 這種方式,要在同一臺計算機的相同狀態下運行,才能比較那個算法速度更快

  2. 事前估算的方法
    通過分析某個算法的時間複雜度來判斷哪個算法更優.

  • 時間頻度

時間頻度:一個算法花費的時間與算法中語句的執行次數成正比例,哪個算法中語句執行次數多,它花費時間就多。一個算法中的語句執行次數稱爲語句頻度或時間頻度。記爲T(n)。[舉例說明如下]

  • 舉例說明-基本案例
public class Sort {

	//比如計算1-100所有數字之和, 設計如下兩種算法:
	public static void main(String[] args) {
		//方法一:
		int total = 0;
		int end = 100;
		for(int i = 1;i <= end;i++){
			total += i;
		}
		System.out.println("total = " + total);	//T(n)=n+1;
		
		//方法二:
		int total2 = 0;
		int end2 = 100;
		total2 = (1 + end2)*end2/2;
		System.out.println("total2 = " + total2); //T(n)=1
	}
}
  • 舉例說明-忽略常數項
T(n)=2n+20 T(n)=2*n T(n)=(3n+10) T(n)=(3n)
1 22 2 13 3
2 24 4 16 6
5 30 10 25 15
8 36 16 34 24
15 50 30 55 45
30 80 60 100 90
100 220 200 310 300
300 620 600 910 900

在這裏插入圖片描述

結論: 
1. 2n+202n 隨着n 變大,執行曲線無限接近, 20可以忽略
2. 3n+103n 隨着n 變大,執行曲線無限接近, 10可以忽略
  • 舉例說明-忽略低次項
T(n)=2n^2+3n+10 T(n)=(2n^2) T(n)=(n^2+5n+20) T(n)=(n^2)
1 15 2 26 1
2 24 8 34 4
5 75 50 70 25
8 162 128 124 64
15 505 450 320 225
30 1900 1800 1070 900
100 20310 20000 10520 10000

在這裏插入圖片描述

結論: 
1. 2n^2+3n+102n^2 隨着n 變大, 執行曲線無限接近, 可以忽略 3n+10
2. n^2+5n+20 和 n^2 隨着n 變大,執行曲線無限接近, 可以忽略 5n+20
  • 舉例說明-忽略係數
T(n)=(3n^2+2n) T(n)=(5n^2+7n) T(n)=(n^3+5n) T(n)=(6n^3+4n)
1 5 12 6 10
2 16 34 18 56
5 85 160 150 770
8 208 376 552 3104
15 705 1230 3450 20310
30 2760 4710 27150 162120
100 30200 50700 1000500 6000400

在這裏插入圖片描述

結論: 
1. 隨着n值變大,5n^2+7n 和 3n^2 + 2n ,執行曲線重合, 說明  這種情況下, 53可以忽略。
2. 而n^3+5n 和 6n^3+4n  ,執行曲線分離,說明多少次方式關鍵
  1. 一般情況下,算法中的基本操作語句的重複執行次數是問題規模n的某個函數,用T(n)表示,若有某個輔助函數f(n),使得當n趨近於無窮大時,T(n) / f(n) 的極限值爲不等於零的常數,則稱f(n)是T(n)的同數量級函數。記作 T(n)=O( f(n) ),稱O( f(n) ) 爲算法的漸進時間複雜度,簡稱時間複雜度。

  2. T(n) 不同,但時間複雜度可能相同。 如:T(n)=n²+7n+6 與 T(n)=3n²+2n+2 它們的T(n) 不同,但時間複雜度相同,都爲O(n²)。

  3. 計算時間複雜度的方法:

  • 用常數1代替運行時間中的所有加法常數 T(n)=n²+7n+6 => T(n)=n²+7n+1

  • 修改後的運行次數函數中,只保留最高階項 T(n)=n²+7n+1 => T(n) = n²

  • 去除最高階項的係數 T(n) = n² => T(n) = n² => O(n²)

常見的時間複雜度

1)常數階O(1)

2)對數階O(log2n)

3)線性階O(n)

4)線性對數階O(nlog2n)

5)平方階O(n^2)

6)立方階O(n^3)

7)k次方階O(n^k)

8)指數階O(2^n)
  • 說明
  • 常見的算法時間複雜度由小到大依次爲:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)< Ο(nk) <Ο(2n) ,隨着問題規模n的不斷增大,上述時間複雜度不斷增大,算法的執行效率越低。

在這裏插入圖片描述

  • 從圖中可見,我們應該儘可能避免使用指數階的算法。
  • 常數階O(1)

無論代碼執行了多少行,只要是沒有循環等複雜結構,那這個代碼的時間複雜度就都是O(1) 。

在這裏插入圖片描述

上述代碼在執行的時候,它消耗的時候並不隨着某個變量的增長而增長,那麼無論這類代碼有多長,即使有幾萬幾十萬行,都可以用O(1)來表示它的時間複雜度。

  • 對數階O(log2n)

在這裏插入圖片描述

說明:在while循環裏面,每次都將 i 乘以 2,乘完之後,i 距離 n 就越來越近了。假設循環x次之後,i 就大於 2 了,此時這個循環就退出了,也就是說 2 的 x 次方等於 n,那麼 x = log2n也就是說當循環 log2n 次以後,這個代碼就結束了。因此這個代碼的時間複雜度爲:O(log2n) 。 O(log2n) 的這個2 時間上是根據代碼變化的,i = i * 3 ,則是 O(log3n) 。

在這裏插入圖片描述

  • 線性階O(n)

在這裏插入圖片描述

說明:這段代碼,for循環裏面的代碼會執行n遍,因此它消耗的時間是隨着n的變化而變化的,因此這類代碼都可以用O(n)來表示它的時間複雜度。

  • 線性對數階O(nlogN)

在這裏插入圖片描述

說明:線性對數階O(nlogN) 其實非常容易理解,將時間複雜度爲O(logn)的代碼循環N遍的話,那麼它的時間複雜度就是 n * O(logN),也就是了O(nlogN)。

  • 平方階O(n²)

在這裏插入圖片描述

說明:平方階O(n²) 就更容易理解了,如果把 O(n) 的代碼再嵌套循環一遍,它的時間複雜度就是 O(n²),這段代碼其實就是嵌套了2層n循環,它的時間複雜度就是 O(n * n),即 O(n²) 如果將其中一層循環的n改成m,那它的時間複雜度就變成了 O(m * n)。

  • 立方階O(n³)K次方階O(n^k)

說明:參考上面的O(n²) 去理解就好了,O(n³)相當於三層n循環,其它的類似。

  • 平均時間複雜度和最壞時間複雜度

1)平均時間複雜度是指所有可能的輸入實例均以等概率出現的情況下,該算法的運行時間。

2)最壞情況下的時間複雜度稱最壞時間複雜度。一般討論的時間複雜度均是最壞情況下的時間複雜度。 這樣做的原因是:最壞情況下的時間複雜度是算法在任何輸入實例上運行時間的界限,這就保證了算法的運行時間不會比最壞情況更長。

3)平均時間複雜度和最壞時間複雜度是否一致,和算法有關(如下表)。

排序法 平均時間 最差情形 穩定度 額外空間 備註
冒泡 O(n2) O(n2) 穩定 O(1) n小時較好
交換 O(n2) O(n2) 不穩定 O(1) n小時較好
選擇 O(n2) O(n2) 不穩定 O(1) n小時較好
插入 O(n2) O(n2) 穩定 O(1) 大部分已排序時較好
基數 O(logRB) O(logRB 穩定 O(n) B是真數(0-9),R是基數(個十百)
Shell O(nlogn) O(ns)1<s<2 不穩定 O(1) s是所選分組
快速 O(nlogn) O(n2) 不穩定 O(nlogn) n大時較好
歸併 O(nlogn) O(nlogn) 穩定 O(1) n大時較好
O(nlogn) O(nlogn) 不穩定 O(1) n大時較好

空間複雜度

  • 基本簡介

1)類似於時間複雜度的討論,一個算法的空間複雜度(Space Complexity)定義爲該算法所耗費的存儲空間,它也是問題規模n的函數。

2)空間複雜度(Space Complexity)是對一個算法在運行過程中臨時佔用存儲空間大小的量度。有的算法需要佔用的臨時工作單元數與解決問題的規模n有關,它隨着n的增大而增大,當n較大時,將佔用較多的存儲單元,例如快速排序和歸併排序算法就屬於這種情況。

3)在做算法分析時,主要討論的是時間複雜度。從用戶使用體驗上看,更看重的程序執行的速度。一些緩存產品(redis, memcache)和算法(基數排序)本質就是用空間換時間。

冒泡排序

基本介紹

冒泡排序(Bubble Sorting)的基本思想是:通過對待排序序列從前向後(從下標較小的元素開始),依次比較相鄰元素的值,若發現逆序則交換,使值較大的元素逐漸從前移向後部,就象水底下的氣泡一樣逐漸向上冒。

優化:
因爲排序的過程中,各元素不斷接近自己的位置,如果一趟比較下來沒有進行過交換,就說明序列有序,
因此要在排序過程中設置一個標誌flag判斷元素是否進行過交換。
從而減少不必要的比較。(這裏說的優化,可以在冒泡排序寫好後,在進行)

圖解冒泡排序算法的過程

具體的案例如下來說明冒泡排序。將五個無序的數:3, 9, -1, 10, -2使用冒泡排序法將其排成一個的有序數列。

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

冒泡排序規則:
(1) 一共進行 數組的大小-1 次 大的循環
(2) 每一趟排序的次數在逐漸的減少
(3) 如果我們發現在某趟排序中,沒有發生一次交換, 可以提前結束冒泡排序。這就是算法的優化。

源自網絡的冒泡排序動圖(僅供參考理解):

在這裏插入圖片描述

排序過程代碼實現

import java.util.Arrays;

public class BubbleSort {

	public static void main(String[] args) {
		int arr[] = {3, 9, -1, 10, -2};
		
		//第一趟排序,將最大的數排在最後
		int temp = 0;	//創建臨時變量,用於數據交換
		for(int i = 0;i < arr.length - 1;i++){
			//如果, 前一個數 > 後一個數
			if(arr[i] > arr[i+1]){
				temp = arr[i];
				arr[i] = arr[i + 1];
				arr[i + 1] = temp;
			}
		}
		System.out.print("第一趟排序後:");
		System.out.println(Arrays.toString(arr));
		
		//第二趟排序,將第二大的數排在倒數第二位
		for(int i = 0;i < arr.length - 1 - 1;i++){
			//如果, 前一個數 > 後一個數
			if(arr[i] > arr[i+1]){
				temp = arr[i];
				arr[i] = arr[i + 1];
				arr[i + 1] = temp;
			}
		}
		System.out.print("第二趟排序後:");
		System.out.println(Arrays.toString(arr));
		
		//第三趟排序,將第三大的數排在倒數第三位
		for(int i = 0;i < arr.length - 1 - 2;i++){
			//如果, 前一個數 > 後一個數
			if(arr[i] > arr[i+1]){
				temp = arr[i];
				arr[i] = arr[i + 1];
				arr[i + 1] = temp;
			}
		}
		System.out.print("第三趟排序後:");
		System.out.println(Arrays.toString(arr));
		
		//第四趟排序,將第四大的數排在倒數第四位
		for(int i = 0;i < arr.length - 1 - 3;i++){
			//如果, 前一個數 > 後一個數
			if(arr[i] > arr[i+1]){
				temp = arr[i];
				arr[i] = arr[i + 1];
				arr[i + 1] = temp;
			}
		}
		System.out.print("第四趟排序後:");
		System.out.println(Arrays.toString(arr));
	}
}
  • 對前面的代碼進行優化:
import java.util.Arrays;

public class BubbleSort {

	public static void main(String[] args) {
		int arr[] = { 3, 9, -1, 10, -2 };

		// 第一趟排序,將最大的數排在最後
		// 冒泡排序 的時間複雜度 O(n^2)
		int temp = 0; // 創建臨時變量,用於數據交換
		for (int j = 0; j < arr.length - 1; j++) {
			for (int i = 0; i < arr.length - 1 - j; i++) {
				// 如果, 前一個數 > 後一個數
				if (arr[i] > arr[i + 1]) {
					temp = arr[i];
					arr[i] = arr[i + 1];
					arr[i + 1] = temp;
				}
			}
			System.out.print("第" + (j + 1) + "趟排序後:");
			System.out.println(Arrays.toString(arr));
		}
    }
}
  • 對前面的代碼進行進一步優化:
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

public class BubbleSort2 {

	public static void main(String[] args) {
		int arr[] = { 3, 9, -1, 10, 20 };
		
		System.out.print("排序前的數組:");
		System.out.println(Arrays.toString(arr));
		
		//測試一下冒泡排序的速度:O(n^2),給80000個的數據,測試
//		int[] arr = new int[80000];
//		for(int i = 0;i < 80000;i++){
//			arr[i] = (int)(Math.random() * 80000);	//自動生成[0,80000)之間的隨機數
//		}
//		System.out.print("排序前的數組:");
//		System.out.println(Arrays.toString(arr));
		
		//排序前的時間:
		Date data = new Date();
		SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS = simt.format(data);
		System.out.println("排序前的時間是:" + dateS);

		//測試冒泡排序:
		bubbleSort(arr);
		
		//輸出排序後的數組
		System.out.print("排序後的數組:");
		System.out.println(Arrays.toString(arr));
		
		//排序後的時間:
		Date data2 = new Date();
		SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS2 = simt2.format(data2);
		System.out.println("排序後的時間是:" + dateS2);

	}

	// 將前面的冒泡排序封裝成一個方法
	public static void bubbleSort(int[] arr) {
		// 冒泡排序 的時間複雜度 O(n^2)
		int temp = 0; // 創建臨時變量,用於數據交換
		boolean falg = false; // 標識變量,表示是否進行過交換
		for (int j = 0; j < arr.length - 1; j++) {
			for (int i = 0; i < arr.length - 1 - j; i++) {
				// 如果, 前一個數 > 後一個數
				if (arr[i] > arr[i + 1]) {
					falg = true;
					temp = arr[i];
					arr[i] = arr[i + 1];
					arr[i + 1] = temp;
				}
			}

			if (falg == false) { // 在一趟排序中,一次交換都沒有,直接退出
				break;
			} else {
				falg = false; // 重置falg,方便進行二次判斷
			}
		}
	}
}

選擇排序

基本介紹

選擇式排序也屬於內部排序法,是從欲排序的數據中,按指定的規則選出某一元素,再依規定交換位置後達到排序的目的。

  • 選擇排序思想:
選擇排序(select sorting)也是一種簡單的排序方法。
它的基本思想是:
	第一次從arr[0]~arr[n-1]中選取最小值,與arr[0]交換;
	第二次從arr[1]~arr[n-1]中選取最小值,與arr[1]交換;
	第三次從arr[2]~arr[n-1]中選取最小值,與arr[2]交換;
	…
    第i次從arr[i-1]~arr[n-1]中選取最小值,與arr[i-1]交換;
	…
    第n-1次從arr[n-2]~arr[n-1]中選取最小值,與arr[n-2]交換,
	總共通過n-1次,得到一個按排序碼從小到大排列的有序序列。

選擇排序思路分析圖:

在這裏插入圖片描述
在這裏插入圖片描述

選擇排序的思路圖解

在這裏插入圖片描述

說明:
1. 選擇排序一共有 數組大小 - 1 輪排序
2.1輪排序,又是一個循環, 循環的規則(見下面代碼)
	2.1 先假定當前這個數是最小數
	2.2 然後和後面的每個數進行比較,如果發現有比當前數更小的數,就重新確定最小數,並得到下標
	2.3 當遍歷到數組的最後時,就得到本輪最小數和下標
	2.4 交換 [見下面代碼]

選擇排序應用實例

有一羣牛 , 顏值分別是 101, 34, 119, 1 請使用選擇排序從低到高進行排序[101, 34, 119, 1]

在這裏插入圖片描述

算法規律推導過程代碼:

import java.util.Arrays;

public class SelectSort {

	public static void main(String[] args) {
		int[] arr = {101, 34, 119, 1};
		System.out.print("排序前:");
		System.out.println(Arrays.toString(arr));
		
		selectSort(arr);	//調用排序
	}

	// 選擇排序
	public static void selectSort(int[] arr) {		
		// 逐步推導過程
		// 第1輪
		// 原始數組: 101, 34, 119, 1
		// 第一輪排序: 1, 34, 119, 101
		// 算法思維:先簡單--》再複雜,即複雜的算法拆分爲簡單的問題,再逐步解決
		
		// 第一輪
		int minIndex = 0;
		int min = arr[0];	//假設某個爲最小值
		for(int j = 0 + 1;j < arr.length;j++){
			if(min > arr[j]){	//說明假定的最小值,並不是最小
				min = arr[j];	//重置最小值
				minIndex = j;	//重置minIndex
			}
		}
		
		//將最小值放在arr[0],即交換
		if(minIndex != 0){
			arr[minIndex] = arr[0];
			arr[0] = min;
		}
		
		System.out.print("第一輪後:");
		System.out.println(Arrays.toString(arr));	//第一輪後:[1, 34, 119, 101]
		
		//第二輪
		minIndex = 1;
		min = arr[1];	//假設某個爲最小值
		for(int j = 1 + 1;j < arr.length;j++){
			if(min > arr[j]){	//說明假定的最小值,並不是最小
				min = arr[j];	//重置最小值
				minIndex = j;	//重置minIndex
			}
		}
		
		//將最小值放在arr[1],即交換
		if(minIndex != 1){
			arr[minIndex] = arr[1];
			arr[1] = min;
		}
		
		System.out.print("第二輪後:");
		System.out.println(Arrays.toString(arr));	//[1, 34, 119, 101]
		
		//第三輪
		minIndex = 2;
		min = arr[2];	//假設某個爲最小值
		for(int j = 1 + 2;j < arr.length;j++){
			if(min > arr[j]){	//說明假定的最小值,並不是最小
				min = arr[j];	//重置最小值
				minIndex = j;	//重置minIndex
			}
		}
		
		//將最小值放在arr[2],即交換
		if(minIndex != 2){
			arr[minIndex] = arr[2];
			arr[2] = min;
		}
		
		System.out.print("第三輪後:");
		System.out.println(Arrays.toString(arr));	//[1, 34, 101, 119]
	}

}

推導過程的代碼優化:

import java.util.Arrays;

public class SelectSort {

	public static void main(String[] args) {
		int[] arr = { 101, 34, 119, 1 };
		System.out.print("排序前:");
		System.out.println(Arrays.toString(arr));

		selectSort(arr); // 調用排序
		
		System.out.print("排序後:");
		System.out.println(Arrays.toString(arr));
	}

	// 選擇排序
	public static void selectSort(int[] arr) {

		// 在推導過程中,通過規律直接循環解決
		// 選擇排序的時間複雜度O(n^2)
		for (int i = 0; i < arr.length - 1; i++) {
			int minIndex = i;
			int min = arr[i]; // 假設某個爲最小值
			for (int j = i + 1; j < arr.length; j++) {
				if (min > arr[j]) { // 說明假定的最小值,並不是最小
					min = arr[j]; // 重置最小值
					minIndex = j; // 重置minIndex
				}
			}

			// 將最小值放在arr[i],即交換
			if (minIndex != i) {
				arr[minIndex] = arr[i];
				arr[i] = min;
			}

			System.out.print("第" + (i+1) + "輪後:");
			System.out.println(Arrays.toString(arr));
		}
    }
}

選擇排序算法速度測試:

import java.text.SimpleDateFormat;
import java.util.Date;
//選擇排序算法速度測試
public class SelectSort2 {

	public static void main(String[] args) {
		//測試一下冒泡排序的速度:O(n^2),給80000個的數據,測試
		int[] arr = new int[80000];
		for(int i = 0;i < 80000;i++){
			arr[i] = (int)(Math.random() * 80000);	//自動生成[0,80000)之間的隨機數
		}
		
		//排序前的時間:
		Date data = new Date();
		SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS = simt.format(data);
		System.out.println("排序前的時間是:" + dateS);

		selectSort(arr); // 調用排序
		
		//排序後的時間:
		Date data2 = new Date();
		SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS2 = simt2.format(data2);
		System.out.println("排序後的時間是:" + dateS2);
	}

	// 選擇排序
	public static void selectSort(int[] arr) {

		// 在推導過程中,通過規律直接循環解決
		// 選擇排序的時間複雜度O(n^2)
		for (int i = 0; i < arr.length - 1; i++) {
			int minIndex = i;
			int min = arr[i]; // 假設某個爲最小值
			for (int j = i + 1; j < arr.length; j++) {
				if (min > arr[j]) { // 說明假定的最小值,並不是最小
					min = arr[j]; // 重置最小值
					minIndex = j; // 重置minIndex
				}
			}

			// 將最小值放在arr[i],即交換
			if (minIndex != i) {
				arr[minIndex] = arr[i];
				arr[i] = min;
			}

		}
	}
}

插入排序

基本介紹

插入式排序屬於內部排序法,是對於欲排序的元素以插入的方式找尋該元素的適當位置,以達到排序的目的。

插入排序法思想

插入排序(Insertion Sorting)的基本思想是:
    把n個待排序的元素看成爲一個有序表和一個無序表,
    開始時有序表中只包含一個元素,無序表中包含有n-1個元素,
    排序過程中每次從無序表中取出第一個元素,
    把它的排序碼依次與有序表元素的排序碼進行比較,
    將它插入到有序表中的適當位置,使之成爲新的有序表。

插入排序思路圖

在這裏插入圖片描述
在這裏插入圖片描述

插入排序應用實例

有一羣小牛,考試成績分別是101,34,119,1請從小到大排序

在這裏插入圖片描述

算法規律推導過程代碼:

import java.util.Arrays;

public class InsertSort {

	public static void main(String[] args) {
		int[] arr = {101, 34, 119, 1};
		
		inserSort(arr);
		
	}
	//插入排序
	public static void inserSort(int[] arr){
		//使用逐步推到的過程
		
		//第1輪{101, 34, 119, 1}; =》 {34, 101, 119, 1};
		
		//定義待插入的數
		int insertVal = arr[1];
		int insertIndex = 1 - 1;	//即arr[1]的前面這個數的下標
		
		//給insertVal 找到插入位置
		//說明:
		//1.insertVal >= 0 保證在找到相應位置時,不會越界
		//2.insertVal < arr[insertIndex] 待插入的數未找到合適位置
		//3.就需要將 arr[insertIndex] 後移
		while(insertIndex >= 0 && insertVal < arr[insertIndex]){
			arr[insertIndex + 1] = arr[insertIndex];	// arr[insertIndex]
			insertIndex--;
		}
		//當退出while循環時,說明找到要插入的位置, insertIndex + 1
		arr[insertIndex + 1] = insertVal;
		
		System.out.print("第1輪插入:");
		System.out.println(Arrays.toString(arr));
		
		// 第2輪
		insertVal = arr[2];
		insertIndex = 2 - 1;	//即arr[2]的前面這個數的下標
		
		while(insertIndex >= 0 && insertVal < arr[insertIndex]){
			arr[insertIndex + 1] = arr[insertIndex];	// arr[insertIndex]
			insertIndex--;
		}
		//當退出while循環時,說明找到要插入的位置, insertIndex + 1
		arr[insertIndex + 1] = insertVal;
		
		System.out.print("第2輪插入:");
		System.out.println(Arrays.toString(arr));
		
		//第3輪
		insertVal = arr[3];
		insertIndex = 3 - 1;	//即arr[3]的前面這個數的下標
		
		while(insertIndex >= 0 && insertVal < arr[insertIndex]){
			arr[insertIndex + 1] = arr[insertIndex];	// arr[insertIndex]
			insertIndex--;
		}
		//當退出while循環時,說明找到要插入的位置, insertIndex + 1
		arr[insertIndex + 1] = insertVal;
		
		System.out.print("第3輪插入:");
		System.out.println(Arrays.toString(arr));
	}
}

推導過程的代碼優化:

import java.util.Arrays;

public class InsertSort {

	public static void main(String[] args) {
		int[] arr = {101, 34, 119, 1,-1,89,25};
		
		inserSort(arr);
		
	}
	//插入排序
	public static void inserSort(int[] arr){
		
		//代碼的簡化
				
		for(int i = 1;i < arr.length;i++){
			int insertVal = arr[i];
			int insertIndex = i - 1;	//即arr[i]的前面這個數的下標
			//給insertVal 找到插入位置
			//說明:
			//1.insertVal >= 0 保證在找到相應位置時,不會越界
			//2.insertVal < arr[insertIndex] 待插入的數未找到合適位置
			//3.就需要將 arr[insertIndex] 後移
			while(insertIndex >= 0 && insertVal < arr[insertIndex]){
				arr[insertIndex + 1] = arr[insertIndex];	// arr[insertIndex]
				insertIndex--;
			}
			//當退出while循環時,說明找到要插入的位置, insertIndex + 1
			arr[insertIndex + 1] = insertVal;
			
			System.out.print("第"+ i +"輪插入:");
			System.out.println(Arrays.toString(arr));
		}
    }
}

插入排序算法速度測試:

import java.text.SimpleDateFormat;
import java.util.Date;
//時間測試
public class InsertSort2 {

	public static void main(String[] args) {
		//測試一下插入排序的速度:O(n^2),給80000個的數據,測試
		int[] arr = new int[80000];
		for(int i = 0;i < 80000;i++){
			arr[i] = (int)(Math.random() * 80000);	//自動生成[0,80000)之間的隨機數
		}
		
		//排序前的時間:
		Date data = new Date();
		SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS = simt.format(data);
		System.out.println("排序前的時間是:" + dateS);	//2020-05-29 11:31:53
		
		inserSort(arr);	//調用插入排序
		
		//排序後的時間:
		Date data2 = new Date();
		SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS2 = simt2.format(data2);
		System.out.println("排序後的時間是:" + dateS2);	//2020-05-29 11:31:54	
	}
	//插入排序
	public static void inserSort(int[] arr){
		int insertVal = 0;
		int insertIndex = 0;
		for(int i = 1;i < arr.length;i++){
			insertVal = arr[i];
			insertIndex = i - 1;	//即arr[i]的前面這個數的下標
			//給insertVal 找到插入位置
			//說明:
			//1.insertVal >= 0 保證在找到相應位置時,不會越界
			//2.insertVal < arr[insertIndex] 待插入的數未找到合適位置
			//3.就需要將 arr[insertIndex] 後移
			while(insertIndex >= 0 && insertVal < arr[insertIndex]){	//從小到大排序
//			while(insertIndex >= 0 && insertVal > arr[insertIndex]){	//從大到小排序
				arr[insertIndex + 1] = arr[insertIndex];	// arr[insertIndex]
				insertIndex--;
			}
			//當退出while循環時,說明找到要插入的位置, insertIndex + 1
			//算法的優化:判斷是否需要賦值
			if(insertVal + 1 == i){	//滿足則沒必要執行
				arr[insertIndex + 1] = insertVal;
			}
		}
	}
}

希爾排序

基本介紹

針對於上述簡單插入排序,它存在一些問題:

我們看簡單的插入排序可能存在的問題. 
數組 arr = {2,3,4,5,6,1} 這時需要插入的數 1(最小), 這樣的過程是:
{2,3,4,5,6,6}
{2,3,4,5,5,6}
{2,3,4,4,5,6}
{2,3,3,4,5,6}
{2,2,3,4,5,6}
{1,2,3,4,5,6}
結論: 當需要插入的數是較小的數時,後移的次數明顯增多,對效率有影響.
針對這個問題,引入另一種方法:    

希爾排序是希爾(Donald Shell)於1959年提出的一種排序算法。希爾排序也是一種插入排序,它是簡單插入排序經過改進之後的一個更高效的版本,也稱爲縮小增量排序。

  • 希爾排序法基本思想
希爾排序是把記錄按下標的一定增量分組,
對每組使用直接插入排序算法排序;
隨着增量逐漸減少,每組包含的關鍵詞越來越多,
當增量減至1時,整個文件恰被分成一組,算法便終止。
  • 希爾排序法的示意圖

在這裏插入圖片描述
在這裏插入圖片描述

源自網絡的圖片:

在這裏插入圖片描述

希爾排序應用實例

有一羣小牛, 考試成績分別是 {8,9,1,7,2,3,5,4,6,0} 請從小到大排序. 請分別使用

1)希爾排序時, 對有序序列在插入時採用交換法, 並測試排序速度.

2)希爾排序時, 對有序序列在插入時採用移動法, 並測試排序速度.

在這裏插入圖片描述

算法規律推導過程代碼:

import java.util.Arrays;

public class ShellSort {

	public static void main(String[] args) {
		int[] arr = { 8, 9, 1, 7, 2, 3, 5, 4, 6, 0 };
		
		shellSort(arr);	//調用排序
	}

	// 逐步推導的方式——希爾排序
	public static void shellSort(int[] arr) {
		int temp = 0;
		// 第1輪排序: 將10個數據分爲5組
		for (int i = 5; i < arr.length; i++) {
			// 遍歷各組中所有的元素(共5組,每組兩個元素),步長是5
			for (int j = i - 5; j >= 0; j -= 5) {
				// 如果當前的元素大於加上步長後的元素,即交換
				if (arr[j] > arr[j + 5]) {
					temp = arr[j];
					arr[j] = arr[j + 5];
					arr[j + 5] = temp;
				}
			}
		}
		System.out.println("第1輪排序:" + Arrays.toString(arr));
		
		//第2輪排序,將10個數據分成了 5/2 = 2組
		for (int i = 2; i < arr.length; i++) {
			// 遍歷各組中所有的元素(共2組,每組5個元素),步長是2
			for (int j = i - 2; j >= 0; j -= 2) {
				// 如果當前的元素大於加上步長後的元素,即交換
				if (arr[j] > arr[j + 2]) {
					temp = arr[j];
					arr[j] = arr[j + 2];
					arr[j + 2] = temp;
				}
			}
		}
		System.out.println("第2輪排序:" + Arrays.toString(arr));
		
		//第3輪排序,將10個數據分成了 2/2 = 1組
		for (int i = 1; i < arr.length; i++) {
			// 遍歷各組中所有的元素(共1組,每組10個元素),步長是1
			for (int j = i - 1; j >= 0; j -= 1) {
				// 如果當前的元素大於加上步長後的元素,即交換
				if (arr[j] > arr[j + 1]) {
					temp = arr[j];
					arr[j] = arr[j + 1];
					arr[j + 1] = temp;
				}
			}
		}
		System.out.println("第3輪排序:" + Arrays.toString(arr));
		
	}
}

推導過程的代碼優化(希爾排序[交換式]):

import java.util.Arrays;

public class ShellSort {

	public static void main(String[] args) {
		int[] arr = { 8, 9, 1, 7, 2, 3, 5, 4, 6, 0 };
		
		shellSort(arr);	//調用排序
	}

	// 逐步推導的方式——希爾排序
	public static void shellSort(int[] arr) {
		
		//使用循環處理
		int temp = 0;
		int count = 0;	//統計排序次數
		for(int num = arr.length / 2;num > 0;num /= 2){
			for (int i = num; i < arr.length; i++) {
				// 遍歷各組中所有的元素(共num組,每組?個元素),步長是num
				for (int j = i - num; j >= 0; j -= num) {
					// 如果當前的元素大於加上步長後的元素,即交換
					if (arr[j] > arr[j + num]) {
						temp = arr[j];
						arr[j] = arr[j + num];
						arr[j + num] = temp;
					}
				}
			}
			System.out.println("第"+ (++count) +"輪排序:" + Arrays.toString(arr));
		}
    }
}

希爾排序[移位式]算法實現

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

public class ShellSort2 {

	public static void main(String[] args) {
//		int[] arr = { 8, 9, 1, 7, 2, 3, 5, 4, 6, 0 };

		 int[] arr = new int[80000];
		 for(int i = 0;i < 80000;i++){
		 arr[i] = (int)(Math.random() * 80000); //自動生成[0,80000)之間的隨機數
		 }

		// 排序前的時間:
		Date data = new Date();
		SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS = simt.format(data);
		System.out.println("排序前的時間是:" + dateS);

//		shellSort(arr); // 調用[交換式]排序
		shellSort2(arr); // 調用[移位式]排序

		// 排序後的時間:
		Date data2 = new Date();
		SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS2 = simt2.format(data2);
		System.out.println("排序後的時間是:" + dateS2);
		
	}

    //[交換式]排序
	public static void shellSort(int[] arr) {

		// 使用循環處理
		int temp = 0;
		int count = 0; // 統計排序次數
		for (int num = arr.length / 2; num > 0; num /= 2) {
			for (int i = num; i < arr.length; i++) {
				// 遍歷各組中所有的元素(共num組,每組?個元素),步長是num
				for (int j = i - num; j >= 0; j -= num) {
					// 如果當前的元素大於加上步長後的元素,即交換
					if (arr[j] > arr[j + num]) {
						temp = arr[j];
						arr[j] = arr[j + num];
						arr[j + num] = temp;
					}
				}
			}
			System.out.println("第" + (++count) + "輪排序:" + Arrays.toString(arr));
		}
	}

	// 對交換式的希爾排序進行優化->移位法
	public static void shellSort2(int[] arr) {

		for (int num = arr.length / 2; num > 0; num /= 2) {
			// 從num個元素,逐個對其所在的組進行直接插入排序
			for (int i = num; i < arr.length; i++) {
				int j = i;
				int temp = arr[j];
				if (arr[j] < arr[j - num]) {
					while (j - num >= 0 && temp < arr[j - num]) {
						//移動
						arr[j] = arr[j - num];
						j -= num;
					}
					//當退出while循環後,就爲temp找到相應的位置
					arr[j] = temp;
				}
			}
		}
	}
}

快速排序

基本介紹

快速排序(Quicksort)是對冒泡排序的一種改進

基本思想是:

通過一趟排序將要排序的數據分割成獨立的兩部分,
其中一部分的所有數據都比另外一部分的所有數據都要小,
然後再按此方法對這兩部分數據分別進行快速排序,
整個排序過程可以遞歸進行,以此達到整個數據變成有序序列.

快速排序法示意圖

在這裏插入圖片描述

快速排序法思路分析圖:

在這裏插入圖片描述

源自網絡的圖片:

在這裏插入圖片描述

快速排序應用實例

對 [-9,78,0,23,-567,70] 進行從小到大的排序,要求使用快速排序法。【測試8w和800w】

說明[驗證分析]:
如果取消左右遞歸,結果是  -9 -567 0 23 78 70
如果取消右遞歸,結果是  -567 -9 0 23 78 70
如果取消左遞歸,結果是  -9 -567 0 23 70 78

算法規律推導過程代碼:

import java.util.Arrays;

public class QuackSort {

	public static void main(String[] args) {
		int[] arr = { -9, 78, 0, 23, -567, 70 };

		quackSort(arr, 0, arr.length - 1);

		System.out.println("arr排序的結果是:" + Arrays.toString(arr));
	}

	//
	public static void quackSort(int[] arr, int left, int right) {
		int l = left; // 左索引
		int r = right; // 右索引
		int pivot = arr[(left + right) / 2]; // pivot 中軸
		int temp = 0; // 臨時變量,作爲交換時使用
		// while循環的目的:讓,比pivot 值小的放到左邊,比pivot 值大的放到右邊
		while (l < r) {
			// 在pivot左邊一直找,找到一個大於等於pivot的值,才退出
			while (arr[l] < pivot) {
				l += 1;
			}
			// 在pivot右邊一直找,找到一個小於等於pivot的值,才退出
			while (arr[r] > pivot) {
				r -= 1;
			}
			// 如果l >= r,則說明pivot 的左右兩的值,已經按照左邊全部是
			// 小於等於pivot值,右邊全部是大於等於pivot值.
			if (l >= r) {
				break;
			}
			// 數據交換
			temp = arr[l];
			arr[l] = arr[r];
			arr[r] = temp;
			
			// 如果交換完後,發現這個arr[l] == pivot值,相等 r--, 前移
			if (arr[l] == pivot) {
				r -= 1;
			}
			// 如果交換完後,發現這個arr[r] == pivot值,相等 l++, 後移
			if (arr[r] == pivot) {
				l += 1;
			}
		}
		//如果 l == r, 必須l++, r--, 否則爲出現棧溢出
		if(l == r){
			l += 1;
			r -= 1;
		}
		//向左遞歸
		if(left < r){
			quackSort(arr, left, r);
		}
		//向右遞歸
		if(right > l){
			quackSort(arr, l, right);
		}
	}
}

快速排序算法速度測試:

import java.text.SimpleDateFormat;
import java.util.Date;
//測試快排的執行速度
public class QuackSort2 {

	public static void main(String[] args) {
		
		int[] arr = new int[8000000];
		for(int i = 0;i < 8000000;i++){
			arr[i] = (int)(Math.random() * 8000000); //自動生成[0,8000000)之間的隨機數
		}

		// 排序前的時間:
		Date data = new Date();
		SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS = simt.format(data);
		System.out.println("排序前的時間是:" + dateS);

		quackSort(arr, 0, arr.length - 1);
		
		// 排序後的時間:
		Date data2 = new Date();
		SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS2 = simt2.format(data2);
		System.out.println("排序後的時間是:" + dateS2);
	}

	//快速排序
	public static void quackSort(int[] arr, int left, int right) {
		int l = left; // 左索引
		int r = right; // 右索引
		int pivot = arr[(left + right) / 2]; // pivot 中軸
		int temp = 0; // 臨時變量,作爲交換時使用
		// while循環的目的:讓,比pivot 值小的放到左邊,比pivot 值大的放到右邊
		while (l < r) {
			// 在pivot左邊一直找,找到一個大於等於pivot的值,才退出
			while (arr[l] < pivot) {
				l += 1;
			}
			// 在pivot右邊一直找,找到一個小於等於pivot的值,才退出
			while (arr[r] > pivot) {
				r -= 1;
			}
			// 如果l >= r,則說明pivot 的左右兩的值,已經按照左邊全部是
			// 小於等於pivot值,右邊全部是大於等於pivot值.
			if (l >= r) {
				break;
			}
			// 數據交換
			temp = arr[l];
			arr[l] = arr[r];
			arr[r] = temp;
			
			// 如果交換完後,發現這個arr[l] == pivot值,相等 r--, 前移
			if (arr[l] == pivot) {
				r -= 1;
			}
			// 如果交換完後,發現這個arr[r] == pivot值,相等 l++, 後移
			if (arr[r] == pivot) {
				l += 1;
			}
		}
		//如果 l == r, 必須l++, r--, 否則爲出現棧溢出
		if(l == r){
			l += 1;
			r -= 1;
		}
		//向左遞歸
		if(left < r){
			quackSort(arr, left, r);
		}
		//向右遞歸
		if(right > l){
			quackSort(arr, l, right);
		}
	}
}

歸併排序

基本介紹

歸併排序(MERGE-SORT)是利用歸併的思想實現的排序方法,該算法採用經典的分治(divide-and-conquer)策略(分治法將問題分(divide)成一些小的問題然後遞歸求解,而治(conquer)的階段則將分的階段得到的各答案"修補"在一起,即分而治之)。

歸併排序思想示意圖1-基本思想:

在這裏插入圖片描述

說明:

可以看到這種結構很像一棵完全二叉樹,本文的歸併排序我們採用遞歸去實現(也可採用迭代的方式去實現)。階段可以理解爲就是遞歸拆分子序列的過程。

歸併排序思想示意圖2-合併相鄰有序子序列:

再來看看階段,我們需要將兩個已經有序的子序列合併成一個有序序列,比如上圖中的最後一次合併,要將[4,5,7,8]和[1,2,3,6]兩個已經有序的子序列,合併爲最終序列[1,2,3,4,5,6,7,8],來看下實現步驟:

在這裏插入圖片描述

在這裏插入圖片描述

源自網絡的圖片:

在這裏插入圖片描述

歸併排序應用實例

給你一個數組, val arr = Array(9,8,7,6,5,4,3,2,1),請使用歸併排序完成排序。

歸併排序算法代碼實現:

import java.util.Arrays;

public class MergetSort {

	public static void main(String[] args) {
		int[] arr = { 8, 4, 5, 7, 1, 3, 6, 2};
		
		int temp[] = new int[arr.length]; //歸併排序需要一個額外空間
		mergeSort(arr, 0, arr.length - 1, temp);
 		
 		System.out.println("歸併排序的結果:" + Arrays.toString(arr));
	}
	
	//分+和的方法
	public static void mergeSort(int[] arr,int left,int right,int[] tem){
		if(left < right){
			int mid = (left + right) / 2;	//中間索引
			//向左進行遞歸
			mergeSort(arr, left, mid, tem);
			//向右遞歸
			mergeSort(arr, mid + 1, right, tem);
			//合併
			marge(arr, left, mid, right, tem);
		}
	}

	//合併的方法
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年5月29日下午5:46:23
	  * @param arr 排序的原始數組
	  * @param left 左邊有序序列的初始索引
	  * @param mid 中間索引
	  * @param right 右邊索引
	  * @param temp 做中轉的數組
	 */
	public static void marge(int[] arr,int left,int mid,int right,int[] temp){
		int i = left; // 初始化i, 左邊有序序列的初始索引
		int j = mid + 1; // 初始化j, 右邊有序序列的初始索引
		int t = 0; // 指向temp數組的當前索引
		
		//一、
		//先把左右兩邊(有序)的數據按照規則填充到temp數組
		//直到左右兩邊的有序序列,有一邊處理完畢爲止
		while(i <= mid && j <= right){	//繼續
			//如果左邊的有序序列的當前元素,小於等於右邊有序序列的當前元素
			//即,將左邊的當前元素,填充到 temp數組 
			//然後 t++, i++ ==》 後移
			if(arr[i] <= arr[j]){
				temp[t] = arr[i];
				t += 1;
				i += 1;
			} else {	//反之,將右邊有序序列的當前元素,填充到temp數組
				temp[t] = arr[j];
				t += 1;
				j += 1;
			}
		}
		
		//二、
		//把有剩餘數據的一邊的數據依次全部填充到temp
		while(i <= mid){	//左邊的有序序列還有剩餘的元素,就全部填充到temp
			temp[t] = arr[i];
			t += 1;
			i += 1;
		}
		while(j <= right){	//右邊的有序序列還有剩餘的元素,就全部填充到temp
			temp[t] = arr[j];
			t += 1;
			j += 1;
		}
		
		//三、
		//將temp數組的元素拷貝到arr,注意,並不是每次都拷貝所有數據
		t = 0;
		int tempLeft = left;
		//第一次合併 tempLeft = 0 , right = 1 
		//第二次合併 tempLeft = 2  right = 3 
		//第三次合併 tempLeft = 0 right=3
		//最後一次合併 tempLeft = 0  right = 7
		System.out.println("tempLeft = " + tempLeft + ", right = " + right);
		while(tempLeft <= right){
			arr[tempLeft] = temp[t];
			t += 1;
			tempLeft += 1; 
		}
	}
}

歸併排序算法速度測試:

import java.text.SimpleDateFormat;
import java.util.Date;

public class MergetSort {

	public static void main(String[] args) {
		
		int[] arr = new int[80000];
		for(int i = 0;i < 80000;i++){
			arr[i] = (int)(Math.random() * 80000); //自動生成[0,80000)之間的隨機數
		}

		// 排序前的時間:
		Date data = new Date();
		SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS = simt.format(data);
		System.out.println("排序前的時間是:" + dateS);
		
		int temp[] = new int[arr.length]; //歸併排序需要一個額外空間
		mergeSort(arr, 0, arr.length - 1, temp);
 				
		// 排序後的時間:
		Date data2 = new Date();
		SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS2 = simt2.format(data2);
		System.out.println("排序後的時間是:" + dateS2);
	}
	
	//分+和的方法
	public static void mergeSort(int[] arr,int left,int right,int[] tem){
		if(left < right){
			int mid = (left + right) / 2;	//中間索引
			//向左進行遞歸
			mergeSort(arr, left, mid, tem);
			//向右遞歸
			mergeSort(arr, mid + 1, right, tem);
			//合併
			marge(arr, left, mid, right, tem);
		}
	}

	//合併的方法
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年5月29日下午5:46:23
	  * @param arr 排序的原始數組
	  * @param left 左邊有序序列的初始索引
	  * @param mid 中間索引
	  * @param right 右邊索引
	  * @param temp 做中轉的數組
	 */
	public static void marge(int[] arr,int left,int mid,int right,int[] temp){
		int i = left; // 初始化i, 左邊有序序列的初始索引
		int j = mid + 1; // 初始化j, 右邊有序序列的初始索引
		int t = 0; // 指向temp數組的當前索引
		
		//一、
		//先把左右兩邊(有序)的數據按照規則填充到temp數組
		//直到左右兩邊的有序序列,有一邊處理完畢爲止
		while(i <= mid && j <= right){	//繼續
			//如果左邊的有序序列的當前元素,小於等於右邊有序序列的當前元素
			//即,將左邊的當前元素,填充到 temp數組 
			//然後 t++, i++ ==》 後移
			if(arr[i] <= arr[j]){
				temp[t] = arr[i];
				t += 1;
				i += 1;
			} else {	//反之,將右邊有序序列的當前元素,填充到temp數組
				temp[t] = arr[j];
				t += 1;
				j += 1;
			}
		}
		
		//二、
		//把有剩餘數據的一邊的數據依次全部填充到temp
		while(i <= mid){	//左邊的有序序列還有剩餘的元素,就全部填充到temp
			temp[t] = arr[i];
			t += 1;
			i += 1;
		}
		while(j <= right){	//右邊的有序序列還有剩餘的元素,就全部填充到temp
			temp[t] = arr[j];
			t += 1;
			j += 1;
		}
		
		//三、
		//將temp數組的元素拷貝到arr,注意,並不是每次都拷貝所有數據
		t = 0;
		int tempLeft = left;
		//第一次合併 tempLeft = 0 , right = 1 
		//第二次合併 tempLeft = 2  right = 3 
		//第三次合併 tempLeft = 0 right=3
		//最後一次合併 tempLeft = 0  right = 7
		while(tempLeft <= right){
			arr[tempLeft] = temp[t];
			t += 1;
			tempLeft += 1; 
		}
	}
}

基數排序

基本介紹

  1. 基數排序(radix sort)屬於“分配式排序”(distribution sort),又稱“桶子法”(bucket sort)或bin sort,顧名思義,它是通過鍵值的各個位的值,將要排序的元素分配至某些“桶”中,達到排序的作用

  2. 基數排序法是屬於穩定性的排序,基數排序法的是效率高的穩定性排序法

  3. 基數排序(Radix Sort)是桶排序的擴展

  4. 基數排序是1887年赫爾曼·何樂禮發明的。它是這樣實現的:將整數按位數切割成不同的數字,然後按每個位數分別比較。

基數排序基本思想:

將所有待比較數值統一爲同樣的數位長度,數位較短的數前面補零。然後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以後, 數列就變成一個有序序列。

這樣說明,比較難理解,下面我們看一個圖文解釋,理解基數排序的步驟。    

將數組 {53, 3, 542, 748, 14, 214} 使用基數排序, 進行升序排序。

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

源自網絡的圖片:

在這裏插入圖片描述

基數排序應用實例

將數組 {53, 3, 542, 748, 14, 214 } 使用基數排序, 進行升序排序。

算法規律推導過程代碼:

import java.util.Arrays;

public class RadixSort {
	
	public static void main(String[] args) {
		int[] arr = {53, 3, 542, 748, 14, 214 };
		radSort(arr);	//調用基數排序
	}
	
	//基數排序方法
	public static void radSort(int[] arr){
		
		//定義一個二維數組,表示10個桶,每個桶即爲一個一維數組
		//說明:
		//1.二維數組包含10個一維數組
		//2.爲了防止在放入數的時候,數據溢出,則每個一維數組(桶),大小定爲arr.length
		//3.需要明確:基數排序是使用空間換時間的經典算法
		int[][] bucket = new int[10][arr.length];
		
		//爲了記錄每個桶中,實際存放了多少個數據,需要定義一個一維數組來記錄各個桶的每次放入的數據個數
		//可以這裏理解
		//比如:bucketNums[0] , 記錄的就是  bucket[0] 桶的放入數據個數
		int[] bucketNums = new int[10];
		
		//第1輪排序(對每個元素的個位進行排序)
		for(int j = 0;j < arr.length;j++){
			//取出每個元素的個位的值
			int digt = arr[j] % 10;	// 748 % 10 => 8
			//放入到對應的桶中
			bucket[digt][bucketNums[digt]] = arr[j];
			bucketNums[digt]++;
		}
		//按照這個桶的順序(將一維數組的下標依次取出數據,放入原來數組)
		int index = 0;
		//遍歷每一個桶,並將桶中的數據,放入原數組中
		for(int k = 0;k < bucketNums.length;k++){
			//如果桶中有數據,才放入原數組
			if(bucketNums[k] != 0) {
				//循環該桶,即第k個桶(即第k個一維數組), 放入數據
				for(int p = 0;p < bucketNums[k];p++){
					//取出元素放入到arr中
					arr[index++] = bucket[k][p];
				}
			}
			//第1輪處理後,需要將每個 bucketNums[k] = 0 !
			bucketNums[k] = 0;	//重置爲0
		}
		System.out.println("第1輪排序:" + Arrays.toString(arr));
		
		//第2輪排序(對每個元素的十位進行排序)
		for(int j = 0;j < arr.length;j++){
			//取出每個元素的十位的值
			int digt = arr[j] / 10 % 10;	// 748 / 10 => 74 % 10 => 4
			//放入到對應的桶中
			bucket[digt][bucketNums[digt]] = arr[j];
			bucketNums[digt]++;
		}
		//按照這個桶的順序(將一維數組的下標依次取出數據,放入原來數組)
		index = 0;
		//遍歷每一個桶,並將桶中的數據,放入原數組中
		for(int k = 0;k < bucketNums.length;k++){
			//如果桶中有數據,才放入原數組
			if(bucketNums[k] != 0) {
				//循環該桶,即第k個桶(即第k個一維數組), 放入數據
				for(int p = 0;p < bucketNums[k];p++){
					//取出元素放入到arr中
					arr[index++] = bucket[k][p];
				}
			}
			//第2輪處理後,需要將每個 bucketNums[k] = 0 !
			bucketNums[k] = 0;	//重置爲0
		}
		System.out.println("第2輪排序:" + Arrays.toString(arr));
		
		//第3輪排序(對每個元素的百位進行排序)
		for(int j = 0;j < arr.length;j++){
			//取出每個元素的百位的值
			int digt = arr[j] / 100;	// 748 / 100 => 7
			//放入到對應的桶中
			bucket[digt][bucketNums[digt]] = arr[j];
			bucketNums[digt]++;
		}
		//按照這個桶的順序(將一維數組的下標依次取出數據,放入原來數組)
		index = 0;
		//遍歷每一個桶,並將桶中的數據,放入原數組中
		for(int k = 0;k < bucketNums.length;k++){
			//如果桶中有數據,才放入原數組
			if(bucketNums[k] != 0) {
				//循環該桶,即第k個桶(即第k個一維數組), 放入數據
				for(int p = 0;p < bucketNums[k];p++){
					//取出元素放入到arr中
					arr[index++] = bucket[k][p];
				}
			}
			//第3輪處理後,需要將每個 bucketNums[k] = 0 !
			bucketNums[k] = 0;	//重置爲0
		}
		System.out.println("第3輪排序:" + Arrays.toString(arr));
		
	}
}

算法的進一步優化:

import java.util.Arrays;

public class RadixSort {
	
	public static void main(String[] args) {
		int[] arr = {53, 3, 542, 748, 14, 214 };
		
		radSort(arr);	//調用基數排序
	}
	
	//基數排序方法
	public static void radSort(int[] arr){
		
		//1.得到數組中的最大的位數
		int max = arr[0];	//假設第一個數即爲最大數
		for(int i = 1;i < arr.length;i++){
			if(arr[i] > max){
				max = arr[i];
			}
		}
		//得到最大數是幾位數
		int maxLen = (max + "").length();
		
		//定義一個二維數組,表示10個桶,每個桶即爲一個一維數組
		//說明:
		//1.二維數組包含10個一維數組
		//2.爲了防止在放入數的時候,數據溢出,則每個一維數組(桶),大小定爲arr.length
		//3.需要明確:基數排序是使用空間換時間的經典算法
		int[][] bucket = new int[10][arr.length];
		
		//爲了記錄每個桶中,實際存放了多少個數據,需要定義一個一維數組來記錄各個桶的每次放入的數據個數
		//可以這裏理解
		//比如:bucketNums[0] , 記錄的就是  bucket[0] 桶的放入數據個數
		int[] bucketNums = new int[10];
		
		for(int i = 0,n = 1;i < maxLen;i++,n *= 10){
			//對每個元素的各位進行排序,第一次是個位,第二次是十位,第三次是百位.
			for(int j = 0;j < arr.length;j++){
				//取出每個元素的對應位的值
				int digt = arr[j] / n % 10;	// 748 / 1 % 10 => 8
				//放入到對應的桶中
				bucket[digt][bucketNums[digt]] = arr[j];
				bucketNums[digt]++;
			}
			//按照這個桶的順序(將一維數組的下標依次取出數據,放入原來數組)
			int index = 0;
			//遍歷每一個桶,並將桶中的數據,放入原數組中
			for(int k = 0;k < bucketNums.length;k++){
				//如果桶中有數據,才放入原數組
				if(bucketNums[k] != 0) {
					//循環該桶,即第k個桶(即第k個一維數組), 放入數據
					for(int p = 0;p < bucketNums[k];p++){
						//取出元素放入到arr中
						arr[index++] = bucket[k][p];
					}
				}
				//第i+1輪處理後,需要將每個 bucketNums[k] = 0 !
				bucketNums[k] = 0;	//重置爲0
			}
			System.out.println("第"+ (i+1) +"輪排序:" + Arrays.toString(arr));
		}
    }
}

算法速度測試:

import java.text.SimpleDateFormat;
import java.util.Date;

public class RadixSort2 {
	
	public static void main(String[] args) {
		
		int[] arr = new int[8000000];
		for(int i = 0;i < 8000000;i++){
			arr[i] = (int)(Math.random() * 8000000); //自動生成[0,8000000)之間的隨機數
		}

		// 排序前的時間:
		Date data = new Date();
		SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS = simt.format(data);
		System.out.println("排序前的時間是:" + dateS);
		
		radSort(arr);	//調用基數排序
		
		// 排序後的時間:
		Date data2 = new Date();
		SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS2 = simt2.format(data2);
		System.out.println("排序後的時間是:" + dateS2);
		
	}
	
	//基數排序方法
	public static void radSort(int[] arr){
		
		//1.得到數組中的最大的位數
		int max = arr[0];	//假設第一個數即爲最大數
		for(int i = 1;i < arr.length;i++){
			if(arr[i] > max){
				max = arr[i];
			}
		}
		//得到最大數是幾位數
		int maxLen = (max + "").length();
		
		//定義一個二維數組,表示10個桶,每個桶即爲一個一維數組
		//說明:
		//1.二維數組包含10個一維數組
		//2.爲了防止在放入數的時候,數據溢出,則每個一維數組(桶),大小定爲arr.length
		//3.需要明確:基數排序是使用空間換時間的經典算法
		int[][] bucket = new int[10][arr.length];
		
		//爲了記錄每個桶中,實際存放了多少個數據,需要定義一個一維數組來記錄各個桶的每次放入的數據個數
		//可以這裏理解
		//比如:bucketNums[0] , 記錄的就是  bucket[0] 桶的放入數據個數
		int[] bucketNums = new int[10];
		
		for(int i = 0,n = 1;i < maxLen;i++,n *= 10){
			//對每個元素的各位進行排序,第一次是個位,第二次是十位,第三次是百位.
			for(int j = 0;j < arr.length;j++){
				//取出每個元素的對應位的值
				int digt = arr[j] / n % 10;	// 748 / 1 % 10 => 8
				//放入到對應的桶中
				bucket[digt][bucketNums[digt]] = arr[j];
				bucketNums[digt]++;
			}
			//按照這個桶的順序(將一維數組的下標依次取出數據,放入原來數組)
			int index = 0;
			//遍歷每一個桶,並將桶中的數據,放入原數組中
			for(int k = 0;k < bucketNums.length;k++){
				//如果桶中有數據,才放入原數組
				if(bucketNums[k] != 0) {
					//循環該桶,即第k個桶(即第k個一維數組), 放入數據
					for(int p = 0;p < bucketNums[k];p++){
						//取出元素放入到arr中
						arr[index++] = bucket[k][p];
					}
				}
				//第i+1輪處理後,需要將每個 bucketNums[k] = 0 !
				bucketNums[k] = 0;	//重置爲0
			}
//			System.out.println("第"+ (i+1) +"輪排序:" + Arrays.toString(arr));
		}
	}
}

基數排序算法注意事項

  • 基數排序是對傳統桶排序的擴展,速度很快.
  • 基數排序是經典的空間換時間的方式,佔用內存很大,當對海量數據排序時,容易造成OutOfMemoryError。
  • 基數排序時穩定的。[注:假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序後的序列中,r[i]仍在r[j]之前,則稱這種排序算法是穩定的;否則稱爲不穩定的]
  • 有負數的數組,我們不用基數排序來進行排序,如果要支持負數,參考:https://code.i-harness.com/zh-CN/q/e98fa9

堆排序(在二叉樹部分)

常用排序算法的總結

一張排序算法的比較圖

在這裏插入圖片描述

相關術語解釋:

  • 穩定:如果a原本在b前面,而a=b,排序之後a仍然在b的前面;
  • 不穩定:如果a原本在b的前面,而a=b,排序之後a可能會出現在b的後面;
  • 內排序:所有排序操作都在內存中完成;
  • 外排序:由於數據太大,因此把數據放在磁盤中,而排序通過磁盤和內存的數據傳輸才能進行;
  • 時間複雜度:一個算法執行所耗費的時間。
  • 空間複雜度:運行完一個程序所需內存的大小。
  • n:數據規模
  • k:“桶”的個數
  • In-place:不佔用額外內存
  • Out-place:佔用額外內存

本章思維的導圖

在這裏插入圖片描述

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