文章目錄
第六章 排序算法
本章源碼:https://github.com/name365/Java-Data-structure
排序算法介紹和分類
排序也稱排序算法 (Sort Algorithm),排序是將一組數據,依指定的順序進行排列的過程。
-
排序的分類:
- 內部排序:指將需要處理的所有數據都加載
到內部存儲器中進行排序。 - 外部排序:數據量過大,無法全部加載到內
存中,需要藉助外部存儲進行排序。
- 內部排序:指將需要處理的所有數據都加載
-
常見的排序算法分類(如圖):
算法的時間複雜度與空間複雜度
時間複雜度
度量一個程序(算法)執行時間的兩種方法
事後統計的方法
這種方法可行, 但是有兩個問題:一是要想對設計的算法的運行性能進行評測,需要實際運行該程序;二是所得時間的統計量依賴於計算機的硬件、軟件等環境因素, 這種方式,要在同一臺計算機的相同狀態下運行,才能比較那個算法速度更快。事前估算的方法
通過分析某個算法的時間複雜度來判斷哪個算法更優.
- 時間頻度
時間頻度:一個算法花費的時間與算法中語句的執行次數成正比例,哪個算法中語句執行次數多,它花費時間就多。一個算法中的語句執行次數稱爲語句頻度或時間頻度。記爲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+20 和 2n 隨着n 變大,執行曲線無限接近, 20可以忽略 2. 3n+10 和 3n 隨着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+10 和 2n^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 ,執行曲線重合, 說明 這種情況下, 5和3可以忽略。 2. 而n^3+5n 和 6n^3+4n ,執行曲線分離,說明多少次方式關鍵
一般情況下,算法中的基本操作語句的重複執行次數是問題規模n的某個函數,用T(n)表示,若有某個輔助函數f(n),使得當n趨近於無窮大時,T(n) / f(n) 的極限值爲不等於零的常數,則稱f(n)是T(n)的同數量級函數。記作 T(n)=O( f(n) ),稱O( f(n) ) 爲算法的漸進時間複雜度,簡稱時間複雜度。
T(n) 不同,但時間複雜度可能相同。 如:T(n)=n²+7n+6 與 T(n)=3n²+2n+2 它們的T(n) 不同,但時間複雜度相同,都爲O(n²)。
計算時間複雜度的方法:
用常數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;
}
}
}
基數排序
基本介紹
基數排序(radix sort)屬於“分配式排序”(distribution sort),又稱“桶子法”(bucket sort)或bin sort,顧名思義,它是通過鍵值的各個位的值,將要排序的元素分配至某些“桶”中,達到排序的作用
基數排序法是屬於穩定性的排序,基數排序法的是效率高的穩定性排序法
基數排序(Radix Sort)是桶排序的擴展
基數排序是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:佔用額外內存