[排序]選擇排序、冒泡排序、插入排序、希爾排序、歸併排序、快速排序、堆排序算法及比較

[排序]選擇排序、冒泡排序、插入排序、希爾排序、歸併排序、快速排序、堆排序算法及比較

1.選擇排序

    從數組中選擇最小的元素,將它與數組的第一個元素交換位置,再講數組剩下的元素中選擇最小的元素,將它與數組的第二個元素交換位置,重複操作,直到將整個數組排序。

選擇排序:

選擇排序
    選擇排序需要N2/2次比較和N次交換,對已經排序的數組也需要這麼多次比較和交換操作。

public void Sort(int[] nums) {
	int len=nums.length();
	for(int i=0;i<len-1;i++){
		int min=nums[i];
		for(int j=i+1;j<len;j++){
			if(nums[j]<min)
				min=nums[j];
		}
		swap(nums[i],min);
	}
}

時間複雜度:最好O(n2) 最壞O(n2)
空間複雜度:O(1)
穩定性:不穩定

2.冒泡排序

    數組中相鄰的元素進行比較,如果順序就不交換,如果順序錯誤就交換,每次讓未排序的最小元素浮到左側,或者最大元素移動右側。

第一次排序:

第一次排序

第二次排序:

第二次排序

第三次排序:

第三次排序

第四次排序:

第四次排序

2.1常規版

public void Sort(int[] nums) {
	int len=nums.length();
	for(int i=0;i<len;i++){
		//最小元素移到左側
		for(int j=len-1;j>i;j--){
			if(nums[j-1]>nums[j])
				swap(nums[j-1],nums[j]);
		}
		
		//如果最大元素移到右側
		/*		
		for(int j=0;j<len-i-1;j++){
			if(nums[j]>nums[j+1])
				swap(nums[j-1],nums[j]);
		}
		*/
	}
}

時間複雜度:最好O(n2) 最壞O(n2)
空間複雜度:O(1)
穩定性:穩定

2.2第一次改進

考慮[2,1,3,4,5]進行冒泡排序
第一次排序:1,2,3,4,5
第二次排序:1,2,3,4,5
第三次排序:1,2,3,4,5
第四次排序:1,2,3,4,5
第一次循環就已經完成了排序,但是仍會繼續後面的流程,顯然是多餘的。
    爲了解決這個問題,可以設置一個標誌位,用來表示是否有交換,如果有交換繼續下一次循環,如果沒有則停止。

public void Sort(int[] nums) {
	int len=nums.length();
	for(int i=0;i<len;i++){
		int flag=1;
		//最小元素移到左側
		for(int j=len-1;j>i;j--){
			if(nums[j-1]>nums[j]){
				swap(nums[j-1],nums[j]);
				flag=0;
			}
		}
		if(flag==1)//如果沒有交換過元素,說明已經有序
			return;
	}
}

    這一次優化之後,假如從小到大排序[1,2,3,4,5]有序數組,則只會進入一次循環,此時的時間複雜度爲O(n)。
時間複雜度:最好O(n) 最壞O(n2)

2.3第二次改進

    考慮內循環長度,假如第i次排序時,最後一次產生交換的位置爲index,則說明index之前的元素已經排好序了,那麼第i+1次排序時,就可以直接從尾判斷到index停止。
    設置一個index標誌位,標記最後一次產生交換時的位置,縮小內循環。

public void Sort(int[] nums) {
	int len=nums.length();
	int temppos=0;
	int index=0;
	for(int i=0;i<len;i++){
		int flag=1;
		//最小元素移到左側
		index=temppos;//判斷到上一次排序時最後一次產生交換的位置
		for(int j=len-1;j>index;j--){
			if(nums[j-1]>nums[j]){
				swap(nums[j-1],nums[j]);
				flag=0;
				temppos=j;
			}
		}
		if(flag==1)//如果沒有交換過元素,說明已經有序
			return;
	}
}

算法得到了進一步的優化,可以去掉內循環中多餘的步驟。
由於至少需要循環進行一次比較,所以時間複雜度還是 最好O(n) 最壞O(n2)

3.插入排序

    直接插入排序將無序序列中的元素插入有序序列中,遍歷無序序列,拿無序序列中的元素與有序序列中的元素進行比較,找到合適的位置然後插入。

插入排序:

插入排序

3.1常規版

public void Sort(int[] nums) {
	int len=nums.length();
	for(int i=0;i<len;i++){
		for(int j=i+1;j>=0;j--){
			if(nums[j]<num[j-1])
				swap(nums[j],nmus[j-1]);
		}
	}
}

時間複雜度主要取決於比較次數和交換次數
比較次數1+2+3+……+n ~= n2/2

時間複雜度:最好O(n2) 最壞O(n2)
空間複雜度:O(1)
穩定性:穩定

3.2改進

    考慮有序數組[1,2,3,4,5]的最後一次循環,5與前面已經排好序的[1,2,3,4]比較,5>4那麼就可以停止內循環不再與前面進行比較。
    設置一個flag判斷第一次比較後是否產生交換,如果沒有,則說明已經有序。

public void Sort(int[] nums) {
	int len=nums.length();
	int flag=1;
	for(int i=0;i<len;i++){
		for(int j=i+1;j>=0;j--){
			if(nums[j]<num[j-1]){
				swap(nums[j],nmus[j-1]);
				flag=0;
			}
			if(flag)
				break;
		}
	}
}

改進後的算法,對於有序數組只需要進行n次比較。
時間複雜度:最好O(n) 最壞O(n2)

4.希爾排序

    對於數組[3,5,2,4,1],包含逆序(5,2),(5,4),(5,1),(2,1),(4,1),插入排序每次只交換相鄰元素,使逆序數量減1,對於大規模的數組,排序速度很慢。希爾排序就是爲了解決插入排序的侷限性,通過交換不相鄰的元素,每次使逆序數量減少大於1。

希爾排序:

希爾排序

public void sort(int[] nums) {
	int len=nums.length();
	int h=len/3;
	while(h>0){
		for(int i=0;i<len;i++){
			for(int j=i+h;j>=h;j=j-h){
				if(nums[j]<num[j-h])
					swap(nums[j],nmus[j-h]);
			}
		}
		h=h/3;
	}
}

    這個代碼不覺得似曾相識的樣子嗎,就是在插入排序的基礎上,把每次+1相鄰比較換成了每次+h個比較,然後增加了外層循環來改變h的值。因此時間複雜度與插入排序時一樣的。
時間複雜度:最好O(n) 最壞O(n2)
空間複雜度:O(1)
穩定性:不穩定

5.歸併排序

    將數組分爲兩部分,分別進行排序,然後歸併起來。

拆分:

拆分

歸併:

歸併

5.1歸併方法

public void Merge(int[] nums,int start,int mid,int end){
	int[] temp;
	int i=start,j=mid+1,k=0;
	for(int i=0;i<end;l++)//構建輔助數組
		temp[i] = nums[i];
	while(i<=mid&&j<=end){
		if(nums[i]<=nums[j])//=保證穩定性
			temp[k++] = nums[i++]
		else
			temp[k++] = nums[j++];
	}
	if(i>mid){
		while(j<=end)
			temp[k++] = nums[j++];
	}
	else{
		while(i<=mid)
			temp[k++] = nums[i++];
	}
	for(int i=0;i<end;i++)//歸併結果複製回nums
		nums[i] = temp[i];
}

5.2自頂向下歸併排序

public void Up2DownMergeSort(int[] nums,int start,int end) {
	if(start>=end)
		return;
	int mid = (strat + end) / 2;
	Up2DownMergeSort(start,mid);
	Up2DownMergeSort(mid+1,end);
	Merge(nums,start,mid,end);
}

    歸併排序每次都將問題對半分成兩個子問題,這種對半分的算法複雜度一般爲 O(nlogn)。
時間複雜度:最好O(nlogn) 最壞O(nlogn)
空間複雜度:O(n)
穩定性:穩定

5.3自底向上歸併排序

    從單個元素開始向上成對歸併。

public void Down2UpMergeSort(int[] nums) {
	int len=nums.length();
	int lo=2;
	while(lo<=len){
		for(int i=0;i<len;i=i+lo){
			int j = i + lo -1;
			int mid = (i + j) / 2;
			Merge(nums,i,mid,j);			
		}
		lo = lo * 2;
	}
}

6.快速排序

    快速排序在每一輪挑選一個基準元素,讓比它大的元素移到右邊,比它小的元素移到左邊,一般取序列的第一個或最後一個元素作爲基準。

快速排序:

快速排序
    例如[4,7,6,5,3,2,8,1],以4爲基準,從右邊找到第一個比4小的,從左邊找到第一個比4大的,交換。

public void QuickSort(int[] nums,int start,int end) {          
	if(start>=end)
		return;    
	int pos = GetPos(nums,start,end);
	QuickSort(nums,start,pos-1);
	QuickSort(nmus,pos+1,end);
}

 public int GetPos(int[] nums,int start,int end){
 	int flag = nmus[start];
    int left = start + 1 ;
    int right = end;
    
    while(left<right){
    	while(nums[left]<flag)
    		left++;
    	while(nums[right]>flag)
    		right--;
    	if(left<right)
    		swap(nums[left++],nums[right--]);
    }
    swap(nums[start],nums[right]);
    return right;
 }

    快速排序的時間複雜度,一次劃分要從兩頭開始搜索,直到low>=high,所以時間複雜度是O(n),整個排序算法的時間複雜度取決於劃分的次數。

  • 理想的情況是,每次劃分所選擇的中間數恰好將當前序列恰好等分,經過log2n次劃分,就可得到長度爲1的子表。這樣整個算法的時間複雜度爲O(nlog2n)。
  • 最壞的情況是,每次劃分所選擇的中間數恰好是最大或最小數,這樣長度爲n的數據表的快速排序需要經過n趟劃分,退化成了冒泡排序。此時整個算法的時間複雜度爲O(n2)。

時間複雜度:最好O(nlogn) 最壞O(n2)
空間複雜度:O(logn)
穩定性:不穩定

6.1算法改進

1.切換到插入排序
    對於很小和部分有序的數組快速排序沒有插入排序效果好,而快速排序在小數組中會遞歸調用自己,因此,在待排序序列的長度分割到一定大小後,可以切換到插入排序。
2.隨機選取基準
    前面提到,如果待排序數組是有序數組,每次取序列第一個元素作爲基準就退化成了冒泡排序,效率低下,爲了緩解這種情況,可以每次從序列中隨機選取一個元素作爲基準。
3.三數取中
    雖然隨機選取基準減少了不好分割的機率,但如果待排序數組元素值全相等時,仍然是O(n2),爲了緩解這種情況引入了三數取中。我們知道理想的情況是每次劃分的中間數將當前序列等分,最佳的狀態是選擇序列排序後的中間值,但這很難算出來。一般的做法是選取序列頭、中間、尾三個元素排列後的中間值作爲基準。
4.三向切分
    對於有大量重複元素的數組,可以將數組切分爲三部分,小於、等於、大於,也就是說在一次切分結束後,可以把與基準相同的元素聚集在一起,下一次切分時,不在對與基準相同的元素進行切分。
    例如[3,1,3,2,3,5,3,7,3]以第一個元素3爲基準
    第一趟快排結果爲[3,1,3,2,3,5,3,7,3],切分成兩個子序列[3,1,3,2]和[5,3,7,3]
三向切分第一趟快排結果爲[1,2,3,3,3,3,3,7,5],切分成兩個子序列[1,2]和[7,5]
對比可見,三向切分能減少迭代次數,提高效率。

public void QuickSort(int[] nums,int start,int end) {      
	int left = start;
	int l = start+1;
	int right = end;
	int flag = nums[start];
	while(l<=right){
		if(nums[l]<flag)
			swap(nums[l++],nums[left++]);//小於基準的數始終在跟基準交換,可以l++
		else if(nmus[l]>flag)
			swap(nmus[l],nums[right--]);//大於基準的數在跟右邊的數交換,不知大小,所以不能l++
		else
			l++;
	}
}

6.2快速選擇算法

    快速排序的GetPos()函數會返回一個j,使得a[0,j-1]小於a[j],a[j+1,len-1]大於a[j],因此,a[j]就是數組的第j大元素,可以利用這個函數找出數組的第j個元素。

public int select(int[] nums, int k) {
    int l = 0, h = nums.length - 1;
    while (h > l) {
        int j = GetPos(nums, l, h);
        if (j == k) 
            return nums[k];
        else if (j > k) 
            h = j - 1;
        else 
            l = j + 1;        
    }
    return nums[k];
}

7.堆排序

7.1堆

    堆中某個節點的值總是大於等於其子節點的值,並且堆是一棵完全二叉樹。
    堆可以用數組來表示,因爲堆是一棵完全二叉樹,而完全二叉樹很容易用數組表示,位置k的節點的父節點在k/2位置,子節點在2k和2k+1位置。爲了更清晰的描述節點的位置關係,這裏不適用數組索引爲0表示。

堆:

堆

7.2上浮和下沉

    在構建大頂堆時,當一個節點比父節點大時,需要交換這兩個節點,交換後的節點可能仍然比父節點大,需要不斷的比較和交換,把這種操作稱爲上浮。

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

private void swim(int k) {
    while (k > 1 && heap(k / 2) < heap(k)) {
        swap(heap(k / 2), heap(k));
        k = k / 2;
    }
}

    類似的,在構建大頂堆時,當一個節點的值比子節點小,也需要不斷向下進行比較和交換,稱爲下浮。如果一個節點有兩個子節點,應該和兩個子節點中值較大的節點進行交換。

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

private void sink(int k) {
    while (2 * k <= N) {
        int j = 2 * k;
        if (j < N && heap(j) < heap(j + 1))
            j++;
        if (heap(k) >= heap(j))
            break;
        swap(heap(k) , heap(j));
        k = j;
    }
}

7.3插入元素

    將插入元素放到數組的末尾,然後上浮到合適位置。

public void insert(int v) {
    heap[++N] = v;
    swim(N);
}

7.4刪除最大元素

    將數組頂端元素刪除,將數組最後一個元素放到頂端,然後下沉到合適位置。

public int delMax() {
    int max = heap[1];
    swap(heap(1), heap(N--));
    heap[N + 1] = null;
    sink(heap(1));
    return max;
}

7.5堆排序

    堆排序的基本思想:將待排序序列構造成一個大頂堆,此時整個序列的最大值就是堆頂的根節點,將其與末尾元素進行交換,此時末尾爲最大值,然後將剩餘N-1個元素重新構造成一個大頂堆,這樣會得到N的元素的第二大值,如此反覆執行,便能得到一個有序序列了。

7.5.1構造堆

    無序數組建立堆最直接的方式是從左到右(從上到下順序遍歷)進行上浮操作,最後構建爲一個大頂堆,但是考慮當一個節點有子節點,而且有子節點的子節點,當它與它的子節點調整後,它可能仍然需要繼續調整,那麼繼續調整之後可能會需要二次調整。
    例如,第一步7,9交換,第二步7,11交換,9,11交換,之後9,10需要二次調整。
在這裏插入圖片描述
    一個更高效的方式是從右到左(從下往上遍歷)進行下沉操作,最後構建爲一個小頂堆,如果一個節點的兩個節點已經堆有序,下沉可以使以這個節點爲根節點的堆有序,此時就算有二次調整也只關子節點,無關父節點。葉子節點不用下沉,從最後一個非葉子節點開始。
    索引從1開始時,最後一個非葉子節點的索引爲節點總數/2
在這裏插入圖片描述

7.5.1交換堆頂元素與最後一個元素

    交換之後需要進行下沉操作維持堆的有序狀態。
在這裏插入圖片描述
    繼續交換下沉
在這裏插入圖片描述
    繼續交換下沉
在這裏插入圖片描述
    繼續交換下沉
在這裏插入圖片描述
    至此,堆排序完成。

public void HeapSort(int[] nums) {
        int N = nums.length;
        for (int k = N / 2; k >= 1; k--)//數組從索引1開始,從最後一個非葉子節點開始構建大頂堆
            sink(nums, N, k);
        while (N > 1) {
            swap(nums[1], nums[N--]);
            sink(nums, N, 1);
        }
    }
private void sink(int[] nums,int N,int k) {
    while (2 * k <= N) {//節點與它的父節點交換後,可能需要與子節點二次調整
        int j = 2 * k;
        if (j < N && nums[j] < nums[j + 1])
            j++;
        if (nums[k] >= nums[j])
            break;
        swap(nums[k] , nmus[j]);
        k = j;
    }
}

    因爲堆排序無關乎初始序列是否已經排序已經排序的狀態,始終有兩部分過程

  • 構建初始的大頂堆的過程時間複雜度爲O(n)
  • 交換及重建大頂堆的過程中,需要交換n-1次,重建大頂堆的過程根據完全二叉樹高度爲logn向下取整的性質,[log2(n-1),log2(n-2)…1]逐步遞減次交換
  • 一共近似爲nlogn,所以它最好和最壞的情況時間複雜度都是O(nlogn)

時間複雜度:最好O(nlogn) 最壞O(nlogn)
空間複雜度:O(1)
穩定性:不穩定

8.對比表格

算法 時間複雜度 空間複雜度 穩定性
選擇排序 最好O(n2) 最壞O(n2) O(1) 不穩定
冒泡排序 最好O(n) 最壞O(n2) O(1) 穩定
插入排序 最好O(n) 最壞O(n2) O(1) 穩定
希爾排序 最好O(n) 最壞O(n2) O(1) 不穩定
歸併排序 最好O(nlogn) 最壞O(nlogn) O(n) 穩定
快速排序 最好O(nlogn) 最壞O(n2) O(logn) 不穩定
堆排序 最好O(nlogn) 最壞O(nlogn) O(1) 不穩定
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章