常見排序算法
一、直接插入排序:
插入排序是從數組的首元素開始,遍歷整個數組,遍歷到每個元素的時候,將這個元素依次和它前面的元素進行比較,如果按照從小到大的方式進行排序的話,要求前面的所有元素的值小於它本身,使用一箇中間變量,將這個元素保存起來,將大於它的元素依次往後移,最後遇到和它一樣大或者小於它的元素,將它放到這個元素的後面。所以排序過程中,需要一個臨時的中間變量。
代碼:
void sort(int* src, int n)
{
int i, j;
int tmp;
for (i = 0; i < n; ++i)
{
tmp = src[i];
for (j = i; j > 0 && src[j - 1] > tmp; --j)
{
/*插入排序的核心思想是這個部分,含義是要排序的第i位也就是tmp值
如果前面有比他還大的數那就將這些數後移一個,給tmp應該在的地方騰出來
*/
src[j] = src[j - 1];
}
src[j] = tmp;
/* 最後再將tmp插進去*/
}
}
直接插入排序特性總結:
1、元素集合越接近有序,直接插入排序的時間複雜度越低。
2、時間複雜度:O(N^2)。
3、空間複雜度:O(1),它是一種穩定的排序算法。
4、穩定性:穩定
2、希爾排序(縮小增量排序)
希爾排序也是一種插入排序,是插入排序後的種更加高效的排序方式,由於插入排序,被排序序列越有序插入排序越快。
圖片來自 https://blog.csdn.net/dujiafengv/article/details/102599103
代碼:
#include<stdio.h>
#include<stdlib.h>
void shellSort(int arr[], int length)
{
int temp = 0;
int count = 0;
// 根據前面的逐步分析,使用循環處理
for (int gap = length / 2; gap > 0; gap /= 2)
{
for (int i = gap; i < length; i++)//一對一對往後走
{
// 遍歷各組中所有的元素(共gap組,每組有個元素), 步長gap
for (int j = i - gap; j >= 0; j -= gap)//一組的前面一個,
{
// 如果當前元素大於加上步長後的那個元素,說明交換
if (arr[j] > arr[j + gap])
{
temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
}
}
int main()
{
int arr[6] = { 5,0,1,6,10,2 };
shellSort(arr, 6);
for (int i=0;i<6;++i)
{
printf("%d ", arr[i]);
}
system("pause");
return 0;
}
希爾排序特性總結:
1、希爾排序是對直接插入排序的優化
2、當排序步長都>1的排序都是預排序,目的是讓數組更加接近有序,當步長==1的時候,數組已經很接近有序了,這樣排序的速度就很快了。
3、希爾排序的時間複雜度不確定,經過推導出來的時間複雜度爲O(N^1.3-N^2)。
4、穩定性:不穩定。
3、選擇排序:
代碼:
#include<stdio.h>
#include<stdlib.h>
#include <algorithm>
void SelectionSort(int arr[], int length)
{
int i, j;
int _min, tmp,min_index;
for (i = 0; i < length; ++i)
{
_min = arr[i];
for (j = i + 1; j < length; ++j)
{
if (arr[j] > _min)
{
_min = arr[j];
min_index = j;
}
}
if (i!=min_index)
{
tmp = arr[min_index];
arr[min_index] = arr[i];
arr[i] = tmp;
}
}
}
int main()
{
int arr[11] = { 5,0,1,123,13,11,6,5,4,3,1};
SelectionSort(arr, 11);
for (int i = 0; i <11; ++i)
{
printf("%d ", arr[i]);
}
system("pause");
return 0;
}
/* 選擇排序將新舊位置交換 */
直接選擇排序的特性總結:
1、直接選擇排序的算法效率不高,實際很少使用。
2、時間複雜度O(N^2)。
3、空間複雜度:O(1)。
4、穩定性:不穩定。
4、堆排序。
堆排序是藉助一種特殊的數據結構來排序的,要使用堆,首先需要建堆,堆在邏輯上是一顆完全二叉樹,在內存中是一個數組(數組是按照二叉樹的層序遍歷存儲的)。堆只分爲大頂堆或者小頂堆,建堆的過程需要用到自頂向下調整算法。
自頂向下調整算法:
/*返回以index爲根的完全二叉樹的左子樹的索引,整個二叉樹索引以0爲開始*/
int left(int index)
{
return ((index << 1) + 1);
}
/*返回以index爲根的完全二叉樹的右子樹的索引,整個二叉樹索引以0爲開始*/
int right(int index)
{
return ((index << 1) + 2);
}
/*兩個數的交換*/
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
return;
}
void max_heap_adjust(int array[], int index)
{ //index是位置
int left_index = left(index);
int right_index = right(index);
int largest = index;
//左子樹和父節點進行對比
if (left_index <= (heap_size - 1) && array[left_index] > array[largest]) {
largest = left_index;
}
//右子樹和父節點進行對比
if (right_index <= (heap_size - 1) && array[right_index] > array[largest]) {
largest = right_index;
}
if (largest == index) {
/*判斷是否要進行遞歸調用,每交換一次最小二叉樹的時候,可能會破壞前面已經調整好的堆
的結構,所以交換一次需要從當前父親節點開始重新進行自頂向下算法,重新調整堆*/
/*這裏的遞歸退出條件是 經過上面的計算最大值仍然是堆頂,即調整不能調整爲止
*/
return;
}
else {
//需要交換
swap(&array[index], &array[largest]);
//遞歸調用
max_heap_adjust(array, largest);
}
}
更加詳細的建堆過程 https://blog.csdn.net/weixin_43447989/article/details/99695304
建立好大頂堆或者小頂堆以後,就可以進行堆排序了。
方式是,依次將堆頂的元素與堆尾的元素交換,交換後的堆尾作爲有序區不再參與堆的自頂向下調整算法。可以發現,要得到遞增序列初始的堆必須爲大頂堆,遞減序列,初始的堆爲小頂堆。
代碼:
/*返回以index爲根的完全二叉樹的左子樹的索引,整個二叉樹索引以0爲開始*/
int left(int index)
{
return ((index << 1) + 1);
}
/*返回以index爲根的完全二叉樹的右子樹的索引,整個二叉樹索引以0爲開始*/
int right(int index)
{
return ((index << 1) + 2);
}
/*兩個數的交換*/
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
return;
}
void max_heap_adjust(int array[], int index)
{ //index是位置
int left_index = left(index);
int right_index = right(index);
int largest = index;
//左子樹和父節點進行對比
if (left_index <= (heap_size - 1) && array[left_index] > array[largest]) {
largest = left_index;
}
//右子樹和父節點進行對比
if (right_index <= (heap_size - 1) && array[right_index] > array[largest]) {
largest = right_index;
}
if (largest == index) {
/*判斷是否要進行遞歸調用,每交換一次最小二叉樹的時候,可能會破壞前面已經調整好的堆
的結構,所以交換一次需要從當前父親節點開始重新進行自頂向下算法,重新調整堆*/
/*這裏的遞歸退出條件是 經過上面的計算最大值仍然是堆頂,即調整不能調整爲止
*/
return;
}
else {
//需要交換
swap(&array[index], &array[largest]);
//遞歸調用
max_heap_adjust(array, largest);
}
}
void heap_sort(int array[], int length) {
int old_heap_size = heap_size;
int i;
for (i = length - 1; i >= 1; --i) {
swap(&array[i], &array[0]);
--heap_size;
max_heap_adjust(array, 0);
}
//恢復堆的大小
heap_size = old_heap_size;
}
建堆的過程需要的時間複雜度是O(logn),堆排序最壞的情況是排N次,所以堆排序的時間複雜度是O(Nlogn),堆排序僅僅需要一個記錄原始堆大小的輔助變量。
堆排序的特性分析:
1、使用堆排序來選擇第幾大的數很好用。
2、時間複雜度:O(logn)。
3、空間複雜度:O(1)。
4、穩定度:不穩定。
5、交換排序
基本思想: 基本思想:所謂交換,就是根據序列中兩個記錄鍵值的比較結果來對換這兩個記錄在序列中的位置,交換排
序的特點是:將鍵值較大的記錄向序列的尾部移動,鍵值較小的記錄向序列的前部移動。
5.1 冒泡排序
代碼:
#include<stdio.h>
#include<stdlib.h>
#define MAX 3
//冒泡排序
int main()
{
int a[MAX] = { 9,6,14 };
int i = 0;
int t = 0;
int j = 0;
for (i = 0; i < MAX; i++)
{
for (j = 0; j < (MAX-1) - i; j++)//此處的MAX-1-i不是MAX-i的原因是j+1不會越界;
{
if (a[j + 1] > a[j])//外層循環循環一次就會把較大數往前移一位
{
t = a[j + 1];
a[j + 1] = a[j];
a[j] = t;
}
}
}
for (i = 0; i < MAX; i++)
{
printf("%d ", a[i]);
}
system("pause");
return 0;
}
對於冒泡排序相信大家都比較熟悉,它的核心思想是,假如是將數組從大到小排序,外層for循環執行一次,數組中最大的那個數將向前移一位。所以外層for循環的循環次數最多是數組的的元素個數次。而內層for循環的循環次數如for (j = 0; j < (MAX-1) - i; j++)//此處的MAX-1-i不是MAX-i的原因是j+1不會越界,因爲後面會有a[j+1]和a[j]的判斷。
冒泡排序的特性分析:
1、時間複雜度:O(N^2)。
2、空間複雜度:O(1)。
3、穩定度:穩定。
6、快速排序:
6.1、 hoare法
實現步驟:
第一步:選擇待排序數組中的三個值,分別是首尾還有中值,start,end 和mid
第二步:對三個數進行排序,從小到大,
第三步:保護基準值,swap(src + mid, src + end - 1);也就是基準值和倒數第二個元素交換
第四步:a = start + 1, b = end - 2,b向前找比基準值小的,a向後找比基準值大的,找到之後 交換所指向的值,然後將基準值和a指向的值交換 (因爲是從小到大排序a找的大的,應該在數組後面所以基準值和它換),產生左邊的都比基準值小,右邊的都比基準值大,然後a作爲二叉樹的根返回,重複上面四步操作,直到類似形狀的二叉樹被遍歷完爲止。
int hoareway(int* src, int start, int end)
{
int a = start + 1, b = end - 2;
int mid = (start + end) / 2;
if (src[start] > src[mid])
{
swap(src + start, src + mid);
}
if (src[mid] > src[end])
{
swap(src + mid, src + end);
}
if (src[start] > src[mid])
{
swap(src + start, src + mid);
/*上面是三數排序部分*/
}
if (end - mid <= 2)
{
return mid;
/*如果小於4個數直接返回輸出*/
}
/*保護基準值*/
swap(src + mid, src + end - 1);
while (a <= b)
{
while (a < end - 1 && src[a] <= src[end - 1])
{
a++;
}
while (b > 0 && src[b] >= src[end - 1])
{
b--;
}
if (a == b && (a == 1 || a == end - 1))
{
break;
/*一種是找到同一個值,一種是找到了已經三數排好的地方,此時就不用排*/
}
if (a < b)
{
swap(src + a, src + b);
/* 交換*/
}
}
swap(src + a, src + end - 1);//將基準值交換回來
return a;
}
void dealquicksort(int *src, int start, int end)
{
int mid;
if (start < end)
{
mid = hoareway(src, start, end);
dealquicksort(src, start, mid - 1);
dealquicksort(src, mid + 1, end);
}
}
void quicksort(int *src, int n)
{
//快速排序
dealquicksort(src, 0, n - 1);
}
6.2 雙指針法
雙指針法:
從小到大
第一步:定義兩個指針一個指向數組頭a一個指向數組尾b;
第二步:以頭指針爲基準值先從尾指針開始向前找比基準值小的元素,找的之後交換a,b所指向的值
第三步:此時b所指向的值就是基準值,a開始向後找,找到比基準值大的交換。
第四步:完成一次交換之後,以基準值下標爲返回值返回遞歸調用點
void swap(int* a, int*b)
{
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
}
int doublepointway1(int *src, int start, int end)
{
int a = start;
int b = end;
int flag = 0;
while ( src[b] > src[a])
{
b--;
}
while (a < b)
{
swap(src + b, src + a);
flag = !flag;
while (src[b] >= src[a])
{
flag ? a++ : b--;
/* tlag作爲標記位,用來判斷是讓a指針尋找還是讓b指針尋找*/
}
}
return flag ? b : a;
}
void dealquicksort(int *src, int start, int end)
{
int mid;
if (start<end)
{
mid = doublepointway2( src, start, end);
dealquicksort( src, start, mid-1);
dealquicksort(src, mid + 1, end);
}
}
void quicksort(int *src, int n)
{
//快速排序
dealquicksort(src, 0, n-1);
}
更好理解的雙指針版快速排序
void qsort(int arr[], int low, int high)
{
if (low >= high)
return;//指針交錯返回
int main = arr[low];//基準值
int a = low, b = high;
while (a < b)
{
while (a < b && main <= arr[b])//退出的條件是找出小於main的值
--b;
arr[a] = arr[b];
while (a < b && main >= arr[a])
++a;
arr[b] = arr[a];
}
arr[a] = main;
qsort(arr, low, a - 1);
qsort(arr, a + 1, high);
}
挖坑法:https://blog.csdn.net/weixin_43447989/article/details/100054493
快速排序的特性總結:
1、快速排序的綜合性能和使用場景都是很好的,所以叫快速排序。
2、時間複雜度,排一次的時間複雜度是O(logn),最壞的情況下排n次,所以整體時間複雜度是O(n*logn)。
3、空間複雜度:O(logn)。
4、穩定性:不穩定。
7、歸併排序
圖片來自https://blog.csdn.net/doubleguy/article/details/81390951
可以看到歸併排序的基本方法是先對數組執行分的操作,將數組分割成二叉樹的樣子,再對其進行排序。
圖片來自https://blog.csdn.net/weixin_44465743/article/details/88902052
從上面的分治圖中可以總結出規律:將數組細分爲最小單元時,形狀像一顆二叉樹,並且它的排序方式和二叉樹的後序遞歸遍歷很像,我們嘗試着使用二叉樹的後序遞歸遍歷解決基本的代碼框架。
二叉樹的後序遍歷是:
void BinaryTreePostOrder(BTNode* root)
{
if (root)
{
BinaryTreePostOrder(root->lchild);
BinaryTreePostOrder(root->rchild);
putchar(root->data);
}
}
代碼實現:
void dealMergeSort(int *src, int* tmp, int str, int end)
{
if (str >= end)
{
return;
}
int mid = (str + end) / 2;
dealMergeSort(src, tmp, str, mid);
dealMergeSort(src, tmp, mid + 1, end);
int a = str;
int b = mid + 1;
int c = str;//1,5,6,15,8,26,5,55,9,0
while (a <= mid && b <= end)
{
if (src[a] > src[b])
{
tmp[c] = src[a];
a++;
}
else
{
tmp[c] = src[b];
b++;
}
c++;
//將兩個已經比較好的數據存入臨時的空間裏面
}
for (; a <= mid; a++, c++)
{
tmp[c] = src[a];
}
for (; b <= end; b++, c++)
{
tmp[c] = src[b];
}
//將剩餘的數據存到臨時數組裏面
int i = 0;
for (i = 0; i <= end; ++i)
{
src[i] = tmp[i];
}
//將臨時數組元素存入到原數組當中
}
void MergeSort(int *src, int n)
{
int *tmp = (int*)malloc(n * sizeof(int));
dealMergeSort(src, tmp, 0, n - 1);//傳入下標
free(tmp);
}
歸併排序特性分析:
1、歸併排序需要額外的空間開銷。
2、時間複雜度:O(nlogn)。
3、空間複雜度O(N)
4、穩定性:穩定。
排序方法 | 平均情況 | 最好情況 | 最壞情況 | 輔助空間 | 穩定性 |
冒泡排序 | O(N^2) | O(N) | O(N^2) | O(1) | 穩定 |
選擇排序 | O(N^2) | O(N^2) | O(N^2) | O(1) | 穩定 |
插入排序 | O(N^2) | O(N) | O(N^2) | O(1) | 穩定 |
希爾排序 | O(N*logN)~O(N^2) | O(N^1.3) | O(N^2) | O(1) | 不穩定 |
堆排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(1) | 不穩定 |
歸併排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(N) | 穩定 |
快速排序 | O(N*logN) | O(N*logN) | O(N^2) | O(logN)~O(N) | 不穩定 |