文章目录
- 排序概念
- 常见的排序算法
- 常见排序算法的实现
- 直接插入排序
- 希尔排序
- 选择排序
- 堆排序
- 冒泡排序
- 快速排序
- 递归实现
- Hoare版本
- 挖坑法
- 前后指针法
- 非递归实现
- Hoare版本
- 挖坑法
- 前后指针法
- 快速排序俩个优化
- 归并排序
- 递归实现
- 非递归实现
- 外排序
- 计数排序
- 常见排序算法的性能分析
排序概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
注:这部分主要是内部排序。排序讲解都以升序为例。
常见的排序算法
常见排序算法的实现
直接插入排序
(点击此处跳转,更加详细的讲解过程,干货满满!)
动图展示:
插入排序,又叫直接插入排序。实际中,我们玩扑克牌的时候,就用了插入排序的思想。
基本思想:
在待排序的元素中,假设前n-1个元素已有序,现将第n个元素插入到前面已经排好的序列中,使得前n个元素有序。按照此法对所有元素进行插入,直到整个序列有序。
但我们并不能确定待排元素中究竟哪一部分是有序的,所以我们一开始只能认为第一个元素是有序的,依次将其后面的元素插入到这个有序序列中来,直到整个序列有序为止。
代码示例:
//插入排序
void InsertSort(int* a, int n)
{
int i = 0;
for (i = 0; i < n - 1; i++)
{
int end = i;//记录有序序列的最后一个元素的下标
int tmp = a[end + 1];//待插入的元素
while (end >= 0)
{
if (tmp < a[end])//还需继续比较
{
a[end + 1] = a[end];
end--;
}
else//找到应插入的位置
{
break;
}
}
a[end + 1] = tmp;
//代码执行到此位置有两种情况:
//1.待插入元素找到应插入位置(break跳出循环到此)。
//2.待插入元素比当前有序序列中的所有元素都小(while循环结束后到此)。
}
}
直接插入排序的特性总结:
1>元素集合越接近有序,直接插入排序算法的时间效率越高
2>时间复杂度:O(N^2)
3>空间复杂度:O(1),它是一种稳定的排序算法
4>稳定性:稳定
希尔排序
(点击此处跳转,更加详细的讲解过程,干货满满!)
动图展示:
希尔排序法又称缩小增量法。
基本思想:
先选定一个整数gap,把待排序文件中所有记录分成gap个组,所有距离为gap的记录分在同一组内,并对每一组内的元素进行排序。
然后将gap逐渐减小重复上述分组和排序的工作。
当到达gap=1时,所有元素在统一组内排好序。
静态图展示:
该题中,前两趟就是希尔排序的预排序,最后一趟就是希尔排序的直接插入排序。
代码示例:
//希尔排序
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 2;//gap折半
int i = 0;
//进行一趟排序
for (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;
}
}
}
希尔排序的特性总结:
1>希尔排序是对直接插入排序的优化。
2>当gap > 1时都是预排序,目的是让数组更接近于有序。当gap ==
1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。
3>希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,这里不深究。
4>时间复杂度O(N^1.5)
5>空间复杂度O(1)
6>稳定性:不稳定。
选择排序
(点击此处跳转,更加详细的讲解过程,干货满满)
动图展示:
基本思想:
第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始(末尾)位置,
然后选出次小(或次大)的一个元素,存放在最大(最小)元素的下一个位置,
重复这样的步骤直到全部待排序的数据元素排完 。
代码示例1:
//选择排序(一次选一个数)
void SelectSort(int* a, int n)
{
int i = 0;
for (i = 0; i < n; i++)//i代表参与该趟选择排序的第一个元素的下标
{
int start = i;
int min = start;//记录最小元素的下标
while (start < n)
{
if (a[start] < a[min])
min = start;//最小值的下标更新
start++;
}
Swap(&a[i], &a[min]);//最小值与参与该趟选择排序的第一个元素交换位置
}
}
代码示例2:
实际上,我们可以一趟选出两个值,一个最大值一个最小值,然后将其放在序列开头和末尾,这样可以使选择排序的效率快一倍。
//选择排序(一次选两个数)
void SelectSort(int* a, int n)
{
int left = 0;//记录参与该趟选择排序的第一个元素的下标
int right = n - 1;//记录参与该趟选择排序的最后一个元素的下标
while (left < right)
{
int minIndex = left;//记录最小元素的下标
int maxIndex = left;//记录最大元素的下标
int i = 0;
//找出最大值及最小值的下标
for (i = left; i <= right; i++)
{
if (a[i] < a[minIndex])
minIndex = i;
if (a[i]>a[maxIndex])
maxIndex = i;
}
//将最大值和最小值放在序列开头和末尾
Swap(&a[minIndex], &a[left]);
if (left == maxIndex)
{
maxIndex = minIndex;//防止最大值位于序列开头,被最小值交换
}
Swap(&a[maxIndex], &a[right]);
left++;
right--;
}
}
注意:
在进行最小值和最大值同时交换时也会出现一个问题,
如果最大值在起始位置的时候,交换了最小值之后,最大值就被交换到了min的位置,
如果继续交换max,就会将最小值交换到末尾位置。
所以,在每次交换了最小值之后应该判断一下最大值是否在起始位置,如果在需要将max赋值为min。
直接选择排序的特性总结:
1>直接选择排序思考非常好理解,但是效率不是很好(不论数组是否有序都会执行原步骤)。实际中很少使用
2>时间复杂度:O(N^2)
3>空间复杂度:O(1)
4>稳定性:不稳定
堆排序
(点击此处跳转,更加详细的讲解过程,干货满满)
动图展示:
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
-
建堆
升序:建大堆
降序:建小堆 -
利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。这里以升序为例:
1.首先应该建一个大堆,不能直接使用堆来实现。可以将需要排序的数组看作是一个堆,但需要将数组结构变成堆。
2.我们可以从堆从下往上的第二行最右边开始依次向下调整直到调整到堆顶,这样就可以将数组调整成一个堆,且如果建立的是大堆,堆顶元素为最大值。
3.然后按照堆删的思想将堆顶和堆底的数据交换,但不同的是这里不删除最后一个元素
4.这样最大元素就在最后一个位置,然后从堆顶向下调整到倒数第二个元素,这样次大的元素就在堆顶,重复上述步骤直到只剩堆顶时停止。
代码示例:
//堆排序
void HeapSort(int* a, int n)
{
//排升序,建大堆
//从第一个非叶子结点开始向下调整,一直到根
int i = 0;
for (i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;//记录堆的最后一个数据的下标
while (end)
{
Swap(&a[0], &a[end]);//将堆顶的数据和堆的最后一个数据交换
AdjustDown(a, end, 0);//对根进行一次向下调整
end--;//堆的最后一个数据的下标减一
}
}
直接选择排序的特性总结:
1>堆排序使用堆来选数,效率就高了很多。
2>时间复杂度:O(N*logN)
3>空间复杂度:O(1)
4>稳定性:不稳定
冒泡排序
(点击此处跳转,更加详细的讲解过程,干货满满)
动图展示:
冒泡排序应该是我们最熟悉的排序了,在C语言阶段我们就学习了冒泡排序。
他的思想也非常简单:
两两元素相比,前一个比后一个大就交换,直到将最大的元素交换到末尾位置。这是第一趟
一共进行n-1趟这样的交换将可以把所有的元素排好。
( n-1趟是因为只剩两个元素时只需要一趟就可以完成)
代码示例:
//冒泡排序
void BubbleSort(int* a, int n)
{
int end = 0;
for (end = n - 1; end >= 0; end--)
{
int exchange = 0;//记录该趟冒泡排序是否进行过交换
int i = 0;
for (i = 0; i < end; i++)
{
if (a[i]>a[i + 1])
{
Swap(&a[i], &a[i + 1]);
exchange = 1;
}
}
if (exchange == 0)//该趟冒泡排序没有进行过交换,已有序
break;
}
}
冒泡排序的特性总结:
1>冒泡排序是一种非常容易理解的排序
2>时间复杂度:O(N^2)
3>空间复杂度:O(1)
4>稳定性:稳定
快速排序
(点击此处跳转,更加详细的讲解过程,干货满满)
这里是排序算法的重点了,非常重要!
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,
其基本思想为:
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
递归实现
Hoare版本
动图展示:
具体思路是:
1.选定一个基准值,最好选定最左边或者最右边,选中间会给自己找麻烦。
2.确定两个指针left 和right 分别从左边和右边向中间遍历数组。
3.如果选最右边为基准值,那么left指针先走,如果遇到大于基准值的数就停下来。
4.然后右边的指针再走,遇到小于基准值的数就停下来。
5.交换left和right指针对应位置的值。
6.重复以上步骤,直到left = right ,最后将基准值与left(right)位置的值交换。
这样基准值左边的所有数都比他小,而他右边的数都比他大,从而他所在的位置就是排序后的正确位置。
之后再递归排以基准值为界限的左右两个区间中的数,当区间中没有元素时,排序完成。
代码展示:
//快速排序(Hoare版本)
void QuickSort1(int* a, int begin, int end)
{
if (begin >= end)//当只有一个数据或是序列不存在时,不需要进行操作
return;
int left = begin;//L
int right = end;//R
int keyi = left;//key的下标
while (left < right)
{
//right先走,找小
while (left < right&&a[right] >= a[keyi])
{
right--;
}
//left后走,找大
while (left < right&&a[left] <= a[keyi])
{
left++;
}
if (left < right)//交换left和right的值
{
Swap(&a[left], &a[right]);
}
}
int meeti = left;//L和R的相遇点
Swap(&a[keyi], &a[meeti]);//交换key和相遇点的值
QuickSort1(a, begin, meeti - 1);//key的左序列进行此操作
QuickSort1(a, meeti + 1, end);//key的右序列进行此操作
}
挖坑法
动图展示:
具体思路是:
1.先将选定的基准值(最左边)直接取出,然后留下一个坑,
2.当右指针遇到小于基准值的数时,直接将该值放入坑中,而右指针指向的位置形成新的坑位,
3.然后左指针遇到大于基准值的数时,将该值放入坑中,左指针指向的位置形成坑位,
4.重复该步骤,直到左右指针相等。最后将基准值放入坑位之中。
代码展示:
//快速排序(挖坑法)
void QuickSort2(int* a, int begin, int end)
{
if (begin >= end)//当只有一个数据或是序列不存在时,不需要进行操作
return;
int left = begin;//L
int right = end;//R
int key = a[left];//在最左边形成一个坑位
while (left < right)
{
//right向左,找小
while (left < right&&a[right] >= key)
{
right--;
}
//填坑
a[left] = a[right];
//left向右,找大
while (left < right&&a[left] <= key)
{
left++;
}
//填坑
a[right] = a[left];
}
int meeti = left;//L和R的相遇点
a[meeti] = key;//将key抛入坑位
QuickSort2(a, begin, meeti - 1);//key的左序列进行此操作
QuickSort2(a, meeti + 1, end);//key的右序列进行此操作
}
前后指针法
动图展示:
具体思路是:
1.选定基准值,定义prev和cur指针(cur = prev + 1)
2.cur先走,遇到小于基准值的数停下,然后将prev向后移动一个位置
3.将prev对应值与cur对应值交换
4.重复上面的步骤,直到cur走出数组范围
5.最后将基准值与prev对应位置交换
6.递归排序以基准值为界限的左右区间
代码展示:
//快速排序(前后指针法)
void QuickSort3(int* a, int begin, int end)
{
if (begin >= end)//当只有一个数据或是序列不存在时,不需要进行操作
return;
//三数取中
int midIndex = GetMidIndex(a, begin, end);
Swap(&a[begin], &a[midIndex]);
int prev = begin;
int cur = begin + 1;
int keyi = begin;
while (cur <= end)//当cur未越界时继续
{
if (a[cur] < a[keyi] && ++prev != cur)//cur指向的内容小于key
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
int meeti = prev;//cur越界时,prev的位置
Swap(&a[keyi], &a[meeti]);//交换key和prev指针指向的内容
QuickSort3(a, begin, meeti - 1);//key的左序列进行此操作
QuickSort3(a, meeti + 1, end);//key的右序列进行此操作
}
非递归实现
当我们需要将一个用递归实现的算法改为非递归时,一般需要借用一个数据结构,那就是栈。将Hoare版本、挖坑法以及前后指针法的快速排序改为非递归版本,其实主体思想一致,只是调用的单趟排序的算法不同而已。
于是我们可以先将Hoare版本、挖坑法和前后指针法的单趟排序单独封装起来。然后写一个非递归的快速排序,在函数内部调用单趟排序的函数即可。
快速排序的非递归算法基本思路:
1、先将待排序列的第一个元素的下标和最后一个元素的下标入栈。
2、当栈不为空时,读取栈中的信息(一次读取两个:一个是L,另一个是R),然后调用某一版本的单趟排序,排完后获得了key的下标,然后判断key的左序列和右序列是否还需要排序,若还需要排序,就将相应序列的L和R入栈;若不需排序了(序列只有一个元素或是不存在),就不需要将该序列的信息入栈。
3、反复执行步骤2,直到栈为空为止。
代码示例:
//快速排序(非递归实现)
void QuickSortNonR(int* a, int begin, int end)
{
Stack st;//创建栈
StackInit(&st);//初始化栈
StackPush(&st, begin);//待排序列的L
StackPush(&st, end);//待排序列的R
while (!StackEmpty(&st))
{
int right = StackTop(&st);//读取R
StackPop(&st);//出栈
int left = StackTop(&st);//读取L
StackPop(&st);//出栈
//该处调用的是Hoare版本的单趟排序
int keyi = PartSort1(a, left, right);
if (left < keyi - 1)//该序列的左序列还需要排序
{
StackPush(&st, left);//左序列的L入栈
StackPush(&st, keyi - 1);//左序列的R入栈
}
if (keyi + 1 < right)// 该序列的右序列还需要排序
{
StackPush(&st, keyi + 1);//右序列的L入栈
StackPush(&st, right);//右序列的R入栈
}
}
StackDestroy(&st);//销毁栈
}
Hoare版本
代码示例:
//Hoare版本(单趟排序)
int PartSort1(int* a, int left, int right)
{
int keyi = left;//key的下标
while (left < right)
{
//right走,找小
while (left < right&&a[right] >= a[keyi])
{
right--;
}
//left先走,找大
while (left < right&&a[left] <= a[keyi])
{
left++;
}
if (left < right)
{
Swap(&a[left], &a[right]);//交换left和right的值
}
}
int meeti = left;//L和R的相遇点
Swap(&a[keyi], &a[meeti]);//交换key和相遇点的值
return meeti;//返回相遇点,即key的当前位置
}
挖坑法
代码示例:
//挖坑法(单趟排序)
int PartSort2(int* a, int left, int right)
{
int key = a[left];//在最左边形成一个坑位
while (left < right)
{
//right向左,找小
while (left < right&&a[right] >= key)
{
right--;
}
//填坑
a[left] = a[right];
//left向右,找大
while (left < right&&a[left] <= key)
{
left++;
}
//填坑
a[right] = a[left];
}
int meeti = left;//L和R的相遇点
a[meeti] = key;//将key抛入坑位
return meeti;//返回key的当前位置
}
前后指针法
代码示例:
//前后指针法(单趟排序)
int PartSort3(int* a, int left, int right)
{
int prev = left;
int cur = left + 1;
int keyi = left;
while (cur <= right)//当cur未越界时继续
{
if (a[cur] < a[keyi] && ++prev != cur)//cur指向的内容小于key
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
int meeti = prev;//cur越界时,prev的位置
Swap(&a[keyi], &a[meeti]);//交换key和prev指针指向的内容
return meeti;//返回key的当前位置
}
快速排序俩个优化
但是上面的程序还有一些缺陷:
在基准值的选择上,如果选择的基准值为恰好为最小值,会进行不必要的递归。
在排序大量有序数据或者接近有序数据时,效率会比较低,甚至可能会出现程序崩溃的情况。
这是因为在排序有序数据时,快速排序的递归调用次数过多,会导致栈溢出的情况。
为了解决这些问题,这里有两种优化方法:
1> 三数取中法选基准值
2> 递归到小的子区间时,可以考虑使用插入排序
三数取中法取基准值:即在在起始位置,中间位置,末尾位置中选出中间值,作为基准值。
代码示例:
//三数取中
int GetMidIndex(int* a, int left, int right)
{
int mid = left + (right - left) / 2;
if (a[mid] > a[left])
{
if (a[mid] < a[right])
return mid;
else if (a[left]>a[right])
return left;
else
return right;
}
else
{
if (a[mid] > a[right])
return mid;
else if (a[left] > a[right])
return right;
else
return left;
}
}
小区间优化:类似于二叉树,每个子树都会进行一次递归调用,越到下面递归调用会越多。为了减少递归调用,当到递归到下层时,我们可以使用其他的排序来替代。这里我们使用插入排序。
代码示例:
//优化后的快速排序
void QuickSort0(int* a, int begin, int end)
{
if (begin >= end)//当只有一个数据或是序列不存在时,不需要进行操作
return;
if (end - begin + 1 > 20)//可自行调整
{
//可调用快速排序的单趟排序三种中的任意一种
//int keyi = PartSort1(a, begin, end);
//int keyi = PartSort2(a, begin, end);
int keyi = PartSort3(a, begin, end);
QuickSort(a, begin, keyi - 1);//key的左序列进行此操作
QuickSort(a, keyi + 1, end);//key的右序列进行此操作
}
else
{
//HeapSort(a, end - begin + 1);
ShellSort(a, end - begin + 1);//当序列长度小于等于20时,使用希尔排序
}
}
快速排序的特性总结:
1>快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2>时间复杂度:O(N*logN)
3>空间复杂度:O(logN)
4>稳定性:不稳定
归并排序
(点击此处跳转,更加详细的讲解过程,干货满满)
动图展示:
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。
将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
若将两个有序表合并成一个有序表,称为二路归并。
图片展示:
按照其算法思想:
首先应在数组中找出有序的序列,但数组是否有序编译器可不知道。
所以可以将数组划分,一分二,二分四…直到每个序列中只有一个数字。
一个数字总可以认为他是有序的吧。
最后将同时划分的序列合并。
递归实现
归并排序,从其思想上看就很适合使用递归来实现,并且用递归实现也比较简单。其间我们需要申请一个与待排序列大小相同的数组用于合并过程两个有序的子序列,合并完毕后再将数据拷贝回原数组。
代码示例:
//归并排序(子函数)
void _MergeSort(int* a, int left, int right, int* tmp)
{
if (left >= right)//归并结束条件:当只有一个数据或是序列不存在时,不需要再分解
{
return;
}
int mid = left + (right - left) / 2;//中间下标
_MergeSort(a, left, mid, tmp);//对左序列进行归并
_MergeSort(a, mid + 1, right, tmp);//对右序列进行归并
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
//将两段子区间进行归并,归并结果放在tmp中
int i = left;
while (begin1 <= end1&&begin2 <= end2)
{
//将较小的数据优先放入tmp
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++];
//归并完后,拷贝回原数组
int j = 0;
for (j = left; j <= right; j++)
a[j] = tmp[j];
}
//归并排序(主体函数)
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);//申请一个与原数组大小相同的空间
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);//归并排序
free(tmp);//释放空间
}
非递归实现
非递归实现的思想与递归实现的思想是类似的。
不同的是,这里的序列划分过程和递归是相反的,不是一次一分为二,而是先1个元素一组,再2个元素一组,4个元素一组…直到将所有的元素归并完。
代码示例:
//归并排序(子函数)
void _MergeSortNonR(int* a, int* tmp, int begin1, int end1, int begin2, int end2)
{
int j = begin1;
//将两段子区间进行归并,归并结果放在tmp中
int i = begin1;
while (begin1 <= end1&&begin2 <= end2)
{
//将较小的数据优先放入tmp
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++];
//归并完后,拷贝回原数组
for (; j <= end2; j++)
a[j] = tmp[j];
}
//归并排序(主体函数)
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);//申请一个与待排序列大小相同的空间,用于辅助合并序列
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
int gap = 1;//需合并的子序列中元素的个数
while (gap < n)
{
int i = 0;
for (i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
if (begin2 >= n)//最后一组的第二个小区间不存在或是第一个小区间不够gap个,此时不需要对该小组进行合并
break;
if (end2 >= n)//最后一组的第二个小区间不够gap个,则第二个小区间的后界变为数组的后界
end2 = n - 1;
_MergeSortNonR(a, tmp, begin1, end1, begin2, end2);//合并两个有序序列
}
gap = 2 * gap;//下一趟需合并的子序列中元素的个数翻倍
}
free(tmp);//释放空间
}
外排序
上面介绍的排序算法均是在内存中进行的,对于数据量庞大的序列,上面介绍的排序算法都束手无策,而归并排序却能胜任这种海量数据的排序。
假设现在有10亿个整数(4GB)存放在文件A中,需要我们进行排序,而内存一次只能提供512MB空间,归并排序解决该问题的基本思路如下:
1、每次从文件A中读取八分之一,即512MB到内存中进行排序(内排序),并将排序结果写入到一个文件中,然后再读取八分之一,重复这个过程。那么最终会生成8个各自有序的小文件(A1~A8)。
2、对生成的8个小文件进行1 1合并,最终8个文件被合成为4个,然后再1 1合并,就变成2个文件了,最后再进行一次1 1合并,就变成1个有序文件了。
注意:这里将两个文件进行1 1合并,并不是先将两个文件读入内存然后进行合并,因为内存装不下。这里的11合并是利用文件输入输出函数,从两个文件中各自读取一个数据,然后进行比较,将较小的数据写入到一个新文件中去,然后再读取,再比较,再写入,最终将两个文件中的数据全部写入到另一个文件中去,那么此时这个文件又是一个有序的文件了。
归并排序的特性总结:
1>归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
2>时间复杂度:O(N*logN)
3>空间复杂度:O(N)
4>稳定性:稳定
计数排序
(点击此处跳转,更加详细的讲解过程,干货满满)
动图展示:
思想:
计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤
- 统计相同元素出现次数
- 根据统计的结果将序列回收到原来的序列中
代码示例:
//计数排序
void CountSort(int* a, int n)
{
int min = a[0];//记录数组中的最小值
int max = a[0];//记录数组中的最大值
for (int i = 0; i < n; i++)
{
if (a[i] < min)
min = a[i];
if (a[i] > max)
max = a[i];
}
int range = max - min + 1;//min和max之间的自然数个数(包括min和max本身)
int* count = (int*)calloc(range, sizeof(int));//开辟可储存range个整型的内存空间,并将内存空间置0
if (count == NULL)
{
printf("malloc fail\n");
exit(-1);
}
//统计相同元素出现次数(相对映射)
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;
}
int i = 0;
//根据统计结果将序列回收到原来的序列中
for (int j = 0; j < range; j++)
{
while (count[j]--)
{
a[i++] = j + min;
}
}
free(count);//释放空间
}
注意:计数排序在排负数时,可将负数的类型转化成 unsigned int。
数组中元素有正有负的情况时不适用计数排序。
计数排序的特性总结:
1>计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。 2>时间复杂度:O(MAX(N,范围))
3>空间复杂度:O(范围)
4>稳定性:稳定
常见排序算法的性能分析
最后我们对各种排序算法进行一个简单的测试:随机测试10000个数据
我们借此来观察一下各个排序算法所用时长的差异
void TestOP()
{
srand(time(0));
const int N = 100000;
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);
for (int i = 0; i < N; ++i)
{
a1[i] = rand(); //获得随机数
a2[i] = a1[i];
a3[i] = a1[i];
a4[i] = a1[i];
a5[i] = a1[i];
a6[i] = a1[i];
}
//直接插入排序
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();
QuickSort(a5, 0, N - 1);
int end5 = clock();
//归并排序
int begin6 = clock();
MergeSort(a6, N);
int end6 = 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("QuickSort:%d\n", end5 - begin5);
printf("MergeSort:%d\n", end6 - begin6);
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
free(a6);
}
由此可见,不同的排序算法之间差异性较大,大家应该根据不同的应用场景来选择合适的排序算法。
最后,给出所有排序算法的时间复杂度、空间复杂度、稳定性的对比表格,希望大家能够有所收获。
如此明了清晰的详解过程,希望各位看官能够一键三连哦💕