排序算法:直接插入排序,希爾排序,冒泡排序,快速排序,簡單選擇排序,歸並排序,堆排序


今天來總結一下常用的排序算法。排序算法們需要掌握的知識點大概有:算法的原理,算法的編碼實現,算法的時空復雜度的計算和記憶,何時出現最差時間復雜度,以及是否穩定,何時不穩定。

整體記憶

名稱 時間復雜度 何時最差 是否穩定
快速排序 平均O(nlogn),最壞O(n^2),最好O(nlogn) 數組本身有序 不穩定
堆排序 都O(nlogn) - 不穩定
歸並排序 都O(nlogn) 空間O(n) - 穩定
冒泡排序 平均O(n^2),最壞O(n^2),最好O(n) 數組逆序 穩定
直接插入排序 平均O(n^2),最壞O(n^2),最好O(n) 數組逆序 穩定
簡單選擇排序 都O(n^2) - 不穩定
希爾排序 O(n^1.3)-O(n^2) - 不穩定

快速排序

本文不從分類順序和發展角度出發,而從重要程度和面試出現概率總結各算法。
那首當其沖的就是快速排序了。
快速排序,是交換排序的一種,思想是每輪把一個數放到它該在的位置(左邊的元素都比它小,右邊都比它大)。即左右各一個指針,先從右邊往左找,找到第一個比pivot(基准)小的值,停下,左指針往右找第一個比pivot大的元素,停下,交換兩個元素,之后繼續,直到兩個指針相遇,右指針的位置的元素應小於pivot的值(右指針先走的原因),即為pivot應在的位置,右指針元素與pivot交換即可。還不明白可以參考http://wiki.jikexueyuan.com/project/easy-learn-algorithm/fast-sort.html的介紹。具體:

	public static void QuickSort(int[] a) {
		Partition(a,0,a.length-1);
	}
	private static void Partition(int[] a , int start ,int end) {
		if(end<=start) return;
		int l = start+1;
		int r = end;
		while(l<=r) {
			while(r>=l&&a[r]>=a[start]) r--;
			while(l<=r&&a[l]<=a[start]) l++;
			if(l<r) {int temp = a[l];a[l]=a[r];a[r]=temp;}
			else {int temp = a[r];a[r]=a[start];a[start]=temp;}
		}
		Partition(a,start,r-1);
		Partition(a,r+1,end);
	}
def quickSort(nums):
    if nums is None or len(nums)<1:
        return []
    partition(nums,0,len(nums)-1)
    return nums
def partition(nums,start,end):
    if start>=end:
        return
    l = start+1
    r = end
    while l<=r:
        while l<=r and nums[r]>=nums[start]:
            r-=1
        while l<=r and nums[l]<=nums[start]:
            l+=1
        if l<r:
            temp = nums[l];nums[l]=nums[r];nums[r]=temp
        else:
            temp = nums[start];nums[start]=nums[r];nums[r]=temp
    partition(nums,start,r-1)
    partition(nums,r+1,end)

時間復雜度平均情況O(nlogn),最好O(nlogn),最壞O(n^2)。證明見快速排序時間復雜度為O(n×log(n))的證明
其中最好情況出現在,每次划分都二分,這樣的話遞歸樹的深度就是logn,每層都要遍歷所有的元素,故O(nlogn)。
最壞情況出現在,數組本身就有序,這樣遞歸樹是一棵斜樹,深度n-1。
不穩定。因為右指針從右往左找到第一個比pivot小的數,也就是比pivot小的數里最靠右的數,這時如果與pivot交換,就會不穩定。如:211113456。2會和1交換,1就跑到了和它相等的一眾1的左邊。

堆排序

堆(heap)又被為優先隊列(priority queue)。盡管名為優先隊列,但堆並不是隊列。堆是一種數據結構,在堆中,我們取出的元素是堆中最小/最大的(小根堆/大根堆)。堆的經典實現方法是使用完全二叉樹(由於建堆和插入刪除操作都可以保證堆的平衡性,所以堆一直會是一棵完全二叉樹),完全二叉樹又可以用數組來替代。
堆有三種基本操作:建堆、插入和刪除,刪除就是彈出堆中最小/最大的元素。

建堆

建堆是一個遞歸的過程:
0.我們的數據構成數組a[0,1,..,n-1],共n個元素
1.我們用該數組構建一個完全二叉樹(用數組實現的完全二叉樹的話其實不用變動)
2.從樹的右下角的第一個非葉子節點開始從右下向左上進行從上至下的調整(從(n-1-1)/2到0進行從上至下調整),每次的調整都是從上至下的(因為對(n-1-1)/2調整后,下面的子樹可能不滿足堆的性質,需要繼續調整)。
3.具體的,對節點k的調整為,(默認小根堆,java中優先隊列就默認小根堆):a[k]和兩個葉子節點a[2k+1],a[2k+2],若a[k]最小,停止調整;否則,和a[2k+1],a[2k+2]中較小的值互換。若互換,則k移位到了葉子上,繼續對新的三個節點遞歸進行以上調整,直到無需調整或葉子節點。
建堆時間復雜度O(n)。

建堆時間復雜度推導

假如有N個節點,那么高度為H=logN,最后一層每個父節點最多只需要下調1次,倒數第二層最多只需要下調2次,頂點最多需要下調H次,而最后一層父節點共有2^(H-1)個,倒數第二層公有2^(H-2),頂點只有1(2^0)個,所以總共的時間復雜度為s = 1 * 2^(H-1) + 2 * 2^(H-2) + ... + (H-1) * 2^1 + H * 2^0
將H代入后s= 2N - 2 - log2(N),近似的時間復雜度就是O(N)。
另外一個更細致的解答:
假設高度為k,則從倒數第二層右邊的節點開始,這一層的節點都要執行子節點比較然后交換(如果順序是對的就不用交換);倒數第三層呢,則會選擇其子節點進行比較和交換,如果沒交換就可以不用再執行下去了。如果交換了,那么又要選擇一支子樹進行比較和交換;
那么總的時間計算為:s = 2^( i - 1 ) * ( k - i );其中 i 表示第幾層,2^( i - 1) 表示該層上有多少個元素,( k - i) 表示子樹上要比較的次數,如果在最差的條件下,就是比較次數后還要交換;因為這個是常數,所以提出來后可以忽略;
S = 2^(k-2) * 1 + 2^(k-3)2.....+2(k-2)+2^(0)*(k-1) ===> 因為葉子層不用交換,所以i從 k-1 開始到 1;
這個等式求解,我想高中已經會了:等式左右乘上2,然后和原來的等式相減,就變成了:
S = 2^(k - 1) + 2^(k - 2) + 2^(k - 3) ..... + 2 - (k-1)
除最后一項外,就是一個等比數列了,直接用求和公式:S = { a1[ 1- (q^n) ] } / (1-q);
S = 2^k -k -1;又因為k為完全二叉樹的深度,所以 (2^k) <= n < (2^k -1 ),總之可以認為:k = logn (實際計算得到應該是 log(n+1) < k <= logn );
綜上所述得到:S = n - longn -1,所以時間復雜度為:O(n)
時間復雜度分析參考

刪除

直接彈出堆頂元素,之后把堆尾元素置於堆頂,對這個新的堆定元素進行從上至下的調整。(下沉)
時間復雜度很直觀,就是O(logn)。

插入

將插入元素放於堆尾,進行從下至上(這里和上面的調整不同)的調整(上浮)。
1.對於該節點k,找到其雙親結點(k-1)/2,若a[k]<a[(k-1)/2],互換,k=(k-1)/2,遞歸向上;否則,停止上浮操作。
時間復雜度很直觀,就是O(logn)。
插入刪除參考

堆排序

堆排序就是利用堆進行的排序算法,每次將堆頂元素和堆尾元素互換,然后堆長度減一,之后做類似刪除操作的下沉重新建堆,遞歸即可得到降序排列的數組。
時間復雜度都是O(nlogn)。
不穩定。因為(小根堆)建堆調整時可能會將更靠右的相等元素調整至堆頂,從而導致更靠右的先輸出。
具體代碼有空再寫,可以參考:堆排序(這個就挺好)堆排序堆排序

歸並排序

歸並排序(MERGE-SORT)是利用歸並的思想實現的排序方法,該算法采用經典的分治(divide-and-conquer)策略(分治法將問題分(divide)成一些小的問題然后遞歸求解,而治(conquer)的階段則將分的階段得到的各答案"修補"在一起,即分而治之)。(快排的Partition也使用了分治的思想)

即在“分”的階段,把數組逐步分解為單個元素組成的數組,那么這個數組就必然是有序的,這個過程在代碼中也就是一個遞歸調用的過程。然后再逐步合成大的有序數組。
這樣每層都對比n次,一共logn層,所以各種情況下時間復雜度都是O(nlogn)。
同時空間復雜度為O(n)。
穩定。

	public static void MergeSort(int[] a) {
		int[] temp = new int[a.length];
		MergeSortHelper(a,temp,0,a.length-1);
	}
	private static void MergeSortHelper(int[] a,int[] temp,int l,int r) {
		if(l<r) {
			int m = (l+r)/2;
			MergeSortHelper(a,temp,l,m);
			MergeSortHelper(a,temp,m+1,r);
			//merge
			int i=l,j=m+1,t=0;
			while(i<=m && j<=r) {
				if(a[i]<=a[j]) temp[t++]=a[i++];
				else temp[t++]=a[j++];
			}
			while(i<=m) temp[t++]=a[i++];
			while(j<=r) temp[t++]=a[j++];
			t=l;
			while(l<=r) a[t++]=temp[t++];
		}
	}
def mergeSort(nums):
    if nums is None or len(nums)<1:
        return []
    copy = nums[:]
    mergeSortHelper(nums,copy,0,len(nums)-1)
    return nums
def mergeSortHelper(nums,copy,l,r):
    if l<r:
        m = (l+r)//2
        mergeSortHelper(nums,copy,l,m)
        mergeSortHelper(nums,copy,m+1,r)
        i=l;j=m+1;t=l
        while i<=m and j<=r:
            if nums[i]<=nums[j]:
                copy[t]=nums[i]
                t+=1;i+=1
            else:
                copy[t]=nums[j]
                t+=1;j+=1
        while i<=m:
            copy[t]=nums[i]
            t+=1;i+=1
        while j<=r:
            copy[t]=nums[j]
            t+=1;j+=1
        t=l
        while t<=r:
            nums[t]=copy[t]
            t+=1

冒泡排序

冒泡排序的思想是,每次將最小的元素交換到隊首。(對比左右元素,只要右元素小,就交換位置)。
時間復雜度:平均O(n^2),最壞O(n^2),最好O(n)。
最好發生在,數組本身有序。最壞發生在,數組本身逆序。
穩定。
具體算法:

    public static void bubbleSort(int[] a) {
        int len = a.length;
        int temp;
        for(int i=0;i<len;i++) {
            boolean flag=false;    //表示本躺冒泡是否發生交換
            for(int j=len-1;j>i;j--) {
                if(a[j]<a[j-1]) {  //穩定
                    temp=a[j];
                    a[j]=a[j-1];
                    a[j-1]=temp;
                    flag=true;
                }
            }
            if(flag==false) return;    //已經有序了,避免后續無用功
        }
    }

直接插入排序

直接插入排序的思想是,在序列前一部分有序時,將后一部分最前面的元素插入到有序部分,使插入后的序列依然有序。(找位置,找位置過程中移動元素騰出位置,然后插入)。
時間復雜度:平均O(n^2),最壞O(n^2),最好O(n)。
最好發生在,數組本身有序。最壞發生在,數組本身逆序。
穩定。
具體算法:

    public static void directInsertSort(int[] a) {
        int len = a.length;
        int temp;
        for(int i=1;i<len;i++) {    //從第二個元素開始,因為一個元素時必然是有序的
            if(a[i]<a[i-1]) {
                temp = a[i];
                int j=i-1;
                for(;j>=0;j--) {
                    if(a[j]>temp) a[j+1]=a[j];    //不用>=是為了保證算法的穩定性,不然后出現的元素就會排在前出現的元素之前了,而且那樣也會增加無謂的開銷
                }
                a[j+1]=temp;
            }
        }
    }

簡單選擇排序

簡單選擇排序的思想是,每輪找到一個最小的數,和無序部分的第一個元素交換位置。(遍歷后交換位置,不存在略過的元素的移動)。
時間復雜度,都O(n^2)。因為沒有像冒泡有提前終止的判斷,或者直接插入排序本身有序就不需要挪動元素了,所有數組的操作都是一樣的。
不穩定。首先由於交換位置的原因,那么第一個最小元素(出現在靠后的位置)就會跑到前面,故不穩定。如:222134會變成122234。
具體代碼:

    public static void selectionSort(int[] a) {
        int len=a.length;
        int temp;
        for(int i=0;i<len;i++) {
            int min = i;
            for(int j=i+1;j<len;j++) {
                if(a[j]<a[min]) min=j;
            }
            if(min!=i) {
                temp=a[i];
                a[i]=a[min];
                a[min]=temp;
            }
        }
    }

希爾排序

希爾排序是升級版的直接插入排序,它引入了步長的概念,先對等步長的每個子序列進行直接插入排序,然后縮短步長,最后步長=1,進行最后一次排序。
希爾排序的分析是一個復雜的問題,以為它的時間是所取“增量”序列的函數,這涉及到一些數學上尚未解決的難題。 直接記結論,最壞O(n^2),最好O(n^1.3)。
希爾排序比直接插入排序快的原因:
當文件初態基本有序時直接插入排序所需的比較和移動次數均較少。在希爾排序開始時增量較大,分組較多,每組的記錄數目少,故各組內直接插入較快,后來增量di逐漸縮小,分組數逐漸減少,而各組的記錄數目逐漸增多,但由於已經按di-1作為距離排過序,使文件較接近於有序狀態,所以新的一趟排序過程也較快。
當相同關鍵字的元素被分到不同的子表時,順序可能會發生變化,故不穩定。

    public static void shellSort(int[] a) {
        int len = a.length;
        int temp;
        for(int dk=len/2;dk>=1;dk/=2) {    //步長變化
            for(int i=dk;i<=len;i++) {
                if(a[i]<a[i-dk]) {//如果需要插入
                    temp=a[i];
                    int j=i-dk;
                    for(;j>=0;j-=dk) {
                        if(a[j]>temp) {a[j+dk]=a[j];}
                    }
                    a[j+dk]=temp;
                }
            }
        }
    }


免責聲明!

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



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