排序算法:插入排序,选择排序,交换排序,归并排序
插入排序:直接插入排序,希尔排序
选择排序:直接选择排序,堆排序
交换排序:快速排序,冒泡排序
归并排序:归并排序
1.插入排序
1.1直接插入排序
算法思想:在要排序的一组数中,假设前边n-1个数是已经排好顺序的,现在要把第n个数插入到前面的有序数中,使得这n个数也是排好顺序的,如此反复循环,直到全部排好顺序。
假设要排序的数组
arr={3,1,9,2,7,5,6,4,8,0};
要求排序结果是升序
一轮排序:
定义有序数列最后一个元素的下标为end,那么要插入的元素是a[end+1],先保存要插入元素的值tmp = a[end+1];
接着tmp向前依次和每个元素的值进行比较,若tmp < a[end],那么把a[end]向后搬运a[end+1] = a[end];
end向前移一位--end;
直到tmp遇到第一个不大于它的数,将tmp插入到它后一位上a[end+1] = tmp;
时间复杂度:一共要执行n-1轮,每轮排序最多挪动end+1次,0<=end
void insert_sort(int a[],int n)
{
int i;
for(i = 0; i < n-1; ++i)
{
int end = i;
int tmp = a[end+1];
for(;end >=0 ; --end)
{
if(a[end] > tmp)
{
a[end+1] = a[end];
}
else
{
break;
}
}
a[end+1] = tmp;
}
}
1.2希尔排序
直接插入排序在排逆序时,效率非常糟糕
算法思想:希尔排序是直接插入排序针对逆序的一种优化
希尔排序:1.预排序 (使序列接近有序) 2.插入排序
希尔排序会先选取一个gap值,距离gap的元素作为一组
假设gap=3,如图
再分别对每组进行直接插入排序,这样每组都是有序的了,得到结果
居然一次就排好了,当然了这是特殊情况,举一个不特殊的例子好了:
这是一次预排序
每一组是同步进行的,不是排好一组再排另一组
预排序后将gap值缩小,直到最后gap=1时,成为直接插入排序
时间复杂度:希尔排序效率优于直接插入排序,但仍旧是O(n2)
稳定性:希尔排序因为进行了分组,所以相同元素可能被分到不同的组,打乱了前后顺序,因此希尔排序是不稳定排序
希尔排序实现代码:
void shell_sort(int a[],int n)
{
int gap = n;
while(gap > 1)
{
gap = gap/3 + 1;
int i = 0;
for(; i < n - gap; ++i)
{
int end = i;
int tmp = a[end + gap];
for(;end >= 0; end-=gap)
{
if(a[end] > tmp)
{
a[end + gap] = a[end];
}
else
break;
}
a[end+gap] = tmp;
}
}
}
2.选择排序
2.1直接选择排序
算法思想:直接选择排序是最简单的一种排序,选出数组中的最小值,放入下标为0的位置,在剩余元素中再次选出最小值,放入下标为1的位置,在剩余元素中再次选出最小值,放入下标为2的位置……直到选出最后一个最小值,放入下标为n-1的位置,排序结束
时间复杂度:直接选择排序要经历n-1趟,每趟要找n-1-i次,因此时间复杂度是O(n^2)
稳定性:直接选择排序是不稳定的排序,如: 3 2 3 5 1 ,在第一轮选择时,要将1放到下标为0的位置,1与第一个3进行了交换,相同元素3的相对位置也变了,因此是不稳定排序
直接选择排序代码:
void select_sort(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[k];
a[k] = a[i];
a[i] = t;
}
}
}
2.2堆排序
算法思想:堆排序是利用堆的性质进行排序的,堆的每个结点的值都大于或等于其左右孩子结点的值,一个大顶堆的堆顶是整个堆中的最大值。
堆排序步骤:
1.构建初始堆,将序列构造成一个大顶堆
从第一个非叶子结点开始向下调整,使所在子树成为一个堆,直到调整到根节点,此时整棵树就是一个大顶堆了
假设待排序数组arr={3,1,9,2,7,5,6,4,8,0};
每个数组都可以视为一个完全二叉树
父节点下标为i,左右孩子结点为2i+1/2i+2
左/右孩子结点下标为i,父节点下标为(i-1)/2
2.将堆顶元素与末尾元素进行交换,那么最大值就跑到最后一个位置了(此时不属于堆了,可以理解为删除了顶堆元素),将堆顶元素(刚刚交换过的,因为它,数组不满足堆的条件了)向下调整,使数组再次成为一个大顶堆,此时堆顶又成为了最大值,再次将堆顶元素与末尾元素交换……直到堆只剩一个元素,数组就排成升序了
时间复杂度:堆排序主要操作在向下调整上,向下调整是最多经历log(n-1)次,在构建初始堆时要经历n次向下调整,因此堆排序的实践复杂度是O(nlogn)
稳定性:堆排序因为在删除时是和最后一个元素交换位置,假设堆为 2 3 5 3,在进行第一次交换后,2放在最后一个位置,第二个3换到堆顶位置,剩下元素仍满足堆,但是相同的元素3已经不是刚开始的相对位置了,因此堆排序是不稳定的
堆排序代码实现:
void swap(int *a,int *b)
{
int t = *a;
*a = *b;
*b = t;
}
void AdjustDown(int a[],int n,int parent)
{
//父节点i 子节点是2i+1和2i+i
//子节点是i 父节点是(i-1)/2
int child = parent*2+1;
//当父节点还有子节点时,要向下调整
while(child < n)
{
//找出较大的子节点与父节点比较
if(child+1 < n && a[child+1] > a[child])
child++;
if(a[child] > a[parent])
{
swap(&a[child],&a[parent]);
parent = child;
child = parent*2 + 1;
}
else
{
break;
}
}
}
void heap_sort(int a[],int n)
{
//最后一个非叶子结点
int i = (n-2)/2;
//先将数组排成一个大堆
for(; i >= 0; --i)
{
AdjustDown(a,n,i);
}
int end = n-1;
for(; end > 0; --end)
{
swap(&a[0],&a[end]);
AdjustDown(a,end,0);
}
}
3.交换排序
3.1快速排序
算法思想:快速排序的每一趟先找一个基准数,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,把基准值放到排好序后它应该在的位置。
再分别用同样的方法排好比它小的那部分区间的元素以及比它大的那部分区间的元素
快速排序有很多种方法,但基本原理都是一样的,现在介绍一种左右指针法
一轮排序:
假设将最后一个元素作为基准值key,让左指针指向数组首元素,让右指针指向数组尾元素,左指针向后走,直到遇到第一个大于key的数字,右指针向前走,直到遇到第一个小于key的值,两者交换,左右区间继续向前走,直到两者相遇,这个位置就是key应该在的位置(因为左指针遇到比key小的值不会停下,所以最后左指针停下的位置是比key大的值的位置或者是key本身的位置)
这样key值就找到了它应该在的位置,再分别在左区间和右区间进行相同的操作,直到所有的值都找到它该在的位置
时间复杂度:最好的情况下快速排序每次划分过程中产生的区间都是n/2,在排每个数的时候时间复杂度是O(n),因此快速排序时间复杂度是O(nlogn),但如果是本身有序的情况下,时间复杂度是O(n^2)
稳定性:快速排序用左右指针交换,很有可能把相同元素靠前的值交换到后边去,因此快速排序是不稳定的
快速排序代码实现:
void swap(int *a,int *b)
{
int t = *a;
*a = *b;
*b = t;
}
int PartSort(int a[],int left,int right)
{
int key = right;
while(left < right)
{
while(a[left] < a[key] && left < right)
left++;
while(a[right] >= a[key] && left < right)
right--;
if(left < right)
{
swap(&a[left],&a[right]);
}
}
swap(&a[left],&a[key]);
return left;
}
void quick_sort(int a[],int left,int right)
{
if(left < right)
{
int div = PartSort(a,left,right);
quick_sort(a,left,div-1);
quick_sort(a,div+1,right);
}
}
3.2冒泡排序
算法思想:冒泡排序一次比较两个相邻的元素,如果他们的顺序错误就把他们交换过来,这样最大值就一点一点的冒到最后一个位置了,经过n次冒泡,就排好序了
时间复杂度:每次冒泡排好一个数,经历n-i-1次比较,需要冒n-1趟,时间复杂度O(n^2)
稳定性:冒泡不改变相同元素的相对位置,因此是稳定排序
冒泡排序代码实现:
void bubble_sort(int a[],int n)
{
int count;
for(count = 0;count < n-1;++count)
{
int cur = 0;
int flag = 0;
for(;cur < n-count-1;++cur)
{
if(a[cur] > a[cur+1])
{
flag = 1;
int t = a[cur];
a[cur] = a[cur+1];
a[cur+1] = t;
}
}
if(flag == 0)
break;
}
}
4.归并排序
算法思想:归并排序采用分治的思想,将已有序的子序列合并,得到完全有序的序列。要先使每个子序列都有序。
时间复杂度:O(nlogn)
空间复杂度:归并排序需要开辟n个空间存放归并好的值,因此空间复杂度是O(n)
稳定性:归并排序是稳定排序
归并排序代码实现:
void swap(int *a,int *b)
{
int t = *a;
*a = *b;
*b = t;
}
void _Merge(int a[],int tmp[],int begin1,int end1,int begin2,int end2)
{
int start = begin1;
int finish = end2;
int index = begin1;
while(begin1 <= end1 && begin2 <= end2)
{
if(a[begin1] <= a[begin2])
{
tmp[index++] = a[begin1++];
}
else
tmp[index++] = a[begin2++];
}
while(begin1 <= end1)
tmp[index++] = a[begin1++];
while(begin2 <= end2)
tmp[index++] = a[begin2++];
memcpy(a+start,tmp+start,sizeof(int)*(finish-start+1));
}
void _MergeSort(int a[],int tmp[],int begin,int end)
{
if(begin >= end)
return;
int mid = begin + ((end-begin)>>1);
_MergeSort(a,tmp,begin,mid);
_MergeSort(a,tmp,mid+1,end);
_Merge(a,tmp,begin,mid,mid+1,end);
}
void merge_sort(int a[],int n)
{
int* tmp =(int*)malloc(sizeof(int)*n);
_MergeSort(a,tmp,0,n-1);
free(tmp);
}