排序總覽
什麼是排序?
?排序:所謂排序,就是使一串記錄,按照其中的某個或某些關鍵字的大小,遞增或遞減的排列起來的操作。
✍️排序的穩定性:假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序後的序列中,r[i]仍在r[j]之前,則稱這種排序演算法是穩定的;否則稱為不穩定的。
排序的分類
插入排序
直接插入排序
⛅基本思想:把待排序的數逐個插入到一個已經排好序的有序序列中,直到所有的記錄插入完為止,得到一個新的有序序列
✍️一般地,我們把第一個看作是有序的,所以我們可以從第二個數開始往前插入,使得前兩個數是有序的,然後將第三個數插入直到最後一個數插入
口頭說還是太抽象了,那麼我們用一個具體例子來介紹一下吧
所以直接插入排序的程式碼實現如下:
void InsertSort(int* a, int n)
{
int i = 0;
for (i = 0; i < n - 1; i++)
{
int end = i;
//先定義一個變數,將要插入的數儲存起來
int x = a[end + 1];
//直到後面的數比前面大的時候就不移動,就直接把這個數放在end的後面
while (end >= 0)
{
if (a[end] > x)
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = x;
}
}
時間複雜度和空間複雜度
⛅時間複雜度: 第一趟end最多往前移動1次,第二趟是2次……第n-1趟是n-1次,所以總次數是1+2+3+……+n-1=n*(n-1)/2,所以說時間複雜度是O(n^2)
最好的情況: 順序
最壞的情況: 逆序
⛅空間複雜度:由於沒有額外開闢空間,所以空間複雜度為O(1)
直接插入排序穩定性
✍️直接插入排序在遇到相同的數時,可以就放在這個數的後面,就可以保持穩定性了,所以說這個排序是穩定的。
希爾排序
?基本思想:希爾排序是建立在直接插入排序之上的一種排序,希爾排序的思想上是把較大的數儘快的移動到後面,把較小的數儘快的移動到後面。先選定一個整數,把待排序檔案中所有記錄分成個組,所有距離為的記錄分在同一組內,並對每一組內的記錄進行排序。(直接插入排序的步長為1),這裡的步長不為1,而是大於1,我們把步長這個量稱為gap,當gap>1時,都是在進行預排序,當gap==1時,進行的是直接插入排序
?同樣的我們看圖說話!
我們先來一個單趟的排序:
int end = 0;
int x = a[end + gap];
while (end >= 0)
{
if (a[end] > x)
{
a[end + gap] = a[end];
end =end - gap;
}
else
{
break;
}
}
a[end + gap] = x;
這裡的單趟排序的實現和直接插入排序差不多,只不過是原來是gap = 1,現在是gap了。
由於我們要對每一組都進行排序,所以我們可以一組一組地排,像這樣:
// gap組
for (int j = 0; j < gap; j++)
{
int i = 0;
for (i = 0; i < n-gap; i+=gap)
{
int end = i;
int x = a[end + gap];
while (end >= 0)
{
if (a[end] > x)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = x;
}
}
也可以對程式碼進行一些最佳化,直接一起排序,不要一組一組地,程式碼如下:
int i = 0;
for (i = 0; i < n - gap; i++)// 一起預排序
{
int end = i;
int x = a[end + gap];
while (end >= 0)
{
if (a[end] > x)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = x;
}
當gap>1時,都是在進行預排序,當gap==1時,進行的是直接插入排序。
- gap越大預排越快,預排後越不接近有序
- gap越小預排越慢,預排後越接近有序
- gap==1時,進行的是直接插入排序。
- 所以接下來我們要控制gap,我們可以讓最初gap為n,然後一直除以2直到gap變成1,也可以這樣:gap = gap/3+1。只要最後一次gap為1就可以了。
所以最後的程式碼實現如下:
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)// 不要寫等於,會導致死迴圈
{
// gap > 1 預排序
// gap == 1 插入排序
gap /= 2;
int i = 0;
for (i = 0; i < n - gap; i++)// 一起預排序
{
int end = i;
int x = a[end + gap];
while (end >= 0)
{
if (a[end] > x)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = x;
}
}
}
時間複雜度和空間複雜度
⛅時間複雜度:外層的迴圈次數,複雜度是O(logN)
每一組的數的個數大概是N/gap,總共有N/n/gap個組,所以調整的次數應該是(1+2+......+N/gap-1)*gap,所以我們分成兩種極端來看待這個問題:
當gap很大,也就是gap = N/2的時候,調整的次數是N/2,也就是O(N)
當gap很小,也就是gap = 1的時候,按道理來講調整的次數應該(1+2+......+N-1)*gap,應該是O(n^2),但是這時候應該已經接近有序,次數沒有那麼多,所以我們不如就看作時間複雜度為O(N)
綜上:希爾排序的時間複雜度應該是接近O(N*logN)
⛅空間複雜度:由於沒有額外開闢空間,所以空間複雜度為O(1)
希爾排序穩定性
✍️我們可以這樣想,相同的數被分到了不同的組,就不能保證原有的順序了,所以說這個排序是不穩定的。
選擇排序
直接選擇排序
?基本思想:每一次從待排序的資料元素中選出最小(或最大)的一個元素,存放在序列的起始位置,直到全部待排序的資料元素排完
?同樣的我們看圖說話!
整體排序就是begin往前走,end往後走,相遇就停下,所以整體程式碼實現如下:void SelectSort(int* a, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int mini = begin;
int maxi = begin;
int i = 0;
for (i = begin; i <= end; i++)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
// 如果maxi和begin相等的話,要對maxi進行修正
if (maxi == begin)
{
maxi = mini;
}
swap(a[begin], a[mini]);
swap(a[end], a[maxi]);
begin++;
end--;
}
}
}
這裡說明一下,其中加了一段修正maxi的程式碼,就是為了防止begin和maxi相等時,mini與begin交換會導致maxi的位置發生變化,而此時begin就是maxi,若此時交換maxi和end,換到end處的不是最大值,而是最小值mini,所以提前將mini賦值給maxi,當begin與mini交換的時候,mini處就是begin也就是最大值,這樣maxi與end交換就不會出現錯誤
時間複雜度和空間複雜度
⛅ 時間複雜度:第一趟遍歷n-1個數,選出兩個數,第二趟遍歷n-3個數,選出兩個數……最後一次遍歷1個數(n為偶數)或2個數(n為奇數),所以總次數是n-1+n-3+……+2,所以說時間複雜度是O(n^2)
✍️最好的情況: O(n^2)(順序)
✍️最壞的情況: O(n^2)(逆序)
⛅空間複雜度:由於沒有額外開闢空間,所以空間複雜度為O(1)
選擇排序穩定性
直接選擇排序是不穩定的
舉個例子:假設順序是3 2 3 0,遍歷選出最小的0,此時0與3交換,兩個3的前後順序明顯發生了變化,所以是不穩定的
堆排序
資料結構初階--堆排序+TOPK問題 - 一隻少年a - 部落格園 (cnblogs.com)
這篇已經介紹過了
補充一點:堆排序是不穩定的
交換排序
氣泡排序
?基本思想:它重複地走訪過要排序的元素列,依次比較兩個相鄰的元素,如果順序(如從大到小、首字母從Z到A)錯誤就把他們交換過來。走訪元素的工作是重複地進行直到沒有相鄰元素需要交換,也就是說該元素列已經排序完成(依次向後比較兩個元素,將大的元素放到後面)
?同樣的我們看圖說話!
氣泡排序整體程式碼實現如下:
void BubbleSort(int* a, int n)
{
int i = 0;
//外層迴圈,需要進行幾次排序
for (i = 0; i < n - 1; i++)
{
int j = 0;
//內部迴圈,比較次數
for (j = 0; j < n - i - 1; j++)
{
if (a[j] > a[j + 1])
{
swap(a[j], a[j + 1]);
}
}
}
}
✍️我們思考一個問題,假設當前的序列已經有序了,我們有沒有什麼辦法直接結束排序?就像圖中的情況,在第三次排序的時候已經有序,後面的比較是沒必要的
這當然是有的,我們可以定義一個exchange的變數,如果這趟排序發生交換就把這個變數置為1,否則就不變,不發生交換的意思就是該序列已經有序了,利用這樣一個變數我們就可以直接結束迴圈了
最佳化後的氣泡排序程式碼:
void BubbleSort(int* a, int n)
{
int i = 0;
for (i = 0; i < n - 1; i++)
{
int exchange = 0;
int j = 0;
for (j = 0; j < n - i - 1; j++)
{
if (a[j] > a[j + 1])
{
exchange = 1;
Swap(&a[j], &a[j + 1]);
}
}
// 不發生交換
if (exchange == 0)
break;
}
}
時間複雜度和空間複雜度
⛅時間複雜度: 第一趟最多比較n-1次,第二趟最多比較n-2次……最後一次最多比較1次,所以總次數是n-1+n-2+……+1,所以說時間複雜度是O(n^2)
最好的情況: O(n)(順序)
最壞的情況: O(n^2)(逆序)
⛅空間複雜度:由於沒有額外開闢空間,所以空間複雜度為O(1)
氣泡排序穩定性
✍️氣泡排序在比較遇到相同的數時,可以不進行交換,這樣就保證了穩定性,所以說氣泡排序數穩定的。
快速排序(遞迴版本)
?基本思想:透過一趟排序將要排序的資料分割成獨立的兩部分,其中一部分的所有資料都比另外一部分的所有資料都要小,然後再按此方法對這兩部分資料分別進行快速排序,整個排序過程可以遞迴進行,以此達到整個資料變成有序序列
✍️快速排序的基本流程
- 首先在待排序列中確定一個基準值,遍歷整個序列,將小於(可包含等於)基準值的元素放到基準值左邊,將大於(可包含等於)基準值的元素放到其右邊。(降序序列可將位置調整)
- 此時基準值將序列分割成倆個部分,左邊的元素全部小於基準值,右邊的元素全部大於基準值
- 將分割的左右倆部分進行如上倆步操作,實則為遞迴
- 透過遞迴將左右倆側排好序,直至分割的小序列個數為1,排序全部完成
hoare版本
?基本思想:任取待排序元素序列中的某元素作為基準值,按照該排序碼將待排序集合分割成兩子序列,左子序列中所有元素均小於基準值,右子序列中所有元素均大於基準值,然後最左右子序列重複該過程,直到所有元素都排列在相應位置上為止。
?同樣的我們看圖說話!
?我們要遵循一個原則:關鍵詞取左,右邊先找小再左邊找大;關鍵詞取右,左邊找先大再右邊找小。
?一次過後,2也就來到了排序後的位置,接下來我們就是利用遞迴來把key左邊區間和右邊的區間遞迴排好就可以了,如下:
遞迴左區間:[left, key-1] key 遞迴右區間:[key+1, right]
hoare版本找key值程式碼實現如下:
int PartSort1(int* a, int left, int right)
{
int key = left;
while (left < right)
{
// 右邊找小
while (left < right && a[right] >= a[key])
{
right--;
}
// 左邊找大
while (left < right && a[left] <= a[key])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[key], &a[left]);
return left;
}
快排程式碼實現如下:
void QuickSort(int* a, int left, int right)
{
if (left > right)
return;
int div = PartSort1(a, left, right);
// 兩個區間 [left, div-1] div [div+1, right]
QuickSort(a, left, div - 1);
QuickSort(a, div + 1, right);
}
✍️我們考慮這樣一種情況,當第一個數是最小的時候,順序的時候會很糟糕,因為每次遞迴right都要走到頭,看下圖:
為了最佳化這裡寫了一個三數取中的程式碼,三數取中就是在序列的首、中和尾三個位置選擇第二大的數,然後放在第一個位置,這樣就防止了首位不是最小的,這樣也就避免了有序情況下,情況也不會太糟糕。
下面是三數取中程式碼:
int GetMidIndex(int* a, int left, int right)
{
int mid = left + (right - left) / 2;
if (a[mid] > a[left])
{
if (a[right] > a[mid])
{
return mid;
}
// a[right] <= a[mid]
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
// a[mid] <= a[left]
else
{
if (a[mid] > a[right])
{
return mid;
}
// a[mid] <= a[right]
else if (a[left] > a[right])
{
return right;
}
else
{
return left;
}
}
}
所以加上三數取中最佳化後的程式碼如下:
int PartSort1(int* a, int left, int right)
{
int index = GetMidIndex(a, left, right);
Swap(&a[index], &a[left]);
int key = left;
while (left < right)
{
// 右邊找小
while (left < right && a[right] >= a[key])
{
right--;
}
// 左邊找大
while (left < right && a[left] <= a[key])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[key], &a[left]);
return left;
}
挖坑法
?基本思想:設定一個基準值(一般為序列的最左邊元素,也可以是最右邊的元素)此時最左邊的是一個坑。開闢兩個指標,分別指向序列的頭結點和尾結點(選取的基準值在左邊,則先從右邊出發。反之,選取的基準值在右邊,則先從左邊出發)。 從右指標出發依次遍歷序列,如果找到一個值比所選的基準值要小,則將此指標所指的值放在坑裡,左指標向前移。 後從左指標出發(選取的基準值在左邊,則後從左邊出發。反之,選取的基準值在右邊,則後從右邊出發),依次便利序列,如果找到一個值比所選的基準值要大,則將此指標所指的值放在坑裡,右指標向前移。 依次迴圈步驟,直到左指標和右指標重合時,我們把基準值放入這兩個指標重合的位置。
?同樣的我們看圖說話!
挖坑法我們要遵循一個原則:坑在左,右邊找小;坑在右,左邊找大。
挖坑法程式碼實現如下(加了三數取中演算法):
int PartSort2(int* a, int left, int right)
{
int index = GetMidIndex(a, left, right);
Swap(&a[index], &a[left]);
//pivot就是那個坑
int pivot = left;
int key = a[pivot];
while (left < right)
{
// 坑在左邊,右邊找小
while (left < right && a[right] >= key)
{
right--;
}
Swap(&a[pivot], &a[right]);
pivot = right;
// 坑在右邊邊,右邊找大
while (left < right && a[left] <= key)
{
left++;
}
Swap(&a[pivot], &a[left]);
pivot = left;
}
a[pivot] = key;
return pivot;
}
前後指標法
?基本思想:前後指標法就是有兩個指標prev和cur,cur個在前,prev在後,cur在前面找小,找到了,prev就往前走一步,然後交換prev和cur所在位置的值,然後cur繼續找小,直到cur走到空指標的位置就結束,最後將prev的值與key交換就完成了一次分割區間的操作
?同樣的我們看圖說話!
程式碼實現:
int PartSort3(int* a, int left, int right)
{
int index = GetMidIndex(a, left, right);
Swap(&a[index], &a[left]);
int key = a[left];
int prev = left;
int cur = left + 1;
while (cur <= right)
{
if (a[cur] < key)
{
prev++;
if (prev != cur)
Swap(&a[cur], &a[prev]);
}
cur++;
}
Swap(&a[prev], &a[left]);
return prev;
}
小區間最佳化快速排序
?小區間最佳化原理:當快速排序在遞迴過程中一直切分割槽間時,最後會被分成很小的區間,當區間中的資料個數很小時,其實這是已經是沒有必要進行再分割的,且最後一層基本上佔據了快速排序一半的遞迴,這是我們可以選擇其他的排序來解決這個小區間的排序
?還有一個我們要思考的問題就是最後這段小區間用什麼排序比較好?
希爾排序適應的是比較多的資料才有優勢,堆排序也不行,需要建堆,有點殺雞用牛刀的感覺,其他三個插入排序、選擇排序和氣泡排序相比,還是插入排序比較優,所以我們小區間選擇用插入排序進行排序
void QuickSort(int* a, int left, int right)
{
if (left > right)
return;
int div = PartSort3(a, left, right);
// 兩個區間 [left, div-1] div [div+1, right]
if (div - 1 - left > 10)
{
QuickSort(a, left, div - 1);
}
else
{
InsertSort(a + left, (div - 1) - left + 1);
}
if (right - div - 1 > 10)
{
QuickSort(a, div + 1, right);
}
else
{
InsertSort(a + div + 1, right - (div + 1) + 1);
}
}
快速排序(非遞迴版本)
? 基本思想:利用棧來模擬實現遞迴呼叫的過程,利用壓棧的順序來實現排序的順序。
給大家看一個利用棧模擬實現的動圖
?同樣的我們看圖說話!
我們拿陣列arr=[5,2,4,7,9,1,3,6]來舉個例子:
第一步:我們先把區間的右邊界值7進行壓棧,然後把區間的左邊界值0進行壓棧,那我們取出時就可以先取到左邊界值,後取到後邊界值
第二步:我們獲取棧頂元素,先取到0給left,後取到7給right,進行單趟排序
第三步:第一趟排完後,區間被分為左子區間和右子區間。為了先處理左邊,所以我們先將右子區間壓棧,分別壓入7和5,然後壓入左子區間,3和0
第四步:取出0和3進行單趟排序
第五步:此時左子區間又被劃分為左右兩個子區間,但是右子區間只有4一個值,不再壓棧,所以只入左子區間,將1和0壓棧
第六步:取出0和1進行單趟排序
第七步:至此,左子區間全部被排完,這時候才可以出5和7排右子區間,是不是很神奇?這個流程其實和遞迴是一模一樣的,順序也沒變,但解決了遞迴的致命缺陷——棧溢位。後面的流程就不一一展現了
void QuickSortNonR(int* a, int left, int right)
{
stack<int> s;
s.push(right);
s.push(left);
while (!s.empty())
{
int newLeft = s.top();
s.pop();
int newRight = s.top();
s.pop();
//挖洞法
int div = PartSort2(a, newLeft, newRight);
// 兩個區間 [left, div-1] div [div+1, right]
// 壓右區間
if (div + 1 < newRight)
{
s.push(newRight);
s.push(div + 1);
}
// 壓左區間
if (newLeft < div - 1)
{
s.push(div - 1);
s.push(newLeft);
}
}
}
快速排序時間複雜度和空間複雜度
⛅空間複雜度:
最優的情況下空間複雜度為:O(logN) ;每一次都平分陣列的情況
最差的情況下空間複雜度為:O( N ) ;退化為氣泡排序的情況
⛅時間複雜度:
快速排序最優的情況下時間複雜度為:O( NlogN )
快速排序最差的情況下時間複雜度為:O( N^2 )
快速排序的平均時間複雜度也是:O(NlogN)
快速排序穩定性
快速排序顯然是不穩定的,我們試想一下:5 ....5...1....5,這種情況,交換第一個5和1的時候,顯然三個5的前後順序發生了變化,是不穩定的
歸併排序
遞迴版本
? 基本思想:(MERGE-SORT)是建立在歸併操作上的一種有效的排序演算法,該演算法是採用分治法(Divide andConquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱為二路歸併。
?歸併條件: 左區間有序 右區間有序
?同樣的我們看圖說話!
上半部分遞迴樹為將當前長度為 n 的序列拆分成長度為 n/2 的子序列,下半部分遞迴樹為合併已經排序的子序列
再來一張動態圖應該更好理解吧~
歸併排序程式碼實現:
void _MergeSort(int* a, int left, int right, int* tmp)
{
if (left >= right)
return;
int mid = left + (right - left) / 2;
// 歸併條件:左區間有序 右區間有序
// 如何做到?遞迴左右區間
// [left, mid] [mid + 1, right]
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid + 1, right, tmp);
//歸併
int begin1 = left;
int end1 = mid;
int begin2 = mid + 1;
int end2 = right;
int i = left;
//對歸併的陣列進行排序,暫存到tmp陣列中
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
//兩個while迴圈將兩個歸併陣列未加入tmp中的元素加入到tmp當中
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//將tmp陣列的值賦值給陣列a,因為a是指標,所以對形參進行修改對應實參也會修改
for (i = left; i <= right; i++)
{
a[i] = tmp[i];
}
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
歸併排序時間複雜度和空間複雜度
⛅時間複雜度:O(N*logN)
⛅空間複雜度: O(N),要來一個臨時空間存放歸併好的區間的資料
歸併排序穩定性
歸併排序在遇到相同的數時,可以就先將放前一段區間的數,再放後一段區間的數就可以保持穩定性了,所以說這個排序是穩定的
非遞迴版本
? 基本思想: 這裡我們用迴圈來實現這個非遞迴的歸併排序,我們可以先兩兩一組,在四個四個一組歸併……
?同樣的我們看圖說話!(分兩種情況討論)
特殊情況(元素個數為2^i)
根據上面這個圖,我們可以很快的寫出一個框架,例如下面的程式碼:
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
int gap = 1;//每趟合併後序列的長度
while (gap < n)//合併趟數的結束條件是:最後合併後的序列長度>=陣列元素的個數
{
int i = 0;
//每趟進行兩兩合併
for (i = 0; i < n; i += 2 * gap)
{
// [i, i+gap-1] [i+gap, i+2*gap-1]
int begin1 = i;
int end1 = i + gap - 1;
int begin2 = i + gap;
int end2 = i + 2 * gap - 1;
int index = i;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
int j = 0;
for (j = i; j <= end2; j++)
{
a[j] = tmp[j];
}
}
gap *= 2;
}
free(tmp);
tmp = NULL;
}
一般情形(陣列的元素個數不一定是2^i )
雖然元素個數不一定是 2^i 個,但是任意元素的個數為n,都必然可以拆寫成 2^j+m 個元素的情況
由圖可知,這種情況下存在兩種特殊情況:
- 橙色箭頭代表無需合併,因為找不到配對的元素,造成該情況的原因是歸併過程中,右半區間不存在,此時我們可以不進行這次歸併,直接跳出迴圈,也就是begin2>=n的時候,我們就break跳出這次迴圈,不進行歸併
- 綠色箭頭代表兩個長度不相等的元素也要合併,歸併過程中,左區間存在,右區間也存在,但是右區間和左區間長度不一樣,就意味著end2>=n的情況,此時我們只需要對end2進行調整,使得右區間範圍縮小,不越界,就可以繼續歸併
我們可以看到這種情況在一次歸併中僅存在一次或者零次。
所以調整後的程式碼如下:
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
int gap = 1;
while (gap < n)
{
int i = 0;
for (i = 0; i < n; i += 2 * gap)
{
// [i, i+gap-1] [i+gap, i+2*gap-1]
// 兩種需要調整的情況:
// 1.右區間不存在
// 2.正要歸併的右區間和左區間長度不一樣
int begin1 = i;
int end1 = i + gap - 1;
int begin2 = i + gap;
int end2 = i + 2 * gap - 1;
int index = i;
// 情況1:當右區間不存在的時候,右區間的範圍是[begin2,end2],所以begin2越界,就代表著右區間不存在的情況
if (begin2 >= n)
break;
// 情況2,左右區間長度不一,同樣此時右區間是存在的,但是end2越界,就代表了左右區間長度不一的情況,此時我們需要做調整
//將end2的長度設定成n-1,將原來的end2(越界)設定成陣列a的最後一個元素的位置,因為不平衡的區間最後一個元素一定是陣列a的最後一個元素
if (end2 >= n)
end2 = n - 1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
int j = 0;
for (j = i; j <= end2; j++)
{
a[j] = tmp[j];
}
}
gap *= 2;
}
free(tmp);
tmp = NULL;
}
?這樣非遞迴的歸併排序就這樣被我們實現了。非遞迴歸併排序的實現的難點不在框架,而在邊界控制,我們要把邊界控制的到位,這樣就能夠很好地實現這個非遞迴
計數排序
? 基本思想: 它的優勢在於在對一定範圍內的整數排序時,它的複雜度為Ο(n+k)(其中k是整數的範圍),快於任何比較排序演算法。 當然這是一種犧牲空間換取時間的做法
?同樣的我們看圖說話!
我們可以先計數出這個序列資料的範圍也就是range = max - min + 1,最大值和最小值都可以透過遍歷一遍序列來選出這兩個數。然後我們可以開一個大小為range的計數的空間count中,然後將序列中的每一個數都減去min,然後對映到count這個空間中,然後我們再一次取出並加上min依次放進原陣列空間中,這樣我們就順利地完成了排序
具體程式碼實現如下:
void CountSort(int* a, int n)
{
int min = a[0];
int max = a[0];
int i = 0;
for (i = 1; i < n; i++)
{
if (a[i] > max)
{
max = a[i];
}
if (a[i] < min)
{
min = a[i];
}
}
int range = max - min + 1;
int* count = (int*)malloc(sizeof(int) * range);
if (count == NULL)
{
printf("malloc error\n");
exit(-1);
}
// 初始化開闢的空間
memset(count, 0, sizeof(int) * range);
for (i = 0; i < n; i++)
{
count[a[i] - min]++;
}
int index = 0;
for (int i = 0; i < range; i++)
{
while (count[i]--)
{
a[index++] = i + min;
}
}
free(count);
count = NULL;
}
計數排序時間複雜度和空間複雜度
⛅空間複雜度: O(N),要來一個臨時空間存放歸併好的區間的資料
⛅時間複雜度:O(MAX(N,範圍))(以空間換時間)
計數排序穩定性
計數排序在我們這個實現裡是不穩定的
排序比較
排序方法 | 平均情況 | 最好情況 | 最壞情況 | 輔助空間 | 穩定性 |
---|---|---|---|---|---|
直接插入排序 | O(n^2) | O(n) | O(n^2) | O(1) | 穩定 |
希爾排序 | O(nlogn~n^2) | O(n^1.3) | O(n^2) | O(1) | 不穩定 |
直接選擇排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不穩定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不穩定 |
氣泡排序 | O(n^2) | O(n) | O(n^2) | O(1) | 穩定 |
快速排序 | O(nlogn) | O(nlogn) | O(n^2) | O(1) | 不穩定 |
歸併排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 穩定 |