一、插入排序
1.直接插入排序
依次将待排序序列中每一个记录插入到有序序列中,直到全部记录都排好序。
#include <bits/stdc++.h>
using namespace std;
void insertsort(int r[], int n)
{
for(int i = 2; i <= n; i++)
{
int j;
r[0] = r[i];//r[0]是作为哨兵
//哨兵作用:1.进入查找(插入位置)循环之前,它保存了r[i]的副本,使不致于因记录后移而丢失r[i]的内容 2.在查找循环中"监视"下标变量j是否越界。一旦越界(即j=0),因为r[0].可以和自己比较,循环判定条件不成立使得查找循环结束,从而避免了在该循环内的每一次均要检测j是否越界(即省略了循环判定条件"j>=1")
for(j = i-1; r[0] < r[j]; j--)
{
r[j+1] = r[j];
}
r[j+1] = r[0];
}
}
int main()
{
int n;
int r[105];
scanf("%d", &n);
for(int i = 1; i <= n; i++)
{
scanf("%d", &r[i]);
}
insertsort(r, n);
for(int i = 1; i <= n; i++)
{
printf("%d%c", r[i], " \n"[i==n]);
}
return 0;
}
/*
7
12 15 9 20 6 31 24
*/
复杂度分析:
最好情况下,待排序为正序,每次只和有序序列最末位比较一次,并只移动两次。复杂度O(n)。
最坏情况下,待排序为逆序,第i次插入要和i-1个数字进行比较,每比较一次都要移动一次。复杂度O(n²)。
平均:O(n²)
辅助空间:O(1)
直接插入排序是稳定的排序方法。
当序列中的记录基本有序或者待排序列记录较少时,它是最佳的排序方法。
2.希尔排序
先将整个待排序列分为若干子序列,在子序列内进行直接插入排序,待整个序列基本有序时,再将全体记录进行一次插入排序。
void shellsort(int r[], int n)
{
for(int d = n/2; d >= 1; d /= 2)//此处d就是以d为间隔来达成逐段分割
{
for(int i = d+1; i <= n; i++)
{
int j;
r[0] = r[i];
for(j = i-d; j > 0 && r[0] < r[j]; j -= d)
{
r[j+d] = r[j];
}
r[j+d] = r[0];
}
}
}
复杂度分析:
时间性能在O(n²)和O(n)之间。n在特定范围内,时间性能在O(n^1.3)。
最好:O(n^1.3)
最坏:O(n²)
平均:O(n)~O(n²)
辅助空间:O(1)
希尔排序是一种不稳定的排序方法。
二、交换排序
1.起泡排序
两两比较相邻的数字,反序则交换,依次比较直到没有反序为止。
void bubblesort(int r[], int n)
{
int flag = n;//flag是用来判断在遍历无序区是否交换过,若交换过则必然不为0,则继续while
while(flag != 0)
{
int last = flag;//last是标记作为无序区的最后一位的位置
flag = 0;
for(int i = 1; i < last; i++)
{
if(r[i] > r[i+1])
{
int tmp = r[i];
r[i] = r[i+1];
r[i+1] = tmp;
flag = i;
}
}
}
}
复杂度分析:
最好情况下:待排序为正序,则执行一次,无交换,复杂度为O(n)。
最坏情况下:待排序为逆序,则每次只有无序序列中最大值交换到了最终位置,执行了n-1次,第i次执行了n-i次的比较与交换。复杂度为O(n²)。
平均:O(n²)
辅助空间:O(1)
起泡排序是一种稳定的排序方法。
2.快速排序
快排是选择一个轴值,将待排根据轴值分为两部分,左边均小于轴值,右边均大于轴值,然后对左右两边都进行这样的操作,直到整个序列有序。
#include <bits/stdc++.h>
using namespace std;
int onesort(int r[], int s, int e)//对当前区间进行划分,s为起始位置,e为结束位置
{
int i = s;
int j = e;
while(i < j)//判断左右两边的下标,
{
while(i < j && r[i] <= r[j])//先对右边进行扫描,扫描到左边大于右边则停止循环并进行交换,下次从左边扫描
{
j--;
}
if(i < j)
{
int tmp = r[i];
r[i] = r[j];
r[j] = tmp;
i++;
}
while(i < j && r[i] <= r[j])//对左边进行扫描,扫描到左边大于右边则停止扫描并进行交换,下次从右边扫描
{
i++;
}
if(i < j)
{
int tmp = r[i];
r[i] = r[j];
r[j] = tmp;
j--;
}
}
return i;//返回当前区间轴值的位置
}
void quicksort(int r[], int s, int e)
{
if(s < e)
{
int mid = onesort(r, s, e);//对当前区间进行划分并取得当前区间的轴值
quicksort(r, s, mid-1);//对轴值左边区间进行划分
quicksort(r, mid+1, e);//对轴值右边区间进行划分
}
}
int main()
{
int n;
int r[105];
scanf("%d", &n);
for(int i = 1; i <= n; i++)
{
scanf("%d", &r[i]);
}
quicksort(r, 1, n);
for(int i = 1; i <= n; i++)
{
printf("%d%c", r[i], " \n"[i==n]);
}
return 0;
}
/*
7
12 15 9 20 6 31 24
8
50 13 55 97 27 38 49 65
7
23 13 49 6 31 19 28
*/
快速排序的次数取决于递归的深度。
复杂度分析:
最好情况下:每次划分的序列左边区间与右边区间长度相等,对每次划分要扫描整个序列,所需O(n),共需划分次,故复杂度为O(n)。
最坏情况下:待排序列为正序或者逆序,则划分左边或者右边总会有一边长度为空,则需调用n-1次递归才能完全排好序。故复杂度为O(n²)。
平均:O(n)。
辅助空间:O()~O(n)。
快速排序是一种不稳定的排序方法。
快速排序平均性能是迄今为止排序算法中最好的一种。
三、选择排序
1.简单选择排序
在r[i]-r[n]中最小的与r[i]进行交换作为有序序列中第i个。
void selectsort(int r[], int n)
{
for(int i = 1; i <= n; i++)
{
int flag = i;
for(int j = i+1; j <= n; j++)
{
if(r[flag] > r[j])
{
flag = j;
}
}
if(flag != i)
{
int tmp = r[flag];
r[flag] = r[i];
r[i] = tmp;
}
}
}
复杂度分析:
无论情况怎样,都会进行n-1次比较,且需要进行n-1次排序。故最好情况最坏情况下复杂度均为O(n²)。
辅助空间:O(1)。
简单选择排序是一种不稳定的排序方法。
2.堆排序
利用二叉树的逻辑思想,将最大值放到根节点(大根堆),然后将最大值放到有序序列中第一个位置,依次下去便可排好序。
void sift(int r[], int s, int e)//每次调整堆,当前节点为s,堆中最后一个节点为e (大根堆)
{
int i = s;
int j = i*2;//左节点
while(j <= e)//还在这个堆里
{
if(j < e && r[j] < r[j+1])//将j指向左节点与右节点中的大者
{
j++;
}
if(r[i] > r[j])//若当前节点大于左右节点,则不用调整
{
break;
}
else//调整这个堆并且把i与j往下,达到完全调整好以s为节点的大根堆
{
int tmp = r[i];
r[i] = r[j];
r[j] = tmp;
i = j;
j = i*2;
}
}
}
void heapsort(int r[], int n)
{
for(int i = n/2; i >= 1; i--)//从最后一个分支节点开始往根节点 都要进行调整 初始堆
{
sift(r, i, n);//调整以i为节点的大根堆
}
for(int i = 1; i < n; i++)//对所有节点都进行 把大根堆的顶点r[1]放到r[n-i+1]相对应的位置,并调整1~n-i的堆,因为n-i以后都是有序的
{
int tmp = r[1];
r[1] = r[n-i+1];
r[n-i+1] = tmp;
sift(r, 1, n-i);
}
}
复杂度分析:
运行时间主要在初始堆与调整堆上,初始堆需要的时间为O(n),第i次调整需要的时间为O(),且需要n-1次调整堆,故最好情况与最坏情况都是时间复杂度为O(n)。
堆排序是一种不稳定的排序方法。
由于堆排交换与比较次数比较多,平均而言比快速排序慢。
四、归并排序
1.二路归并排序
把整个序列递归分下去分成若干小块,将两两相邻的小块进行排序,之后在一层层向上整合。
void Merge(int r[], int s, int m, int e)//将s~m与m+1~e的两小块整合排序到s~e中
{
int i = s;
int j = m+1;
int l = s;
int tmp[105];
while(i <= m && j <= e)//将左右小块中的小值放到辅助数组中
{
if(r[i] <= r[j])
{
tmp[l++] = r[i++];
}
else
{
tmp[l++] = r[j++];
}
}
while(i <= m)//若右边小块完了,就把左边小块剩余的放到辅助数组中
{
tmp[l++] = r[i++];
}
while(j <= e)
{
tmp[l++] = r[j++];
}
for(int i = s; i <= e; i++)//把辅助数组给传参进来的数组
{
r[i] = tmp[i];
}
}
void Mergesort(int r[], int s, int e)
{
if(s == e)//起始位置和结束位置相等表示这小块只有一个数字,不用排序了
{
return;
}
else
{
int m = (s+e)>>1;//找中值并分别对左右小块进行递归
Mergesort(r, s, m);
Mergesort(r, m+1, e);
Merge(r, s, m, e);//整合排序左右小块
}
}
复杂度分析:
一次归并排序需要将待排小块扫描一遍,故时间复杂度为O(n),且归并需要进行次,故最好情况最坏情况时间复杂度为O(n)。
辅助空间需要一个等大的辅助数组来支持,故空间复杂度为O(n)。
二路归并排序时一种稳定的排序方法。
五、分配排序
1.桶排序
桶排序思路就是将序列中的数字放到哈希表中,再遍历这个哈希表依次将元素取出便成为有序的序列。
可以用链表与队列进行实现。
复杂度分析:
由于是放入哈希表中,再遍历,故初始化放入就需要O(n)的时间,再者序列中可能会有相同元素,设相同元素有m个,故遍历这些相同元素就需要O(m)的时间,故总时间为O(n+m)。
空间复杂度为O(m),表示储存m个静态队列表示的桶。
桶排序是稳定的排序方法。
2.基数排序
基数排序是将序列中所有数字补位补到等长,数位短的就在前面补0以此达到目的。之后从最低位开始往高位进行排序。就会变成有序序列。
设待排序列为n个,d为等长的长度,取值范围为m,每一次分配的时间为O(n),每一次收集需要的时间为O(n+m),需要执行d次,故时间复杂度为O(d(n+m))。
空间复杂度,因为需要m个队列,故空间复杂度为O(m)。
六、大概总结
1.各排序的复杂度
对于基本有序的序列,直接插入排序最合适!
快速排序目前被认为是最快的排序方法!
对于序列里记录多的情况下,归并排序较堆排序更快!
最好情况下:直接插入排序和起泡排序最快
最坏情况下:堆排序和归并排序最快
平均情况下:快速排序最快
2.各排序的稳定性
稳定的:直接插入排序、起泡排序、归并排序、桶排序和基数排序。
不稳定的:希尔排序、快速排序、简单选择排序、堆排序。