用大白话描述排序算法与实现(C语言)
本文描述了部分简单的排序算法,希望能通过简单易懂的文字将算法的思想介绍给大家,同时也作为自己技术的积累,也方便自己日后对排序算法的复习与提升。
本篇博客目录如下:
-
插入类排序
1.直接插入排序
2.折半插入排序
-
交换类排序
1.冒泡排序
2.快速排序
-
选择类排序
1.简单排序
2.堆排序
-
二路归并排序
直接插入排序
算法执行流程
设置原始序列: 12 32 15 2
先把第一个数12设置为有序序列,将第二个开始的数设置为无序序列。从第二个数32作为外层循环开始遍历,每遍历一个数,将其移到有序序列的位置中,在有序序列中通过从后往前遍历作为内层循环的方式,将遍历到的数往后推一位;若找到第一个小于或等于该外层循环遍历的数,遍历将停止,同时把外层循环的数存进有序序列相应的位置。
流程如下:
第一层循环:12 (有序) 32 15 2 (无序)
12是有序数,现在轮到32来插入到有序序列,因为12<32,所以第一层循环直接把32放近有序序列即可。
第二层循环: 12 32 (有序) 15 2 (无序)
现在轮到15来插入到有序序列,于是从32开始检查,发现32>15,然后将32往后移一位,15往前移一位;12<15,所以停止移动;把15放进原来32的位置,所以有序序列为12 15 32。
第三层循环: 12 15 32 (有序) 2 (无序)
第四层循环: 2 12 15 32(有序)
// 直接插入排序 平均时间复杂度O(n^2)
void insertSort(int array[], int n){
for(int i = 1; i < n; i++){
int temp = array[i];
int j = i - 1;
while(j >= 0 && temp < array[j]){
array[j+1] = array[j];
j--;
}
array[j+1] = temp;
}
}
复杂度
时间复杂度:最好情况是完全顺序,时间复杂度是O(n),最坏情况是完全逆序,时间复杂度是O(n2)平均时间复杂度是O(n2)。
空间复杂度:因为辅助存储的只有常量,与规模无关,所以空间复杂度为O(1)。
折半插入排序
折半插入排序就是直接插入排序的升级版。它们两者的不同就是查找插入位置的方式不同。直接插入排序是顺序查找,而折半插入排序是用折半的方法。
设置原始序列 12 32 2 6 13 15 16
流程如下:
现在假设有序序列为:2 6 12 32,无序序列为:13 15 16
这里详细介绍如何将无序序列的第一位13在有序序列中查找并插入。
前提:low代表的是有序序列里的数组的低位,在这里是0,代表的是2;high表示有序序列里数组的高位,在这里是3,代表的是32;mid是该有序序列的中位数,即(low+high)/2。这里是1,代表的是6。
- 在有序序列中找到array[mid] = 6,6>13,所以需要在有序序列中查找到[mid+1,high]的范围里的数。即此时(low=2,high=3,mid=2)在数组下标为[2,3]范围里面找到应该插入的位置。
- 在数组下标为[2,3]范围里面找。array[mid]=12,12<13。所以要继续在此时(low=3,high=3,mid=3)即[3,3]的范围里面找。
- 因为此时low=3,high=3,mid=3,low==high。所以需要插入的位置就是这里。数组下标为3的位置上。
后续:还是需要一个接一个地把大于需要插入的数往后退,然后把插入的数插入到响应的位置上的。
// 折半插入排序 最好情况是O(nlog2^n) 平均情况是O(n^2),但是折半插入排序有效减少查找的位置,只不过是插入的操作跟直接插入排序是一样的
void middleInsertSort(int array[], int n){
for(int i = 0; i < n; i++){
int temp = array[i];
int j = 0;
int low = 0;
int high = i;
int pos = findNum(array, low, high, temp);
int b = i;
while(b > pos){
array[b] = array[b-1];
b--;
}
array[pos] = temp;
}
}
// 折半查找法,用于查找插入的位置
int findNum(int array[], int low, int high, int temp){
while(low < high){
int mid = (low + high)/2;
// 如果与要插入的数相等,那么直接选择在这里插入了
if(array[mid] == temp){
return mid;
}
// 如果找不到了相等的数,那么就要在大于插入数中最接近插入数的找到插入的位置的
if(low == high){
return low;
}
// 在前半部分找的
if(array[mid] > temp){
high = mid ;
}
// 在后半部分找的
if(array[mid] < temp){
low = mid +1;
}
}
}
复杂度
时间复杂度:最好情况O(nlog2n),最坏情况O(n2),平均情况O(n2)
空间复杂度:O(1)
冒泡排序
算法执行流程
这个算法通过一系列的交换动作完成的,所以是交换类排序。从一开始第一位数与第二位数进行比较,如果前者大,就把大的数换到后者。然后再把这个大的数与后面的第三个数比较,把大数再放到第三位数上…直到遍历到最后,然后即可以把最大的数换到最后一个位置上,并且将其作为结果集。重复第二轮互换,把第二大的数换到倒数第二个位置上…重复第n轮,或者是没有再需要互换的数,即排序成功。
原始序列为
32 15 2 7
流程如下:
-
32与15比较,32大,互换。
15 32 2 7
-
32与2比较,32大,互换。
15 2 32 7
-
32与7比较,32大,互换。
15 2 7 32
至此第一轮结束,把最大的数即32移到了最后一位上。
-
15与2比较,15大,互换。
2 15 7
-
15与7比较,15大,互换。
2 7 15
至此第二轮结束,把最大的数即15移到了最后一位上。
- 2与7比较,不换。
- 7与15比较,不换。
至此第三轮结束,发现没有需要把最高数移到最后一位的行为,所以该排序已经是排序好的了。
// 冒泡排序
void pubbleSort(int array[],int n){
int flag = 0;
for(int i = n - 1; i > 0; i--){
for(int j = 0; j < i; j++){
//将大的数移到后面一位
if(array[j + 1] < array[j]){
int temp = array[j+1];
array[j+1] = array[j];
array[j] = temp;
// 有调整过的痕迹
flag = 1;
}
}
//如果没有再排序,那么该序列已经是顺序的了,可以停止。
if(flag == 0){
break;
}
}
}
复杂度
时间复杂度:最好情况O(n),最坏情况O(n2),平均情况O(n2)
空间复杂度:O(1)
快速排序
算法执行流程
快速排序每一趟都使用该排序的序列的第一位数作为枢轴,将序列中比枢轴小的数移到枢轴的左边,比枢轴大的数移到枢轴的右边。移动完成后可以发现,枢轴已经放入在最终位置,不会再改变。而开始分别遍历枢轴的左右两边进行进一步的排序运作。
原始序列为
32 15 2 7
流程如下:
-
将32作为枢轴,array[low]是32,array[high]是7
32(low=0) 15 2 7(high=3)
先用high找到比枢轴32小的数,然后直接发现7<32,所以把7放到32(low=0)的位置,low往后走一位。
-
此时32枢轴,array[low]是15,array[high]是7
7 15(low=1) 2 7(high=3)
然后用low找到比枢轴32大的数,遍历到high的位置发现没有该数
-
此时32枢轴,low=high,array[low]是7
7 15 2 7(low,high)
-
把32放到low与high相等的位置
7 15 2 32
至此第一轮结束。已确定的序列为 _ _ _ 32
将该序列从枢轴32的位置分成两部分,①是32往前的数,②是32往后(32后面没有数了,所以第二部分不存在了)。
-
将7作为枢轴,array[low]是7,array[high]是2.
7(枢轴,low=0) 15 2(high=2)
用high找到比枢轴7小的数,直接发现2<7,所以把2放到7(low=0)的位置,low往后走一位。
-
2 15(low=1) 2(high)
再用low找到比枢轴7大的数,直接发现15>7,所以把15放到2(high=2)的位置,high往前走一位。
-
2 15(low=high) 15
low=high,把枢轴数放进去。
-
2 7 15
至此第二轮也结束了。已确定的序列为 _ 7 _ 32
现在把low=high的位置上的数做为分割点,把7往前的数作为前一部分,把7往后的数作为后一部分。
前一部分:
- 2(low=high)。前一部分的第三轮排序结束。
后一部分:
- 15(low=high)。后一部分的第三轮排序结束。
至此第三轮也结束了。已确定的序列为2 7 15 32。
至此快速排序完成了。
// 快速排序 时间复杂度为n(log2^n) 待排序列越接近无序,本算法效率越高
void quickSort(int array[], int low, int high,int size){
// 找到第一个点
int i = low, j = high;
if(low < high){
int temp = array[low];
while(low < high){
// 找到右边比第一个点小的数
while(low < high && array[high] > temp){
high--;
}
if(low < high){
array[low] = array[high];
low++;
}
//找到左边点比第一个点大的数
while(low < high && array[low] < temp){
low++;
}
if(low < high){
array[high] = array[low];
high--;
}
}
// 这一轮停止啦,把第一个数放进去
if(low == high){
array[low] = temp;
}
quickSort(array,i,low-1,size);
quickSort(array,low+1,j,size);
}
}
复杂度
时间复杂度:最好的情况下接近无序的情况下,时间复杂度为O(nlog2n);最坏的情况接近有序的情况下,时间复杂度是O(n2),平均时间复杂度为O(nlog2n)
空间复杂度:O(log2n)
简单排序
简单排序真的很简单。分成有序序列和无序序列两列,每遍历一次无序序列,就等于在无序序列中找到最小的数插入到有序序列。这里直接贴代码。
//简单选择排序 时间复杂度O(n^2)
void selectSort(int array[], int size){
for(int i = 0; i < size; i++){
int best = i ;
for(int j = i; j < size; j++){
if(array[j] < array[i] && array[j] < array[best]){
best = j;
}
}
int temp = array[i];
array[i] = array[best];
array[best] = temp;
}
}
复杂度:
时间复杂度:O(n2)
空间复杂度:O(1)
堆排序
什么是堆
堆是一种数据结构,实际上也可以把堆看成一颗完全二叉树。这颗完全二叉树满足的是:任何一个非叶节点的值都不大于(或不小于)其左右孩子节点的值。若所有父亲节点都大于其所有儿子节点,那么这样的堆则成为大顶堆,如果父亲节点都小于儿子节点,那么这样的堆则称为小顶堆。
在这里要使用到完全二叉树的定义
假设根节点的序号为1,根节点的儿子节点从左往右数分别是2,3。儿子节点3的儿子节点从左往右数分别是4,5。儿子节点4的儿子节点从左往右数分别是6,7。那么假设父节点的序号为i,那么其左儿子节点的序号则为2*i,其右儿子节点的序号则为2 * i +1。
堆排序需要用到非叶子节点来进行建立初始大顶堆。假设最后一个叶子节点的序号为i,那么最后一个非叶子节点的序号是i/2
算法执行流程
堆排序就是通过不断地将序列调整成堆,使得不符合堆定义的完全二叉树完全调整成为符合堆定义的完全二叉树。
- 先把序列建成大顶堆。
- 从最后一个叶子节点开始往前数,每一次把最后一个节点与第一个根节点(已经是最大节点),将根节点放入到最终位置,在减少了最后一个节点(即原来根节点的数)的无序序列中进行下一步的调整。
void sift(int array[], int low, int high){
int i = low; //要查找的而非叶子节点
int j = 2 * i; //子节点
int temp = array[i];
while (j <= high){
//先找到最大的子节点
if(j < high && array[j+1] > array[j]){
j++;
}
//子节点大于父节点,需要将子节点与父节点互换,然后子节点开始
if(array[j] > temp){
array[i] = array[j];
i = j;
j = 2 * i;
}else{
//子节点小于父节点,说明符合大根堆的情况的
break;
}
}
//放入到最终位置
array[i] = temp;
}
void heapSort(int array[], int size){
// 这里先建立大顶堆,注意需要从非叶子节点往前数
for(int i = size/2; i >= 1; i--){
sift(array, i, size);
}
//这里进行堆排序了,就是把最大顶的调到最后一个叶子节点,然后交换最后一个叶子节点到第一个节点,通过swift函数再找到剩下的而最大的数啦
int temp;
for(i = size; i >= 2; i--){
temp = array[i];
array[i] = array[1];
array[1] = temp;
sift(array, 1, i-1);
}
}
复杂度
时间复杂度: O(log2n)
空间复杂度:O(1)
二路归并排序
算法执行流程
实际上就是先分再合。先把完整的序列通过递归两两分开,化成最小的子序列。然后再通过无数个子序列两两合并,得到合并后有序的大子序列,直到最后合并成一个完整有序的序列。
原始序列: 32 15 2 7
- 递归分开,最后得到4个序列,分别是32,15,2,7。
- 序列两两合并,合并成有序序列。32与15合并在一起,因为15<32,合并序列为15 32。2与7合并在一起,因为2<7,合并序列为2。
- 序列再次两两合并,不过这里出现问题,因为两序列长度各自为2,怎么融合成一个大完整的序列呢?这里需要用到归并操作。
归并操作
两序列分别是
15 32 (序列A)
2 7 (序列B)
1.先比较A与B的第一个数,如果A1>B1,序列B的比较数往后退一个。此时合并的最终有序序列为 2
2.此时用A2比较B1,因为A1>B2,所以要使用B2作为最终合并有序序列的下一个数。序列B的比较数往后退1个。此时合并的最终有序序列为2 7
3.因为序列B已经比较完全了,现在就可以把序列A的所有数全部复制到最终的合并序列中。此时合并的最终有序序列为2 7 15 32。
- 通过这里的归并操作,使两有序序列两两合并,最终合并成为一个完整的有序序列。
/二路归并排序,就是分而治之,时间复杂度为O(log2^n),但是空间复杂度为O(n)
void mergeSort(int array[], int low, int high){
if(low < high){
int mid = (low+high)/2;
mergeSort(array, low, mid);
mergeSort(array, mid+1, high);
// 将low与mid mid+1与high归并成有序的数组列
merge(array, low, mid, high);
}
}
//归并序列
void merge(int array[], int low, int mid, int high){
// 先定义一个空数组
int copy[10];
int c = 0;
int i = 0, j = 0;
while(c < high - low + 1){
// 第一个序列的比较位<第二个序列的比较位,或者第二个序列的已经比较完全了,同时第一个序列还没比较完全。
if((array[low+i] < array[mid+1+j] || mid+1+j > high) && low+i <= mid){
copy[c] = array[low+i];
i++;
}else if(array[low+i] > array[mid+1+j] || low+i > mid && mid+1+j <= high){
copy[c] = array[mid+1+j];
j++;
}
c++;
}
// 将copy数组的内容复制到array中
for(i = 0; i < high - low + 1; i++){
array[low+i] = copy[i];
}
}
复杂度
时间复杂度:最好情况O(log2n),最坏情况O(log2n),平均情况O(log2n)。
空间复杂度:O(n)
总结
本文描述了部分简单的排序算法,但排序算法远不止这么一点。比如插入类排序中还有希尔排序,还有基数排序,外部排序的置换-选择排序、最佳归并树、败者树排序等等。排序算法研究是一个老生常谈的话题,在日常工作中也经常用到排序的功能,但工作不只是套工具、套SDK,更重要的是了解内部算法,针对不同情况变相使用不同的算法,达到程序运行的最优化~~~
这里附上数据结构的排序方法的比较
排序方法 | 最好情况 | 最坏情况 | 平均情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
直接插入排序 | O(n) | O(n2) | O(n2) | O(1) | 稳定 |
折半插入排序 | O(nlog2n) | O(n2) | O(n2) | O(1) | 稳定 |
希尔排序 | O(n1.3) | O(1) | 不稳定 | ||
冒泡排序 | O(n) | O(n2) | O(n2) | O(1) | 稳定 |
简单选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 稳定 |
快速排序 | O(nlog2n) | O(n2) | O(nlog2n) | O(log2n) | 不稳定 |
堆排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 不稳定 |
归并排序 | O(nlog2n) | O(nlog2n) | O(log2n) | O(n) | 稳定 |
基数排序 | O(d(n+rd)) | O(d(n+rd)) | O(d(n+rd)) | O(n+rd) | 稳定 |