前言
排序是《數據結構》中最基本的學習內容。排序算法可以分爲內部排序和外部排序,內部排序是數據記錄在內存中進行排序。而外部排序是因排序的數據很大,一次不能容納全部的排序記錄,在排序過程中需要訪問外存。常見的內部排序算法有:插入排序、希爾排序、選擇排序、冒泡排序、歸併排序、快速排序、堆排序、基數排序等。知識框架如下:
插入排序
插入排序是一種簡單直觀的排序方法,其基本思想在於每次將一個待排序記錄,按其關鍵字大小插入到前面已經排好序的子序列中,直到全部記錄插入完成。
直接插入排序
直接插入排序把要排序的序列分成兩部分:第一部分是有序子序列,而第二部分是無序序列。每次挑選無序序列中的第一個元素與第一部分的有序子序列從後往前逐個比較,當待排元素大於子序列中的元素時,將其插入到該元素的後方。直接插入排序會使用一個"哨兵",用於存放元素。直接插入排序每次插入一個元素,所以排序趟數固定爲n-1。
空間複雜度:O(1) 時間複雜度:O(n^2) 穩定性:穩定
#include <stdio.h>
void insertsort(int a[],int n)
{
int i,j,k;
for(i=2;i<n;i++){
a[0]=a[i];
for(j=i-1;j>0;j--){
if(a[0]>a[j]){
break;
}
else {
a[j+1]=a[j];
}
}
a[j+1]=a[0];
}
}
int main()
{
int i,a[11];
printf("輸入10個數:");
for(i=1;i<11;i++)
scanf("%d",&a[i]);
insertsort(a,11);
printf("排序結果是:");
for(i=1;i<11;i++)
printf("%d ",a[i]);
printf("\n");
}
折半插入排序
折半插入排序僅僅是減少了比較元素的次數,約爲O(nlogn),該比較次數與待排序列的初始狀態無關,僅取決於序列中的元素個數n;而元素的移動次數沒有改變,它依賴於待排序列的初始狀態。因此,折半插入排序的時間複雜度仍是O(n^2)。
空間複雜度:O(1) 時間複雜度:O(n^2) 穩定性:穩定
#include <stdio.h>
void BinaryInsertSort(int *arry, int n)
{
int i,j;
int high,low,mid;
int temp;
for(i = 2; i < n; i++)
{
arry[0] = arry[i];
low = 1;
high = i-1;
while (low <= high)
{
mid = (low+high)/2;
if (arry[mid] > arry[0])
{
high = mid-1;
}
else if (arry[mid] <= arry[0])
{
low = mid+1;
}
}
for(j = i-1; j >= low; j--)
{
arry[j+1] = arry[j];
}
arry[low] = arry[0];
}
}
int main()
{
int a[] = {0,2,45,7,8,45,3,6,0};
int iLen =sizeof(a)/sizeof(a[0]);
for(int i = 1; i < iLen; i++)
{
printf("%d ", a[i]);
}
printf("\n");
BinaryInsertSort(a, iLen);
for(int i = 1; i < iLen; i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
希爾排序
希爾排序是插入排序的一種又稱“縮小增量排序”,是直接插入排序算法的一種更高效的改進版本。基本思想:先取一個小於n的整數d1作爲第一個增量,把文件的全部記錄分組。所有距離爲d1的倍數的記錄放在同一個組中。先在各組內進行直接插入排序;然後,取第二個增量d2<d1重複上述的分組和排序,直至所取的增量 = 1(<…<d2<d1),即所有記錄放在同一組中進行直接插入排序爲止。空間複雜度:O(1) 時間複雜度:O(n^2) 穩定性:不穩定
#include <stdio.h>
void ShellInsertSort(int *arry, int n, int d)
{
int i,j;
for(i = d+1; i < n; i++)
{
if (arry[i-d] > arry[i])
{
arry[0] = arry[i];
for(j = i-d; j > 0 && arry[0] < arry[j]; j = j-d)
{
arry[j+d] = arry[j];
}
arry[j+d] = arry[0];
}
}
}
void ShellSort(int *arry, int n, int d)
{
int i;
for(i = d; i > 0; i--)
{
ShellInsertSort(arry, n, i);
}
}
int main()
{
int a[] = {0,2,4,7,8,1,3,6,0};
int iLen = sizeof(a)/sizeof(a[0]);
for(int i = 1; i < iLen; i++)
{
printf("%d ", a[i]);
}
printf("\n");
ShellSort(a, iLen, 4);
for(int i = 1; i < iLen; i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
交換排序
交換排序就是根據序列中兩個元素關鍵字的比較結果來對換這兩個記錄在序列中的位置。常用的交換排序算法有冒泡和快排。交換類的排序,其趟數和原始序列狀態有關。
冒泡排序
冒泡排序算法的基本思想是:假設待排序列長爲n,從前往後比較相鄰元素的值,若爲逆序,則交換它們,直到序列比較完。每趟冒泡至少把序列中的一個元素放到序列的最終位置,且最多做n-1趟冒泡就可以把所有元素排好序。注意:冒泡排序中所產生的有序子序列一定是全局有序(不同於直接插入排序),也就是說有序子序列中的所有元素的關鍵字一定小於或大於無序序列中所有元素的關鍵字,這樣每一趟排序都會將一個元素放置到其最終的位置上。
空間複雜度:O(1) 時間複雜度:O(n^2) 穩定性:穩定
#include <stdio.h>
void bubblesort1(int *arry,int n)
{
int i,j,k;
int temp,flag;
for(i=0;i<n;i++)
{
flag=0;
for(j=0;j<n-i-1;j++)
{
if(arry[j]>arry[j+1])
{
temp=arry[j];
arry[j]=arry[j+1];
arry[j+1]=temp;
flag=1;
}
}
if(flag==0) break;
}
}
void bubblesort2(int a[], int n) //雙向冒泡排序
{
int low = 0,high = n-1;
int i,t,flag = 1;
while (low < high && flag)
{
flag = 0;
for(i = low;i < high; i++)
{
if (a[i] > a[i+1])
{
t = a[i];
a[i] = a[i+1];
a[i+1] = t;
flag = 1;
}
}
high--;
for(i = high;i > low; i--)
{
if (a[i] < a[i-1])
{
t = a[i-1];
a[i-1] = a[i];
a[i] = t;
flag = 1;
}
}
low++;
}
}
int main()
{
int a[10]={5,4,8,7,9,5,4,6,3,2};
int i;
for(i=0;i<10;i++)
printf("%d ",a[i]);
bubblesort1(a,10);
printf("\n");
for(i=0;i<10;i++)
printf("%d ",a[i]);
printf("\n");
return 0;
}
快速排序
快排算法是基於分治策略的排序算法,其基本思想是,對於輸入的數組a[low, high],按以下三個步驟進行排序。
(1)分解:以a[p]爲基準將a[low: high]劃分爲三段a[low:p-1],a[p]和a[p+1:high],使得a[low:p-1]中任何一個元素小於等於a[p], 而a[p+1: high]中任何一個元素大於等於a[p]。
(2)遞歸求解:通過遞歸調用快速排序算法分別對a[low:p-1]和a[p+1:high]進行排序。
(3)合併:由於對a[low:p-1]和a[p+1:high]的排序是就地進行的,所以在a[low:p-1]和a[p+1:high]都已排好序後,不需要執行任何計算,a[low:high]就已經排好序了。
想要更詳細的瞭解快排,可以看這篇文章:快速排序的4種優化
空間複雜度:O(logn) 時間複雜度:O(nlogn) 穩定性:不穩定
#include <stdio.h>
int Partition(int a[], int low, int high)
{
int i,j,k,temp;
i = low;
j = high+1;
k = a[low];
while(1)
{
while(a[++i] < k && i < j);
while(a[--j] > k);
if(i >= j) break;
else
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
a[low] = a[j];
a[j] = k;
return j;
}
void QuickSort(int a[], int low, int high)
{
if(low < high)
{
int q = Partition(a, low, high);
QuickSort(a, low, q-1);
QuickSort(a, q+1, high);
}
}
int main()
{
int i;
int a[10] = {3,4,5,6,1,2,0,7,8,9};
QuickSort(a, 0, 9);
for(i = 0; i < 10; ++i){
printf("[%d]", a[i]);
}
printf("\n");
return 0;
}
選擇排序
選擇排序是一種簡單直觀的排序算法。它的工作原理是:第一次從待排序的數據元素中選出最小(或最大)的一個元素,存放在序列的起始位置,然後再從剩餘的未排序元素中尋找到最小(大)元素,然後放到已排序的序列的末尾。以此類推,直到全部待排序的數據元素的個數爲零。選擇排序是不穩定的排序方法。
簡單選擇排序
空間複雜度:O(1) 時間複雜度:O(n^2) 穩定性:不穩定
#include <stdio.h>
void selectionsort(int a[],int n)
{
int i,j,k,t;
for(i=0;i<n-1;i++)
{
k=i;
for(j=i+1;j<n;j++)
{
if(a[j]<a[k])
k=j;
}
if(k!=i)
{
t=a[i];
a[i]=a[k];
a[k]=t;
}
}
}
void main()
{
int i,a[10];
for(i=0;i<10;i++)
scanf("%d",&a[i]);
selectionsort(a,10);
for(i=0;i<10;i++)
printf("%d ",a[i]);
printf("\n");
}
堆排序
堆排序是指利用堆這種數據結構所設計的一種排序算法。分爲兩種方法:大頂堆:每個節點的值都大於或等於其子節點的值,在堆排序算法中用於升序排列;小頂堆:每個節點的值都小於或等於其子節點的值,在堆排序算法中用於降序排列;
想要更詳細的瞭解堆,可以看這篇文章:通俗易懂的講解堆排序(含Gif圖)
空間複雜度:O(1) 時間複雜度:O(nlogn) 穩定性:不穩定
#include<stdio.h>
#include<math.h>
void heap_ajust_min(int *b, int i, int size) //a爲數組,size爲堆的大小
{
int lchild = 2*i; //i的左孩子節點序號
int rchild = 2*i +1; //i的右孩子節點序號
int min = i; //記錄根和左右子樹中最小的數的下標
int temp;
if(i <= size/2) //調整不需要從葉結點開始
{
if(lchild<=size && b[lchild]<b[min])
{
min = lchild;
}
if(rchild<=size && b[rchild]<b[min])
{
min = rchild;
} //兩個if語句尋找三個結點中最小的數
if(min != i) //如果min不等於i,說明最小的值在左右子樹中
{
temp = b[i]; //交換a[i]和a[min]的值
b[i] = b[min];
b[min] = temp;
heap_ajust_min(b, min, size); //被交換的子樹可能不滿足堆的定義,需要對被交換的子樹重新調整
}
}
}
void build_heap_min(int *b, int size) //建立小根堆
{
int i;
for(i = size/2; i >= 1; i--) //非葉子節點最大序號值爲size/2,從這個結點開始調整
{ //注意for中的循環條件(i = size/2; i >= 1; i--)
heap_ajust_min(b, i, size);
}
}
void heap_sort_min(int *a, int size)
{
int i;
int temp;
for(i = size; i >= 1; i--)
{
temp = a[1];
a[1] = a[i];
a[i] = temp; //交換堆頂和最後一個元素
heap_ajust_min(a, 1, i-1); //再一次調整堆頂節點成爲小頂堆
}
}
int main(int argc, char *argv[])
{
int a[]={0,5,8,45,9,36,35,22,46,37,10,79,100,63,12,18,77,88,50,99,95};
int size = sizeof(a)/sizeof(int) -1;
int i,j;
int count=1;
build_heap_min(a, size);
printf("小頂堆:\n");
for(i=0;i<=4;i++)
{
for(j=0;j<pow(2,i);j++)
{
if(count<=size)
{
printf("%d ",a[count++]);
}else{
break;
}
}
printf("\n");
}
printf("\n");
heap_sort_min(a, size);
printf("堆排序之後的序列爲:\n");
for(i=1;i <= size; i++)
printf("%d ", a[i]);
printf("\n");
return 0;
}
歸併排序
歸併排序是建立在歸併操作上的一種有效的排序算法,該算法是採用分治法的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲二路歸併。歸併排序是一種穩定的排序方法。注意:一般而言,對於N個元素進行k-路歸併排序時,排序的趟數m滿足k^m = N,從而m = logk(N)向上取整。
空間複雜度:O(n) 時間複雜度:O(nlogn) 穩定性:穩定
#include <stdlib.h>
#include <stdio.h>
void Merge(int sourceArr[],int tempArr[], int startIndex, int midIndex, int endIndex)
{
int i = startIndex, j=midIndex+1, k = startIndex;
while(i!=midIndex+1 && j!=endIndex+1)
{
if(sourceArr[i] > sourceArr[j])
tempArr[k++] = sourceArr[j++];
else
tempArr[k++] = sourceArr[i++];
}
while(i != midIndex+1)
tempArr[k++] = sourceArr[i++];
while(j != endIndex+1)
tempArr[k++] = sourceArr[j++];
for(i=startIndex; i<=endIndex; i++)
sourceArr[i] = tempArr[i];
}
//內部使用遞歸
void MergeSort(int sourceArr[], int tempArr[], int startIndex, int endIndex)
{
int midIndex;
if(startIndex < endIndex)
{
midIndex = startIndex + (endIndex-startIndex) / 2;//避免溢出int
MergeSort(sourceArr, tempArr, startIndex, midIndex);
MergeSort(sourceArr, tempArr, midIndex+1, endIndex);
Merge(sourceArr, tempArr, startIndex, midIndex, endIndex);
}
}
int main(int argc, char * argv[])
{
int a[8] = {50, 10, 20, 30, 70, 40, 80, 60};
int i, b[8];
MergeSort(a, b, 0, 7);
for(i=0; i<8; i++)
printf("%d ", a[i]);
printf("\n");
return 0;
}
常用排序算法複雜度和穩定性總結
動態圖片來源於: