写在前面
2023年的第一篇博客,在这里先祝大家兔年快乐.
文章比较长,App端会比较卡,尽量到网页端访问
本文从学习到搜寻各种资料,整理成博客的形式展现足足花了一个月的时间,慢工出细活,希望本篇文章可以真正带你学懂排序,不再为写排序算法而苦恼
【核心思路】:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列
对直接插入排序有了一个基本的了解以后,我们需要将这个思想去转化为代码
很多同学一开始刚有点思路就开始写代码,甚至什么都不想直接开始写,然后运行报错一大堆,各种数组访问越界、边界问题没有考虑、代码逻辑混乱。这其实是写程序的一个大忌,我们应该逐步分析,从简单到复杂、从单趟的排序到整体的排序去过渡
①写出单趟的的插入过程
a[end + 1] = tmp;
就可以将这个待插入数据完整地放入有序区中并且使这个有序区依旧保持有序int end;
int tmp = a[end + 1]; //将end后的位置先行保存起来
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end]; //比待插值来得大的均往后移动
end--; //end前移
}
else
{
break; //若是发现有相同的或者小于带插值的元素,则停下,跳出循环
}
}
a[end + 1] = tmp; //将end + 1的位置放入保存的tmp值
②利用循环控制每一趟插入排序
i < n - 1
,这里不可以写成i < n
,若是写成这样那么【i】最后落的位置就是【n - 1】此时end获取到这个位置后取保存tmp的值时就会造成造成数组的越界访问,那么这个位置就会出现一个随机值,所以大家在写这种循环的边界条件时一定提前做好分析,在运行之前保证心里胸有成竹for (int i = 0; i < n - 1; ++i)
{
int end = i;
//单趟插入逻辑...
}
整体代码展示
/*直接插入排序*/
void InsertSort(int* a, int n)
{
//不可以< n,否则最后的位置落在n-1,tmp访问end[n]会造成越界
for (int i = 0; i < n - 1; ++i)
{
int end = i;
int tmp = a[end + 1]; //将end后的位置先行保存起来
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end]; //比待插值来得大的均往后移动
end--; //end前移
}
else
{
break; //若是发现有相同的或者小于带插值的元素,则停下,跳出循环
}
}
a[end + 1] = tmp; //将end + 1的位置放入保存的tmp值
}
}
运行结果
【时间复杂度】:O(N2)
【空间复杂度】:O(1)
基本思想:希尔排序又称缩小增量排序。是按照每次的gap值,也就是增量,对原本的数据进行一个分组,对每组使用直接插入排序算法排序。增量【gap】随着排序次数的增加而减少
当增量减少到1时,整个序列恰好被分为一组,算法便终止。
【核心思路】:通过预排序将大的数尽快放到后面,将小的数尽快放到前面
还是一样,我们由简至易,做单步分析
①确定单趟排序的过程
tmp = a[end + gap];
,保存的就是当前待比较元素的后移gap位元素a[end + gap]
的地方int gap = 3;
int end;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
②循环控制每一趟插入排序
Way1:
for (int i = 0; i < n - gap; i += gap) //一组一组走
Way2:
for (int i = 0; i < n - gap; i++) //一位一位走
我们来看看上面两种情况的不同之处
i += gap
来说,在排序的过程中是先把每一组先排好,再排下一组i++
来说,在排序的过程中第一组排好了一个数据后,i++又进行下一组的排序,每次只插入一个,然后到后面又回到这一组来,像是一个轮回的过程。你可以自己去试试看
③单趟gap的排序控制好后,我们就需要去控制这个增量gap了,网上很多写法都是gap /= 2
,这种比较经典一些,也是我上面所展示的一种
int gap = n / 2;
for (int j = 0; j < gap; ++j)
{
gap /= 2;
for (int i = j; i < n - gap; i += gap)
{ //一组一组走
//单趟插入过程...
}
}
int gap = n;
while (gap > 1)
{
/*
* gap > 1 —— 预排序
* gap == 1 —— 直接插入排序
*/
//gap /= 2;
gap = gap / 3 + 1; //保证最后的gap值为1,为直接插入排序
for (int i = 0; i < n - gap; i++)
{
//单趟插入过程...
}
}
整体代码展示
/*希尔排序*/
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
/*
* gap > 1 —— 预排序
* gap == 1 —— 直接插入排序
*/
//gap /= 2;
gap = gap / 3 + 1; //保证最后的gap值为1,为直接插入排序
for (int i = 0; i < n - gap; i++)
{ //一位一位走
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
运行结果
【时间复杂度】:O(NlogN)
【空间复杂度】:O(1)
有关选择排序,大家应该是见过不少,这里我会讲两种,一种是传统的选择排序,一种则是我改进之后略微优化一些的选择排序
if (a[j] < a[k]) k = j;
一直寻找,直到搜寻完这个区间为止a[k] != a[i]
则表示需要交换k和i这两个位置的数据,将它们交换之后小的数就会跑到前面了整体代码展示
void SelectedSort(int* a, int n)
{
for (int i = 0; i < n - 1; ++i)
{
int k = i;
for (int j = i + 1; j < n; ++j)
{
if (a[j] < a[k]) k = j;
}
if (a[k] != a[i])
{
swap(&a[k], &a[i]);
}
}
}
运行结果
[begin,end]
这段区间里去不断寻找然后更新最大值和最小值begin++;end--;
不断缩小需要查找的区间。这样小的数就会被慢慢放到前面,大的数就会被慢慢放到后面,序列便会逐渐趋向有序/*简单选择排序*/
void SelectSort(int* a, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int mini = begin;
int maxi = begin;
for (int i = begin + 1; i <= end; ++i)
{
if (a[i] < a[mini])
mini = i;
if (a[i] > a[maxi])
maxi = i;
}
swap(&a[begin], &a[mini]);
swap(&a[end], &a[maxi]);
begin++;
end--;
}
}
if (maxi == begin) //若是最大值和begin重合了,则重置一下交换后的最大值
maxi = mini;
整体代码展示
/*简单选择排序*/
void SelectSort(int* a, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int mini = begin;
int maxi = begin;
for (int i = begin + 1; i <= end; ++i)
{
if (a[i] < a[mini])
mini = i;
if (a[i] > a[maxi])
maxi = i;
}
swap(&a[begin], &a[mini]);
if (maxi == begin) //若是最大值和begin重合了,则重置一下交换后的最大值
maxi = mini;
swap(&a[end], &a[maxi]);
begin++;
end--;
}
}
运行结果
【时间复杂度】:O(N2)
【空间复杂度】:O(1)
首先在学习堆排序前要了解堆是个什么东西
如果有一个关键码的集合K = { k0, k1, k2,…,kn-1 },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中。并满足:Ki <= K2*i+1 且 Ki <= K2*i+2 ( Ki >= K2*i+1 且 Ki >= K2*i+2 ) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆
堆的性质
【lchild = parent * 2 + 1】 左孩子
【rchild = parent * 2 + 2】 右孩子
parent = (lchild - 1)/ 2 或者 parent = (rchild - 2)/ 2 ===> 【parent = (child - 1) / 2】
对于堆排序来说,核心的一块就是【向下调整算法】,领悟了这一块,那你堆排序也就掌握一半了
清楚了向下调整算法的主要原理和思路,接下去我们就要将其转化为代码
void Adjust_Down(int* a, int n, int parent)
int child = parent * 2 + 1;
//判断是否存在右孩子,防止越界访问
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 Adjust_Down(int* a, int n, int parent)
{
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;
}
}
}
//建立大根堆(倒数第一个非叶子结点)
for (int i = ((n - 1) - 1) / 2 ; i >= 0; --i)
{
Adjust_Down(a, n, i);
}
具体过程如下
/*堆排序*/
void HeapSort(int* a, int n)
{
//建立大根堆(倒数第一个非叶子结点)
for (int i = ((n - 1) - 1) / 2 ; i >= 0; --i)
{
Adjust_Down(a, n, i);
}
int end = n - 1;
while (end > 0)
{
swap(&a[0], &a[end]); //首先交换堆顶结点和堆底末梢结点
Adjust_Down(a, end, 0); //一一向前调整
end--;
}
}
整体代码展示
/*交换*/
void swap(int* x, int* y)
{
int t = *x;
*x = *y;
*y = t;
}
/*向下调整算法*/
void Adjust_Down(int* a, int n, int parent)
{
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 HeapSort(int* a, int n)
{
//建立大根堆(倒数第一个非叶子结点)
for (int i = ((n - 1) - 1) / 2; i >= 0; --i)
{
Adjust_Down(a, n, i);
}
int end = n - 1;
while (end > 0)
{
swap(&a[0], &a[end]); //首先交换堆顶结点和堆底末梢结点
Adjust_Down(a, end, 0); //一一向前调整
end--;
}
}
运行结果
【时间复杂度】:O(NlogN)
【空间复杂度】:O(1)
核心思想 :根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置。即每次两两比较,将大(小)的往后方,像一个冒泡的过程
交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
看了上面的单趟冒泡过程,相信你对冒泡排序有了一个基本的认识,每次进行两两比较,将大的数往后放,这个过程就像是冒泡一样,接下去我们来看看代码该如何书写
①单趟的冒泡过程
for (int j = 0; j < n - i - 1; ++j)
{
if (a[j] > a[j + 1])
swap(&a[j], &a[j + 1]);
}
②外部的循环遍历
for (int i = 1; i < n; ++i)
,那么你内部的单趟冒泡结束条件就要变一下了,否则的话就会造成数组访问越界的问题for (int i = 0; i < n - 1; ++i)
{
//单趟冒泡...
}
整体代码展示
/*冒泡排序*/
void BubbleSort(int* a, int n)
{
//[0,n - 1)
for (int i = 0; i < n - 1; ++i)
{
int changed = 0;
for (int j = 0; j < n - 1 - i; ++j)
{
if (a[j] > a[j + 1])
{
swap(&a[j], &a[j + 1]);
changed = 1;
}
}
if (changed == 0)
break;
PrintArray(a, n);
}
//[1,n)
//for (int i = 1; i < n; ++i)
//{
// for (int j = 0; j < n - i; ++j)
// {
// if (a[j] > a[j + 1])
// swap(&a[j], &a[j + 1]);
// }
//}
}
运行结果
【时间复杂度】:O(N2)
【空间复杂度】:O(1)
这一块是后来加的,刚好在看完这三个排序算法后进行一个对比,也可以起到巩固的效果
排序算法 | 平均情况 | 最好情况 | 最坏情况 |
---|---|---|---|
直接插入排序 | O(N2) | O(N) | O(N2) |
简单选择排序 | O(N2) | O(N2) | O(N2) |
冒泡排序 | O(N2) | O(N) | O(N2) |
我们通过对比来看看它们之间究竟差在哪里
首先来说一说对于hoare版本的这个排序思路的概要,也就是大家常说的左右指针法
【核心思路】:任取待排序元素序列中的某元素作为基准值(一般我们选择取最左边),按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程(递归),直到所有元素都排列在相应位置上为止
首先再度明确规则:找最前的值,也就是left作为基准值,然后让right去找比基准值小的数字。即左边做key,右边先走
①第一种【R停住】:右边R找到小的了,停住了。此时左边L在找的过程中并没有找到比key大的,因此二者只能会面,那么会面的这个值一定要比key要来的小,因此交换后不会出问题
②第二种【L停住】:R在右边找到了比key小的,L在左边找到了比key大的,二者进行交换,此时L上的数就是比key要小的(交换过来了)。后面R在进行第二轮搜寻的时候并没有再找到比key小的了,只能和L相遇然后和key交换,此时也不会出问题
③第三种【L停住】:这是一种特殊情况,因为是R先走,但是呢R在一开始就没有找到比key来得小的值,因此它会在一个循环当中一直跑一直跑,直到和L相遇为止,那有的同学说L为什么不跑?因为L要等到R找到对应的值之后它才能跑
先给出整体代码,你可以先看看,然后再听我分析
/*快速排序 —— hoare版本(左右指针法)*/
int PartSort1(int* a, int begin, int end)
{
/*
* 找小 —— 是找真的比我小的
* 找大 —— 是找真的比我大的
* --->相等均要略过
*
* 还要防止越界【left < right】
*/
int left = begin;
int right = end;
int keyi = begin; //以最左边作为对照值记录,右边先走
while (left < right)
{
//右边找比keyi所在位置小的值,若是 >= 则忽略(加上=防止死循环)
while (left < right && a[right] >= a[keyi])
{ //left < right —— 防止特殊情况越界
right--;
}
//左边找比keyi所在位置大的值,若是 <= 则忽略
while (left < right && a[left] <= a[keyi])
{
left++;
}
//都找到了,则交换
swap(&a[left], &a[right]);
}
//最后当left和right相遇的时候将相遇位置的值与keyi位置的值交换
swap(&a[left], &a[keyi]);
keyi = left; //因keyi位置的值被交换到相遇点,因此更新准备分化递归
return keyi;
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return; //最后递归下去区间不存在了,进行递归回调
int key = PartSort1(a, begin, end); //单趟排序获取key值
//左右区间分化递归
QuickSort(a, begin, key - 1);
QuickSort(a, key + 1, end);
}
//右边找比keyi所在位置小的值,若是 >= 则忽略(加上=防止死循环)
while (left < right && a[right] >= a[keyi])
{ //left < right —— 防止特殊情况越界
right--;
}
//左边找比keyi所在位置大的值,若是 <= 则忽略
while (left < right && a[left] <= a[keyi])
{
left++;
}
//右边找比keyi所在位置小的值,若是 >= 则忽略(加上=防止死循环)
while (a[right] > a[keyi])
{ //left < right —— 防止特殊情况越界
right--;
}
//左边找比keyi所在位置大的值,若是 <= 则忽略
while (a[left] < a[keyi])
{
left++;
}
⚠没控制端点导致越界风险
⚠没考虑相等情况导致死循环
keyi = left; //因keyi位置的值被交换到相遇点,因此更新准备分化递归
//左右区间分化递归
QuickSort1(a, begin, keyi - 1);
QuickSort1(a, keyi + 1, end);
if (begin >= end)
return; //最后递归下去区间不存在了,进行递归回调
【时间复杂度】:O(NlogN)
【空间复杂度】:O(logN)
看了Hoare版本的快排后,我们立马来分析一下其时间复杂度,因为后面要对其进行优化,所以要提前讲一下复杂度这一块
在分析完了快排的时间复杂度之后,也知晓了面对两种缺陷可以使用的优化方法,接下去我们来讲讲这两种方法
【针对待排序列呈现有序状态】
int mid = (left + right) >> 1;
int mid = (left + right) >> 1;
if (a[mid] > a[left])
{
if (a[mid] < a[right])
{ //left mid right
return mid;
}
//另外两种mid一定是最大的,比较left和right
else if (a[left] > a[right])
{ //right left mid
return left;
}
else
{ //left right mid
return right;
}
}
else //a[left] >= a[mid]
{
if (a[mid] > a[right])
{ //right mid left
return mid;
}
//另外两种mid一定是最小的,比较left和right
else if (a[left] < a[right])
{ //mid left right
return left;
}
else
{ //mid right left
return right;
}
}
//三数取中
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]); //交换begin上的值和找出的中间值
int left = begin;
int right = end;
int keyi = begin; //以最左边作为对照值记录,右边先走
针对递归层数过深导致栈溢出
运用三数取中法,对快速排序进行了一个优化,接下去我们再来将一种优化方式,叫做左右小区间法
对于三数取中法,是在开头优化;对于左右小区间法,则是在结尾优化
好,这里给大家【简单】画了一张图,其实随着这个递归次数的增加,递归的层层深入,这个数据量也会被倍增,那么这个程序所需要消耗的内存就会越多,那我们有没有办法将最后的这几层递归消除呢?
//小区间优化
if ((end - begin + 1) < 15)
{
//在数据量少的时候改用直接插入排序
InsertSort(a + begin, end - begin + 1);
}
①直接将最左端的值选出来作为key值,然后【右边找小】,放入坑位,然后更新坑位值为右侧找到的那个数所在的下标;
②出现了新的坑位后,【左边找大】,找到之后将数字放到新的坑位中,然后继续更新坑位。
③循环往复上面的步骤,直到两者相遇为止,更新相遇处为最新的坑位,然后将key值放入坑位即可,保证左边比key小,右边比key大
以下是动图的算法分解图,可以对照理解一下
/*快速排序 —— 挖坑法*/
int PartSort2(int* a, int begin, int end)
{
//三数取中
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]);
int left = begin, right = end;
int hole = left; //坑位
int key = a[hole]; //记录坑位上的值
while (left < right)
{
//右边找小,放入坑位,然后更新坑位
while (left < right && a[right] >= key)
{
right--;
}
a[hole] = a[right];
hole = right;
//左边找大,放入坑位,然后更新坑位
while (left < right && a[left] <= key)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
a[hole] = a[right]
与a[hole] = a[left]
就是在找到符合条件的数之后将其扔入坑中的过程hole = right
与hole = left
就是在填完上一个坑位之后更新坑位的过程//右边找小,放入坑位,然后更新坑位
while (left < right && a[right] >= key)
{
right--;
}
a[hole] = a[right];
hole = right;
//左边找大,放入坑位,然后更新坑位
while (left < right && a[left] <= key)
{
left++;
}
a[hole] = a[left];
hole = left;
a[hole] = key
就是在最后两者相遇后将原先记录的key值放入这个相遇的坑位。最后return hole
这个坑位坐标即是找到的那个对照值所在的位置①定义一个prev指针位于起始端,再定义一个cur指针就位于它的后方,记录当前位置上的key值
②cur指针向后找比key小的值,若是找不到,则一直++;若是cur找到了比key小的值,prev++,然后交换二者的值之后cur再++
③直到cur超过右边界之后,退出循环逻辑,将此时prev位置上的值与key值做一个交换,保证左边比key小,右边比key大
/*快速排序 —— 前后指针法*/
int PartSort3(int* a, int begin, int end)
{
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]);
int prev = begin;
int cur = prev + 1;
int keyi = prev;
while (cur <= end)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
swap(&a[++prev], &a[cur]);
}
cur++; //cur无论如何都要++,因此直接写在外面
}
//cur超出右边界后交换prev处的值和key
swap(&a[keyi], &a[prev]);
return prev;
}
&& ++prev != cur
呢,此时你可以返回去看上面的算法分解图,可以看到第一、二两次在交换swap(&a[++prev], &a[cur]);
的时候,完全就没有动,因为它们是相同的,此时我们其实可以去做一个优化,那就是判断++prev
之后的位置是否与cur
是相同的,若是则不进行交换while (cur <= end)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
swap(&a[++prev], &a[cur]);
}
cur++; //cur无论如何都要++,因此直接写在外面
}
swap(&a[keyi], &a[prev]);
上述三种是快速排序比较经典的方法,有关【三路划分法】某些高手针对快速排序的缺陷发明出来的一种方法,很是巧妙,所以这里给大家介绍一下
a[cur]
上的值与【left】和【right】上的值进行一个对比,然后将比key小的值扔到前面,将比key大的值扔到后面,若是和key相同则放到中间。在【left】与【cur】不断后移,【right】不断前移的过程中,三个区间就会很明显地被分化出来,直到cur > right
时,便终止比较。接下去中间的这一块与key相等的值不需要去管他,我们只需要再去递归其左右区间即可,这就可以防止重复key值带来的多次递归的风险代码展示
void QuickSortThreeDivisioin(int* a, int begin, int end)
{
if (begin >= end)
return; //最后递归下去区间不存在了,进行递归回调
//小区间优化
if ((end - begin + 1) < 15)
{
//在数据量少的时候改用直接插入排序
InsertSort(a + begin, end - begin + 1);
}
else
{
//三数取中
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]);
int key = a[begin];
int left = begin;
int cur = left + 1;
int right = end;
while (cur <= right)
{
if (a[cur] < key)
{
swap(&a[left++], &a[cur++]);
}
else if (a[cur] > key)
{
swap(&a[cur], &a[right--]);
//此时cur不变化是因为从right换到中间的值可能还是比key大,为了在下一次继续进行比较
}
else
{
cur++;
}
}
//[begin, left - 1][left, right][right + 1, end]
QuickSortThreeDivisioin(a, begin, left - 1);
QuickSortThreeDivisioin(a, right + 1, end);
}
}
看完了上面三种快速排序的,我们可以看出,都是使用递归的方法去实现的,也就是通过一层的遍历找出一个中间值,然后根据这个中间值进行一个左右划分,分别去进行分治递归。可以看出三种方法虽然类似,但都有自己的独特之处
但是大家肯定有一个疑问,既然都已经学了四种方法了,那为什么还要再去学习非递归的写法呢?我们来探究一下
那非递归改递归这一块要怎么实现呢?我们来看一下
void QuickSortNonR(int* a, int begin, int end)
{
ST st;
InitStack(&st);
//首先将整体区间入栈
PushStack(&st, begin);
PushStack(&st, end);
while (!StackEmpty(&st))
{
//出栈分别获取右左两个端点
int right = StackTop(&st);
PopStack(&st);
int left = StackTop(&st);
PopStack(&st);
//求解keyi的位置
int keyi = PartSort3(a, left, right);
//先入右
if (keyi + 1 < right)
{ //若是区间的值 > 1,则继续入栈
PushStack(&st, keyi + 1);
PushStack(&st, right);
}
//后入左
if (left < keyi - 1)
{ //若是区间的值 > 1,则继续入栈
PushStack(&st, left);
PushStack(&st, keyi - 1);
}
}
DestroyStack(&st);
}
接下来稍微描述一下,主要是讲给还没有理解的同学
right
和left
进行保存【核心思路】:分治思想。使原有的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序;
有关归并排序会讲解【递归】和【非递归】两种解法,因为在校招中都会涉及
首先来说一下有关递归版本的实现思路分析
void MergeSort(int* a, int n)
{
//开辟空间,存放归并后的数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("fail malloc");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp); //防止内存泄漏
tmp = NULL; //防止野指针
}
void _MergeSort(int* a, int begin, int end, int* tmp)
int mid = (begin + end) >> 1;
/*
* [begin, mid][mid + 1, end]
* --->继续进行子区间归并,相当于后序遍历【左,右,根】
*/
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
if (begin >= end)
{
return;
}
若是上面这段逻辑没有听懂,可以看看这篇文章,也是讲解有关归并逻辑的题解 ----> 合并两个有序数组
int i = begin; //i要从每次递归进来的begin开始放,可能是在右半区间
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
//若是还有区间存在数据,表示没有归并完全,直接放入tmp即可
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//最后将归并完后后的数据拷贝回原数组
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
void _MergeSort(int* a, int begin, int end, int* tmp)
{
//递归出口
if (begin >= end)
{
return;
}
int mid = (begin + end) >> 1;
/*
* [begin, mid][mid + 1, end]
* --->继续进行子区间归并,相当于后序遍历【左,右,根】
*/
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
int i = begin; //i要从每次递归进来的begin开始放,可能是在右半区间
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
//若是还有区间存在数据,表示没有归并完全,直接放入tmp即可
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//最后将归并完后后的数据拷贝回原数组
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
/*归并排序*/
//时间复杂度:O(NlogN)
//空间复杂度:O(N)
void MergeSort(int* a, int n)
{
//开辟空间,存放归并后的数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("fail malloc");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp); //防止内存泄漏
tmp = NULL; //防止野指针
以下是递归分解算法图
以下是我录制的讲解视频(如果模糊就前后拖动一下)
归并排序递归版
说完递归,我们再来讲讲对于归并排序的非递归写法,重点在于理解递归写法,但是非递归校招也有要求,所以我会讲
int rangeN = 1;
for (int i = 0; i < n; i += 2 * rangeN)
{
int begin1 = i, end1 = i + rangeN - 1;
int begin2 = i + rangeN, end2 = i + 2 * rangeN - 1;
int j = i; //表示tmp每次都从上一次归并完放置后的地方开始
//归并...
//归并完一小组,拷贝一小组
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
// + i表示每次归并的组在发生变化 //因为end是最后落的位置,i是初始化位置,不会改变
}
int begin1 = i, end1 = i + rangeN - 1;
int begin2 = i + rangeN, end2 = i + 2 * rangeN - 1;
int j = i; //表示tmp每次都从上一次归并完放置后的地方开始
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
//若是还有区间存在数据,表示没有归并完全,直接放入tmp即可
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
while (rangeN < n)
{
//单组的归并回拷逻辑
rangeN *= 2;
}
int rangeN = 1;
while (rangeN < n)
{
for (int i = 0; i < n; i += 2 * rangeN)
{
int begin1 = i, end1 = i + rangeN - 1;
int begin2 = i + rangeN, end2 = i + 2 * rangeN - 1;
printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
int j = i; //表示tmp每次都从上一次归并完放置后的地方开始
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
//若是还有区间存在数据,表示没有归并完全,直接放入tmp即可
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
//归并完一小组,拷贝一小组
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
// + i表示每次归并的组在发生变化 //因为end是最后落的位置,i是初始化位置,不会改变
}
rangeN *= 2;
}
//这两句别忘了加
free(tmp); //防止内存泄漏
tmp = NULL; //防止野指针
printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
通过上一模块中的层层的DeBug调试,我们看到了 当rangeN = 2,也就是两两一归并的时候出现了越界的情况,为什么会发生这样的事情呢?记得我在一开始的时候有说过,对于归并排序,和快速排序不一样的地方在于,其每次两个区间大小是确定的,若是左半区间有四个数,那么右半区间的大小也必须要能够存放得下四个数
所以当待排序的数字有十个的时候,两两一归并,当前面八个归并完成后,后面的两个自动作为左半区间,那么此时右半区间就会产生越界的情况,在数组章节我们有讲到过,若是出现越界的情况就会出现随机值
解决方案 —> 一 一分析越界的几种情况,然后对其分别做考虑判断
/*
* 处理越界的情况
*/
if (end1 >= n) {
break;
}
else if (begin2 >= n) {
break;
}
else if (end2 >= n) {
end2 = n - 1; //end2越界要修正一下,不可以break,因为最后需要求整个数组的长度
}
//归并完一小组,拷贝一小组
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
// + i表示每次归并的组在发生变化 //因为end是最后落的位置,i是初始化位置,不会改变
/*
* 处理越界的情况
*/
if (end1 >= n) {
end1 = n - 1;
begin2 = n; //begin2要比end2来的大,才不构成区间
end2 = n - 1;
}
else if (begin2 >= n) {
begin2 = n; //begin2要比end2来的大,才不构成区间
end2 = n - 1;
}
else if (end2 >= n) {
end2 = n - 1;
}
memcpy(a, tmp, sizeof(int) * n);
就要放在单层循环外了,可以看到每次拷贝的区间大小都是固定的可以看到,对于归并排序来说,无论是递归还是非递归,都是存在一定难度的,特别是对于归并的非递归这一块,光边界修正这一块就让人头大(((φ(◎ロ◎;)φ))),但是大家还是要有所掌握,上面说过,对于一些场景使用非递归要比递归来的安全很多
上述的七个排序均为比较类排序,接下去我们来介绍三种非比较类排序
【核心思路】:通过统计相同元素出现次数,存放到一个数组中,然后再根据统计的结果将序列回收到原来的序列中
接下去讲刚才分析的思路转化为代码
①开辟统计次数的数组
//1.找出数组中的最大值和最小值,然后开辟统计数组
int min = a[0];
int max = a[0];
for (int i = 0; i < n; ++i)
{
if (a[i] > max)
max = a[i];
if (a[i] < min)
min = a[i];
}
int range = max - min + 1; //数据范围
int* CountArray = (int*)malloc(sizeof(int) * range);
memset(CountArray, 0, sizeof(int) * range); //初始化数组均为0
②遍历原数组,统计出每个数字出现的次数,将其一一映射到CountArray数组中
//2.统计数组中每一个数出现的个数,映射到CountArray数组中
for (int i = 0; i < n; ++i)
{
CountArray[a[i] - min]++;
//a[i] - min 表示找出相对位置
}
a[i] - min
是什么意思,对于我在上一模块展示的只是一个特殊情况,最小值是从0开始的,刚好可以和CountArray数组中的下标对上,但是对于大多数的情况来说,最小值不会是0,所以在一一映射的过程中就需要做一些变化a[i] - min
即可③根据每个数字统计完后的次数,一一放回原数组
a[i] - min
映射到了CountArray数组,现在我们要将其再一一放回去,此时你就会发现数组便会呈现有序j + min
//3.将统计数组中的数写回原数组中,进行排序
int index = 0;
for (int j = 0; j < range; ++j)
{
//根据统计数组去找出每个数要写回几次
while (CountArray[j]--)
{
a[index++] = j + min;
//每次循环中的j不会发生变化,加上最小值可以找回原来的数字
}
}
整体代码展示
/*计数排序*/
void CountSort(int* a, int n)
{
//1.找出数组中的最大值和最小值,然后开辟统计数组
int min = a[0];
int max = a[0];
for (int i = 0; i < n; ++i)
{
if (a[i] > max)
max = a[i];
if (a[i] < min)
min = a[i];
}
int range = max - min + 1; //数据范围
int* CountArray = (int*)malloc(sizeof(int) * range);
memset(CountArray, 0, sizeof(int) * range); //初始化数组均为0
//2.统计数组中每一个数出现的个数,映射到CountArray数组中
for (int i = 0; i < n; ++i)
{
CountArray[a[i] - min]++;
//a[i] - min 表示找出相对位置
}
//3.将统计数组中的数写回原数组中,进行排序
int index = 0;
for (int j = 0; j < range; ++j)
{
//根据统计数组去找出每个数要写回几次
while (CountArray[j]--)
{
a[index++] = j + min;
//每次循环中的j不会发生变化,加上最小值可以找回原来的数字
}
}
}
运行结果
来看看计数排序的时空复杂度
【时间复杂度】:O(N + range)
【空间复杂度】:O(range)
对于计数排序而言,因为需要统计数据出现的次数,所以只能用与整型的数据,如果是浮点数或字符串排序还得用比较排序
看了上面的通动图之后,你应该对桶排序有了以及基本的认识,接下去我们一起来了解一下如何使用【桶】来进行排序
int bucket[5][5]; // 分配五个桶。
int bucketsize[5]; // 每个桶中元素个数的计数器。
// 初始化桶和桶计数器。
memset(bucket, 0, sizeof(bucket));
memset(bucketsize, 0, sizeof(bucketsize));
// 把数组a的数据按照范围放入对应桶中
for (int i = 0; i < n; ++i)
{
bucket[a[i] / 10][bucketsize[a[i] / 10]++] = a[i];
}
// 分别对每个桶中的数据进行排序
for (int i = 0; i < 5; ++i)
{
QuickSort(bucket[i], 0, bucketsize[i] - 1);
}
// 将把每个桶中的数据依次放回数组a中
int index = 0;
for (int i = 0; i < 5; ++i)
{
for (int j = 0; j < bucketsize[i]; ++j)
{
a[index++] = bucket[i][j];
}
}
整体代码展示
/*桶排序*/
void BucketSort(int* a, int n)
{
int bucket[5][5]; // 分配五个桶。
int bucketsize[5]; // 每个桶中元素个数的计数器。
// 初始化桶和桶计数器。
memset(bucket, 0, sizeof(bucket));
memset(bucketsize, 0, sizeof(bucketsize));
// 把数组a的数据按照范围放入对应桶中
for (int i = 0; i < n; ++i)
{
bucket[a[i] / 10][bucketsize[a[i] / 10]++] = a[i];
}
// 分别对每个桶中的数据进行排序
for (int i = 0; i < 5; ++i)
{
QuickSort(bucket[i], 0, bucketsize[i] - 1);
}
// 将把每个桶中的数据依次放回数组a中
int index = 0;
for (int i = 0; i < 5; ++i)
{
for (int j = 0; j < bucketsize[i]; ++j)
{
a[index++] = bucket[i][j];
}
}
}
运行结果
这一环节我们跟着调试信息一步步地;来分析一下
接下去展示数据放入对应的桶中,这里我们通过视频来观看
接下去就是对每个桶中的数据进行排序
【时间复杂度】:O(K + N)
【空间复杂度】:O(K + N)
是的,你没有看错,除了【计数排序】之外,还有一个叫做【基数排序】的,不过它和计数排序可完全不同。它是桶排序的升级版
①第一轮(个位的比较)
②第二轮(十位的比较)
③第三轮(百位的比较)
#include
#define RADIX 10 //表示基数的个数
queue<int> qu[RADIX]; //定义桶(每个桶均为一个队列)
void RadixSort(int* a, int n)
{
//首先求出数组中的最大值
int max = GetMax(a, n);
//求出最大值的位数
int k = GetDigit(max);
//进行k次的数据分发和回收
for (int i = 0; i < k; ++i)
{
//分发数据
Distribute(a, n, i);
//回收数据
Collect(a);
}
}
求解数组中的最大值
//求解数组中的最大值
int GetMax(int* a, int n)
{
int max = a[0];
for (int i = 0; i < n; ++i)
{
if (a[i] > max)
max = a[i];
}
return max;
}
求解最大值的位数
//求解最大值的位数
int GetDigit(int num)
{
//num : 10000
int count = 0;
while (num > 0)
{
count++;
num /= 10;
}
return count;
}
获取位数位逻辑
//value: 789
// k: 0
int GetKey(int value, int k)
{
int key = 0;
while(k >= 0)
{
key = value % 10;
value /= 10;
k--;
}
return key;
}
分发数据逻辑
获取key之后,q[key]
即为每一个桶。使用push往里入数据
//分发数据
void Distribute(int* a, int n, int k)
{
for (int i = 0; i < n; ++i)
{
int key = GetKey(a[i], k);
qu[key].push(a[i]);
}
}
回收数据逻辑
接着一一获取每个桶中的头部数据,放回数组中,然后出队该数据即可
//回收数据
void Collect(int* a)
{
int index = 0;
for (int i = 0; i < RADIX; ++i)
{
while (!qu[i].empty())
{
a[index++] = qu[i].front();
qu[i].pop();
}
}
}
运行结果
【时间复杂度】:O(K * N)
【空间复杂度】:O(K + N)
排序这一块,除了对内部的数据进行排序,很多场合下还会对文件中的数据去进行一个排序,而对于文件外排序这一块,我们主要使用上面所学的归并排序来完成
将一个大文件平均分割成N份,保证每份的大小可以加载到内存中,然后使用快排将其排成有序再写回一个个小文件,此时就拥有了文件中归并的先决条件
这里我设置一个这样的规则,令文件1为【1】,文件2位【2】,它们归并之后即为【12】,然后再让【12】和文件3即【3】归并变成【123】,以此类推,所以最后归出的文件名应该是【12345678910】
下面是大文件分割成10个小文件的逻辑,首先来讲解一下这块,代码中很多内容涉及到文件操作,如果有文件操作还不是很懂的小伙伴记得再去温习一下
void MergeSortFile(const char* file)
{
FILE* fout = fopen(file, "r");
if (!fout)
{
perror("fopen fail");
exit(-1);
}
int num = 0;
int n = 10;
int i = 0;
int b[10];
char subfile[20];
int filei = 1;
//1.读取大文件,然后将其平均分成N份,加载到内存中后对每份进行排序,然后再写回小文件
memset(b, 0, sizeof(int) * n);
while (fscanf(fout, "%d\n", &num) != EOF)
{
if (i < n - 1)
{
b[i++] = num; //首先读9个数据到数组中
}
else
{
b[i] = num; //再将第十个输入放入数组
QuickSort(b, 0, n - 1); //对其进行排序
sprintf(subfile, "%d", filei++);
FILE* fin = fopen(subfile, "w");
if (!fin)
{
perror("fopen fail");
exit(-1);
}
//再进本轮排好序的10个数以单个小文件的形式写到工程文件下
for (int j = 0; j < n; ++j)
{
fprintf(fin, "%d\n", b[j]);
}
fclose(fin);
i = 0; //i重新置0,方便下一次的读取
memset(b, 0, sizeof(int) * n);
}
}
//文件归并逻辑
void _MergeSortFile(const char* file1, const char* file2, const char* mfile)
{
FILE* fout1 = fopen(file1, "r");
if (!fout1)
{
perror("fopen fail");
exit(-1);
}
FILE* fout2 = fopen(file2, "r");
if (!fout2)
{
perror("fopen fail");
exit(-1);
}
FILE* fin = fopen(mfile, "w");
if (!fin)
{
perror("fopen fail");
exit(-1);
}
int num1, num2;
//返回值拿到循环外来接受
int ret1 = fscanf(fout1, "%d\n", &num1);
int ret2 = fscanf(fout2, "%d\n", &num2);
while (ret1 != EOF && ret2 != EOF)
{
if (num1 < num2)
{
fprintf(fin, "%d\n", num1);
ret1 = fscanf(fout1, "%d\n", &num1);
}
else
{
fprintf(fin, "%d\n", num2);
ret2 = fscanf(fout2, "%d\n", &num2);
}
}
while (ret1 != EOF)
{
fprintf(fin, "%d\n", num1);
ret1 = fscanf(fout1, "%d\n", &num1);
}
while (ret2 != EOF)
{
fprintf(fin, "%d\n", num2);
ret2 = fscanf(fout2, "%d\n", &num2);
}
fclose(fout1);
fclose(fout2);
fclose(fin);
}
最后在打开文件后不要忘了将文件关闭哦,不然就白操作了
//利用互相归并到文件,实现整体有序
char file1[100] = "1";
char file2[100] = "2";
char mfile[100] = "12";
for (int i = 2; i <= n; ++i)
{
_MergeSortFile(file1, file2, mfile);
//迭代
strcpy(file1, mfile);
sprintf(file2, "%d", i + 1);
sprintf(mfile, "%s%d", mfile, i + 1);
}
整体代码展示
//文件归并逻辑
void _MergeSortFile(const char* file1, const char* file2, const char* mfile)
{
FILE* fout1 = fopen(file1, "r");
if (!fout1)
{
perror("fopen fail");
exit(-1);
}
FILE* fout2 = fopen(file2, "r");
if (!fout2)
{
perror("fopen fail");
exit(-1);
}
FILE* fin = fopen(mfile, "w");
if (!fin)
{
perror("fopen fail");
exit(-1);
}
int num1, num2;
//返回值拿到循环外来接受
int ret1 = fscanf(fout1, "%d\n", &num1);
int ret2 = fscanf(fout2, "%d\n", &num2);
while (ret1 != EOF && ret2 != EOF)
{
if (num1 < num2)
{
fprintf(fin, "%d\n", num1);
ret1 = fscanf(fout1, "%d\n", &num1);
}
else
{
fprintf(fin, "%d\n", num2);
ret2 = fscanf(fout2, "%d\n", &num2);
}
}
while (ret1 != EOF)
{
fprintf(fin, "%d\n", num1);
ret1 = fscanf(fout1, "%d\n", &num1);
}
while (ret2 != EOF)
{
fprintf(fin, "%d\n", num2);
ret2 = fscanf(fout2, "%d\n", &num2);
}
fclose(fout1);
fclose(fout2);
fclose(fin);
}
/*文件外排序*/
void MergeSortFile(const char* file)
{
srand((unsigned int)time(NULL));
FILE* fout = fopen(file, "r");
if (!fout)
{
perror("fopen fail");
exit(-1);
}
//先写100个随机数进文件
//for (int i = 0; i < 100; ++i)
//{
// int num = rand() % 100;
// fprintf(fout, "%d\n", num);
//}
int num = 0;
int n = 10;
int i = 0;
int b[10];
char subfile[20];
int filei = 1;
//1.读取大文件,然后将其平均分成N份,加载到内存中后对每份进行排序,然后再写回小文件
memset(b, 0, sizeof(int) * n);
while (fscanf(fout, "%d\n", &num) != EOF)
{
if (i < n - 1)
{
b[i++] = num; //首先读9个数据到数组中
}
else
{
b[i] = num; //再将第十个输入放入数组
QuickSort(b, 0, n - 1); //对其进行排序
sprintf(subfile, "%d", filei++);
FILE* fin = fopen(subfile, "w");
if (!fin)
{
perror("fopen fail");
exit(-1);
}
//再进本轮排好序的10个数以单个小文件的形式写到工程文件下
for (int j = 0; j < n; ++j)
{
fprintf(fin, "%d\n", b[j]);
}
fclose(fin);
i = 0; //i重新置0,方便下一次的读取
memset(b, 0, sizeof(int) * n);
}
}
//利用互相归并到文件,实现整体有序
char file1[100] = "1";
char file2[100] = "2";
char mfile[100] = "12";
for (int i = 2; i <= n; ++i)
{
_MergeSortFile(file1, file2, mfile);
//迭代
strcpy(file1, mfile);
sprintf(file2, "%d", i + 1);
sprintf(mfile, "%s%d", mfile, i + 1);
}
}
运行结果展示
本次的代码较以往都多了许多,因为是集结了所有的排序算法还有案例测试、性能测试相关的代码,都给到大家
sort.h
#pragma once
#include
#include
#include
#include
#include
#include
using namespace std;
/*直接插入排序*/
void InsertSort(int* a, int n);
/*希尔排序*/
void ShellSort(int* a, int n);
/*传统选择排序*/
void SelectedSort(int* a, int n);
/*简单选择排序*/
void SelectSort(int* a, int n);
/*堆排序*/
void HeapSort(int* a, int n);
/*冒泡排序*/
void BubbleSort(int* a, int n);
/*hoare版本(左右指针法)*/
int PartSort1(int* a, int begin, int end);
/*挖坑法*/
int PartSort2(int* a, int begin, int end);
/*前后指针法*/
int PartSort3(int* a, int begin, int end);
/*三数取中*/
int GetMid(int* a, int left, int right);
/*快速排序*/
void QuickSort(int* a, int begin, int end);
/*快速排序——三路划分法*/
void QuickSortThreeDivisioin(int* a, int begin, int end);
/*快速排序——非递归*/
void QuickSortNonR(int*, int begin, int end);
/*归并排序*/
void MergeSort(int* a, int n);
/*归并排序——非递归*/
void MergeSortNonR1(int* a, int n); //不修边界——部分拷贝
void MergeSortNonR2(int* a, int n); //修边界——整体拷贝
/*文件外排序*/
void MergeSortFile(const char* file);
/*计数排序*/
void CountSort(int* a, int n);
/*基数排序*/
void RadixSort(int* a, int n);
/*桶排序*/
void BucketSort(int* a, int n);
/*打印*/
void PrintArray(int* a, int n);
/*交换函数*/
void swap(int* x1, int* x2);
/*向下调整算法*/
void Adjust_Down(int* a, int n, int parent);
sort.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "sort.h"
#include "stack.h"
//#define K 3 //表示数字的位数
#define RADIX 10 //表示桶的个数(固定)
queue<int> qu[RADIX]; //定义基数(每个基数均为一个队列)
/*打印*/
void PrintArray(int* a, int n)
{
for (int i = 0; i < n; ++i)
{
printf("%d ", a[i]);
}
printf("\n");
}
/*交换*/
void swap(int* x, int* y)
{
int t = *x;
*x = *y;
*y = t;
}
/*直接插入排序*/
void InsertSort(int* a, int n)
{
//不可以< n,否则最后的位置落在n-1,tmp访问end[n]会造成越界
for (int i = 0; i < n - 1; ++i)
{
int end = i;
int tmp = a[end + 1]; //将end后的位置先行保存起来
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end]; //比待插值来得大的均往后移动
end--; //end前移
}
else
{
break; //若是发现有相同的或者小于带插值的元素,则停下,跳出循环
}
}
a[end + 1] = tmp; //将end + 1的位置放入保存的tmp值
}
}
//void ShellSort(int* a, int n)
//{
// int gap = n / 2;
//
// for (int j = 0; j < gap; ++j)
// {
// gap /= 2;
// for (int i = j; i < n - gap; i += gap)
// { //一组一组走
// int end = i;
// int tmp = a[end + gap];
// while (end >= 0)
// {
// if (tmp < a[end])
// {
// a[end + gap] = a[end];
// end -= gap;
// }
// else
// {
// break;
// }
// }
// a[end + gap] = tmp;
// }
// }
//}
/*希尔排序*/
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
/*
* gap > 1 —— 预排序
* gap == 1 —— 直接插入排序
*/
//gap /= 2;
gap = gap / 3 + 1; //保证最后的gap值为1,为直接插入排序
for (int i = 0; i < n - gap; i++)
{ //一位一位走
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
PrintArray(a, n);
}
}
/*传统选择排序*/
void SelectedSort(int* a, int n)
{
for (int i = 0; i < n - 1; ++i)
{
int k = i;
for (int j = i + 1; j < n; ++j)
{
if (a[j] < a[k]) k = j;
}
if (a[k] != a[i])
{
swap(&a[k], &a[i]);
}
}
}
/*简单选择排序*/
void SelectSort(int* a, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int mini = begin;
int maxi = begin;
for (int i = begin + 1; i <= end; ++i)
{
if (a[i] < a[mini])
mini = i;
if (a[i] > a[maxi])
maxi = i;
}
swap(&a[begin], &a[mini]);
if (maxi == begin) //若是最大值和begin重合了,则重置一下交换后的最大值
maxi = mini;
swap(&a[end], &a[maxi]);
begin++;
end--;
}
}
/*向下调整算法*/
void Adjust_Down(int* a, int n, int parent)
{
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 HeapSort(int* a, int n)
{
//建立大根堆(倒数第一个非叶子结点)
for (int i = ((n - 1) - 1) / 2; i >= 0; --i)
{
Adjust_Down(a, n, i);
}
int end = n - 1;
while (end > 0)
{
swap(&a[0], &a[end]); //首先交换堆顶结点和堆底末梢结点
Adjust_Down(a, end, 0); //一一向前调整
end--;
}
}
/*冒泡排序*/
void BubbleSort(int* a, int n)
{
//[0,n - 1)
for (int i = 0; i < n - 1; ++i)
{
int changed = 0;
for (int j = 0; j < n - 1 - i; ++j)
{
if (a[j] > a[j + 1])
{
swap(&a[j], &a[j + 1]);
changed = 1;
}
}
if (changed == 0)
break;
//PrintArray(a, n);
}
//[1,n)
//for (int i = 1; i < n; ++i)
//{
// for (int j = 0; j < n - i; ++j)
// {
// if (a[j] > a[j + 1])
// swap(&a[j], &a[j + 1]);
// }
//}
}
/*三数取中*/
int GetMid(int* a, int left, int right)
{
/*
* 不是取中间的那个值,而是取三个数中不是最大也不是最小的那个
* --->进来可能是一个随机或有序序列,保证key最大或者最小就行
*/
int mid = (left + right) >> 1;
//int mid = left + rand() % (right - left);
if (a[mid] > a[left])
{
if (a[mid] < a[right])
{ //left mid right
return mid;
}
//另外两种mid一定是最大的,比较left和right
else if (a[left] > a[right])
{ //right left mid
return left;
}
else
{ //left right mid
return right;
}
}
else //a[left] >= a[mid]
{
if (a[mid] > a[right])
{ //right mid left
return mid;
}
//另外两种mid一定是最小的,比较left和right
else if (a[left] < a[right])
{ //mid left right
return left;
}
else
{ //mid right left
return right;
}
}
}
/*快速排序 —— hoare版本(左右指针法)*/
int PartSort1(int* a, int begin, int end)
{
/*
* 找小 —— 是找真的比我小的
* 找大 —— 是找真的比我大的
* --->相等均要略过
*
* 还要防止越界【left < right】
*/
//三数取中
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]); //交换begin上的值和找出的中间值
int left = begin;
int right = end;
int keyi = begin; //以最左边作为对照值记录,右边先走
while (left < right)
{
//右边找比keyi所在位置小的值,若是 >= 则忽略(加上=防止死循环)
while (left < right && a[right] >= a[keyi])
{ //left < right —— 防止特殊情况越界
right--;
}
//左边找比keyi所在位置大的值,若是 <= 则忽略
while (left < right && a[left] <= a[keyi])
{
left++;
}
//都找到了,则交换
swap(&a[left], &a[right]);
}
//最后当left和right相遇的时候将相遇位置的值与keyi位置的值交换
swap(&a[left], &a[keyi]);
keyi = left; //因keyi位置的值被交换到相遇点,因此更新准备分化递归
return keyi;
}
/*快速排序 —— 挖坑法*/
int PartSort2(int* a, int begin, int end)
{
//三数取中
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]);
int left = begin, right = end;
int hole = left; //坑位
int key = a[hole]; //记录坑位上的值
while (left < right)
{
//右边找小,放入坑位,然后更新坑位
while (left < right && a[right] >= key)
{
right--;
}
a[hole] = a[right];
hole = right;
//左边找大,放入坑位,然后更新坑位
while (left < right && a[left] <= key)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
/*快速排序 —— 前后指针法*/
int PartSort3(int* a, int begin, int end)
{
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]);
int prev = begin;
int cur = prev + 1;
int keyi = prev;
while (cur <= end)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
swap(&a[prev], &a[cur]);
}
cur++; //cur无论如何都要++,因此直接写在外面
}
//cur超出右边界后交换prev处的值和key
swap(&a[keyi], &a[prev]);
return prev;
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return; //最后递归下去区间不存在了,进行递归回调
//小区间优化
if ((end - begin + 1) < 15)
{
//在数据量少的时候改用直接插入排序
InsertSort(a + begin, end - begin + 1);
}
//int key = PartSort1(a, begin, end); //左右指针法
//int key = PartSort2(a, begin, end); //挖坑法
int key = PartSort3(a, begin, end); //前后指针法
//左右区间分化递归
QuickSort(a, begin, key - 1);
QuickSort(a, key + 1, end);
}
/*快速排序——三路划分法*/
void QuickSortThreeDivisioin(int* a, int begin, int end)
{
if (begin >= end)
return; //最后递归下去区间不存在了,进行递归回调
//小区间优化
if ((end - begin + 1) < 15)
{
//在数据量少的时候改用直接插入排序
InsertSort(a + begin, end - begin + 1);
}
else
{
//三数取中
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]);
int key = a[begin];
int left = begin;
int cur = left + 1;
int right = end;
while (cur <= right)
{
if (a[cur] < key)
{
swap(&a[left++], &a[cur++]);
}
else if (a[cur] > key)
{
swap(&a[cur], &a[right--]);
//此时cur不变化是因为从right换到中间的值可能还是比key大,为了在下一次继续进行比较
}
else
{
cur++;
}
}
//[begin, left - 1][left, right][right + 1, end]
QuickSortThreeDivisioin(a, begin, left - 1);
QuickSortThreeDivisioin(a, right + 1, end);
}
}
/*快速排序——非递归*/
void QuickSortNonR(int* a, int begin, int end)
{
ST st;
InitStack(&st);
//首先将整体区间入栈
PushStack(&st, begin);
PushStack(&st, end);
while (!StackEmpty(&st))
{
//出栈分别获取右左两个端点
int right = StackTop(&st);
PopStack(&st);
int left = StackTop(&st);
PopStack(&st);
//求解keyi的位置
int keyi = PartSort3(a, left, right);
//先入右
if (keyi + 1 < right)
{ //若是区间的值 > 1,则继续入栈
PushStack(&st, keyi + 1);
PushStack(&st, right);
}
//后入左
if (left < keyi - 1)
{ //若是区间的值 > 1,则继续入栈
PushStack(&st, left);
PushStack(&st, keyi - 1);
}
}
DestroyStack(&st);
}
void _MergeSort(int* a, int begin, int end, int* tmp)
{
//递归出口
if (begin >= end)
{
return;
}
int mid = (begin + end) >> 1;
/*
* [begin, mid][mid + 1, end]
* --->继续进行子区间归并,相当于后序遍历【左,右,根】
*/
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
int i = begin; //i要从每次递归进来的begin开始放,可能是在右半区间
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
//若是还有区间存在数据,表示没有归并完全,直接放入tmp即可
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//最后将归并完后后的数据拷贝回原数组
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
/*归并排序*/
//时间复杂度:O(NlogN)
//空间复杂度:O(N)
void MergeSort(int* a, int n)
{
//开辟空间,存放归并后的数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("fail malloc");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp); //防止内存泄漏
tmp = NULL; //防止野指针
}
/*归并排序——非递归*/
void MergeSortNonR1(int* a, int n)
{
//开辟空间,存放归并后的数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("fail malloc");
exit(-1);
}
int rangeN = 1;
while (rangeN < n)
{
for (int i = 0; i < n; i += 2 * rangeN)
{
int begin1 = i, end1 = i + rangeN - 1;
int begin2 = i + rangeN, end2 = i + 2 * rangeN - 1;
printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
int j = i; //表示tmp每次都从上一次归并完放置后的地方开始
/*
* 处理越界的情况
*/
if (end1 >= n) {
break;
}
else if (begin2 >= n) {
break;
}
else if (end2 >= n) {
end2 = n - 1; //end2越界要修正一下,不可以break,因为最后需要求整个数组的长度
}
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
//若是还有区间存在数据,表示没有归并完全,直接放入tmp即可
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
//归并完一小组,拷贝一小组
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
// + i表示每次归并的组在发生变化 //因为end是最后落的位置,i是初始化位置,不会改变
}
rangeN *= 2;
}
free(tmp); //防止内存泄漏
tmp = NULL; //防止野指针
}
void MergeSortNonR2(int* a, int n)
{
//开辟空间,存放归并后的数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("fail malloc");
exit(-1);
}
int rangeN = 1;
while (rangeN < n)
{
for (int i = 0; i < n; i += 2 * rangeN)
{
int begin1 = i, end1 = i + rangeN - 1;
int begin2 = i + rangeN, end2 = i + 2 * rangeN - 1;
int j = i; //表示tmp每次都从上一次归并完放置后的地方开始
printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
/*
* 处理越界的情况
*/
if (end1 >= n) {
end1 = n - 1;
begin2 = n; //begin2要比end2来的大,才不构成区间
end2 = n - 1;
}
else if (begin2 >= n) {
begin2 = n; //begin2要比end2来的大,才不构成区间
end2 = n - 1;
}
else if (end2 >= n) {
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
//若是还有区间存在数据,表示没有归并完全,直接放入tmp即可
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
}
//整组中的所有小组都归并完了,一起拷贝回去
memcpy(a, tmp, sizeof(int) * n);
rangeN *= 2;
}
free(tmp); //防止内存泄漏
tmp = NULL; //防止野指针
}
/*计数排序*/
void CountSort(int* a, int n)
{
//1.找出数组中的最大值和最小值,然后开辟统计数组
int min = a[0];
int max = a[0];
for (int i = 0; i < n; ++i)
{
if (a[i] > max)
max = a[i];
if (a[i] < min)
min = a[i];
}
int range = max - min + 1; //数据范围
int* CountArray = (int*)malloc(sizeof(int) * range);
memset(CountArray, 0, sizeof(int) * range); //初始化数组均为0
//2.统计数组中每一个数出现的个数,映射到CountArray数组中
for (int i = 0; i < n; ++i)
{
CountArray[a[i] - min]++;
//a[i] - min 表示找出相对位置
}
//3.将统计数组中的数写回原数组中,进行排序
int index = 0;
for (int j = 0; j < range; ++j)
{
//根据统计数组去找出每个数要写回几次
while (CountArray[j]--)
{
a[index++] = j + min;
//每次循环中的j不会发生变化,加上最小值可以找回原来的数字
}
}
}
//获取当前数字的待判位数【个、十、百】
//value: 789
// k: 0
int GetKey(int value, int k)
{
int key = 0;
while(k >= 0)
{
key = value % 10;
value /= 10;
k--;
}
return key;
}
//分发数据
void Distribute(int* a, int n, int k)
{
for (int i = 0; i < n; ++i)
{
int key = GetKey(a[i], k);
qu[key].push(a[i]);
}
}
//回收数据
void Collect(int* a)
{
int index = 0;
for (int i = 0; i < RADIX; ++i)
{
while (!qu[i].empty())
{
a[index++] = qu[i].front();
qu[i].pop();
}
}
}
//求解数组中的最大值
int GetMax(int* a, int n)
{
int max = a[0];
for (int i = 0; i < n; ++i)
{
if (a[i] > max)
max = a[i];
}
return max;
}
//求解最大值的位数
int GetDigit(int num)
{
//num : 10000
int count = 0;
while (num > 0)
{
count++;
num /= 10;
}
return count;
}
/*基数排序*/
void RadixSort(int* a, int n)
{
//首先求出数组中的最大值
int max = GetMax(a, n);
//求出最大值的位数
int k = GetDigit(max);
//进行K次的数据分发和回收
for (int i = 0; i < k; ++i)
{
//分发数据
Distribute(a, n, i);
//回收数据
Collect(a);
}
}
/*桶排序*/
void BucketSort(int* a, int n)
{
int bucket[5][5]; // 分配五个桶。
int bucketsize[5]; // 每个桶中元素个数的计数器。
// 初始化桶和桶计数器。
memset(bucket, 0, sizeof(bucket));
memset(bucketsize, 0, sizeof(bucketsize));
// 把数组a的数据按照范围放入对应桶中
for (int i = 0; i < n; ++i)
{
bucket[a[i] / 10][bucketsize[a[i] / 10]++] = a[i];
}
// 分别对每个桶中的数据进行排序
for (int i = 0; i < 5; ++i)
{
QuickSort(bucket[i], 0, bucketsize[i] - 1);
}
// 将把每个桶中的数据依次放回数组a中
int index = 0;
for (int i = 0; i < 5; ++i)
{
for (int j = 0; j < bucketsize[i]; ++j)
{
a[index++] = bucket[i][j];
}
}
}
//文件归并逻辑
void _MergeSortFile(const char* file1, const char* file2, const char* mfile)
{
FILE* fout1 = fopen(file1, "r");
if (!fout1)
{
perror("fopen fail");
exit(-1);
}
FILE* fout2 = fopen(file2, "r");
if (!fout2)
{
perror("fopen fail");
exit(-1);
}
FILE* fin = fopen(mfile, "w");
if (!fin)
{
perror("fopen fail");
exit(-1);
}
int num1, num2;
//返回值拿到循环外来接受
int ret1 = fscanf(fout1, "%d\n", &num1);
int ret2 = fscanf(fout2, "%d\n", &num2);
while (ret1 != EOF && ret2 != EOF)
{
if (num1 < num2)
{
fprintf(fin, "%d\n", num1);
ret1 = fscanf(fout1, "%d\n", &num1);
}
else
{
fprintf(fin, "%d\n", num2);
ret2 = fscanf(fout2, "%d\n", &num2);
}
}
while (ret1 != EOF)
{
fprintf(fin, "%d\n", num1);
ret1 = fscanf(fout1, "%d\n", &num1);
}
while (ret2 != EOF)
{
fprintf(fin, "%d\n", num2);
ret2 = fscanf(fout2, "%d\n", &num2);
}
fclose(fout1);
fclose(fout2);
fclose(fin);
}
/*文件外排序*/
void MergeSortFile(const char* file)
{
srand((unsigned int)time(NULL));
FILE* fout = fopen(file, "r");
if (!fout)
{
perror("fopen fail");
exit(-1);
}
//先写100个随机数进文件
//for (int i = 0; i < 100; ++i)
//{
// int num = rand() % 100;
// fprintf(fout, "%d\n", num);
//}
int num = 0;
int n = 10;
int i = 0;
int b[10];
char subfile[20];
int filei = 1;
//1.读取大文件,然后将其平均分成N份,加载到内存中后对每份进行排序,然后再写回小文件
memset(b, 0, sizeof(int) * n);
while (fscanf(fout, "%d\n", &num) != EOF)
{
if (i < n - 1)
{
b[i++] = num; //首先读9个数据到数组中
}
else
{
b[i] = num; //再将第十个输入放入数组
QuickSort(b, 0, n - 1); //对其进行排序
sprintf(subfile, "%d", filei++);
FILE* fin = fopen(subfile, "w");
if (!fin)
{
perror("fopen fail");
exit(-1);
}
//再进本轮排好序的10个数以单个小文件的形式写到工程文件下
for (int j = 0; j < n; ++j)
{
fprintf(fin, "%d\n", b[j]);
}
fclose(fin);
i = 0; //i重新置0,方便下一次的读取
memset(b, 0, sizeof(int) * n);
}
}
//利用互相归并到文件,实现整体有序
char file1[100] = "1";
char file2[100] = "2";
char mfile[100] = "12";
for (int i = 2; i <= n; ++i)
{
_MergeSortFile(file1, file2, mfile);
//迭代
strcpy(file1, mfile);
sprintf(file2, "%d", i + 1);
sprintf(mfile, "%s%d", mfile, i + 1);
}
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "sort.h"
void TestInsertSort()
{
int a[] = { 3,5,9,1,7,4,2,10,6,8 };
int sz = sizeof(a) / sizeof(int);
InsertSort(a, sz);
PrintArray(a, sz);
}
void TestShellSort()
{
int a[] = { 9,5,3,1,7,4,2,10,6,8 };
int sz = sizeof(a) / sizeof(int);
PrintArray(a, sz);
ShellSort(a, sz);
PrintArray(a, sz);
}
void TestSelectSort()
{
int a[] = { 3,5,9,1,7,4,2,10,6,8 };
int a2[] = { 10,5,9,1,7,4,2,3,6,8 };
int sz = sizeof(a) / sizeof(int);
int sz2 = sizeof(a2) / sizeof(int);
//SelectedSort(a, sz);
SelectSort(a, sz);
PrintArray(a, sz);
SelectSort(a2, sz2);
PrintArray(a2, sz2);
}
void TestHeapSort()
{
int a[] = { 3,5,9,1,7,4,2,10,6,8 };
int sz = sizeof(a) / sizeof(int);
HeapSort(a, sz);
PrintArray(a, sz);
}
void TestBubbleSort()
{
int a[] = { 3,5,9,1,7,4,2,10,6,8 };
int sz = sizeof(a) / sizeof(int);
BubbleSort(a, sz);
PrintArray(a, sz);
}
void TestQuickSort()
{
//int a[] = { 6,1,2,7,9,3,4,5,10,8 };
//int a[] = { 6,1,2,7,9,6,3,6,4,5,6,10,8,6 };
int a[] = { 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2 };
//int a[] = { 0 };
int sz = sizeof(a) / sizeof(int);
QuickSort(a, 0, sz - 1); //递归
//QuickSortNonR(a, 0, sz - 1); //非递归
//QuickSortThreeDivisioin(a, 0, sz - 1);
PrintArray(a, sz);
}
void TestMergeSort()
{
int a[] = { 3,5,9,1,7,4,2,10,6,8 };
//int a[] = { 10,6,7,1,3,9,4,2 };
int sz = sizeof(a) / sizeof(int);
//MergeSort(a, sz);
//MergeSortNonR1(a, sz);
MergeSortNonR2(a, sz);
PrintArray(a, sz);
}
void TestCountSort()
{
//int a[] = {3,2,5,0,3,2,0,3};
int a[] = {1000, 1001, 1003, 1001};
int sz = sizeof(a) / sizeof(int);
PrintArray(a, sz);
CountSort(a, sz);
PrintArray(a, sz);
}
void TestBucketSort()
{
int a[] = { 32,16,21,1,39,9,7,44,25,48 };
int sz = sizeof(a) / sizeof(int);
PrintArray(a, sz);
BucketSort(a, sz);
PrintArray(a, sz);
}
void TestRadixSort()
{
//int a[] = { 789,159,357,14,2,590,447,123,1};
//int a[] = { 789,159,357,14,2,590,447,123,1,4444};
int a[] = { 78999,159,357,14,2,590,447,123,1,4444};
int sz = sizeof(a) / sizeof(int);
RadixSort(a, sz);
PrintArray(a, sz);
}
// 测试排序的性能对比
void TestOP()
{
srand(time(0));
const int N = 10000;
int* a1 = (int*)malloc(sizeof(int) * N);
int* a2 = (int*)malloc(sizeof(int) * N);
int* a3 = (int*)malloc(sizeof(int) * N);
int* a4 = (int*)malloc(sizeof(int) * N);
int* a5 = (int*)malloc(sizeof(int) * N);
int* a6 = (int*)malloc(sizeof(int) * N);
int* a7 = (int*)malloc(sizeof(int) * N);
for (int i = 0; i < N; ++i)
{
a1[i] = rand();
//a1[i] = i;
a2[i] = a1[i];
a3[i] = a1[i];
a4[i] = a1[i];
a5[i] = a1[i];
a6[i] = a1[i];
a7[i] = a1[i];
}
//int j = 0; //统计放入了几个随机值
//for (int i = 0; i < N; ++i)
//{
// int x = rand();
// if (x % 7 == 0 && x % 3 == 0)
// {
// a1[i] = x; //在随机的位置放入这个随机数
// ++j; //插入多少随机数的累加
// }
// else
// {
// a1[i] = i; //表示整体是有序的
// }
// a2[i] = a1[i];
// a3[i] = a1[i];
// a4[i] = a1[i];
// a5[i] = a1[i];
// a6[i] = a1[i];
// a7[i] = a1[i];
//}
//printf("%d\n", j);
int begin1 = clock();
//InsertSort(a1, N); //直接插入排序
int end1 = clock();
int begin2 = clock();
ShellSort(a2, N); //希尔排序
int end2 = clock();
int begin3 = clock();
//SelectSort(a3, N); //选择排序
int end3 = clock();
int begin4 = clock();
HeapSort(a4, N); //堆排序
int end4 = clock();
int begin5 = clock();
//BubbleSort(a5, N); //冒泡排序
int end5 = clock();
int begin6 = clock();
QuickSort(a5, 0, N - 1); //快速排序
int end6 = clock();
int begin7 = clock();
//(a7, N); //归并排序
int end7 = clock();
printf("InsertSort:%d\n", end1 - begin1);
printf("ShellSort:%d\n", end2 - begin2);
printf("SelectSort:%d\n", end3 - begin3);
printf("HeapSort:%d\n", end4 - begin4);
printf("BubbleSort:%d\n", end5 - begin5);
printf("QuickSort:%d\n", end6 - begin6);
//printf("MergeSort:%d\n", end7 - begin7);
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
free(a6);
}
int main(void)
{
//TestInsertSort();
//TestShellSort();
//TestSelectSort();
//TestHeapSort();
//TestBubbleSort();
//TestQuickSort();
//TestMergeSort();
//TestCountSort();
//TestBucketSort();
//TestRadixSort();
MergeSortFile("SortData.txt");
//TestOP();
return 0;
}
有关排序相关的OJ,LeetCode上的很多题目都可以穿插进去,但是比较有代表性的还是这一道,可以直接测试你的排序代码写的是否有问题
原题传送门
超时通不过的几个O(N2)排序
整体通过率
首先我们来聊聊排序的【稳定性】,然后再把我们上面所讲的排序总结一下
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的【来源于百度百科】
接下去我们对各大排序的算法性能做一个测试
不要问为什么少了一个,那就是桶排序,因为随着数据量的增大,无法确定桶的数量以及平均分配每个桶中的数据个数,所以桶排序只适合于小型数据的场合,它已退出本趴的聊天
首先我们来看看数据无序的场景
接下去我们来看看数据有序的场景
看到上面的的测试中,计数排序总是那么能打,现在我们遏制一下它,增大这个range范围
最后,我们来总结和回顾一下本文学习的所有内容
本文,我们对十大排序算法进行了一个总结和回顾,分别从它们的整体过程、算法思路、代码分析、复杂度分析这几块做了一个了解
[直接插入排序]:使用得比较广泛,虽属于O(N2)的排序算法,但是在数据接近有序时能体现出优势,需要掌握
[希尔排序]:看起来不起眼,但是性能不错,平均时间复杂度也能达到O(NlogN),至于O(N1.3)了解一下即可,主要还是记住它排序的这个过程
[选择排序]:如果能想得起其他排序算法,就不要用这个了,在各种场合测试下它都是最差劲的,无论如何都是在选数然后交换的过程
[堆排序]:性能不错,重点掌握的是【向下调整算法】以及如何建堆的过程,在数据量特大的情况下可以使用
[冒泡排序]:大学生最喜欢用排序算法,性能不高,只有在序列已然有序或者接近有序的情况下才能展现出优势
[快速排序]:综合性能最优,包含【左右指针法】【挖坑法】【前后指针法】【三路划分法】,学有余力都应掌握,重点在理解Hoare版本的思路。对于要参加校招的同学还要求掌握非递归的写法
[归并排序]:即使内排序也是排序,性能较高又可以达到稳定,常用于文件外排序。也有递归和非递归两种写法,最好是都要掌握
[计数排序]:唯一一个可能达到O(N)时间复杂度的排序算法,若是序列中没有很极端的数据出现,那用它还是不错的
[桶排序]:对于桶和桶中数据的分配在数据量增大时候很难分配,总体性能也不是很优,需要很强的代码控制能力
[基数排序]:属于桶排序的一种,校招不考,原理思路以及数据的划分过程可以了解一下,很是巧妙,不过平常用得也不是很广泛
最后还对所有排序的整体做了一个稳定性和性能方面的测试,也很好地帮助大家对排序算法更上一层,文章中图示均是使用电脑自带的画图完成的,如有需要可以私信我
完结撒花
<参考文献资料>
以下是我在写这篇文章时参考的资料和内容,尊重作者,附上链接
1、排序的概念详解—— 科普中国
2、快速排序(Quicksort)—— 百度百科
3、十大经典排序算法详解(三)-堆排序,计数排序,桶排序,基数排序
4、排序算法之 计数排序 及其时间复杂度和空间复杂度
5、基数排序(详细图解)
6、八大排序算法(C语言实现)
7、【数据结构从青铜到王者】第九篇:数据结构之排序
8、常见排序算法详解:插入,冒泡,希尔,选择,快速排序,归并,计数排序,堆排序(已完结)
9、【数据结构初阶】八大排序算法+时空复杂度
10、排序算法稳定性 —— 百度百科
最后非常感谢您对本文的阅读,如果觉得写的还可以,非常希望得到您的三连支持。如有疑问请于评论区留言或者私信我