目录
前言
排序是《数据结构》中最基本的学习内容。排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序。而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。知识框架如下:
插入排序
插入排序是一种简单直观的排序方法,其基本思想在于每次将一个待排序记录,按其关键字大小插入到前面已经排好序的子序列中,直到全部记录插入完成。
直接插入排序
直接插入排序把要排序的序列分成两部分:第一部分是有序子序列,而第二部分是无序序列。每次挑选无序序列中的第一个元素与第一部分的有序子序列从后往前逐个比较,当待排元素大于子序列中的元素时,将其插入到该元素的后方。直接插入排序会使用一个 "哨兵",用于存放元素。直接插入排序每次插入一个元素,所以排序趟数固定为 n-1。
空间复杂度:O(1) 时间复杂度:O(n^2) 稳定性:稳定
#include <stdio.h>
void Insertsort(int a[], int low, int high)
{
int i, j;
int temp;
for(i = low+1; i <= high; i++)
{
temp = a[i];
for(j = i-1; (j >= low)&&(a[j] > temp); j--)
a[j+1] = a[j];
a[j+1] = temp;
}
}
int main(int argc, char *argv[])
{
int i,a[10];
printf("输入10个数:");
for(i = 0; i < 10; i++)
scanf("%d", &a[i]);
insertsort(a, 0, 9);
printf("排序结果是:");
for(i = 0; i < 10; i++)
printf("%d ", a[i]);
printf("\n");
return 0;
}
折半插入排序
折半插入排序仅仅是减少了比较元素的次数,约为 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 argc, char *argv[])
{
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 argc, char *argv[])
{
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 argc, char *argv[])
{
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) 稳定性:不稳定
快排动图(网上找的动图,其中有一个基准为 6 的标识错误。虽然基准选择方法不一样,但排序过程还是一样的):
#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 argc, char *argv[])
{
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;
}
}
}
int main(int argc, char *argv[])
{
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");
return 0;
}
堆排序
堆排序是指利用堆这种数据结构所设计的一种排序算法。分为两种方法:大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
想要更详细的了解堆,可以看这篇文章:通俗易懂的讲解堆排序(含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,从这个结点开始调整
heap_ajust_min(b, i, size); //注意for中的循环条件(i = size/2; i >= 1; i--)
}
}
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 <iostream>
using namespace std;
int *temp;
//将两个非降序序列low--mid,mid+1--high合并为一个新的非降序序列
void Merge(int a[], int low, int mid, int high)
{
int len = high - low + 1;
int i = low, j = mid+1; //i,j分别为两个子序列的游标
int k = 0; //为新合并序列的游标
while(i <= mid && j <= high)
{
if(a[i] <= a[j]){
temp[k++] = a[i++];
}else{
temp[k++] = a[j++];
}
}
while(i <= mid){ //若第一个子序列有剩余,则直接接到尾部
temp[k++] = a[i++];
}
while(j <= high){ //若第二个子序列有剩余,则直接接到尾部
temp[k++] = a[j++];
}
//copy到a[]
for(k = 0; k < len; k++)
a[low+k] = temp[k];
}
//low high为待排序列左右边界
void MergeSort(int a[], int low, int high)
{
if(low < high){
int mid = (low + high)/2;
MergeSort(a, low, mid); //递归的划分左右两个子序列
MergeSort(a, mid+1, high);
Merge(a, low, mid, high); //合并
}
}
int main()
{
int a[10] = {9,8,7,6,5,4,3,2,1,0};
temp = new int[10];
MergeSort(a, 0, 9);
for(int i = 0; i < 10; i++)
cout << a[i] <<" ";
cout <<endl;
delete []temp;
return 0;
}
常用排序算法复杂度和稳定性总结
动态图片来源于: