接着上一篇的排序算法,我們廢話不多說,直接進入主題。
1.快速排序
快速排序(Quicksort)是對冒泡排序的一種改進。
快速排序由C. A. R. Hoare在1960年提出。它的基本思想是:通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。
演示:
代碼如下:
public static void quickSort(int[] arr, int left, int right) {
int l = left;// 左下標
int r = right;// 右下標
int pivot = arr[(left + right) / 2];// 找到中間的值
// 將比pivot小的值放在其左邊,比pivot大的值放在其右邊
while (l < r) {
// 在pivot左邊尋找,直至找到大於等於pivot的值才退出
while (arr[l] < pivot) {
l += 1;// 將l右移一位
}
// 在pivot右邊尋找,直至找到小於等於pivot的值才退出
while (arr[r] > pivot) {
r -= 1;// 將r左移一位
}
if (l >= r) {
// 左右下標重合,尋找完畢,退出循環
break;
}
// 交換元素
int 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,要把這兩個下標錯開,否則會出現無限遞歸,導致棧溢出的情況
if (l == r) {
l += 1;
r -= 1;
}
// 向左遞歸
if (left < r) {
quickSort(arr, left, r);
}
// 向右遞歸
if (right > l) {
quickSort(arr, l, right);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
測試代碼:
public static void main(String[] args) {
int[] arr = { 3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48 };
quickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
1
2
3
4
5
運行結果:
[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
1
快速排序的實現原理很簡單,就是將原數組分成兩部分,然後以中間值爲標準,比它小的就放其左邊,比它大的就放其右邊,然後在左右兩邊又以相同的方式繼續排序。
所以在代碼實現過程中,首先要創建兩個移動的變量,一個從最左邊開始往右移動,一個從最右邊開始往左移動,通過這兩個變量來遍歷左右兩部分的元素。當發現左邊有大於中間數的元素,右邊有小於中間數的元素,此時就進行交換。當兩個變量重合也就是相等的時候遍歷結束,然後左右兩部分作遞歸處理。
2.歸併排序
歸併排序(MERGE-SORT)是建立在歸併操作上的一種有效的排序算法,該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲二路歸併。
演示:
歸併排序使用了一種分治思想,分治思想的意思就是’分而治之",就是把一個複雜的問題分成兩個或更多的相同或相似的子問題,再把子問題分成更小的子問題……直到最後子問題可以簡單地直接求解。
通過這個動圖來看的話,相信很多人都一臉懵,沒關係,我們通過靜態圖來分析一下:
假設現在有一個待排序的序列,[5,2,4,7,1,3,2,2],那麼我們就需要將該序列進行分治,先將其分成兩份:[5,2,4,7]和[1,3,2,2],再將這兩份分別分成兩份:[5,2]和[4,7];[1,3]和[2,2],最後將這四部分再次分別分爲兩份,最後就將整個序列分爲了八份。需要注意的是,在分的過程中,不需要遵循任何規則,關鍵在於歸併,歸併的過程中便實現了元素的排序。
代碼如下:
public static void mergeSort(int[] arr, int left, int right, int[] temp) {
// 分解
if (left < right) {
int mid = (left + right) / 2;// 中間索引
// 向左遞歸進行分解
mergeSort(arr, left, mid, temp);
// 向右遞歸進行分解
mergeSort(arr, mid + 1, right, temp);// mid + 1,中間位置的後一個位置纔是右邊序列的開始位置
// 每分解一輪便合併一輪
merge(arr, left, right, mid, temp);
}
}
/**
* 合併的方法
*
* @param arr 待排序的數組
* @param left 左邊有序序列的初始索引
* @param right 中間索引
* @param mid 右邊有序序列的初始索引
* @param temp 做中轉的數組
*/
public static void merge(int[] arr, int left, int right, int mid, int[] temp) {
int i = left; // 初始化i,左邊有序序列的初始索引
int j = mid + 1;// 初始化j,右邊有序序列的初始索引(右邊有序序列的初始位置即爲中間位置的後一個位置)
int t = 0;// 指向temp數組的當前索引,初始爲0
// 先把左右兩邊的數據(已經有序)按規則填充到temp數組
// 直到左右兩邊的有序序列,有一邊處理完成爲止
while (i <= mid && j <= right) {
// 如果左邊有序序列的當前元素小於或等於右邊有序序列的當前元素,就將左邊的元素填充到temp數組中
if (arr[i] <= arr[j]) {
temp[t] = arr[i];
t++;// 索引後移
i++;// i後移
} else {
// 反之,將右邊有序序列的當前元素填充到temp數組中
temp[t] = arr[j];
t++;// 索引後移
j++;// j後移
}
}
// 把有剩餘數據的一邊的元素填充到temp中
while (i <= mid) {
// 此時說明左邊序列還有剩餘元素
// 全部填充到temp數組
temp[t] = arr[i];
t++;
i++;
}
while (j <= right) {
// 此時說明左邊序列還有剩餘元素
// 全部填充到temp數組
temp[t] = arr[j];
t++;
j++;
}
// 將temp數組的元素複製到原數組
t = 0;
int tempLeft = left;
while (tempLeft <= right) {
arr[tempLeft] = temp[t];
t++;
tempLeft++;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
關於歸併排序的算法思想確實比較繞,所以我也在代碼中寫了很多註釋。
我們先來測試一下:
public static void main(String[] args) {
int[] arr = { 3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48 };
int[] temp = new int[arr.length];
mergeSort(arr, 0, arr.length - 1, temp);
System.out.println(Arrays.toString(arr));
}
1
2
3
4
5
6
運行結果:
[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
1
來分析一下吧,對於該排序算法,有兩個部分組成,分解和合並。首先講講分解,在前面也說到了,我們需要將待排序的序列不停地進行分解,通過兩個索引變量控制,一個初始索引,一個結尾索引。只有當兩索引重合才結束分解。此時序列被分解成了十五個小份,這樣分解工作就完成了。接下來是合併,合併操作也是最麻煩的,也是通過兩個索引變量i,j。開始i在左邊序列的第一個位置,j在右邊序列的第一個位置,然後就是尋找左右兩個序列中的最小值,放到新序列中,這時可能會出現一邊的元素都放置完畢了,而另外一邊還存在元素,此時只需將剩餘的元素按順序放進新序列即可,因爲這時左右兩邊的序列已經是有序的了,最後將新序列複製到舊序列。這裏也特別需要注意,因爲合併的過程是分步的,而並非一次合併完成,所以數組的索引是在不斷變化的。
自己手動畫了個圖,左右兩個箭頭就是索引變量i,j,當i所指的元素也就是1和j所指的元素也就是2進行比較,發現1小,就將1放到新數組的第一個位置,此時應該將i和新數組的索引都右移一位,然後繼續比較,以此類推,相信這樣大家應該能理解了吧。
3.基數排序
基數排序(radix sort)屬於“分配式排序”(distribution sort),又稱“桶子法”(bucket sort)或bin sort,顧名思義,它是透過鍵值的部份資訊,將要排序的元素分配至某些“桶”中,藉以達到排序的作用,基數排序法是屬於穩定性的排序,其時間複雜度爲O(nlog( r )m),其中r爲所採取的基數,而m爲堆數,在某些時候,基數排序法的效率高於其它的穩定性排序法。基數排序是用空間換時間的經典算法。
演示:
基數排序的基本思想是:
將所有待比較的數值統一爲同樣的數位長度,數位較短的數前面補零。然後,從最低位開始,依次進行一次排序,這樣從最低位排序一直到最高位排序完成以後,數列就變爲了一個有序序列。
這樣說可能過於抽象,我們通過詳細步驟來分析一下:
我們假設有一個待排序數組[53,3,542,748,14,214],那麼如何使用基數排序對其進行排序呢?
首先我們有這樣的十個一維數組,在基數排序中也叫桶。
那麼第一輪排序開始,我們依次遍歷每個元素,並得到元素的個位數。拿到的第一個元素爲53,其個位數爲3,所以將53放入編號爲3的桶中,第二個元素3的個位數也是3,所以也放在編號爲3的桶中,而第三個元素542的個位數爲2,所以將542放入編號爲2的桶中,以此類推。
所以結果爲:
將元素全部放入桶中之後,我們需要按照桶的順序(也就是一維數組的下標)依次取出數據,並放回原來的數組。
那麼很簡單,按順序取出數據並放回原數組之後,原數組將變爲[542,53,3,14,214,748]。
這樣第一輪就完成了,接下來開始第二輪。
第二輪排序和第一輪類似,也要去遍歷數組元素,但不同的是第二輪的存放順序取決於十位數。
取出數據的第一個元素爲542,十位數爲4,所以放入編號爲4的桶;第二個元素53,十位數爲5,所以放入編號爲5的桶;第三個元素3,十位數爲0,所以放入編號爲0的桶,以此類推。
所以結果爲:
然後同樣按照桶的順序將數據從中取出並放入原數組,此時原數組變爲[3,14,214,542,748,53]。
接下來又進行第三輪排序,以元素的百位數進行區分,結果爲:
按順序取出數據後,原數組變爲[3,14,53,214,542,748]。這時的數組已經完成排序。
從中我們也可以知道,基數排序的排序輪數取決於數組元素中最大位數的元素。
代碼如下:
public static void raixSort(int[] arr) {
// 第一輪(針對每個元素的個位進行排序處理)
// 定義一個二維數組,模擬桶,每個桶就是一個一維數組
// 爲了防止放入數據的時候桶溢出,我們應該儘量將桶的容量設置得大一些
int[][] bucket = new int[10][arr.length];
// 記錄每個桶中實際存放的元素個數
// 定義一個一維數組來記錄每個桶中每次放入的元素個數
int[] bucketElementCounts = new int[10];
for (int j = 0; j < arr.length; j++) {
// 取出每個元素的個位
int digitOfElement = arr[j] % 10;
// 將元素放入對應的桶中
// bucketElementCounts[digitOfElement]就是桶中的元素個數,初始爲0,放在第一位
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
// 將桶中的元素個數++
// 這樣接下來的元素就可以排在前面的元素後面
bucketElementCounts[digitOfElement]++;
}
// 按照桶的順序取出數據並放回原數組
int index = 0;
for (int k = 0; k < bucket.length; k++) {
// 如果桶中有數據,才取出放回原數組
if (bucketElementCounts[k] != 0) {
// 說明桶中有數據,對該桶進行遍歷
for (int l = 0; l < bucketElementCounts[k]; l++) {
// 取出元素放回原數組
arr[index++] = bucket[k][l];
}
}
// 第一輪處理後,需要將每個bucketElementCounts[k]置0
bucketElementCounts[k] = 0;
}
System.out.println("第一輪:" + Arrays.toString(arr));
// ----------------------------
// 第二輪(針對每個元素的十位進行排序處理)
for (int j = 0; j < arr.length; j++) {
// 取出每個元素的十位
int digitOfElement = arr[j] / 10 % 10;
// 將元素放入對應的桶中
// bucketElementCounts[digitOfElement]就是桶中的元素個數,初始爲0,放在第一位
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
// 將桶中的元素個數++
// 這樣接下來的元素就可以排在前面的元素後面
bucketElementCounts[digitOfElement]++;
}
// 按照桶的順序取出數據並放回原數組
index = 0;
for (int k = 0; k < bucket.length; k++) {
// 如果桶中有數據,才取出放回原數組
if (bucketElementCounts[k] != 0) {
// 說明桶中有數據,對該桶進行遍歷
for (int l = 0; l < bucketElementCounts[k]; l++) {
// 取出元素放回原數組
arr[index++] = bucket[k][l];
}
}
// 第二輪處理後,需要將每個bucketElementCounts[k]置0
bucketElementCounts[k] = 0;
}
System.out.println("第二輪:" + Arrays.toString(arr));
// ----------------------------
// 第三輪(針對每個元素的百位進行排序處理)
for (int j = 0; j < arr.length; j++) {
// 取出每個元素的百位
int digitOfElement = arr[j] / 100 % 10;
// 將元素放入對應的桶中
// bucketElementCounts[digitOfElement]就是桶中的元素個數,初始爲0,放在第一位
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
// 將桶中的元素個數++
// 這樣接下來的元素就可以排在前面的元素後面
bucketElementCounts[digitOfElement]++;
}
// 按照桶的順序取出數據並放回原數組
index = 0;
for (int k = 0; k < bucket.length; k++) {
// 如果桶中有數據,才取出放回原數組
if (bucketElementCounts[k] != 0) {
// 說明桶中有數據,對該桶進行遍歷
for (int l = 0; l < bucketElementCounts[k]; l++) {
// 取出元素放回原數組
arr[index++] = bucket[k][l];
}
}
// 第三輪處理後,需要將每個bucketElementCounts[k]置0
bucketElementCounts[k] = 0;
}
System.out.println("第三輪:" + Arrays.toString(arr));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
爲了方便大家理解,這裏我並沒有使用循環處理,而是詳細寫出了每一輪的步驟,代碼註釋也很詳細。
接下來編寫測試代碼:
public static void main(String[] args) {
int[] arr = { 53, 3, 542, 748, 14, 214 };
raixSort(arr);
}
1
2
3
4
運行結果:
第一輪:[542, 53, 3, 14, 214, 748]
第二輪:[3, 14, 214, 542, 748, 53]
第三輪:[3, 14, 53, 214, 542, 748]
1
2
3
如果你能夠看懂上面的代碼,那麼接下來就是整合了,通過循環對上面的代碼進行優化。
代碼如下:
public static void raixSort(int[] arr) {
// 得到數組中最大的數
int max = arr[0];// 假設第一個數就是數組中的最大數
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
// 得到最大數是幾位數
// 通過拼接一個空串將其變爲字符串進而求得字符串的長度,即爲位數
int maxLength = (max + "").length();
// 定義一個二維數組,模擬桶,每個桶就是一個一維數組
// 爲了防止放入數據的時候桶溢出,我們應該儘量將桶的容量設置得大一些
int[][] bucket = new int[10][arr.length];
// 記錄每個桶中實際存放的元素個數
// 定義一個一維數組來記錄每個桶中每次放入的元素個數
int[] bucketElementCounts = new int[10];
// 通過變量n幫助取出元素位數上的數
for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
for (int j = 0; j < arr.length; j++) {
// 針對每個元素的位數進行處理
int digitOfElement = arr[j] / n % 10;
// 將元素放入對應的桶中
// bucketElementCounts[digitOfElement]就是桶中的元素個數,初始爲0,放在第一位
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
// 將桶中的元素個數++
// 這樣接下來的元素就可以排在前面的元素後面
bucketElementCounts[digitOfElement]++;
}
// 按照桶的順序取出數據並放回原數組
int index = 0;
for (int k = 0; k < bucket.length; k++) {
// 如果桶中有數據,才取出放回原數組
if (bucketElementCounts[k] != 0) {
// 說明桶中有數據,對該桶進行遍歷
for (int l = 0; l < bucketElementCounts[k]; l++) {
// 取出元素放回原數組
arr[index++] = bucket[k][l];
}
}
// 每輪處理後,需要將每個bucketElementCounts[k]置0
bucketElementCounts[k] = 0;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
測試代碼:
public static void main(String[] args) {
int[] arr = { 53, 3, 542, 748, 14, 214 };
raixSort(arr);
System.out.println(Arrays.toString(arr));
}
1
2
3
4
5
運行結果:
[3, 14, 53, 214, 542, 748]
1
這樣,基數排序就完成了。大家不要看到代碼很多就怕了、煩了,覺得好難,其實也不能說一點難度都沒有吧,只是要去理解這個過程,所以對於排序過程的分析我寫了很多,也是爲了能讓你們更加理解,掌握了過程之後,相信理解這些代碼也不是難事了。
其它:
這裏說一說基數排序的一些其它內容,爲什麼單獨只說基數排序呢?我們在前面提到了,基數排序是用空間換時間的經典算法,所以基數排序對於元素排序是非常快的。不信我們可以測試一下(先測試八十萬個數據的排序時間):
public static void main(String[] args) {
int[] arr = new int[800000];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) (Math.random() * 8000000);
}
Date date = new Date();
SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
String dateStr = format.format(date);
System.out.println("排序前的時間是:" + dateStr);
raixSort(arr);
Date date2 = new Date();
String dateStr2 = format.format(date2);
System.out.println("排序前的時間是:" + dateStr2);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
運行結果:
排序前的時間是:17:37:21
排序前的時間是:17:37:21
1
2
一秒鐘時間不到就完成排序了。
我們再測試一下八百萬個數據的排序:
public static void main(String[] args) {
int[] arr = new int[8000000];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) (Math.random() * 8000000);
}
Date date = new Date();
SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
String dateStr = format.format(date);
System.out.println("排序前的時間是:" + dateStr);
raixSort(arr);
Date date2 = new Date();
String dateStr2 = format.format(date2);
System.out.println("排序前的時間是:" + dateStr2);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
運行結果:
排序前的時間是:17:38:04
排序前的時間是:17:38:05
1
2
只需要一秒鐘即完成了排序。
我們再測試一下八千萬個數據的排序:
public static void main(String[] args) {
int[] arr = new int[80000000];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) (Math.random() * 8000000);
}
Date date = new Date();
SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
String dateStr = format.format(date);
System.out.println("排序前的時間是:" + dateStr);
raixSort(arr);
Date date2 = new Date();
String dateStr2 = format.format(date2);
System.out.println("排序前的時間是:" + dateStr2);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
運行結果:
排序前的時間是:17:41:07
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.itcast.sort.RadixSortDemo.raixSort(RadixSortDemo.java:42)
at com.itcast.sort.RadixSortDemo.main(RadixSortDemo.java:22)
1
2
3
4
結果爲堆內存溢出,所以在對大量數據進行排序的時候,基數排序顯然不是一個好的選擇,因爲提升排序效率的條件是犧牲大量的內存空間,當數據足夠多時,內存空間就不夠用了。
4.堆排序
本來這篇文章打算介紹四種排序算法,這最後一種便是堆排序,但是寫着寫着,回過頭髮現竟然也寫了這麼多了,考慮到不宜篇幅過長,我打算將堆排序放到後面說,如果你能掌握前面的這七種排序算法並能隨時手寫出來,也是很了不起了。
本篇文章雖然只介紹了三種排序算法,但這三種排序算法都有一定的難度,想要徹底掌握它們大家還是要多花點心思啊!
推薦閱讀
————————————————
版權聲明:本文爲CSDN博主「~wangweijun」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/qq_42453117/article/details/100036347