目录
2. 归并排序 (稳定)
复杂度: 时间O(NlogN) 空间O(N) 保证稳定性。
时间复杂度分析: 详解归并排序 附代码 时间复杂度推导_梦想远航的博客-CSDN博客_归并排序时间复杂度推导
思想:首先考虑一个数组 【first,mid】 【mid+1,last】都排好序了,那应该怎么把所有的都排好呢, 搞一个辅助数组, 只要从比较二个数列的第一个数,谁小就先取谁,取了后放在辅助数组里去。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。最后记得把辅助数组里的值赋给数组A
并且记得merge的时候,假如左边=右边,先放左边的,为了保证稳定性
代码:
//归并排序
//合并两个有序数组
void merge_array(int a[], int first, int mid, int last, int temp[]) {
int i = first, j = mid + 1;
int k = 0;
while (i <= mid && j <= last) {
temp[k++] = a[i]<=a[j]?a[i++]:a[j++]; //<= 保证稳定性
}
while (i <= mid)
temp[k++] = a[i++];
while (j <= last)
temp[k++] = a[j++];
for (i = 0; i < k; ++i)
a[first + i] = temp[i]; //不要忘了复制回去
}
//merge内部方法
void merge(int a[], int first, int last, int temp[]) {
if (last <= first) //当数组size小于等于1的时候 递归结束
return;
int mid = first + (last - first) / 2; // first+((last-first)>>1) 因为c++ 移位运算符优先级低于加号,所以()必不可少 ,
merge(a, first, mid, temp);
merge(a, mid + 1, last, temp);
merge_array(a, first, mid, last, temp);
}
//外部接口
void merge(int a[], int n) {
if(n<2)
return;
int *p = new int[n];
merge(a, 0, n - 1, p);
delete[] p;
}
3.冒泡排序(稳定)
冒泡排序是非常容易理解和实现,总结起来就一句话:从左到右,数组中相邻的两个元素进行比较,将较大的放到后面
时间复杂度: O(N^2) 空间O(1) 可以保证稳定性,只要写代码的时候 a[i]>a[i+1] 的时候才交换,即可。
void bubblesort(int a[],int n){
if(n<2)
return;
for(int end =n-1;end>0;end--){ //end表示这趟的终点
for(int i=0;i<end;i++){
if(a[i]>a[i+1])
swap(a[i],a[i+1]);
}
}
}
一种优化,(不用管),设置一个标志,如果这一趟发生了交换,则为true,否则为false。明显如果有一趟没有发生交换,说明排序已经完成。
void bubblesort2(int a[],int n){
if(n<2)
return;
int end = n-1;
bool flag =1;
while(flag){
flag =0;
for(int i =0;i<end;++i){
if(a[i]>a[i+1]){
swap(a[i],a[i+1]);
flag=1;
}
}
--end;
}
}
4.选择排序
总结一句话就是:从第一个位置开始比较,找出最小的,和第一个位置互换,开始下一轮
选择排序需要 ~N2/2 次比较和 ~N 次交换,它的运行时间与输入无关,这个特点使得它对一个已经排序的数组也需要这么多的比较和交换操作。
时间复杂度 O(N^2) 空间O(1) 不能保证稳定性
void Selectsort(int a[], int n)
{
int i, j, nMinIndex;
for (i = 0; i < n; i++)
{
nMinIndex = i; //找最小元素的位置
for (j = i + 1; j < n; j++)
if (a[j] < a[nMinIndex])
nMinIndex = j;
Swap(a[i], a[nMinIndex]); //将这个元素放到无序区的开头
}
}
5.插入排序 (稳定)
思想:简单插入排序算法原理:从整个待排序列中选出一个元素插入到已经有序的子序列中去,得到一个有序的、元素加一的子序列,直到整个序列的待插入元素为0,则整个序列全部有序。
在实际的算法中,我们经常选择序列的第一个元素作为有序序列(因为一个元素肯定是有序的),我们逐渐将后面的元素插入到前面的有序序列中,直到整个序列有序。
一个维度为n的数组A[n] , 第i轮排序:把A[i] 从 A[i-1] 比较到A[0] 如果A[i] 小 则互换位置,继续往左比较,直到碰到第一个比它小的停下来 进行下一轮比较, i从0-n-1
分析:插入排序,每次都将当前元素插入到左侧已经排序的数组中,使得插入之后左侧数组依然有序。
对于数组 {3, 5, 2, 4, 1},它具有以下逆序:(3, 2), (3, 1), (5, 2), (5, 4), (5, 1), (2, 1), (4, 1),插入排序每次只能交换相邻元素,令逆序数量减少 1,因此插入排序需要交换的次数为逆序数量。
插入排序的时间复杂度取决于数组的初始顺序,如果数组已经部分有序了,那么逆序较少,需要的交换次数也就较少,时间复杂度较低。
- 平均情况下插入排序需要 ~N2/4 比较以及 ~N2/4 次交换;
- 最坏的情况下需要 ~N2/2 比较以及 ~N2/2 次交换,最坏的情况是数组是倒序的;
- 最好的情况下需要 N-1 次比较和 0 次交换,最好的情况就是数组已经有序了。
复杂度: 时间复杂度O(N^2) 空间复杂度 O(1) 稳定性,只要写代码的时候,保证a[j]<a[j-1]才交换,就ok了
void insertsort(int a[],int n){
if(n<2)
return;
for(int i=1;i<n;++i){ //从1开始
for(int j=i;j>0 && a[j]<a[j-1];--j) //注意得是大于0
swap(a[j],a[j-1]);
}
}
6. 希尔排序
思想:设待排序元素序列有n个元素,首先取一个整数increment(小于n)作为间隔将全部元素分为increment个子序列,所有距离为increment的元素放在同一个子序列中,在每一个子序列中分别实行直接插入排序。然后缩小间隔increment,重复上述子序列划分和排序工作。直到最后取increment=1,将所有元素放在同一个子序列中排序为止。
复杂度: 时间复杂度O(N^2) 空间复杂度 O(1)
void shellsort(int a[],int n){
int h = 1;
while(h<n/3) h =3h+1; //1 4 13 40...
while(h>=1){
for(int i =h ;i<n;++i){
for(int j=i;j>=h && a[j]<a[j-h];j -= h)
swap(a[j],a[j-h]);
}
h = h/3;
}
}
7. 堆排序
基础:一般用数组来实现堆。
当下标是i的时候,左孩 2*i+1,右孩2*i+2,父节点(i-1)/2,第一个非叶节点 n/2-1。
基本思想:先把一个数组构造成一个最大堆,然后再反复删除最大元素。
1)构造堆的方法:从右往左sink,时间复杂度 O(N)
2)反复删除最大元素:把A[0]与堆的最后一个元素交换,再对新A[0]执行sink ,重新恢复堆。 时间复杂度 O(N * logN)
分析:
在堆中插入元素和删除最大元素的复杂度都为 logN。
对于堆排序,由于要对 N 个节点进行下沉操作,因此复杂度为 NlogN。
堆排序是一种原地排序,没有利用额外的空间,但并不稳定。
因为排序的时候,第一个元素跟最后一个元素交换了,就可能破坏了之前的前后关系。
比如9 5 9 这个最大堆,最后得到第一个9 反而在最后了
现代操作系统很少使用堆排序,因为它无法利用局部性原理进行缓存,也就是数组元素很少和相邻的元素进行比较和交换。
代码:
// 堆排序
// n代表构成大根堆的数组长度
void sink(int *a, int n, int i)
{
int left = 2*i + 1; // 左孩子的下标
while (left < n) // 下方还有孩子的时候
{
int largest = left + 1 < n && a[left] < a[left + 1]? left+1: left; // 取最大孩子的下标
if (a[i] >= a[largest])
break;
std::swap(a[i], a[largest]);
i = largest;
left = 2*i + 1;
}
}
void swim(int *a, int n, int i)
{
// 父节点的下标 0的父节点是0 (0-1)/2 向下取整等于0
int father = (i-1) / 2;
while (a[i] > a[father])
{
std::swap(a[i], a[father]);
i = father;
father = (i-1)/2 ;
}
}
void heap_sort(int *a, int n)
{
if (n < 2)
return;
// 构建最大堆 时间复杂度O(N)
// 进一步优化
// for(int i=n/2-1;i>=0;--i)
for (int i = n - 1; i >= 0; i--)
{
sink(a, n, i);
}
// 排序 时间复杂度O(NlogN)
for (int i = n - 1; i > 0; i--)
{
std::swap(a[0], a[i]); // 把堆顶(堆的最大值) 扔到数组尾
sink(a, i, 0); // 堆的大小变为了i ,所以形参n这边传了i
}
}
8. 桶排序(不基于比较,稳定)
计数排序和基数排序都是桶排序的一种实现,桶排序是一个大的逻辑概念
8.1 计数排序
适用场景:有n个数a[n]待排序 ,每个数都在0~k之间,适用于数的取值范围比较小,且得是整数(也就是能一个个被举出来,且总的范围比较小的),能够做到线性时间排好序,且是稳定的。
算法复杂度:时间复杂度 O(N+K) 空间复杂度O(K) 或者 O(N+K), 取决于纯数,还是不是纯数。
思想:
1.假如是纯数字的桶排序。
a)先准备一个count数组(长度为k+1)元素全部初始为0。
b)第一次遍历n个数,给count[a[i]]++。
c)第二次遍历count数组,count[j] 的值是多少,就输出多少个j。
2.假如元素不是纯数字,而是一个类student,并且要我们针对student的年龄排序。
a)先准备一个count数组(长度为k+1)元素全部初始为0。
b) 统计频率,第一次遍历n个数,给count[a[i]]++
c)count数组转为前缀和数组,第二次遍历count数组, 从1开始,让 count[i] += count[i-1]。现在count[i] ,就代表数组a里面 有多少个小于等于i的数。
d)排序,遍历数组a,但是这次从右到左遍历,这是为了保证稳定性。count【a【i】】-1就是a[i] 应该被放的下标(因为 是下标 ,所以减1),并且之后count【a【i】】要减减, 这里我们准备一个数组b,来保存结果。
e)最后回写 把b[i] 挨个赋给a[i]
int countsort(int a[],int n,int k){
int count[k+1]={0};
int b[n]={0};
for(int i=0;i<n;++i){
count[a[i]]++;
}
for(int i=1;i<k+1;++i){
count[i] +=count[i-1];
}
for(int i=n-1;i>=0;--i){ //从右到左遍历,为了保证稳定性
b[count[a[i]]-1]=a[i];
count[a[i]]--;
}
for(int i=0;i<n;++i){
a[i]=b[i];
}
}
8.2.基数排序(稳定)
思想:简单的讲就是循环的进行桶排序。
我有n个数,每个数的范围是很大的,但是每个数的进制比较小,这时候就不适用桶排序了,但是却适合基数排序。
步骤:
1)首先将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。
2)然后按从低位开始,还是高位开始分为LSD,MSD
LSD:低位优先,即从右向左依次调用桶排序。
MSD:高位优先,即从左往右依次调用桶排序。
假设一组数 A[1,11,20, 21]
我们先补0,使得每个数的数位一样 变成A[01,11,20,21]LSD:
先从个位开始桶排序 结果是 A[20,01,11,21]
再十位桶排序 [01,11,20,21],结束
简单讲,低位优先利用的是桶排序之间的稳定性。MSD:
先十位桶排序, 01 一个桶,11一个桶,20,21一个桶,再在每个桶里面各自再桶排序
复杂度分析: 时间复杂度 O(P(N+K)) P表示最大的位数,也就是要进行的桶排序次数。
基数排序是稳定的算法。因为每次都是将当前位数上相同数值的元素统一“装桶”,并不需要交换位置