一、內排序
1、排序基本概念
(1)什么是排序?
排序指將一個數據元素集合或者序列 按照某種規則 重新排列成一個 有序的集合或者序列。分為內排序、外排序。排序算法的好壞直接影響程序的執行速度以及存儲空間的占有量。
(2)什么是內排序?外排序?
內排序:指待排序的序列完全存放在內存中所進行的排序過程(不適合大量數據排序)。
外排序:指大數據的排序,待排序的數據無法一次性讀取到內存中,內存與外存需進行多次數據交換,以達到排序的目的。
(3)什么是穩定排序?
穩定排序指的是 相等的數據經過某種排序算法排序后,仍能保證它們的相對順序與未排序之前相同。
比如一個序列 a1 a2 a3 a4 a5, 且 a1 < a2 = a3 < a4 < a5。
若經過某種排序算法后,結果仍為 a1 < a2 = a3 < a4 < a5,那么該排序算法是穩定的。
若經過某種排序算法后,結果為 a1 < a3 = a2 < a4 < a5,那么該排序算法是不穩定的。
2、內排序分類
(1)按種類划分:
插入排序:直接插入排序、希爾排序。
選擇排序:選擇排序、堆排序。
交換排序:冒泡排序、快速排序。
歸並排序:歸並排序。
(2)按穩定排序划分:
穩定排序:冒泡排序、歸並排序、直接插入排序。
非穩定排序:快速排序、希爾排序、堆排序、選擇排序。
(3)比較:
排序算法 最好時間 平均時間 最壞時間 空間復雜度 穩定性 直接插入排序 O(n) O(n^2) O(n^2) O(1) 穩定 希爾排序 O(n) O(nlogn) O(n^s) (1<s<2) O(1) 不穩定 選擇排序 O(n^2) O(n^2) O(n^2) O(1) 不穩定 堆排序 O(nlogn) O(nlogn) O(nlogn) O(1) 不穩定 冒泡排序 O(n) O(n^2) O(n^2) O(1) 穩定 快速排序 O(nlogn) O(nlogn) O(n^2) O(logn) 不穩定 歸並排序 O(nlogn) O(nlogn) O(nlogn) O(n) 穩定
二、內排序 -- 穩定排序
1、 冒泡排序(Bubble Sort)
(1)基本原理:(以升序為例)
對於給定n個數據,從第一個數據開始,與相鄰的數據比對,若當前數據大於后面數據,則交換位置,否則不交換位置,然后接着從相鄰位置處開始與后一位置的數據比較,直至到最后一個數據,經一次冒泡過程后,n個數據中最大的數在第n位置上。同理,接下來的冒泡是對前 n-1 個數據進行比較。
(2)舉例:
【給數據 {38, 65, 97, 76, 13, 27, 49} 排序,並按照從小到大的順序輸出】 第一次冒泡: Step1: 比較 38 和 65, 38 < 65, 不交換位置。當前數據順序 38 65 97 76 13 27 49 Step2: 比較 65 和 97, 65 < 97, 不交換位置。當前數據順序 38 65 97 76 13 27 49 Step3: 比較 97 和 76, 97 > 76, 交換位置。當前數據順序 38 65 76 97 13 27 49 Step4: 比較 97 和 13, 97 > 13, 交換位置。當前數據順序 38 65 76 13 97 27 49 Step5: 比較 97 和 27, 97 > 27, 交換位置。當前數據順序 38 65 76 13 27 97 49 Step6: 比較 97 和 49, 97 > 49, 交換位置。當前數據順序 38 65 76 13 27 49 97 經過第一次冒泡,得到最大的數 97,且在最右側。 接下來只需同理在 除了 97 這個數據的集合中選出最大值即可。 第二次冒泡: 當前數據順序 38 65 13 27 49 76 97 第三次冒泡: 當前數據順序 38 13 27 49 65 76 97 第四次冒泡: 當前數據順序 13 27 38 49 65 76 97 第五次冒泡: 當前數據順序 13 27 38 49 65 76 97 第六次冒泡: 當前數據順序 13 27 38 49 65 76 97
(3)代碼:
【src/sort/BubbleSort.java】 package sort; import java.util.Arrays; public class BubbleSort { public static void main(String[] args) { int[] arrays = new int[] {38, 65, 97, 76, 13, 27, 49}; System.out.println("=================從小到大排列===================="); System.out.println("冒泡排序前:" + Arrays.toString(arrays)); bubbleSort(arrays,false); System.out.println("冒泡排序后:" + Arrays.toString(arrays)); System.out.println("================================================"); System.out.println("=================數據有序時,最小時間復雜度 O(n)===================="); System.out.println("冒泡排序前:" + Arrays.toString(arrays)); bubbleSort(arrays,false); System.out.println("冒泡排序后:" + Arrays.toString(arrays)); System.out.println("================================================"); System.out.println("=================從大到小排列===================="); System.out.println("冒泡排序前:" + Arrays.toString(arrays)); bubbleSort(arrays,true); System.out.println("冒泡排序后:" + Arrays.toString(arrays)); System.out.println("================================================"); } /** * 冒泡排序,此方法用於 將數組排序,從小到大(或從大到小)輸出。 * 交換規則未采用第三方變量,是采用 A = A + B; B = A - B; A = A - B; 的形式。 * @param arrays 待排序的數組 * @param reverse 冒泡規則。 true(表示從大到小排序),false(表示從小到大排序) */ public static void bubbleSort(int[] arrays, boolean reverse) { // 用於判斷當前數組是否有序,true表示無序,需要進行排序,false表示有序,不用排序 boolean sortFlag = false; // 第一個循環用於定義執行冒泡的次數, n 個數據需執行 n-1 次冒泡 for(int i = 0; i < arrays.length - 1; i++) { // 第二個循環用於定義每次冒泡時比較的次數,第 i 次冒泡,需比較 n - i 次 for(int j = 0; j < arrays.length - 1 - i; j++) { if (!reverse) { // 比較數據,將大的數據向后移動 if (arrays[j] > arrays[j+1]) { arrays[j] += arrays[j+1]; arrays[j+1] = arrays[j] - arrays[j+1]; arrays[j] -= arrays[j+1]; // 發生數據比較時,設標志為 true,即當前數據需要進行排序操作 sortFlag = true; } } else { // 比較數據,將小的數據向后移動 if (arrays[j] < arrays[j+1]) { arrays[j] += arrays[j+1]; arrays[j+1] = arrays[j] - arrays[j+1]; arrays[j] -= arrays[j+1]; sortFlag = true; } } } System.out.println("第" + (i + 1) + "次冒泡:" + Arrays.toString(arrays)); // 當排序標志為 false時,即第一次冒泡確認數據不需要排序,直接退出循環,不進行接下來的冒泡操作 if (!sortFlag) { break; } } } }
(4)輸出結果
=================從小到大排列==================== 冒泡排序前:[38, 65, 97, 76, 13, 27, 49] 第1次冒泡:[38, 65, 76, 13, 27, 49, 97] 第2次冒泡:[38, 65, 13, 27, 49, 76, 97] 第3次冒泡:[38, 13, 27, 49, 65, 76, 97] 第4次冒泡:[13, 27, 38, 49, 65, 76, 97] 第5次冒泡:[13, 27, 38, 49, 65, 76, 97] 第6次冒泡:[13, 27, 38, 49, 65, 76, 97] 冒泡排序后:[13, 27, 38, 49, 65, 76, 97] ================================================ =================數據有序時,最小時間復雜度 O(n)==================== 冒泡排序前:[13, 27, 38, 49, 65, 76, 97] 第1次冒泡:[13, 27, 38, 49, 65, 76, 97] 冒泡排序后:[13, 27, 38, 49, 65, 76, 97] ================================================ =================從大到小排列==================== 冒泡排序前:[13, 27, 38, 49, 65, 76, 97] 第1次冒泡:[27, 38, 49, 65, 76, 97, 13] 第2次冒泡:[38, 49, 65, 76, 97, 27, 13] 第3次冒泡:[49, 65, 76, 97, 38, 27, 13] 第4次冒泡:[65, 76, 97, 49, 38, 27, 13] 第5次冒泡:[76, 97, 65, 49, 38, 27, 13] 第6次冒泡:[97, 76, 65, 49, 38, 27, 13] 冒泡排序后:[97, 76, 65, 49, 38, 27, 13] ================================================
(5)分析:
分析上面的代碼、數據。
若數據中出現相同的值,且相同值比較的過程中不會出現交換值的情況,故排序是穩定的。
當數據有序時,即排序前后數據順序一致的情況。此時需要執行 1 次冒泡(用於確認是否需要進行排序),進行 n - 1 次比較,但是不會進行數據交換。此時為最好的情況,時間復雜度為 O(n-1),即 O(n)。
當數據反序時,即排序前后數據順序正好相反的情況。此時需要執行 n - 1 次冒泡,且第 i 次冒泡就得執行 n - i 次 比較,然后執行數據交換操作。此時為最壞的情況,時間復雜度為 O(1 + 2 + ... + n - 1) = O(n(n-1)/2) ,即 O(n^2)。
至於空間復雜度,指的就是算法中所需要的輔助空間。如上述代碼中,空間復雜度為0,若進行數據交換的代碼采用第三方變量的形式,那么空間復雜度為 O(1)。
【空間復雜度為0:】 a = a + b; b = a - b; a = a - b; 【空間復雜度為1:】 t = a; a = b; b = t;
2、 直接插入排序(Insertion Sort)
(1)基本原理:
對於給定的一組數據,初始時假設第一個元素為一個有序序列,其余元素為無序序列,從第二個數據開始,按照大小將該數據插入有序序列中,形成一個新有序序列,同理直至最后一個數據插入有序序列中。
(2)舉例:
【給數據 {38, 65, 97, 76, 13, 27, 49} 排序,並按照從小到大的順序輸出】 第一次插入: 將原序列分為 {38} 、{65, 97, 76, 13, 27, 49}兩個序列, 將無序序列第一個數 (65) 插入到有序序列中。 得 {38, 65}、 {97, 76, 13, 27, 49} 兩個序列。 同理 第二次插入(97): 得 {38, 65, 97}、{76, 13, 27, 49} 第三次插入(76): 得 {38, 65, 76, 97}、{13, 27, 49} 第四次插入(13): 得 {13, 38, 65, 76, 97}、{27, 49} 第五次插入(27): 得:{13, 27, 38, 65, 76, 97}、{49} 第六次插入(49): 得: {13, 27, 38, 49, 65, 76, 97}
(3)代碼:
【src/sort/InsertionSort.java】 package sort; import java.util.Arrays; public class InsertionSort { public static void main(String[] args) { int[] arrays = new int[]{38, 65, 97, 76, 13, 27, 49}; System.out.println("=================從小到大排列===================="); System.out.println("直接插入排序前:" + Arrays.toString(arrays)); insertionSort(arrays, false); System.out.println("直接插入排序后:" + Arrays.toString(arrays)); System.out.println("================================================"); System.out.println("=================數據有序時,最小時間復雜度 O(n)===================="); System.out.println("直接插入排序前:" + Arrays.toString(arrays)); insertionSort(arrays, false); System.out.println("直接插入排序后:" + Arrays.toString(arrays)); System.out.println("================================================"); System.out.println("=================從大到小排列===================="); System.out.println("直接插入排序前:" + Arrays.toString(arrays)); insertionSort(arrays, true); System.out.println("直接插入排序后:" + Arrays.toString(arrays)); System.out.println("================================================"); } /** * 直接插入排序,此方法用於 將數組排序,從小到大(或從大到小)輸出。 * @param arrays 待排序的數組 * @param reverse 插入規則。 true(表示從大到小排序),false(表示從小到大排序) */ public static void insertionSort(int[] arrays, boolean reverse) { // 第一個循環用於定義執行直接插入的次數, n 個數據需執行 n-1 次插入 for(int i = 1; i <= arrays.length - 1; i++) { int temp = arrays[i]; int j = i; if (!reverse) { if (temp < arrays[j-1]) { // 第二個循環用於定義直接插入的位置 while(j > 0 && temp < arrays[j-1]) { arrays[j] = arrays[j-1]; j--; } // 直接插入的真正操作 arrays[j] = temp; } } else { if (temp > arrays[j-1]) { while(j > 0 && temp > arrays[j-1]) { arrays[j] = arrays[j-1]; j--; } arrays[j] = temp; } } System.out.println("第" + i + "次插入:" + Arrays.toString(arrays)); } } }
(4)結果:
=================從小到大排列==================== 直接插入排序前:[38, 65, 97, 76, 13, 27, 49] 第1次插入:[38, 65, 97, 76, 13, 27, 49] 第2次插入:[38, 65, 97, 76, 13, 27, 49] 第3次插入:[38, 65, 76, 97, 13, 27, 49] 第4次插入:[13, 38, 65, 76, 97, 27, 49] 第5次插入:[13, 27, 38, 65, 76, 97, 49] 第6次插入:[13, 27, 38, 49, 65, 76, 97] 直接插入排序后:[13, 27, 38, 49, 65, 76, 97] ================================================ =================數據有序時,最小時間復雜度 O(n)==================== 直接插入排序前:[13, 27, 38, 49, 65, 76, 97] 第1次插入:[13, 27, 38, 49, 65, 76, 97] 第2次插入:[13, 27, 38, 49, 65, 76, 97] 第3次插入:[13, 27, 38, 49, 65, 76, 97] 第4次插入:[13, 27, 38, 49, 65, 76, 97] 第5次插入:[13, 27, 38, 49, 65, 76, 97] 第6次插入:[13, 27, 38, 49, 65, 76, 97] 直接插入排序后:[13, 27, 38, 49, 65, 76, 97] ================================================ =================從大到小排列==================== 直接插入排序前:[13, 27, 38, 49, 65, 76, 97] 第1次插入:[27, 13, 38, 49, 65, 76, 97] 第2次插入:[38, 27, 13, 49, 65, 76, 97] 第3次插入:[49, 38, 27, 13, 65, 76, 97] 第4次插入:[65, 49, 38, 27, 13, 76, 97] 第5次插入:[76, 65, 49, 38, 27, 13, 97] 第6次插入:[97, 76, 65, 49, 38, 27, 13] 直接插入排序后:[97, 76, 65, 49, 38, 27, 13] ================================================
(5)分析:
分析上面的代碼、數據。
若數據中出現相同的值,且相同值比較的過程中不會出現交換值的情況,故排序是穩定的。
當數據有序時,即排序前后數據順序一致的情況。此時需要執行 n - 1 次插入操作,但是不會進行數據的交換。此時為最好的情況,時間復雜度為 O(n-1),即 O(n)。
當數據反序時,即排序前后數據順序正好相反的情況。此時需要執行 n - 1 次插入操作,且第 i 次 插入需要進行 i 次數據比較。此時為最壞的情況,時間復雜度為 O(1 + 2 + ... + n - 1) = O(n(n-1)/2) ,即 O(n^2)。
上述代碼,采用第三方變量用於保存交換的數據,故空間復雜度為 O(1)。
3、歸並排序(Merge Sort)
(1)基本原理:
采用分治法,將數據序列分成足夠小的子序列,並使每個子序列有序,最后將子序列合並成一個完整有序的序列,此處介紹2路歸並。
分治法:就是把一個復雜的問題分解成兩個或多個相似的子問題,然后根據需要將子問題分解成更小的子問題,直至子問題可以簡單地解決,子問題的解的集合即為原問題的解。
2路歸並:采用分治法,將一個數據序列分為兩個子序列(子序列還可分為更小的子序列),並分別進行排序,最后將兩個有序子序列合成一個有序序列。
(2)舉例:
【給數據 {38, 65, 97, 76, 13, 27, 49} 排序,並按照從小到大的順序輸出】 第一次划分: 將原序列分為 A:{38, 65, 97, 76} 、B:{13, 27, 49} 兩個子序列。 對序列 A 划分: 分為 A1:{38, 65}, A2:{97, 76} 兩個子序列。 再對 A1 划分: 分為 A11:{38}、A12:{65} 兩個子序列。 此時 A11、A12 已經足夠小,可以合並成有序序列: A1:{38, 65} 對 A2 划分: 分為 A21:{97}、A22:{76} 兩個子序列。 此時 A21、A22 已經足夠小,可以合並成有序序列: A2:{76, 97} 合並 A1、A2: A:{38, 65, 76, 97} 同理划分並合並 B: B:{13, 27, 49} 合並 A、B: {13, 27, 38, 49, 65, 76, 97} 形如: {38, 65, 97, 76, 13, 27, 49} 划分: {38, 65, 97, 76} {13, 27, 49} {38, 65} {97, 76} {13, 27} {49} {38} {65} {97} {76} {13} {27} {49} 合並: {38, 65} {76, 97} {13, 27} {49} {38, 65, 76, 97} {13, 27, 49} {13, 27, 38, 49, 65, 76, 97}
(3)代碼:
【src/sort/MergeSort.java】 package sort; import java.util.Arrays; public class MergeSort { public static void main(String[] args) { int[] arrays = new int[]{38, 65, 97, 76, 13, 27, 49}; // 原序列 System.out.println("====================原序列====================="); System.out.println(Arrays.toString(arrays)); System.out.println("================================================"); int[] result = mergeSort(arrays); // 歸並排序后的序列 System.out.println("==============歸並排序后的序列==================="); System.out.println(Arrays.toString(result)); System.out.println("================================================"); } /** * 用於划分序列 * Arrays.copyOfRange(T[ ] original,int from,int to) 用於將一個原數組復制到一個新數組,數據范圍為 from <= x < to。 */ public static int[] mergeSort(int[] arrays) { // 如果序列已經足夠小,可以返回該序列並進行合並 if (arrays.length < 2) return arrays; // 若序列還可划分,則繼續划分 int[] left = Arrays.copyOfRange(arrays, 0, (arrays.length + 1) / 2); int[] right = Arrays.copyOfRange(arrays, (arrays.length + 1) / 2, arrays.length); // 划分到最小序列后,得合並序列 return merge(mergeSort(left), mergeSort(right)); } /** * 用於合並序列。 * Arrays.toString(T[]) 用於輸出一個數組。 */ public static int[] merge(int[] left, int[] right) { // 用於保存合並后的序列 int[] result = new int[left.length + right.length]; // 循環將數據填入新數組中,index 用於表示新序列當前位置,i 表示 left 序列數據位置,j 表示 right 序列數據位置。 for (int index = 0, i = 0, j = 0; index < result.length; index++) { if (i >= left.length) { // 如果 left 序列已經填入完畢,則新序列后面的數據將由 right 填充。 result[index] = right[j++]; } else if (j >= right.length) { // 如果 right 序列已經填入完畢,則新序列后面的數據將由 left 填充。 result[index] = left[i++]; } else if (left[i] > right[j]) { // 如果 left、right 數據均未填充完畢,則比較當前數據,將較小的數據填入 result[index] = right[j++]; } else { // 如果 left、right 數據均未填充完畢,則比較當前數據,將較大的數據填入 result[index] = left[i++]; } } // 划分后的 left 序列 System.out.println("left:" + Arrays.toString(left)); // 划分后的 right 序列 System.out.println("right" + Arrays.toString(right)); // 合並的 result 序列 System.out.println("result" + Arrays.toString(result)); System.out.println(); return result; } }
(4)結果:
====================原序列===================== [38, 65, 97, 76, 13, 27, 49] ================================================ left:[38] right[65] result[38, 65] left:[97] right[76] result[76, 97] left:[38, 65] right[76, 97] result[38, 65, 76, 97] left:[13] right[27] result[13, 27] left:[13, 27] right[49] result[13, 27, 49] left:[38, 65, 76, 97] right[13, 27, 49] result[13, 27, 38, 49, 65, 76, 97] ==============歸並排序后的序列=================== [13, 27, 38, 49, 65, 76, 97] ================================================
(5)分析:
分析上面的代碼、數據。
若數據中出現相同的值,且相同值比較的過程中不會出現交換值的情況,故排序是穩定的。
無論數據是否有序,都會進行 划分操作余合並操作。划分操作最后形如二叉樹,而二叉樹的高度為 floor(logn) + 1,合並操作每層的操作均為 n,即時間復雜度為 O(n * ( floor(logn) + 1)) = O(nlogn)。即最好最壞的時間復雜度均為 O(nlogn)。
三、內排序 -- 非穩定排序
1、快速排序(Quick Sort)
(1)基本原理:
采用分治法,選擇一個基准,通過一趟排序,將一組序列分成左右兩個序列,且左邊的序列均小於右邊的序列,再分別對左右序列進行類似的排序,分成更小的左右序列,直至所有的序列有序。
注:
快速排序與歸並排序的區別:
快速排序主旨是根據元素的值划分,大的序列為一組,小的序列為一組,然后對兩個序列進行進一步的划分,最后直接合並序列即可得到有序的序列。即先排序、再遞歸細分序列。
歸並排序主旨是根據元素的數量划分,比如 2n 個數對半切開(2路歸並),下標為 0 ~ n 的數為一組,下標 n ~ 2n 的數為一組,然后對兩個序列進行進一步的划分,最后需要兩個序列相互比較后,合並成一個有序的序列。 即先遞歸細分序列、再排序。
(2)舉例:(雙路快排)
雙路快排:從序列的兩端向中間挺近,建立兩個區,一個小於等於區(左側),一個大於等於區(右側)。先從某一側開始,比如從右側開始,若出現值小於基准值,則需將值交換到左側,並從左側開始逼近。當左側出現大於基准值,則需將值交換到右側,並從右側開始逼近。如此往復,直至左側與右側出現重合,此時重合點即為新的基准點。
【給數據 {38, 65, 97, 76, 13, 27, 49} 排序,並按照從小到大的順序輸出】 雙路快排從兩端向中間靠近,設兩個值 start、end. 對於第一趟排序,首先選擇第一個值作為基准,選 index = 38,start = 0, end = 7。 從右側開始向中間逼近: 38 < 49, 不用交換。end-- 38 > 37, 需要交換。此時序列為 {27, 65, 97, 76, 13, 27, 49}。start++ 從左側開始向中間逼近: 38 < 65, 需要交換。此時序列為 {27, 65, 97, 76, 13, 65, 49}。end-- 從右側開始向中間逼近: 38 > 13, 需要交換。此時序列為 {27, 13, 97, 76, 13, 65, 49}。start++ 從左側開始向中間逼近: 38 < 97, 需要交換。此時序列為 {27, 13, 97, 76, 97, 65, 49}。end-- 從右側開始向中間逼近: 38 < 76, 不用交換。end-- 此時 start 與 end 重合,即出現新的基准點,將基准值交換到此處,序列為 {27, 13, 38, 76, 97, 65, 49} 同理對 {27, 13, 38}、{76, 97, 65, 49}進行排序。 最終得到序列 {13, 27, 38, 49, 65, 76, 97}
(3)代碼:
【src/sort/QuickSort.java】 package sort; import java.util.Arrays; public class QuickSort { public static void main(String[] args) { int[] arrays = new int[]{38, 65, 97, 76, 13, 27, 49}; System.out.println("====================原序列====================="); System.out.println(Arrays.toString(arrays)); System.out.println("================================================"); quickSort(arrays, 0, arrays.length - 1); System.out.println("\n====================快速排序后====================="); System.out.println(Arrays.toString(arrays)); System.out.println("================================================"); } /** * 快速排序 * @param arrays 待排序序列 * @param start 序列頭下標 * @param end 序列尾下標 */ public static void quickSort(int[] arrays, int start, int end) { if (start < end) { // 進行一次排序,並得到新的基准下標 int index = getIndex(arrays, start, end); // 對基准左側進行排序 if (index > start) { quickSort(arrays, start, index - 1); } // 對基准右側進行排序 if (index < end) { quickSort(arrays, index + 1, end); } } } /** * 雙路快排,並獲取基准下標 * @param arrays 待排序的序列 * @param start 序列頭下標 * @param end 序列尾下標 * @return 返回基准下標 */ public static int getIndex(int[] arrays, int start, int end) { // 選取序列第一個值為基准 int index = arrays[start]; // 開始從兩邊向中間比較 while (start < end) { // 從右側向中間逼近 while (start < end && arrays[end] >= index) { end--; } // 如果 arrays[end] < index,即右側出現小於基准的值,則將該值交換到左側,且左側下標增1 if (start < end) { arrays[start++] = arrays[end]; // 打印右側逼近過程 System.out.println("\n====start====" + start + "====end====" + end + "====基准值====" + index); System.out.println(Arrays.toString(arrays)); } // 從左側向中間逼近 while (start < end && arrays[start] <= index) { start++; } // 如果 arrays[start] > index,即左側出現大於基准的數,將該值交換到右側,且右側下標減1 if (start < end) { arrays[end--] = arrays[start]; // 打印左側逼近過程 System.out.println("\n====start====" + start + "====end====" + end + "====基准值====" + index); System.out.println(Arrays.toString(arrays)); } } // start >= end,即新基准下標出現,將基准值替換到中間 arrays[start] = index; // 打印一次排序后的結果 System.out.println("\n====一次排序后的序列===="); System.out.println(Arrays.toString(arrays)); // 返回新的基准下標 return start; } }
(4)結果:
====================原序列===================== [38, 65, 97, 76, 13, 27, 49] ================================================ ====start====1====end====5====基准值====38 [27, 65, 97, 76, 13, 27, 49] ====start====1====end====4====基准值====38 [27, 65, 97, 76, 13, 65, 49] ====start====2====end====4====基准值====38 [27, 13, 97, 76, 13, 65, 49] ====start====2====end====3====基准值====38 [27, 13, 97, 76, 97, 65, 49] ====一次排序后的序列==== [27, 13, 38, 76, 97, 65, 49] ====start====1====end====1====基准值====27 [13, 13, 38, 76, 97, 65, 49] ====一次排序后的序列==== [13, 27, 38, 76, 97, 65, 49] ====start====4====end====6====基准值====76 [13, 27, 38, 49, 97, 65, 49] ====start====4====end====5====基准值====76 [13, 27, 38, 49, 97, 65, 97] ====start====5====end====5====基准值====76 [13, 27, 38, 49, 65, 65, 97] ====一次排序后的序列==== [13, 27, 38, 49, 65, 76, 97] ====一次排序后的序列==== [13, 27, 38, 49, 65, 76, 97] ====================快速排序后===================== [13, 27, 38, 49, 65, 76, 97] ================================================
(5)分析:
分析上面的代碼、數據。
若數據中出現相同的值,比如:{38, 65, 97, 76, 13, 27, 13} 經過第一次快排,最后一位的 13 移到第一位, 此時,兩個13的先后順序已經發生了變化,即排序是非穩定的。
最優時,時間復雜度計算類似於歸並排序,即 O(nlogn)。
最壞時,代碼中出現雙層循環,時間復雜度會退化成 O(n^2)。
2、選擇排序(Selection Sort)
(1)基本原理:
簡單直觀的排序,給定一組序列,從序列中選出最小的值與第一個元素交換位置,從剩余元素中選出最小的值與第二個元素交換位置,同理,直至剩下最后一個數據。
(2)舉例:
【給數據 {38, 65, 97, 76, 13, 27, 49} 排序,並按照從小到大的順序輸出】 第一次排序: 最小值為 13,交換后得:{13, 65, 97, 76, 38, 27, 49} 第二次排序: 最小值為 27,交換后得:{13, 27, 97, 76, 38, 65, 49} 第三次排序: 最小值為 38,交換后得:{13, 27, 38, 76, 97, 65, 49} 第四次排序: 最小值為 49,交換后得:{13, 27, 38, 49, 97, 65, 76} 第五次排序: 最小值為 65,交換后得:{13, 27, 38, 49, 65, 97, 76} 第六次排序: 最小值為 76,交換后得:{13, 27, 38, 49, 65, 76,97}
(3)代碼:
【src/sort/SelectionSort.java】 package sort; import java.util.Arrays; public class SelectionSort { public static void main(String[] args) { int[] arrays = new int[]{38, 65, 97, 76, 13, 27, 49}; System.out.println("====================原序列====================="); System.out.println(Arrays.toString(arrays)); System.out.println("================================================"); selectionSort(arrays); System.out.println("\n====================選擇排序后====================="); System.out.println(Arrays.toString(arrays)); System.out.println("================================================"); } /** * 選擇排序 * @param arrays 待排序的序列 */ public static void selectionSort(int[] arrays) { // 第一個循環定義排序的次數 for (int i = 0; i < arrays.length - 1; i++) { // 用於保存最小值的下標 int min = i; // 第二個循環用於比較出最小值的位置 for (int j = i + 1; j <= arrays.length - 1; j++) { // 找到最小值的下標 if (arrays[min] > arrays[j]) { min = j; } } // 交換值,第 i 次排序,最小值與第 i 個值交換 swap(arrays, i, min); // 打印排序過程 System.out.println("\n============第" + (i + 1) + "次排序============="); System.out.println(Arrays.toString(arrays)); } } /** * 交換序列中的兩個值 * @param arrays 待排序的序列 * @param oldIndex 交換值A * @param newIndex 交換值B */ public static void swap(int[] arrays, int oldIndex, int newIndex) { arrays[oldIndex] += arrays[newIndex]; arrays[newIndex] = arrays[oldIndex] - arrays[newIndex]; arrays[oldIndex] -= arrays[newIndex]; } }
(4)結果:
====================原序列===================== [38, 65, 97, 76, 13, 27, 49] ================================================ ============第1次排序============= [13, 65, 97, 76, 38, 27, 49] ============第2次排序============= [13, 27, 97, 76, 38, 65, 49] ============第3次排序============= [13, 27, 38, 76, 97, 65, 49] ============第4次排序============= [13, 27, 38, 49, 97, 65, 76] ============第5次排序============= [13, 27, 38, 49, 65, 97, 76] ============第6次排序============= [13, 27, 38, 49, 65, 76, 97] ====================選擇排序后===================== [13, 27, 38, 49, 65, 76, 97] ================================================
(5)分析:
分析上面的代碼、數據。
不管數據是否有序,其均會執行 n * n 次,即最壞、最好時間復雜度均為 O(n^2)。
若數據中出現相同的值,比如:{38, 65, 97, 38, 13, 27, 49} 第一次選擇排序時選擇最小值 13 與第一個值 38置換。 此時,兩個 38 的先后順序已經發生了變化,即排序是非穩定的。
3、希爾排序(Shell Sort)
(1)基本原理:
希爾排序又稱縮小增量排序,其本質屬於一種插入排序。將一個序列按照增量分成多個序列,子序列分別進行插入排序,待序列基本有序后(即增量變為1時),最后進行一個直接插入排序。
(2)舉例:
常用增量為 序列長度 / 2,直至為 1,即 增量序列為 {序列長度 / 2, ... , 1}。
增量可以理解為步長,比如一個序列 {38, 65, 97, 76, 13, 27, 49} ,長度為 7,步長為 3,則可以將其分為序列:{38, 76, 49}、{65, 13}、{97, 27}。然后分別對子序列進行插入排序。
【給數據 {38, 65, 97, 76, 13, 27, 49} 排序,並按照從小到大的順序輸出】 增量序列為:{3, 1} 則第一次以 增量 3 划分序列為 {38, 76, 49}、{65, 13}、{97, 27} 對其進行插入排序得:{38, 49, 76}, {13, 65}, {27, 97} 即:{38, 13, 27, 49, 65, 97, 76},此時增量 3 的插入排序執行完畢。 執行增量為 1 的插入排序,即直接插入排序,(參考上面的直接插入排序,此處省略步驟) 最后得到:{13, 27, 38, 49, 65, 76, 97}
(3)代碼:
【src/sort/ShellSort.java】 package sort; import java.util.Arrays; public class ShellSort { public static void main(String[] args) { int[] arrays = new int[]{38, 65, 97, 76, 13, 27, 49}; System.out.println("====================原序列====================="); System.out.println(Arrays.toString(arrays)); System.out.println("================================================"); shellSort(arrays); System.out.println("\n====================希爾排序后====================="); System.out.println(Arrays.toString(arrays)); System.out.println("================================================"); } /** * 希爾排序 * @param arrays 待排序的序列 */ public static void shellSort(int[] arrays) { // 設置增量,一般為 序列長度 / 2 int interval = arrays.length/2; // 對每個增量進行插入排序 while(interval > 0) { // 根據增量去定義執行插入排序的次數,與直接插入排序類似,直接插入排序步長為1,此處步長為 interval for (int i = interval, j, temp; i < arrays.length; i++) { // 用於保存當前待插入的值 temp = arrays[i]; // 每次向前減 interval,用於確定插入的位置 j = i - interval; while(j >= 0 && arrays[j] > temp) { // 找到該位置,並將值向后移 arrays[j + interval] = arrays[j]; j -= interval; } // 插入的真正操作 arrays[j + interval] = temp; // 打印排序過程 System.out.println("\n==========interval====" + interval + "=========="); System.out.println(Arrays.toString(arrays)); } // 增量每次遞減 interval /= 2; } } }
(4)結果:
====================原序列===================== [38, 65, 97, 76, 13, 27, 49] ================================================ ==========interval====3========== [38, 65, 97, 76, 13, 27, 49] ==========interval====3========== [38, 13, 97, 76, 65, 27, 49] ==========interval====3========== [38, 13, 27, 76, 65, 97, 49] ==========interval====3========== [38, 13, 27, 49, 65, 97, 76] ==========interval====1========== [13, 38, 27, 49, 65, 97, 76] ==========interval====1========== [13, 27, 38, 49, 65, 97, 76] ==========interval====1========== [13, 27, 38, 49, 65, 97, 76] ==========interval====1========== [13, 27, 38, 49, 65, 97, 76] ==========interval====1========== [13, 27, 38, 49, 65, 97, 76] ==========interval====1========== [13, 27, 38, 49, 65, 76, 97] ====================希爾排序后===================== [13, 27, 38, 49, 65, 76, 97] ================================================
(5)分析:
分析上面的代碼、數據。
若數據中出現相同的值,比如:{38, 65, 27, 27, 13, 37, 49} 第一次希爾排序時,步長為 3, 27 與 38 交換, 此時,兩個 27 的先后順序已經發生了變化,即排序是非穩定的。
4、堆排序(Heap Sort)
(1)基本原理:
堆是一種樹形結構,即完全二叉樹。可分為最大堆、最小堆。最大堆指的是每個節點均大於其左右孩子節點(升序),最小堆指的是每個節點均小於其左右孩子節點(降序)。
以最大堆為例,先將序列初始化成一個堆,找到棧頂元素並與序列最后一個數據互換位置(初始化堆后,棧頂元素最大,將其放於序列末尾),接着對剩下的數據排成堆(重建堆),進行類似的操作,直至最后一個數據。
注:
完全二叉樹的特點:若 n 個節點的完全二叉樹從左到右編號(即 0 ~ n-1),
那么序號為 0 的節點為 根節點。
對於第 i 個(i 從 1 開始計數)位置的節點,
其父節點的編號為 (i - 1) / 2.
其左孩子節點的編號為 2 * i + 1.
其右孩子節點的編號為 2 * i + 2.
(2)舉例:
【給數據 {38, 65, 97, 76, 13, 27, 49} 排序,並按照從小到大的順序輸出】 以初始化堆為例: 結構為: 38 65 97 76 13 27 49 調整初始化堆: 第一趟:比較節點 97,以及其孩子27, 49, 節點大於孩子,不用調整。 38 65 97 76 13 27 49 即{38, 65, 97, 76, 13, 27, 49} 第二趟:比較節點 65,以及其孩子76, 13, 65 < 76,交換得 38 76 97 65 13 27 49 即{38, 76, 97, 65, 13, 27, 49} 第三趟:由於出現交換,對交換后的點進行大頂堆化,此時比較節點65,沒有孩子,不用調整。 38 76 97 65 13 27 49 即{38, 76, 97, 65, 13, 27, 49} 第四趟:此時比較節點38,以及其孩子65,97, 38 < 97,交換, 此時堆序列為 97 76 38 65 13 27 49 即{97, 76, 38, 65, 13, 27, 49} 第五趟:由於出現交換,對交換后的點進行大頂堆化,此時比較節點38,以及其孩子27,49, 38 < 49,交換,[97, 76, 49, 65, 13, 27, 38] 此時堆序列為 97 76 49 65 13 27 38 即{97, 76, 49, 65, 13, 27, 38} 第六趟:此時比較節點38,沒有孩子,不用調整。 此時堆序列為 97 76 49 65 13 27 38 即{97, 76, 49, 65, 13, 27, 38} 至此,初始化堆完成。 交換根節點以及序列最大值, 此時堆序列為 38 76 49 65 13 27 97 即{97, 76, 49, 65, 13, 27, 38} 同理:對除了97的元素進行堆排序。過程省略。。。
(3)代碼:
【src/sort/HeapSort.java】 package sort; import java.util.Arrays; public class HeapSort { public static void main(String[] args) { int[] arrays = new int[]{38, 65, 97, 76, 13, 27, 49}; System.out.println("====================原序列====================="); System.out.println(Arrays.toString(arrays)); System.out.println("================================================"); buildMaxHeap(arrays); System.out.println("\n====================選擇排序后====================="); System.out.println(Arrays.toString(arrays)); System.out.println("================================================"); } /** * 構建最大堆,初始化堆 * @param arrays 待排序的序列 */ public static void buildMaxHeap(int[] arrays) { // 初始化堆,對每個父節點進行大堆化,調整位置 for (int i = arrays.length / 2 - 1; i >= 0; i--) { // 調整父節點的位置 adjustHeap(arrays, i, arrays.length); } // 循環將堆頂的值交換到序列末尾,並對剩余的數據進行大堆化調整 for (int j = arrays.length - 1; j >= 0; j--) { // 將堆頂的值交換到序列末尾 swap(arrays, 0, j); // 打印交換后的序列 System.out.println("\n==調整最大堆==swap(" + arrays[0] + ", " + arrays[j] + ")===="); System.out.println(Arrays.toString(arrays)); // 將剩余數據進行大堆化 adjustHeap(arrays, 0, j); } } /** * 調整節點的位置 * @param arrays 待排序的序列 * @param start 序列開始位置 * @param length 序列的長度 */ public static void adjustHeap(int[] arrays, int start, int length) { // 保存最大值的位置,初始為父節點 int maxIndex = start; // 保存父節點左孩子的位置 int leftChildren = start * 2 + 1; // 保存父節點右孩子的位置 int rightChildren = start * 2 + 2; // 如果左孩子存在,且大於父節點 if (leftChildren < length && arrays[leftChildren] > arrays[maxIndex]) { // 記錄左孩子的位置,更新最大值的位置 maxIndex = leftChildren; } // 如果右孩子存在,且大於父節點 if (rightChildren < length && arrays[rightChildren] > arrays[maxIndex]) { // 記錄右孩子的位置,更新最大值的位置 maxIndex = rightChildren; } // 如果最大值位置發生變化 if (maxIndex != start) { // 交換父節點與最大值的位置 swap(arrays, maxIndex, start); // 打印交換后的序列 System.out.println("\n====swap(" + arrays[maxIndex] + ", " + arrays[start] + ")===="); System.out.println(Arrays.toString(arrays)); // 對新的最大值的位置進行大堆化調整 adjustHeap(arrays, maxIndex, length); } else { // 打印無須交換的序列 System.out.println("\n=======無須交換值======" + arrays[maxIndex]); System.out.println(Arrays.toString(arrays)); } } /** * 交換序列中的兩個值 * @param arrays 待排序的序列 * @param oldIndex 交換值A * @param newIndex 交換值B */ public static void swap(int[] arrays, int oldIndex, int newIndex) { if (oldIndex != newIndex) { arrays[oldIndex] += arrays[newIndex]; arrays[newIndex] = arrays[oldIndex] - arrays[newIndex]; arrays[oldIndex] -= arrays[newIndex]; } } }
(4)結果:
====================原序列===================== [38, 65, 97, 76, 13, 27, 49] ================================================ =======無須交換值======97 [38, 65, 97, 76, 13, 27, 49] ====swap(65, 76)==== [38, 76, 97, 65, 13, 27, 49] =======無須交換值======65 [38, 76, 97, 65, 13, 27, 49] ====swap(38, 97)==== [97, 76, 38, 65, 13, 27, 49] ====swap(38, 49)==== [97, 76, 49, 65, 13, 27, 38] =======無須交換值======38 [97, 76, 49, 65, 13, 27, 38] ==調整最大堆==swap(38, 97)==== [38, 76, 49, 65, 13, 27, 97] ====swap(38, 76)==== [76, 38, 49, 65, 13, 27, 97] ====swap(38, 65)==== [76, 65, 49, 38, 13, 27, 97] =======無須交換值======38 [76, 65, 49, 38, 13, 27, 97] ==調整最大堆==swap(27, 76)==== [27, 65, 49, 38, 13, 76, 97] ====swap(27, 65)==== [65, 27, 49, 38, 13, 76, 97] ====swap(27, 38)==== [65, 38, 49, 27, 13, 76, 97] =======無須交換值======27 [65, 38, 49, 27, 13, 76, 97] ==調整最大堆==swap(13, 65)==== [13, 38, 49, 27, 65, 76, 97] ====swap(13, 49)==== [49, 38, 13, 27, 65, 76, 97] =======無須交換值======13 [49, 38, 13, 27, 65, 76, 97] ==調整最大堆==swap(27, 49)==== [27, 38, 13, 49, 65, 76, 97] ====swap(27, 38)==== [38, 27, 13, 49, 65, 76, 97] =======無須交換值======27 [38, 27, 13, 49, 65, 76, 97] ==調整最大堆==swap(13, 38)==== [13, 27, 38, 49, 65, 76, 97] ====swap(13, 27)==== [27, 13, 38, 49, 65, 76, 97] =======無須交換值======13 [27, 13, 38, 49, 65, 76, 97] ==調整最大堆==swap(13, 27)==== [13, 27, 38, 49, 65, 76, 97] =======無須交換值======13 [13, 27, 38, 49, 65, 76, 97] ==調整最大堆==swap(13, 13)==== [13, 27, 38, 49, 65, 76, 97] =======無須交換值======13 [13, 27, 38, 49, 65, 76, 97] ====================選擇排序后===================== [13, 27, 38, 49, 65, 76, 97] ================================================
(5)分析:
分析上面的代碼、數據。
若數據中出現相同的值,比如:{38, 13, 13, 76, 36, 37, 49} 38 13 13 76 36 27 49 此時進行初始化堆第一次時,13, 76, 36比較,交換 13, 76,得 38 76 13 13 36 27 49 此時序列為:{38, 76, 13, 13, 36, 37, 49},兩個 13 的順序已發生變化,故排序是非穩定的。