[排序]選擇排序、冒泡排序、插入排序、希爾排序、歸併排序、快速排序、堆排序算法及比較
目錄
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) | 不穩定 |