其中每個算法都有其相應的時間複雜度和空間複雜度,這裏我也對它們做了一個彙總:
排序算法 |
時間複雜度 |
空間複雜度 |
穩定性 |
複雜性 |
||
平均情況 |
最壞情況 |
最好情況 |
||||
冒泡排序 |
O(n^2) |
O(n^2) |
O(n) |
O(1) |
穩定 |
簡單 |
快速排序 |
O(nlog2n) |
O(n^2) |
O(nlog2n) |
O(nlog2n) |
不穩定 |
較複雜 |
直接插入排序 |
O(n^2) |
O(n^2) |
O(n) |
O(1) |
穩定 |
簡單 |
希爾排序 |
O(nlog2n) |
O(nlog2n) |
- |
O(1) |
不穩定 |
較複雜 |
直接選擇排序 |
O(n^2) |
O(n^2) |
O(n^2) |
O(1) |
不穩定 |
簡單 |
堆排序 |
O(nlog2n) |
O(nlog2n) |
O(nlog2n) |
O(1) |
不穩定 |
較複雜 |
歸併排序 |
O(nlog2n) |
O(nlog2n) |
O(nlog2n) |
O(n) |
穩定 |
較複雜 |
基數排序 |
O(d(n+r)) |
O(d(n+r)) |
O(d(n+r)) |
O(n+r) |
穩定 |
較複雜 |
計數排序 |
O(n+k) |
O(n+k) |
O(n+k) |
O(n+k) |
穩定 |
簡單 |
一些聲明:
本系列用到的所有算法均是以從小到大爲例的。
文章後面將會提到的有序區,無序區是指,在待排序列中,已經排好順序的元素,就認爲其是處於有序區中,還沒有被排序的元素就處於無序區中。
接下來,我們開始進入正文。
交換排序
交換排序主要包括兩種排序算法,分別是冒泡排序和快速排序
冒泡排序
【基本思想】
兩兩比較待排序列中的元素,若其次序相反,則進行交換,直到沒有逆序的元素爲止。
通過無序區中相鄰元素的比較和交換,使最小的元素如氣泡一般逐漸往上漂浮至水面
【空間複雜度】O(1)
【時間複雜度】
平均情況:O(n^2)
最好情況:正序有序時,但對於普通冒泡仍然是O(n^2),比如說下面的算法。對於優化後的冒泡纔是O(n),最後面我會給出優化後的代碼,正序有序時只需要對n個元素進行一趟冒泡排序即可,即複雜度爲O(n)。
最壞情況:逆序有序時,複雜度O(n^2)
【穩定性】穩定
【優點】
簡單,穩定,空間複雜度低
【缺點】
時間複雜度高,效率不好,每次只能移動相鄰兩個元素,比較次數多
【算法實現】
- /**
- * 冒泡排序
- * @param arr
- */
- public static void bubbleSort(int[] arr) {
- for (int i = 0; i < arr.length - 1; i++) {
- for (int j = arr.length - 1; j > i; j--) {
- if (arr[j] < arr[j - 1]) {
- swap(arr, j, j - 1);
- }
- }
- }
- }
- //實現將指定下標的兩個元素在數組中交換位置
- public static void swap(int[] arr, int i, int j) {
- int temp = arr[i];
- arr[i] = arr[j];
- arr[j] = temp;
- }
其中swap(int[] arr, int i, int j)方法,在下文的許多算法中都會用到,後面就不再單獨寫出該方法的實現
【本算法解讀】
算法的外層循環可以理解爲要進行n趟排序,每次排序都可以確定一個最小值。初始時有序區的個數爲0,數組中的所有元素都在無序區。內層循環依次對無序區中相鄰元素進行比較,如果位置靠後的元素小於位置靠前的元素,則交換兩個元素。如此一來,無序區中最小的元素就被交換到了有序區的末尾,然後有序區的元素個數加1(對應代碼:內存循環結束後,i+1),無序區的元素個數減1(對應代碼:無序區是從arr.leng-1到i+1,每次執行i+1時,相應的無序區就會減少)。
重複上述操作,直到所有元素都處在有序區中(即i加到了arr.length),就完成了排序。
【舉個栗子】
對於待排序列4,1,3,2
首先依次比較無序區(初始時爲所有元素)中的相鄰元素,比較3,2,位置靠後的元素小於位置靠前的元素,則交換。序列爲4,1,2,3。繼續比較1,2,位置靠後的元素大於位置靠前的元素,不交換。繼續比較4,1,需要交換,此時無序區中的元素全部比較完畢,一趟冒泡排序結束,序列爲1,4,2,3。可以看到最小元素1已經移動到序列首部,即處於有序區內,確定了一個元素的位置。重複上述操作完成最終排序。
【冒泡排序優化算法實現】
- /**
- * 冒泡排序優化
- * @param arr
- */
- public static void bubbleSortOptimize(int[] arr) {
- boolean didSwap; //標誌位,判斷每完成一趟冒泡排序,是否發生數據交換
- for (int i = 0; i < arr.length - 1; i++) {
- didSwap = false;
- for (int j = arr.length - 1; j > i; j--) {
- if (arr[j] < arr[j - 1]) {
- swap(arr, j, j - 1);
- didSwap = true;
- }
- }
- //如果沒有發生數據交換則終止算法
- //沒有發生數據交換,意味着無序區中的每對相鄰元素的位置都是正確的,即無序區中的元素已經是有序的了
- if (didSwap == false) {
- return;
- }
- }
- }
快速排序
【基本思想】
快速排序又稱爲分區交換排序,是目前已知的平均速度最快的一種排序方法,它採用了一種分治的策略,是對冒泡排序的一種改進。在待排序列中任取其中一個元素(稱其爲目標元素),通常選取第一個元素。以該元素爲分界點(pivot)經過一趟排序後,將待排序列分成兩個部分,所有比分界點小的元素都存放在目標元素之前,所有比分界點大的元素都存放在目標元素之後,然後再分別對這兩個部分重複上述過程,直到每一部分只剩一個元素爲止。顯然,每趟快速排序後,分界點都找到了自己在有序序列中的位置
【空間複雜度】O(nlog2n)取決於遞歸深度
【時間複雜度】
平均情況:O(nlog2n)
最好情況:O(nlog2n)當每次劃分的結果得到的pivot左,右兩個分組的長度大致相等時,快速排序算法的性能最好
最壞情況:O(n^2)當每次選擇分界點均是當前分組的最大關鍵字或最小關鍵字時,快速算法退化爲冒泡算法。
【穩定性】不穩定
【優點】
極快,數據移動少
【缺點】
不穩定,難以在單向鏈表結構上實現
- /**
- * 快速排序
- * @param arr
- * @param left
- * @param right
- */
- public static void quickSort(int arr[], int left, int right) {
- if (left >= right) {
- return;
- }
- int pivot = partition(arr, left, right);
- //通過遞歸不斷將待排序列分成兩部分
- quickSort(arr, left, pivot - 1);
- quickSort(arr, pivot + 1, right);
- }
- //分區,返回目標元素的最終位置
- private static int partition(int[] arr, int left, int right) {
- //每次以子部分的首元素作爲目標元素,並保存在target中
- int target = arr[left];
- while (left < right) {
- //右指針開始移動,找到比目標元素小的元素則停止
- while (right > left && arr[right] >= target) {
- right--;
- }
- if (left < right) {
- //將找到的比目標元素小的元素移動到left指針指向的位置
- arr[left] = arr[right];
- left++;
- }
- while (left < right && arr[left] <= target) {
- left++;
- }
- if (left < right) {
- //將找到的比目標元素大的元素移動到right指針指向的位置
- arr[right] = arr[left];
- right--;
- }
- }
- //將目標元素移動到最終位置
- arr[left] = target;
- return left;
- }
【本算法解讀】
算法首先選取待排序列的首元素,調用partition()方法,將待排序列分成兩個子部分。然後通過遞歸繼續將每個子部分分成兩個子部分。直到每部分只剩一個元素(對應代碼:當left>=right時return)。partition()方法內部,通過移動左右指針不斷進行比較。首先準備移動右指針(因爲當找到比目標元素小的元素時,可以先將其移動到左指針指向的位置,而left所指向位置的元素已經被保存到target中,不用擔心被覆蓋),找到比目標元素小的元素後移動到left指向的位置(此時left位置的元素是被移動過來的元素,肯定比目標元素小,所以左指針掃描時就可以不用比較該元素,對應代碼:left++),右指針停止。準備移動左指針,找到比目標元素大的元素後,將其移動到right指向的位置(此時原來在right位置的元素已經被移動,可以直接覆蓋),左指針停止。再次開始移動右指針,重複上述操作。
直到左指針和右指針重合,即它們所指向的位置,就是目標元素應該在的最終位置。
【舉個栗子】
對於待排序列3,1,4,2
先看圖:
首先將選取首元素3作爲目標元素,並將其保存在臨時變量target中。起始left指向3,right指向2。開始移動右指針,發現2比目標元素3小,則將2移動到left指向的位置,注意此時left向前移動一位,指向1。右指針停止。開始移動左指針,1比3小符合要求,繼續移動,發現4比3大,不符合要求,將4移動到right位置(即原來2的位置,同理right也移動一位,圖中未畫出),left指針停止。
又重新準備移動右指針,發現right與left重合則第一次分區結束。3找到了它的最終位置即left,right指向的位置,將3移動到該位置。此時序列爲2,1,3,4。
繼續遞歸重複上述操作。
插入排序
插入排序主要包括兩種排序算法,分別是直接插入排序和和希爾排序
直接插入排序
【基本思想】
每次將一個待排序列的元素,按其大小插入到前面已經排好序的記錄序列中的適當位置,直到全部元素插完爲止。
插入排序不是通過交換位置而是通過比較找到合適的位置插入元素來達到排序的目的
【空間複雜度】O(1)
【時間複雜度】
平均情況:O(n^2)
最好情況:O(n),正序有序
最壞情況:O(n^2),逆序有序
【穩定性】穩定
【優點】
穩定,快,如果序列是基本有序的,使用直接插入排序效率就非常高
【缺點】
比較次數不一定,比較次數越多,插入點後的數據移動越多,特別是數據量龐大的時候,但用鏈表可以 解決這個問題。
- /**
- * 直接插入排序
- * @param arr
- */
- public static void insertSort(int arr[]) {
- //默認認爲第一個元素是有序的
- for (int i = 1; i < arr.length; i++) {
- int j = i;
- //待插入的目標元素
- int target = arr[i];
- while (j > 0 && target < arr[j - 1]) {
- //後移
- arr[j] = arr[j - 1];
- j--;
- }
- arr[j] = target;
- }
- }
【本算法解讀】
算法默認待排序列的第一個元素是排好序的,處於有序區。從第二個元素開始,直到到末尾元素,依次作爲目標元素(對應代碼:for(int i = 1, i < arr.length; i ++)),向有序區中插入。那麼如何插入呢?將目標依次和有序區的元素進行比較,若目標元素小於該元素,則目標元素就應處於該元素的前面,則該元素後移一個位置(對應代碼:arr[j] = arr[j - 1])。不斷比較直到找到不比目標元素小的元素,則目標元素就應在該元素的後面位置,將目標元素插入到該位置。繼續下一個目標元素,直到所有元素插入完畢。
在插入第i個元素時,前i-1個元素已經是排好序的。
對於待排序列3,1,4,2
首先認爲3是有序的,處於有序區。將1作爲目標元素,依次和有序區中的元素進行比較,和3進行比較,1<3,則3後移,有序區中沒有待比較的數據了,所以將1插入到3原來的位置。此時序列:1,3,4,2。有序區內元素爲1,3。繼續將4作爲目標元素,先和3比較,4>3,則插入到4的後面位置。此時序列1,3,4,2。此時有序區內元素爲1,3,4。繼續將2作爲目標元素,和4比較,2<4,4後移,和3比較,2<3,3後移,和1比較,2>1,則插入到1的後面。此時序列1,2,3,4。所有元素插入完畢,即排序完成。
希爾排序
【基本思想】
希爾排序是插入排序的一種高效率的實現,又稱縮小增量式插入排序。它也是直接插入排序算法的一種改進算法。
先選定一個數作爲第一個增量,將整個待排序列分割成若干個組,就是將所有間隔爲增量值的元素放在同一個組內。各組內部進行直接插入排序。然後取第二個增量,重複上述分組和排序操作,直到所取增量減少爲1時,即所有元素放在同一個組中進行直接插入排序。
爲什麼希爾排序的時間性能是優於直接插入排序的呢?
開始時,增量較大,每組中的元素少,因此組內的直接插入排序較快,當增量減少時,分組內的元素增加,但此時分組內的元素基本有序,所以使得組內的直接插入排序也較快,因此,希爾排序在效率上較直接插入排序有較大的改進
【空間複雜度】O(1)
【時間複雜度】
平均情況:O(nlog2n)
最好情況:因爲希爾排序的執行時間依賴於增量序列,如何選擇增量序列使得希爾排序的元素比較次數和移動次數較少,這個問題目前還未能解決。但有實驗表明,當n較大時,比較和移動次數約在 n^1.25~1.6n^1.25。
最壞情況:O(nlog2n)
【穩定性】不穩定
【優點】
快,數據移動少
【缺點】
不穩定,d的取值是多少,應取多少個不同的值,都無法確切知道,只能憑經驗來取
- /**
- * 希爾排序
- * @param arr
- */
- public static void shellSort(int arr[]) {
- //首先取增量爲待排序列長度的一半
- int d = arr.length / 2;
- while (d >= 1) {
- //對每個分組進行直接插入排序
- for (int i = d; i < arr.length; i++) {
- //目標值總是每個分組內無序區的首元素
- int target = arr[i];
- int j = i - d;
- while (j >= 0 && target < arr[j]) {
- arr[j + d] = arr[j]; //後移
- j -= d;
- }
- //判讀是否發生後移,如果發生後移,則將目標元素插入指定位置
- if (j != i - d) {
- arr[j + d] = target;
- }
- }
- //增量每次減半
- d /= 2;
- }
- }
【本算法解讀】
希爾排序算法是對直接插入排序的改進,所以如果對直接插入排序還不夠理解的話,建議先去看一下上面的直接插入排序算法。
算法首先取增量爲待排序列長度的一半,通過增量進行分組,每個分組都進行直接插入排序。然後,增量減半,重複上述操作直至增量減少爲1(對應代碼:while(d>=1))。算法的要點在於對每個分組進行直接插入排序。首先從i=d位置上的元素開始,一直到待排序列的最後一個元素,依次作爲目標元素,在它們所在的小組中進行直接插入排序。當目標元素位置爲i時,則目標元素所在小組有序區內的元素位置分別爲i-d,i-d-d,i-d-d-d...直到該位置大於0。目標元素只需要和所在小組有序區內的元素進行比較,找到自己的最終位置即可。當增量減少爲1時,則相當於只有一個分組,此時就是對所有元素進行直接插入排序,完成最終的希爾排序。
【舉個栗子】
對於待排序列4,1,3,2
先看圖:
首先取增量爲序列長度4的一半,即爲2。此時的分組是(4,3),(1,2)。則從位置爲2的元素3開始作爲目標元素,位置2減去增量2,則找到目標元素3所在小組有序區內的元素4,和4進行比較,3<4,則4後移(這裏的後移都是相對於自己所在小組裏的元素),有序區內沒有其它元素,則目標元素3插入到元素4之前的位置。繼續從位置爲3的元素2開始作爲目標元素,找到目標元素2所在小組有序區內的元素1,比較2<1,不需要後移,目標元素插入到元素1的後面,其實就是沒右移動。此時完成一趟希爾排序。序列爲3,1,4,2。增量減半,即爲1。此時就是對所有元素進行直接插入排序,不再贅述。
希爾排序的關鍵點在於分組,我們再舉個栗子看是如何分組的:
對於序列25,36,48,59,13,21,74,32,60
第一次增量爲序列長度9的一半,取下限是4,此時的分組情況是(25,13,60)(36,21)(48,74)(59,32),如下圖: