前言
排序算法:将一堆数据元素按关键字递增或者递减的顺序,进行排序。
排序算法的评价指标:时间复杂度,空间复杂度,算法稳定性。
算法稳定性: 关键字相同的元素在排序之后相对位置不变。
内部排序:数据都在内存中,数据量不大,能放在内存中(关注时、空复杂度)
外部排序:数据量太大,无法放入全部内存,数据放在在磁盘中(关注磁盘读写次数)
插入排序
算法思想:每次将一个待排序的元素按其关键字大小插入到前面已经排好的子序列中,知道其全部记录插入完成。
void Insert_sort1(int* a,int n)
{
for (int i = 1; i < n; i++)
{
if (a[i] < a[i - 1]){//后小于前
int j = i - 1;//标记前
int temp = a[i];//保存后
while (j >= 0 && a[j]>temp)
/*后移,腾位,不能写成a[j]<a[i],
因为第一次移位(i=j+1)后,a[i]就被覆盖了,并不是原先的值,
这也是为什么要保存a[i]的值的原因
*/
{
a[j+1] = a[j];
--j;
}//当a[j]<temp时循环跳出,此时腾出了j+1这个位置
a[j+1] = temp;
}
}
}
对于排序算法,要明确知道所定义的每一个变量的含义,以及在循环过程中的变化情况,死记硬背是万不可行,要掌握核心思想,仔细体会在细节的处理。
折半插入排序
在前面已有序序列中查找,待插入元素位置,因为前面已经是有序序列所以可用折半查找,折半查找效率O(logn)。
void Binary_insert_sort(int* a, int n)
{
int i, j, low, high, mid;
for (i = 1; i < n; i++)
{
int temp = a[i];
low = 0; high = i - 1;
while (low<=high){
mid = (low + high) / 2;
if (a[mid]>temp)high = mid - 1;
else low = mid + 1;
}
for (int j = i - 1; j >= high + 1; --j)
a[j + 1] = a[j];
a[high + 1] = temp;
}
}
分析优化情况,实际上这样优化只是减少了之前在while循环中的比较次数,总体时间复杂度还是O(n^2),因为依然要移动大量元素。想到这里,能不能在链表中用折半插入排序呢?答案是:不可以!
原因在于折半查找,利用了数组能够通过小标随机访问的特性,而链表无法做到随机访问,只能从前往后遍历,所以无法应用折半查找,但可以利用插入排序。
希尔排序
对待排序表,按一定增量进行分割成子表,对每个子表进行直接插入排序,增量d=d/2。
不断缩小增量,最后d=1时为直接插入排序,因为前面的每次排序致使元素已经基本有序,而直接插入排序,当元素基本有序时,时间复杂度较低,这也就是希尔排序时间复杂度降低的原因。
/*
算法思想:缩小增量的直接插入排序,利用直接插入排序在元素基本有序时,时间复杂度接近O(n),来降低时间复杂度
每次对小规模的子表进行排序,最后一次的子表已经是表本身
理解希尔排序代码,首先要充分理解直接插入排序的思想
*/
int shell_sort(int* a, int n)
{
int num = 0;
for (int d = n / 2; d >= 1; d /= 2)//增量设置,每次÷2,终值为1,直接插入排序
{
for (int i = d; i < n; i++)//对i不可以进每次加d操作,对i的遍历,已经从0开始变为d开始
{
int j = i - d;//原先是减一,现在为减d
int temp = a[i];
if (a[i]<a[i - d])
{
num++;
for (j = i - d; j >= 0 && a[j] > temp; j -= d)//循环每次减d
a[j + d] = a[j];
a[j + d] = temp;//参照插入排序,1变d
}
}
}
return 1;
}
不稳定排序,只能用于线性表,不能用于链表。
冒泡排序
从后往前或从前往后两两比较相邻元素的值,若为逆序,则交换,直到序列比较完。
每一趟排序,是最小的元素冒到最前。每一趟确定一个元素的位置。
稳定排序,适用于链表
#include<stdbool.h>
void swap(int* a, int j)
{
int temp = a[j];
a[j] = a[j - 1];
a[j - 1] = temp;
}
int Bubble_sort(int* a, int n)
{
for (int i = 0; i < n-1; i++)
{
bool flag = false;
for (int j = n -1; j >i; j--)//j>i,i前的元素已经有序,从后往前冒泡,前面元素有序
{
if (a[j - 1] > a[j])//左大于右才交换,稳定
{
swap(a,j);
flag = true;//标记一趟循环是否进行过交换
}
}
if (flag == false)//没有进行交换,说明表中元素已有序,提前退出
return 1;
}
return 1;
}
注意两个for循环的n-1,对i的n-1是最后一个元素不判断
对j的n-1是从最后一个元素开始判断是否逆序,逆序则交换,直到j<=i
快速排序
分治思想,每次选一位置元素作为枢轴,进行数组划分,划分结果要让枢轴之前都<=,枢轴之后都>,这样也就确定了一个枢轴元素的位置,并接着以此枢轴划分为两个子数组,对子数组再进行划分,最终将所有元素排好序。
int Partition(int*, int, int);
void Quick_sort(int* a,int low,int high)
{
if (low < high)//递归跳出
{
int pivotpos = Partition(a, low, high);//划分
Quick_sort(a, low, pivotpos - 1);//枢轴以左
Quick_sort(a, pivotpos + 1, high);//枢轴以右
}
}
int Partition(int* a, int low, int high)//时间复杂度O(n),对表用low,high指针扫描一遍
{
int pivot = a[low];
while (low < high)//low,high指针相遇时跳出,共同所指位置即枢轴元素应放位置,此时前都比其小,后都比其大
{
while (low < high&&a[high] >= pivot) --high;//当出现小于枢轴时跳出while,此时high所指就是小于枢轴
a[low] = a[high];//low最初的值已经保存在pivot中,可视为low此时空出,直接放入,每次while只能交换一个值
while (low < high&&a[low] <= pivot) ++low;//low指针右移找到比枢轴大的跳出while,low此时所指为大于枢轴的元素,下步移动
a[high] = a[low];//在上面high的值已经被存入,所以视high为空,放入low值
}
//跳出最外层while,此时low=high,所指相同,且其原值已经被保存到别处,可视为空,则放入pivot
a[high] = pivot;//a[high]=pivot;是一样的
return high;//return high;是一样的
}
/*
快排时间复杂度O(n*递归层数)
如果每次选择的枢轴元素都刚好是顺序的中间元素的值,则递归层数为logn
最好时间复杂度O(nlogn)
最坏O(n^2)
*/
跑一亿随机数排序时会崩。
int Partition(int*, int, int);
void Quick_sort(int* a,int low,int high)
{
if (low < high)
{
int pivotpos = Partition(a, low, high);
Quick_sort(a, low, pivotpos - 1);
Quick_sort(a, pivotpos + 1, high);
}
}
int Partition(int* a, int low, int high)
{
int pivot = a[low];
while (low < high)
{
while (low < high&&a[high] >= pivot) --high;
a[low] = a[high];
while (low < high&&a[low] <= pivot) ++low;
a[high] = a[low];
}
a[high] = pivot;
return high;
}
选择排序
每次选取未排序列的最小值,与当前未排序的第一个位置交换,知道最后一个元素。
void Choice_sort(int* a, int n)
{
for (int i = 0; i < n-1; i++)
{
int min=i;
for (int j = i+1; j < n; j++)//在二层for循环外无判断,所以内层for始终都要被执行
{
if (a[j] < a[min])min = j;//min保存最小值下标,用小标处理相对于用值处理要简单
}
//找到最小a[j],与a[i]交换
if (min!=i)//若当前a[i]就是未排序中最小,则不用交换
{
int temp = a[i];
a[i] = a[min];
a[min] = temp;
}
}
}
/*
不稳定,也使用于链表
无论待排序列是否有序,时间复杂度都是 n(n-1)/2 -> O(n^2)
*/
堆排序
/*
大根堆,小根堆(完全二叉树)逻辑视角,因为是完全二叉树,可以用数组存(存储视角)
根>=左右孩子
根<=左右孩子
i的左孩子 2i
i的右孩子 2i+1
i的父节点 i/2(向下取整)
构建和维护大根堆
检查非终端节点 i<=(n/2),是否满足大根堆的要求
*/
void HeadAdjust(int*, int, int);
void BuildMaxHeap(int* a, int len)
{
for (int i = len / 2; i > 0; i--)//从后往前遍历非终端结点
HeadAdjust(a, i, len);
}
//将以k为根的子树调整为大根堆(局部微调)
void HeadAdjust(int* a, int k, int len)
{
int root = a[k];//暂存子树的根节点,注意是子树
for (int i = 2 * k; i <= len; i *= 2)//i=2*k,所以此时i表达k的左孩子
{
if (i < len&&a[i] < a[i + 1])//取左右孩子的较大值
i++;
if (root >= a[i])break;//根大于较大值,说明该子树满足大根堆,退出
else //否则交换二者
{
a[k] = a[i];
k = i;//进行交换,则改变了其他子树的大根堆,所以要更新k值,继续向下检查
}
}
a[k] = root;
}
void Heap_Sort(int* a, int len)
{
BuildMaxHeap(a, len);
for (int i = len; i >= 0; i--)
{
int temp = a[i];
a[i] = a[0];
a[0] = temp;//大根堆,所以和堆顶元素交换
HeadAdjust(a, 0, i - 1);//交换后,堆顶元素到了叶子结点不必再考虑它,修正大根堆
}
}
构建大根堆,每次选择堆顶元素,已选元素与叶子交换,就不再看,维护其他结点构成大根堆,大根堆逐渐缩小,最后只剩一个根结点时,其实已经变成小根堆。
不稳定排序
时间复杂度O(nlogn)
归并排序
把两个或多个已经有序的序列合并成一个
所以分为“2路”归并和多路归并
m路归并,每次选出一个关键字需对比m-1次。
内部排序中一般采用2路归并
//递归实现
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<stdbool.h>
int* b;
void meger(int* a, int l, int mid, int r)
{
// int* b=(int*)malloc(sizeof(int)*n);
//辅助数组b不能在这里申请,应定义为全局,首先定义全局指针,然后再main函数中申请空间
int k = l;
int i, j;
for (i = l, j = mid + 1; i <= mid&&j <= r;k++)//不能在for中进行i,j的增
{
if (a[i]>a[j])
b[k] = a[j++];
else b[k] = a[i++];
}//循环结束条件就是i,j之一越界其子数组,只是其中一个下标越界
while (i<=mid)b[k++] = a[i++];
while (j<=r)b[k++] = a[j++];
for (int i = l; i <= r; i++)//处理好的b赋给a
a[i] = b[i];
}
void megersort(int* a, int l, int r)
{
if (l >= r) return;
int mid = (l + r) / 2;
megersort(a, l, mid);
megersort(a, mid + 1, r);
meger(a, l, mid, r);
}
void pint(int* a, int n)
{
for (int i = 0; i<n; i++)
printf("%d ", a[i]);
}
int main()
{
clock_t s, f;
srand(time(0));
int n;
scanf("%d", &n);
b = (int*)malloc(4 * n);
int* a = (int*)malloc(4 * n);
for (int i = 0; i<n; i++)
a[i] = rand() % 100 + 1;
//pint(a, n);
s = clock();
megersort(a, 0, n - 1);
f = clock();
printf("time=%f\n", (float)(f - s)/1000);
printf("\n");
//pint(a, n);
return 0;
}
递归结束:当划分的数组只有一个元素时,开始合并,并且只有一个元素的子数组必然是有序的
归并的过程就像形成一颗二叉树,二叉树有logn层,归并排序要logn趟,每趟都要O(n)时间,就像当于遍历一遍,时间复杂度为O(nlogn)
空间复杂度O(n),主要是要用到一个和原数组大小相同辅助数组,还有递归栈但是栈只要logn,低阶舍去。
稳定排序。