動畫 | 大學四年結束之前必須透徹的排序算法


現如今大學生學習排序算法,除了學習它的算法原理、代碼實現之外,作為一個大學生更重要的往往是要學會如何評價、分析一個排序算法。排序對於任何一個程序員來說,可能都不會陌生。大部分編程語言中,也都提供了排序函數。在平常的項目中,我們也經常會用到排序。排序非常重要!本章主要從如何分析一個算法開始入手,從而循進漸進的分析那些大學四年結束之前必須掌握的排序算法!
@

目錄

當然你可以先思考一兩分鍾,帶着這個問題,我們開始如下的內容!並且注意我標紅的字體,往往是起眼或者不起眼的重點

如何分析一個“排序算法”?

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),但是如果我們希望把性能優化做到極致,那肯定首選插入排序。插入排序的算法思路也有很大的優化空間,我們只是講了最基礎的一種。如果你對插入排序的優化感興趣,可以自行再溫習一下希爾排序。

下面是八大經典算法的分析圖:
在這里插入圖片描述

到這里,以上八大經典算法分析,都是基於數組實現的。如果數據存儲在鏈表中,這些排序算法還能工作嗎?如果能,那相應的時間、空間復雜度又是多少呢?期待大牛評論出來~

如果本文章對你有幫助,哪怕是一點點,請點個贊唄,謝謝~

歡迎各位關注我的公眾號,一起探討技術,向往技術,追求技術...說好了來了就是盆友喔...

在這里插入圖片描述


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM