總結:排序演算法
一、直接插入排序
基本思想: |
把n個待排序的元素看成為一個有序表和一個無序表,開始時有序表中只包含1個元素,無序表中包含有n-1個元素,排序過程中每次從無序表中取出第一個元素,將它插入到有序表中的適當位置,使之成為新的有序表,重複n-1次可完成排序過程.
圖解: |
程式碼實現: |
void InsertSort(int *a, int size)
{
assert(a);
for (int i = 0; i < size - 1; i++)
{
int end = i; //有序表最後一個下標
int key = a[end + 1]; //待插入元素
while (end >= 0 && key < a[end]) //查詢合適位置
{
a[end + 1] = a[end];
end--;
}
a[end + 1] = key; //插入
}
}
分析: |
時間複雜度:
- 最好情況:
如果待排序的元素本身有序,那麼在進行插入排序時,每一個元素直接在前面有序表末尾處進行插入,整個過程下來,時間複雜度為O(N)- 最壞情況:
如果待排序的元素無序,那麼在進行插入排序時,每一個元素都需要在前面的有序表中找到其合適的插入位置,整個過程下來,時間複雜度為O(N^2)- 平均情況:O(N^2)
空間複雜度:O(1)
穩定性:穩定
說明:設在數列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面,在排序之後,a[i]仍然在a[j]前面,則這個排序演算法是穩定的。
二、希爾排序
基本思想: |
(1)預排序:先將整個待排元素序列分割成若干個子序列(由相隔某個“增量”的元素組成),分別進行直接插入排序,然後依次縮減增量再進行排序,使整個序列接近有序
(2)當整個序列中的元素基本有序時,再對全體元素進行一次直接插入排序
因為直接插入排序在元素基本有序的情況下效率是很高的,因此希爾排序在時間效率上相對於直接插入排序有較大提高。
圖解: |
程式碼實現: |
void ShellSort(int *a, int size)
{
assert(a);
int gap = size;
while (gap > 1)
{
gap = gap / 3 + 1; //增量(步長)
for (int i = 0; i < size - gap; i++)
{
int end = i; //有序表最後一個下標
int tmp = a[end + gap]; //待插入元素
while (end >= 0 && tmp < a[end]) //查詢合適位置
{
a[end + gap] = a[end];
end -= gap;
}
a[end + gap] = tmp; //插入
}
}
}
分析: |
時間複雜度:
- 最好情況:O(N)
- 最壞情況:O(N^2)
- 平均情況:O(N^1.3)
空間複雜度:O(1)
穩定性:不穩定
例如:待排序列3 2 2* 4,當gap為2時進行希爾排序,經過排序後變為2* 2 3 4,此時2和2*之間的相對位置發生變化。
三、直接選擇排序
基本思想: |
在元素序列a[i]~a[n-1]中選擇關鍵碼最大(最小)的資料元素,將它與這組元素中的最後一個(第一個)元素進行交換,接著在剩餘的元素序列a[i]~a[n-2](a[i+1]~-a[n-1])中重複上述步驟,直到剩餘1個元素時完成排序。
圖解: |
程式碼實現: |
void SelectSort(int *a, int size)
{
assert(a);
int left = 0;
int right = size - 1;
while (left < right)
{
int min = left;
int max = left;
for (int i = left; i <= right; i++)
{
if (a[i] < a[min]) //選出最小
{
min = i;
}
if (a[i] > a[max]) //選出最大
{
max = i;
}
}
Swap(&a[left], &a[min]);
if (max == left) //若max和left相等,則經過上一步交換,導致原max處為最小值,而min處為最大值
{
max = min; //更新max,讓其位置為最大值
}
Swap(&a[right], &a[max]);
left++;
right--;
}
}
分析: |
時間複雜度:O(N^2)
空間複雜度:O(1)
穩定性:不穩定
例如上述圖解,排序前25在25*之前,而在排序後25在25*後。
四、堆排序
基本思想: |
升序建大堆,降序建小堆
以升序為例,先將整個序列的元素建造成一個大堆,接著把堆頂元素和當前堆的最後一個元素進行交換,然後堆元素個數減1,接著從根節點通過向下調整使得當前堆恢復到大堆,重複上述過程,直到當前堆的元素個數為1時完成排序。
圖解: |
程式碼實現: |
//自頂向下調整
void AdjustDown(int *a, int size, int root)
{
int parent = root;
int child = parent * 2 + 1;
while (child < size)
{
int flag = 0;
if (child + 1 < size)
{
if (a[child + 1] > a[child])
{
child++;
}
}
if (a[child]>a[parent])
{
flag = 1;
Swap(&a[child], &a[parent]);
}
if (flag == 0) //優化
{
break;
}
parent = child;
child = parent * 2 + 1;
}
}
void HeapSort(int *a, int size)
{
assert(a);
int i = (size - 2) / 2;
for (; i >= 0; i--) //先建大堆
{
AdjustDown(a, size, i);
}
for (i = size - 1; i > 0; i--) //排序
{
Swap(&a[0], &a[i]);
AdjustDown(a, i, 0);
}
}
分析: |
時間複雜度:O(N*lgN)
空間複雜度:O(1)
穩定性:不穩定
五、氣泡排序
基本思想: |
從元素序列第一個位置開始,進行兩兩比較,根據大小交換位置,直到最後將最大(最小)的資料元素交換到了當前序列的最後一個位置,成為有序序列的一部分,然後元素個數減1,重複上述過程,直到所有資料元素都排好序。
圖解: |
程式碼實現: |
void BubbleSort(int *a, int size)
{
assert(a);
for (int i = 0; i < size - 1; i++)
{
int flag = 0;
for (int j = 0; j < size - i - 1; j++) //一趟排序
{
if (a[j]>a[j + 1])
{
flag = 1;
Swap(&a[j], &a[j + 1]);
}
}
if (flag == 0) //如果一趟排序後發現沒有一次交換,則說明已經有序
{
break;
}
}
}
分析: |
時間複雜度:
- 最好情況:O(N)
- 最壞情況:O(N^2)
- 平均情況:O(N^2)
空間複雜度:O(1)
穩定性:穩定
六、快速排序
基本思想: |
任取待排列元素序列中的一個元素作為基準值,通過一趟排序將要排序的序列分割成獨立的兩子序列,其中左子序列的所有元素都比基準值小,右子序列的所有元素都比基準值大,然後左右子序列重複此過程,直到所有元素都排列在相應的位置上為止。
圖解: |
將區間按照基準值劃分成左右兩部分的方法有: |
定義兩個指標begin和end,將基準值放在最右邊,begin從頭開始找比基準值大的值(begin++),end從尾開始找比基準值小的值(end–),若都找到且begin小於end,則兩者值交換,重複上述過程,直到begin>=end時,將begin所對應的值和最右邊的基準值交換,此時整個序列被基準值劃分成左右兩個子序列。
int PartSort1(int *a, int left, int right)
{
int index = GetMid(a, left, right); //三數取中,選取基準值
Swap(&a[index], &a[right]); //將基準值和最右邊的值交換
int key = a[right];
int begin = left;
int end = right;
while (begin < end)
{
//begin選大
while (begin < end && a[begin] <= key) //"="可省略
{
begin++;
}
//end選小
while (begin < end && a[end] >= key) //"="不可省略
{
end--;
}
if (begin < end)
{
Swap(&a[begin], &a[end]);
}
}
Swap(&a[begin], &a[right]);
return begin;
}
(2)挖坑法:
定義兩個指標begin和end,將基準值放在最右邊並儲存該值,此時該位置可視為一個坑。begin從頭開始找比基準值大的值,找到後將begin所對應的值填入到剛才的坑中,此時begin這個位置成為新的坑;begin不動,接著end從尾開始找比基準值小的值,找到後將end所對應的值填入到剛才的新坑中,此時end這個位置成為新的坑;end不動,begin從上次的位置接著往後找,重複上述過程,直到begin>=end時,將儲存的基準值填入到begin所對應的坑中,此時整個序列被基準值劃分成左右兩個子序列。
int PartSort2(int *a, int left, int right)
{
int index = GetMid(a, left, right); //三數取中,選取基準值
Swap(&a[index], &a[right]); //將基準值和最右邊的值交換
int key = a[right]; //儲存基準值
int begin = left;
int end = right;
while (begin < end)
{
//begin選大
while (begin < end && a[begin] <= key)
{
begin++;
}
a[end] = a[begin];
//end選小
while (begin < end && a[end] >= key)
{
end--;
}
a[begin] = a[end];
}
a[begin] = key;
return begin;
}
(3)前後指標法:
定義兩個指標prev和cur,將基準值放在最右邊,prev初始位置在left-1處,cur初始位置在left處。cur從頭開始找比基準值小的值,找到後若此時++prev的位置和cur的位置不在同一處(說明++prev對應的值一定比基準值大),則交換這兩處的值,cur接著剛才的位置往後找,重複上述過程,直到cur>=right時,將最右邊的基準值和++prev所對應的值進行交換,此時整個序列被基準值劃分成左右兩個子序列。
int PartSort3(int *a, int left, int right)
{
int index = GetMid(a, left, right); //三數取中,選取基準值
Swap(&a[index], &a[right]); //將基準值和最右邊的值交換
int prev = left - 1;
int cur = left;
while (cur < right)
{
if (a[cur] < a[right] && ++prev != cur)
{
Swap(&a[cur], &a[prev]);
}
cur++;
}
Swap(&a[++prev], &a[right]);
return prev;
}
三數取中法: |
在選擇基準值時,為了提高排序效率,我們常常利用三數取中法來選擇基準值,所謂的三數指的是:序列最左端的值、中間位置的值和最右端的值,計算它們的中位數來作為基準值。
int GetMid(int *a, int left, int right)
{
int mid = (left + right) >> 1;
if (a[left] < a[right])
{
if (a[mid] < a[left])
{
return left;
}
else if (a[mid] > a[right])
{
return right;
}
else
{
return mid;
}
}
else
{
if (a[mid] > a[left])
{
return left;
}
else if (a[mid] < a[right])
{
return right;
}
else
{
return mid;
}
}
}
程式碼實現: |
(1)遞迴法:
void QuickSortR(int *a, int left, int right)
{
assert(a);
if (left >= right)
{
return;
}
if (right - left < 10) //優化:在區間較小時,插入排序的效率高
{
InsertSort(a, right - left + 1);
}
else
{
int div = PartSort1(a, left, right);
QuickSortR(a, left, div - 1);
QuickSortR(a, div + 1, right);
}
}
(2)非遞迴法:
借用棧的結構來模仿遞迴(相關棧的函式定義請檢視順序棧)
void QuickSort(int *a, int left, int right)
{
assert(a);
Stack s;
StackInit(&s);
StackPush(&s, left);
StackPush(&s, right);
while (!StackEmpty(&s))
{
int end = StackTop(&s);
StackPop(&s);
int begin = StackTop(&s);
StackPop(&s);
int div = PartSort1(a, begin, end);
if (begin < div - 1)
{
StackPush(&s, begin);
StackPush(&s, div - 1);
}
if (div + 1 < end)
{
StackPush(&s, div + 1);
StackPush(&s, end);
}
}
}
分析: |
時間複雜度:
- 最好情況:O(N*lgN)
- 最壞情況:O(N^2)
- 平均情況:O(N*lgN)
空間複雜度:O(lgN)——遞迴深度
穩定性:不穩定
七、歸併排序
基本思想: |
將待排序的元素序列分成兩個長度相等的子序列,對每一個子序列排序,然後再將它們合併成一個有序序列。
圖解: |
合併圖解: |
程式碼實現: |
//歸併區域性遞迴
void _MergeSort(int *a, int left, int right, int *tmp)
{
if (left >= right)
{
return;
}
if (right - left < 10) //優化:在區間較小時,插入排序的效率高
{
InsertSort(a, right - left + 1);
return;
}
int mid = left + ((right - left) >> 1);
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid + 1, right, tmp);
int p = left;
int q = mid + 1;
int index = 0;
while (p <= mid&&q <= right)
{
if (a[p] <= a[q]) //加上"="可保持穩定性
{
tmp[index++] = a[p++];
}
else
{
tmp[index++] = a[q++];
}
}
while (p <= mid)
{
tmp[index++] = a[p++];
}
while (q <= mid)
{
tmp[index++] = a[q++];
}
int j = 0;
for (int i = left; i <= right; i++)
{
a[i] = tmp[j++];
}
}
void MergeSort(int *a, int left, int right)
{
assert(a);
int *tmp = (int*)malloc((right - left + 1)*sizeof(int));
memset(tmp, 0, (right - left + 1)*sizeof(int));
//或者:int *tmp = (int *)calloc(right - left + 1,sizeof(int));
_MergeSort(a, left, right, tmp);
free(tmp);
}
分析: |
時間複雜度:O(N*lgN)
空間複雜度:O(N)——臨時陣列
穩定性:穩定
八、計數排序
基本思想: |
統計待排序序列中每個元素出現的次數,再根據統計的結果重新對元素進行回收。
圖解: |
程式碼實現: |
void CountSort(int *a, int size)
{
assert(a);
//(1)找出最值,確定範圍,以便開闢空間
int max = a[0];
int min = a[0];
int index = 0;
for (index = 1; index < size; index++)
{
if (a[index]>max)
{
max = a[index];
}
if (a[index] < min)
{
min = a[index];
}
}
int range= max - min + 1;
//(2)統計出現次數
int *tmp = (int*)calloc(range, sizeof(int));
for (index = 0; index < size; index++)
{
tmp[a[index] - min]++;
}
//(3)回收
int i = 0;
for (index = 0; index < range; index++)
{
while (tmp[index])
{
a[i++] = index + min;
tmp[index]--;
}
}
free(tmp);
tmp = NULL;
}
分析: |
時間複雜度:O(N+range)
空間複雜度:O(range)
穩定性:穩定