動畫 | 大學四年結束之前必須透徹的排序演算法
目錄
- 如何分析一個“排序演算法”?
- 開始分析冒泡“排序演算法”
- 開始分析“插入排序演算法”
- 開始分析“選擇排序演算法”
- 開始分析“希爾排序演算法”
- 開始分析“快速排序演算法”
- 開始分析“並歸排序演算法”
- 開始分析“基數排序演算法”
- 開始分析“堆排序演算法”
- 為什麼插入排序要比氣泡排序更受歡迎?
現如今大學生學習排序演算法,除了學習它的演算法原理、程式碼實現之外,作為一個大學生更重要的往往是要學會如何評價、分析一個排序演算法。排序對於任何一個程式設計師來說,可能都不會陌生。大部分程式語言中,也都提供了排序函式。在平常的專案中,我們也經常會用到排序。排序非常重要!本章主要從如何分析一個演算法開始入手,從而循進漸進的分析那些大學四年結束之前必須掌握的排序演算法!
@
當然你可以先思考一兩分鐘,帶著這個問題,我們開始如下的內容!並且注意我標紅的字型,往往是起眼或者不起眼的重點。
如何分析一個“排序演算法”?
1、排序演算法的執行效率
對於排序演算法執行效率的分析,我們一般會從這三個方面來衡量:
1.1.最好、最壞、平均時間複雜度
我們在分析排序演算法的時間複雜度時,要分別給出最好情況、最壞情況、平均情況下的時間複雜度。除此之外,你還要說出最好、最壞時間複雜度對應的要排序的原始資料是什麼樣的。
為什麼要區分這三種時間複雜度呢?第一,有些排序演算法會區分,為了好對比,所以我們最好都做一下區分。第二,對於要排序的資料,有的接近有序,有的完全無序。有序度不同的資料,對於排序的執行時間肯定是有影響的,我們要知道排序演算法在不同資料下的效能表現。
1.2.時間複雜度的係數、常數 、低階
我們知道,時間複雜度反應的是資料規模n很大的時候的一個增長趨勢,所以它表示的時候會忽略係數、常數、低階。但是實際的軟體開發中,我們排序的可能是10個、100個、1000個這樣規模很小的資料,所以,在對同一階時間複雜度的排序演算法效能對比的時候,我們就要把係數、常數、低階也考慮進來。
1.3.比較次數和交換(或移動)次數
這一節和下一節講的都是基於比較的排序演算法。基於比較的排序演算法的執行過程,會涉及兩種操作,一種是元素比較大小,另一種是元素交換或移動。所以,如果我們在分析排序演算法的執行效率的時候,應該把比較次數和交換(或移動)次數也考慮進去。
2、排序演算法的記憶體消耗
我們前面講過,演算法的記憶體消耗可以通過空間複雜度來衡量,排序演算法也不例外。不過,針對排序演算法的空間複雜度,我們還引入了一個新的概念,原地排序(Sorted in place)。原地排序演算法,就是特指空間複雜度是O(1)的排序演算法。
3、排序演算法的穩定性
穩定性千萬不要忽略,僅僅用執行效率和記憶體消耗來衡量排序演算法的好壞是不夠的。針對排序演算法,我們還有一個重要的度量指標,穩定性。這個概念是說,如果待排序的序列中存在值相等的元素,經過排序之後,相等元素之間原有的先後順序不變。
我通過一個例子來解釋一下。比如我們有一組資料2,9,3,4,8,3,按照大小排序之後就是2,3,3,4,8,9。
這組資料裡有兩個3。經過某種排序演算法排序之後,如果兩個3的前後順序沒有改變,那我們就把這種排序演算法叫作穩定的排序演算法;如果前後順序發生變化,那對應的排序演算法就叫作不穩定的排序演算法。
你可能要問了,兩個3哪個在前,哪個在後有什麼關係啊,穩不穩定又有什麼關係呢?為什麼要考察排序演算法的穩定性呢?
很多資料結構和演算法課程,在講排序的時候,都是用整數來舉例,但在真正軟體開發中,我們要排序的往往不是單純的整數,而是一組物件,我們需要按照物件的某個key來排序。
比如說,我們現在要給電商交易系統中的“訂單”排序。訂單有兩個屬性,一個是下單時間,另一個是訂單金額。如果我們現在有10萬條訂單資料,我們希望按照金額從小到大對訂單資料排序。對於金額相同的訂單,我們希望按照下單時間從早到晚有序。對於這樣一個排序需求,我們怎麼來做呢?
最先想到的方法是:我們先按照金額對訂單資料進行排序,然後,再遍歷排序之後的訂單資料,對於每個金額相同的小區間再按照下單時間排序。這種排序思路理解起來不難,但是實現起來會很複雜。
藉助穩定排序演算法,這個問題可以非常簡潔地解決。解決思路是這樣的:我們先按照下單時間給訂單排序,注意是按照下單時間,不是金額。排序完成之後,我們用穩定排序演算法,按照訂單金額重新排序。兩遍排序之後,我們得到的訂單資料就是按照金額從小到大排序,金額相同的訂單按照下單時間從早到晚排序的。為什麼呢?
穩定排序演算法可以保持金額相同的兩個物件,在排序之後的前後順序不變。第一次排序之後,所有的訂單按照下單時間從早到晚有序了。在第二次排序中,我們用的是穩定的排序演算法,所以經過第二次排序之後,相同金額的訂單仍然保持下單時間從早到晚有序。
到這裡,分析一個“排序演算法”就結束了,你get到了嗎?接下來,我們進入實戰演算法分析。
開始分析冒泡“排序演算法”
1.氣泡排序描述
氣泡排序描述:氣泡排序只會操作相鄰的兩個資料。每次冒泡操作都會對相鄰的兩個元素進行比較,看是否滿足大小關係要求。如果不滿足就讓它倆互換。一次冒泡會讓至少一個元素移動到它應該在的位置,重複n次,就完成了n個數據的排序工作。
2.圖解氣泡排序
如果還是不能一眼看出其靈魂,沒事,我還有一招:
怎麼樣,夠不夠直觀,就是有點慢,哈哈~
3.程式碼實現氣泡排序
package BubbleSort;
import java.util.Arrays;
public class generalBubble {
public static void main(String[] args) {
int[] arr=new int[] {5,7,2,9,4,1,0,5,8,7};
System.out.println(Arrays.toString(arr));
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
//氣泡排序
public static void bubbleSort(int[] arr) {
//控制共比較多少輪
for(int i=0;i<arr.length-1;i++) {
//控制比較的次數
for(int j=0;j<arr.length-1-i;j++) {
if(arr[j]>arr[j+1]) {
int temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
}
}
測試效果:
4.程式碼優化氣泡排序
實際上,剛講的冒泡過程還可以優化。當某次冒泡操作已經沒有資料交換時,說明已經達到完全有序,不用再繼續執行後續的冒泡操作。我這裡還有另外一個例子,這裡面給6個元素排序,只需要4次冒泡操作就可以了。
// 氣泡排序,a表示陣列,n表示陣列大小
public void bubbleSort(int[] a, int n) {
if (n <= 1) return;
for (int i = 0; i < n; ++i) {
// 提前退出冒泡迴圈的標誌位
boolean flag = false;
for (int j = 0; j < n - i - 1; ++j) {
if (a[j] > a[j+1]) { // 交換
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true; // 表示有資料交換
}
}
if (!flag) break; // 沒有資料交換,提前退出
}
}
現在,結合剛才我分析排序演算法的三個方面,開始分析氣泡排序演算法。
第一:氣泡排序是原地排序演算法嗎?
首先,原地排序演算法就是特指空間複雜度是O(1)的排序演算法,我在上文提及過的,再提一遍(我猜你們肯定沒仔細看文章。。。)
冒泡的過程只涉及相鄰資料的交換操作,只需要常量級的臨時空間,所以它的空間複雜度為O(1),是一個原地排序演算法。
第二:氣泡排序是穩定的排序演算法嗎?
在氣泡排序中,只有交換才可以改變兩個元素的前後順序。為了保證氣泡排序演算法的穩定性,當有相鄰的兩個元素大小相等的時候,我們不做交換,相同大小的資料在排序前後不會改變順序,所以氣泡排序是穩定的排序演算法。
第三:氣泡排序的時間複雜度是多少?
最好情況下,要排序的資料已經是有序的了,我們只需要進行一次冒泡操作,就可以結束了,所以最好情況時間複雜度是O(n)。而最壞的情況是,要排序的資料剛好是倒序排列的,我們需要進行n次冒泡操作,所以最壞情況時間複雜度為O(n2),平均情況下的時間複雜度就是O(n2)。
開始分析“插入排序演算法”
1.插入排序描述
插入排序(Insertion-Sort)的演算法描述是一種簡單直觀的排序演算法。它的工作原理是通過構建有序序列,對於未排序資料,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,通常採用in-place排序(即只需用到O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,為最新元素提供插入空間。
2.圖解插入排序
同樣,我也準備了數字版的,是不是很貼心?
3.程式碼實現插入排序
public class InsertSort {
public static void main(String[] args) {
int[] arr = new int[] {5,3,2,8,5,9,1,0};
insertSort(arr);
System.out.println(Arrays.toString(arr));
}
//插入排序
public static void insertSort(int[] arr) {
//遍歷所有的數字
for(int i=1;i<arr.length;i++) {
//如果當前數字比前一個數字小
if(arr[i]<arr[i-1]) {
//把當前遍歷數字存起來
int temp=arr[i];
int j;
//遍歷當前數字前面所有的數字
for(j=i-1;j>=0&&temp<arr[j];j--) {
//把前一個數字賦給後一個數字
arr[j+1]=arr[j];
}
//把臨時變數(外層for迴圈的當前元素)賦給不滿足條件的後一個元素
arr[j+1]=temp;
}
}
}
}
現在,結合剛才我分析排序演算法的三個方面,開始分析插入排序演算法。
第一:插入排序是原地排序演算法嗎?
從實現過程可以很明顯地看出,插入排序演算法的執行並不需要額外的儲存空間,所以空間複雜度是O(1),也就是說,這是一個原地排序演算法。
第二:插入排序是穩定的排序演算法嗎?
在插入排序中,對於值相同的元素,我們可以選擇將後面出現的元素,插入到前面出現元素的後面,這樣就可以保持原有的前後順序不變,所以插入排序是穩定的排序演算法。
第三:插入排序的時間複雜度是多少?
如果要排序的資料已經是有序的,我們並不需要搬移任何資料。如果我們從尾到頭在有序資料組裡面查詢插入位置,每次只需要比較一個數據就能確定插入的位置。所以這種情況下,最好是時間複雜度為O(n)。注意,這裡是從尾到頭遍歷已經有序的資料。
如果陣列是倒序的,每次插入都相當於在陣列的第一個位置插入新的資料,所以需要移動大量的資料,所以最壞情況時間複雜度為O(n2)。
還記得我們在陣列中插入一個數據的平均時間複雜度是多少嗎?沒錯,是O(n)。所以,對於插入排序來說,每次插入操作都相當於在陣列中插入一個數據,迴圈執行n次插入操作,所以平均時間複雜度為O(n2)。
開始分析“選擇排序演算法”
1.選擇排序描述
選擇排序(Selection sort)是一種簡單直觀的排序演算法。它的工作原理是:第一次從待排序的資料元素中選出最小(或最大)的一個元素,存放在序列的起始位置,然後再從剩餘的未排序元素中尋找到最小(大)元素,然後放到已排序的序列的末尾。以此類推,直到全部待排序的資料元素的個數為零。選擇排序是不穩定的排序方法。
2.圖解選擇排序
3.程式碼實現選擇排序
public class SelectSort {
public static void main(String[] args) {
int[] arr = new int[] {3,4,5,7,1,2,0,3,6,8};
selectSort(arr);
System.out.println(Arrays.toString(arr));
}
//選擇排序
public static void selectSort(int[] arr) {
//遍歷所有的數
for(int i=0;i<arr.length;i++) {
int minIndex=i;
//把當前遍歷的數和後面所有的數依次進行比較,並記錄下最小的數的下標
for(int j=i+1;j<arr.length;j++) {
//如果後面比較的數比記錄的最小的數小。
if(arr[minIndex]>arr[j]) {
//記錄下最小的那個數的下標
minIndex=j;
}
}
//如果最小的數和當前遍歷數的下標不一致,說明下標為minIndex的數比當前遍歷的數更小。
if(i!=minIndex) {
int temp=arr[i];
arr[i]=arr[minIndex];
arr[minIndex]=temp;
}
}
}
}
4.分析選擇排序演算法
選擇排序演算法是一種原地、不穩定的排序演算法,最好時間複雜度情況:T(n) = O(n2) 最差時間複雜度情況:T(n) = O(n2) 平均時間複雜度情況:T(n) = O(n2)
開始分析“希爾排序演算法”
1.希爾排序描述
希爾排序也是一種插入排序,它是簡單插入排序經過改進之後的一個更高效的版本,也稱為縮小增量排序,同時該演算法是衝破O(n2)的第一批演算法之一。它與插入排序的不同之處在於,它會優先比較距離較遠的元素。希爾排序又叫縮小增量排序。希爾排序是把記錄按下表的一定增量分組,對每組使用直接插入排序演算法排序;隨著增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個檔案恰被分成一組,演算法便終止。
希爾排序常規步驟:
1、選擇增量gap=length/2
2、縮小增量繼續以gap = gap/2的方式,n/2,(n/2)/2...1 ,有點暈了對吧,還是看圖解吧哈哈~
同樣是二圖(捂臉)
3.程式碼實現希爾排序
public class ShellSort {
public static void main(String[] args) {
int[] arr = new int[] { 3, 5, 2, 7, 8, 1, 2, 0, 4, 7, 4, 3, 8 };
System.out.println(Arrays.toString(arr));
shellSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void shellSort(int[] arr) {
int k = 1;
// 遍歷所有的步長
for (int d = arr.length / 2; d > 0; d /= 2) {
// 遍歷所有有元素
for (int i = d; i < arr.length; i++) {
// 遍歷本組中所有的元素
for (int j = i - d; j >= 0; j -= d) {
// 如果當前元素大於加上步長後的那個元素
if (arr[j] > arr[j + d]) {
int temp = arr[j];
arr[j] = arr[j + d];
arr[j + d] = temp;
}
}
}
System.out.println("第" + k + "次排序結果:" + Arrays.toString(arr));
k++;
}
}
}
4.分析希爾排序演算法
希爾排序演算法是一種原地、不穩定的排序演算法,最好時間複雜度情況:T(n) = O(nlog2 n) 最差時間複雜度情況:T(n) = O(nlog2 n) 平均時間複雜度情況:T(n) =O(nlog2n)
開始分析“快速排序演算法”
1.快速排序描述
我們習慣性把它簡稱為“快排”。快排利用的也是分治思想。乍看起來,它有點像歸併排序,但是思路其實完全不一樣。通過一趟排序將待排記錄分隔成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序。
快速排序常規步驟:
1、從數列中挑出一個元素,稱為 “基準”(pivot),一般第一個基數取第一個數;
2、重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分割槽退出之後,該基準就處於數列的中間位置。這個稱為分割槽(partition)操作;
3、遞迴地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。
2.圖解快速排序
貌似上圖太過於抽象,還是看下圖吧,哈哈~
3.程式碼實現快速排序
public class QuickSort {
public static void main(String[] args) {
int[] arr = new int[] {3,4,6,7,2,7,2,8,0,9,1};
quickSort(arr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}
public static void quickSort(int[] arr,int start,int end) {
if(start<end) {
//把陣列中的第0個數字做為標準數
int stard=arr[start];
//記錄需要排序的下標
int low=start;
int high=end;
//迴圈找比標準數大的數和比標準數小的數
while(low<high) {
//右邊的數字比標準數大
while(low<high&&stard<=arr[high]) {
high--;
}
//使用右邊的數字替換左邊的數
arr[low]=arr[high];
//如果左邊的數字比標準數小
while(low<high&&arr[low]<=stard) {
low++;
}
arr[high]=arr[low];
}
//把標準數賦給低所在的位置的元素
arr[low]=stard;
//處理所有的小的數字
quickSort(arr, start, low);
//處理所有的大的數字
quickSort(arr, low+1, end);
}
}
}
4.分析快速排序演算法
快速排序演算法是一種原地、不穩定的排序演算法,最好時間複雜度情況:T(n) = O(nlogn) 最差時間複雜度情況:T(n) = O(n2) 平均時間複雜度情況:T(n) = O(nlogn)
開始分析“並歸排序演算法”
歸併排序(MERGE-SORT)是建立在歸併操作上的一種有效的排序演算法,該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱為二路歸併。
1.並歸排序描述
歸併操作的工作原理如下:
第一步:申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合併後的序列
第二步:設定兩個指標,最初位置分別為兩個已經排序序列的起始位置
第三步:比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間,並移動指標到下一位置
重複步驟3直到某一指標超出序列尾
將另一序列剩下的所有元素直接複製到合併序列尾
2.圖解並歸排序
3.程式碼實現並歸排序
public class MergeSort {
public static void main(String[] args) {
int[] arr = new int[] {1,3,5,2,4,6,8,10};
System.out.println(Arrays.toString(arr));
mergeSort(arr, 0, arr.length-1);
System.out.println(Arrays.toString(arr));
}
//歸併排序
public static void mergeSort(int[] arr,int low,int high) {
int middle=(high+low)/2;
if(low<high) {
//處理左邊
mergeSort(arr, low, middle);
//處理右邊
mergeSort(arr, middle+1, high);
//歸併
merge(arr,low,middle,high);
}
}
public static void merge(int[] arr,int low,int middle, int high) {
//用於儲存歸併後的臨時陣列
int[] temp = new int[high-low+1];
//記錄第一個陣列中需要遍歷的下標
int i=low;
//記錄第二個陣列中需要遍歷的下標
int j=middle+1;
//用於記錄在臨時陣列中存放的下標
int index=0;
//遍歷兩個陣列取出小的數字,放入臨時陣列中
while(i<=middle&&j<=high) {
//第一個陣列的資料更小
if(arr[i]<=arr[j]) {
//把小的資料放入臨時陣列中
temp[index]=arr[i];
//讓下標向後移一位;
i++;
}else {
temp[index]=arr[j];
j++;
}
index++;
}
//處理多餘的資料
while(j<=high) {
temp[index]=arr[j];
j++;
index++;
}
while(i<=middle) {
temp[index]=arr[i];
i++;
index++;
}
//把臨時陣列中的資料重新存入原陣列
for(int k=0;k<temp.length;k++) {
arr[k+low]=temp[k];
}
}
}
4.分析並歸排序演算法
並歸排序演算法是一種穩定的排序演算法,最好時間複雜度情況:T(n) = O(n) 最差時間複雜度情況:T(n) = O(nlogn) 平均時間複雜度情況:T(n) = O(nlogn)
開始分析“基數排序演算法”
基數排序也是非比較的排序演算法,對每一位進行排序,從最低位開始排序,複雜度為O(kn),為陣列長度,k為陣列中的數的最大的位數;基數排序是按照低位先排序,然後收集;再按照高位排序,然後再收集;依次類推,直到最高位。有時候有些屬性是有優先順序順序的,先按低優先順序排序,再按高優先順序排序。最後的次序就是高優先順序高的在前,高優先順序相同的低優先順序高的在前。
2.圖解基數排序
小提示:注意進度條擋住的0~9的數字歸類
3.程式碼實現基數排序
public class RadixSort {
public static void main(String[] args) {
int[] arr = new int[] {23,6,189,45,9,287,56,1,798,34,65,652,5};
radixSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void radixSort(int[] arr) {
//存最陣列中最大的數字
int max=Integer.MIN_VALUE;
for(int i=0;i<arr.length;i++) {
if(arr[i]>max) {
max=arr[i];
}
}
//計算最大數字是幾位數
int maxLength = (max+"").length();
//用於臨時儲存資料的陣列
int[][] temp = new int[10][arr.length];
//用於記錄在temp中相應的陣列中存放的數字的數量
int[] counts = new int[10];
//根據最大長度的數決定比較的次數
for(int i=0,n=1;i<maxLength;i++,n*=10) {
//把每一個數字分別計算餘數
for(int j=0;j<arr.length;j++) {
//計算餘數
int ys = arr[j]/n%10;
//把當前遍歷的資料放入指定的陣列中
temp[ys][counts[ys]] = arr[j];
//記錄數量
counts[ys]++;
}
//記錄取的元素需要放的位置
int index=0;
//把數字取出來
for(int k=0;k<counts.length;k++) {
//記錄數量的陣列中當前餘數記錄的數量不為0
if(counts[k]!=0) {
//迴圈取出元素
for(int l=0;l<counts[k];l++) {
//取出元素
arr[index] = temp[k][l];
//記錄下一個位置
index++;
}
//把數量置為0
counts[k]=0;
}
}
}
}
}
4.分析基數排序演算法
基數排序演算法是一種穩定的排序演算法,最好時間複雜度情況:T(n) = O(n * k) 最差時間複雜度情況:T(n) = O(n * k) 平均時間複雜度情況:T(n) = O(n * k)。
開始分析“堆排序演算法”
1.堆排序描述
堆排序(英語:Heapsort)是指利用堆這種資料結構所設計的一種排序演算法。堆是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。
在堆的資料結構中,堆中的最大值總是位於根節點(在優先佇列中使用堆的話堆中的最小值位於根節點)。堆中定義以下幾種操作:
最大堆調整(Max Heapify):將堆的末端子節點作調整,使得子節點永遠小於父節點
建立最大堆(Build Max Heap):將堆中的所有資料重新排序
堆排序(HeapSort):移除位在第一個資料的根節點,並做最大堆調整的遞迴運算
2.圖解堆排序
3.程式碼實現堆排序
public class HeapSort {
public static void main(String[] args) {
int[] arr = new int[] {9,6,8,7,0,1,10,4,2};
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void heapSort(int[] arr) {
//開始位置是最後一個非葉子節點,即最後一個節點的父節點
int start = (arr.length-1)/2;
//調整為大頂堆
for(int i=start;i>=0;i--) {
maxHeap(arr, arr.length, i);
}
//先把陣列中的第0個和堆中的最後一個數交換位置,再把前面的處理為大頂堆
for(int i=arr.length-1;i>0;i--) {
int temp = arr[0];
arr[0]=arr[i];
arr[i]=temp;
maxHeap(arr, i, 0);
}
}
public static void maxHeap(int[] arr,int size,int index) {
//左子節點
int leftNode = 2*index+1;
//右子節點
int rightNode = 2*index+2;
int max = index;
//和兩個子節點分別對比,找出最大的節點
if(leftNode<size&&arr[leftNode]>arr[max]) {
max=leftNode;
}
if(rightNode<size&&arr[rightNode]>arr[max]) {
max=rightNode;
}
//交換位置
if(max!=index) {
int temp=arr[index];
arr[index]=arr[max];
arr[max]=temp;
//交換位置以後,可能會破壞之前排好的堆,所以,之前的排好的堆需要重新調整
maxHeap(arr, size, max);
}
}
}
4.分析堆排序演算法
基數排序演算法是一種原地、不穩定的排序演算法,最好時間複雜度情況:T(n) = O(nlogn) 最差時間複雜度情況:T(n) = O(nlogn) 平均時間複雜度情況::T(n) = O(nlogn)
為什麼插入排序要比氣泡排序更受歡迎?
基本的知識都講完了,不知道各位有木有想過這樣一個問題:氣泡排序和插入排序的時間複雜度都是O(n2),都是原地排序演算法,為什麼插入排序要比氣泡排序更受歡迎呢?
我們前面分析氣泡排序和插入排序的時候講到,氣泡排序不管怎麼優化,元素交換的次數是一個固定值,是原始資料的逆序度。插入排序是同樣的,不管怎麼優化,元素移動的次數也等於原始資料的逆序度。
但是,從程式碼實現上來看,氣泡排序的資料交換要比插入排序的資料移動要複雜,氣泡排序需要3個賦值操作,而插入排序只需要1個。我們來看這段操作:
氣泡排序中資料的交換操作:
if (a[j] > a[j+1]) { // 交換
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true;
}
插入排序中資料的移動操作:
if (a[j] > value) {
a[j+1] = a[j]; // 資料移動
} else {
break;
}
我們把執行一個賦值語句的時間粗略地計為單位時間(unit_time),然後分別用氣泡排序和插入排序對同一個逆序度是K的陣列進行排序。用氣泡排序,需要K次交換操作,每次需要3個賦值語句,所以交換操作總耗時就是3*K單位時間。而插入排序中資料移動操作只需要K個單位時間。
這個只是我們非常理論的分析,為了實驗,針對上面的氣泡排序和插入排序的Java程式碼,我寫了一個性能對比測試程式,隨機生成10000個數組,每個陣列中包含200個數據,然後在我的機器上分別用冒泡和插入排序演算法來排序,氣泡排序演算法大約700ms才能執行完成,而插入排序只需要100ms左右就能搞定!
所以,雖然氣泡排序和插入排序在時間複雜度上是一樣的,都是O(n2),但是如果我們希望把效能優化做到極致,那肯定首選插入排序。插入排序的演算法思路也有很大的優化空間,我們只是講了最基礎的一種。如果你對插入排序的優化感興趣,可以自行再溫習一下希爾排序。
下面是八大經典演算法的分析圖:
到這裡,以上八大經典演算法分析,都是基於陣列實現的。如果資料儲存在連結串列中,這些排序演算法還能工作嗎?如果能,那相應的時間、空間複雜度又是多少呢?期待大牛評論出來~
如果本文章對你有幫助,哪怕是一點點,請點個讚唄,謝謝~
歡迎各位關注我的公眾號,一起探討技術,嚮往技術,追求技術...說好了來了就是盆友喔...
相關推薦
動畫 | 大學四年結束之前必須透徹的排序演算法
目錄 如何分析一個“排序演算法”? 開始分析冒泡“排序演算法” 開始分析“插入排序演算法” 開始分析“選擇排序演算法” 開始分析“希爾排序演算法” 開始分析“快速排序演算法”
如何高質量地走完大學四年?
首先問你一個問題,什麼樣的四年才算是高質量? 不同的人會給你不一樣的答案。 有人會告訴你:大學四年應該好好學習,多參加科研專案,成功保研。 有人會告訴你:你要多參加比賽和活動,為自己畢業找工作增加背書。 有人會告訴你:要談戀愛、要多考證、要多去社會上體驗體驗… 每一個人都嘗試站在自己的角度告
大學生如何過一個充實或者說無悔的大學四年
原文作者:aircraft 原文連結:https://www.cnblogs.com/DOMLX/p/9627946.html 序章:看著大一的新生們再一次的踏進了這最後的象牙塔,在下這個老人自以為平靜的心田好像被一滴清晨的露水捲起了一圈圈的漣漪,開始回想起自己那時剛來象牙塔的感受,那並不是一個像電視劇
李開復給大學生的第四封---大學四年應是這樣度過
今天,我回復了“開復學生網”開通以來的第1000個問題。關掉電腦後,始終有一封學生來信縈繞在我的腦海裡,揮之不去: 開復老師: 就要畢業了。 回頭看自己所謂的大學生活, 我想哭,不是因為離別,而是因為什麼都沒學到。 我不知,簡歷該怎麼寫,若是以往我會
大學四年我是如何學習程式設計的
最近,有很多同學關注我,尤其是不少在校大學生,其中幾位同學很想知道,大學四年,我是如何學習程式設計的。 我以前也計劃寫大學生活系列文章,既然有同學期待,我就開始寫第1篇。 “我是如何學習程式設計的”是我畢業後回顧大學生活的第1篇。(高中生活系列已經寫了6篇) 希望,能幫助到一些
追憶我的大學四年
追憶我的大學四年 畢業快兩年了,卻依然懷念著大學的點點滴滴,大學讓我有了一次脫胎換骨的改變,大學讓我成長了太多。 一,青澀的大一 2006年9月,伴隨著和父母在西安火車站的揮手告別,我踏上了開往杭州的火車,生平第一次出省,生平第一次坐火車,生平第一次一個人出遠門。告別古城西安,來到千里之
大學四年應當如何渡過(轉)
今天,我回復了“開復學生網”開通以來的第1000個問題。關掉電腦後,始終有一封學生來信縈繞在我的腦海裡,揮之不去: 開復老師: 就要畢業了。 回頭看自己所謂的大學生活, 我想哭,不是因為離別,而是因為什麼都沒學到。 我不知,簡歷該怎麼寫,若是以往我會讓它空白。 最大的收穫也
回憶自己的大學四年得與失
轉眼間,大學四年就過去了,我一直在猶豫到底要不要寫一篇文章來回憶自己大學四年的所得所失,最後還是準備寫下這樣一篇文章來紀念自己的大學四年吧!這篇文章是寫給自己的,多少年之後回想起自己的大學青春也是美好
大學四年專業學習規劃目標
通過大學四年的學習,我希望自己能夠在各個方面都得到提高。 一、全面發展 (1)性格品質:自信心強,肯吃苦;具有一顆善良的心,待人真誠,能得到大部分人的尊重;
對大學四年的總結
去年,也就是2017,我順利從一個普通二本的計算機學院畢業,這是一篇遲來的總結。 因為一些原因,可能自己沒有能抽出時間和精力來寫這麼一篇文章,所以一直拖到了現在。儘管已經過去了這麼久,還是該對自己人生中很珍貴的四年青春做一個全面的認識,放下一些東西才能更好開始,也正好元旦假
光陰似箭,大學四年
都說畢業遙遙無期,轉眼各奔東西。 當年年少,不甚懂得這句話的意義,如今,大學真的畢業了,似乎感慨頗多~~~~~ 大學四年,所獲頗多,不管是學習,還是做人。 感謝大學四年所有的朋友,一起瘋狂,一起愉快地
大學四年經典語錄
永遠不要參加任何社團招新,除非它讓你作社長。任何社團存在的理由,就是增加社團負責人的競爭資本。 永遠不要指望哪個plmm沒有男朋友,不排除有mm這麼說,這是因為她的男友太多了,她數不過來。 永遠不要指望踢足球能夠吸引mm的注意,在學校,你踢得再好,也只有你的對手知道,
大學四年自學走來,這些私藏的實用工具/學習網站我貢獻出來了
點贊再看,養成習慣,微信搜尋【敖丙】關注這個網際網路苟且偷生的工具人。 本文 GitHub https://github.com/JavaFamily 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。 ##前言 在大學的時候我們有大量的業餘時間,我們可以拿出一部分時間去自學,也可以自學你感興趣的非你
大學四年我是怎麼寫作業系統和計算機網路的?掏心掏肺的分享!
最近收到不少讀者留言,關於怎麼學「作業系統」和「計算機網路」的留言,小林寫這一塊的內容也有半年多了,啃非常多的書,也看了很多視訊,有好的有差的,今天就掏心掏肺地分享給大家。 作業系統和計算機網路有多重要呢?如果沒有作業系統,我們的手機和電腦可以說是廢鐵了,自然它們都沒有使用價值了,另外如果沒有計算機網路,我
動畫+原理+程式碼,解讀十大經典排序演算法
來源:https://github.com/hustcc/JS-Sorting-Algorithm 排序演算法是《資料結構與演算法》中最基本的演算法之一。 排序演算法可以分為內部排序和外部排序,內部排序是資料記錄在記憶體中進行排序,而外部排序是因排序的資料很大,一次不能容納全部的排序記錄,在
【牛客---湖南大學2018年第十四屆程序設計競賽重現賽】
給定 lap 就是 cst color size pre AC img (第三次與中獎擦肩而過? 【A---A game】 題意:雙人博弈:有N堆,每堆數量石子為M。給定K,每次可以把某一堆均分為石子數不小於K的幾堆,(如果大於K,下次還可以用來分)。不可分的人輸。
我的大學-迷失的四年(09/1/2更新
=================================人活著,就是一柄刀,只有不斷的拼殺,才能印證自身的存在!我覺得我們大家既然都奮鬥了,就應該把奮鬥過的教訓和經驗拿給大家。這樣後來的人就更容易成功,也不會走我們走過的彎路。我們每點都是星星之火,都燃燒了,就可以燎
我的四年大學生活總結
序 2013年夏末,我來到了天津大學,開始為期四年的大學本科學習與生活。在中學時代其實對大學是非常憧憬和嚮往的,最終在天大的軟體學院軟體工程系開啟了這段對人生十分重要的歲月。 四年之後,在即將進入職場之際,寫下此文,紀念我的四年大學生活。 大一,起點
匆匆結束的大學兩年
寫了部落格不到一年,知識沒有寫多少,總結倒是一篇篇的。其實也說不上總結,總是想寫點什麼記下自己的生活嘛。 今天下午實訓答辯結束,大二最後一門課程也結束了,晚上在教室裡空落落的不知道該幹
畢業了,我的四年大學:平凡但不平庸(寫給每一位想要認真學習的小夥伴)
去年十月份的時候,我分享了一篇關於我三年大學的文章:[普普通通,我的三年大學](https://mp.weixin.qq.com/s?__biz=Mzg2NzA4MTkxNQ==&mid=2247486062&idx=1&sn=6e2876ebd2031bb8ea49170cca744