排序
引言
在计算机科学领域,排序是数据结构与算法中最基本且最重要的操作之一。无论是处理简单的日常任务还是解决复杂的计算问题,排序算法都发挥着关键作用。排序的主要目的是将一组无序的数据元素按照特定的顺序进行排列,使得后续的查找、分析和操作变得更加高效。
排序算法种类繁多,从简单易懂的冒泡排序、选择排序到高效复杂的快速排序、归并排序,每种算法都有其独特的应用场景和性能特点。在C语言中实现这些排序算法不仅能帮助我们深入理解其工作原理,还能提升我们编写高效代码的能力。
本文将介绍排序的基本概念,探讨几种常见的排序算法,并通过具体的C语言代码实例展示它们的实现方法。无论你是初学者还是有经验的开发者,希望通过这篇文章,能够对排序算法有更深入的理解,并能够在实际项目中灵活应用。
目录
1. 排序的概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j]
,且r[i]
在r[j]
之前,而在排序后的序列中,r[i]
仍在r[j]
之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不断地在内外存之间移动数据的排序。
2. 常见的排序算法
2.1 插入排序
基本思想
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。 实际中我们玩扑克牌时,就用了插入排序的思想
2.1.1 直接插入排序
当插入第i(i>=1)
个元素时,前面的array[0],array[1],…,array[i-1]
已经排好序,此时用array[i]
的排序码与array[i-1],array[i-2],…
的排序码顺序进行比较,找到插入位置即将array[i]
插入,原来位置上的元素顺序后移。
代码实现
//插入排序
// 时间复杂度:O(N^2) 什么情况最坏:逆序 最好:顺序有序,O(N)
void InsertSort(int* a, int n) //传入数组的首地址和数组的大小
{
// [0, n-2]是最后一组待排序列
// [0,end]是有序,将end+1位置的值插入[0,end],并使[0,end+1]保持有序
for (int i = 0; i < n - 1; i++) //i<n-1是为了防止下面tmp的end+1越界访问
{
int end = i; //待排数列的最后一个被比数
//因为下面要对end--来达到对待排数列单趟排序的效果,所以不能直接用i,否则会影响到整体排序
int tmp = a[end + 1]; //比数 //tmp范围是[1,n-1]
while (end >= 0) //单趟排序
{
if (tmp < a[end]) //只要比数小于前边的被比数
{
a[end + 1] = a[end]; //就将当前的被比数向后移
--end; //然后再去比较前一个被比数
}
else
{
break; //为了防止前边的数(被比数)都比待排数(比数)大
} //循环一直end--,直到不满足条件跳出循环,都没有进入else插入待排数的情况出现
} //因此要在循环的外面进行插值,而不是else中。
a[end + 1] = tmp; //当进行到这条语句就只有两种可能:待排数为最小和找到了比待排数小的数
} //此时都可以将值放到a[end-1]中
}
直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
2.1.2 希尔排序
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数gap
,把待排序文件中所有记录分成gap
个组,所有距离为gap
的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达gap=1
时,所有记录在同一组内排好序。
下面这个图片中,相同颜色的为一组
其实希尔排序就是改良版的插入排序,它与插入排序的不同之处在于,它会优先比较距离较远的元素,但是他的底层(gap=1
时)跟插入排序是一模一样的。
代码实现
//希尔排序
// O(N ^ 1.3)
void ShellSort(int* a, int n)
{
int gap = n;//gap是距离,是每一组中待排数之间的距离,也是每组的第一个待排数之间的距离
while (gap > 1)
{
// gap > 1时是预排序
// gap == 1时是插入排序
gap = gap / 3 + 1; // +1保证最后一个gap一定是1
//从这里开始,就是插入排序了,只不过是分成了gap组同时进行
//当gap等于1的时候,就是把所有数分成一组,也就是上边的插入排序
for (size_t i = 0; i < n - gap; ++i) //i < n-gap 依然是防止tmp越界访问
{
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;
}
}
}
- 此算法就是多组间隔为
gap
的预排序,gap
由大变小 gap
越大,大的数可以越快的到后面,小的数越快的到前面,但是预排完越不容易接近有序。gap
越小,即相反,排的慢,但是越接近有序。- 当
gap==1
时,就是直接插入排序。
如果大家到这里还不明白的话,建议大家可以再多看看上边的插入排序。博主能力有限,希望大家可以多多包涵。
希尔排序的特性总结:
- 希尔排序是对直接插入排序的优化。
- 当
gap > 1
时都是预排序,目的是让数组更接近于有序。当gap == 1
时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。 - 希尔排序的时间复杂度不好计算,因为
gap
的取值方法很多,导致很难去计算。我们在这里就记住O=(n^1.3)
就可以了。 - 稳定性:不稳定。
2.2 选择排序
基本思想
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
2.2.1 直接选择排序
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
代码实现
//选择排序————大家可以直接看我下面的改良版
void selectionSort(int arr[], int n) {
int i, j, minIndex, temp;
// 移动数组边界
for (i = 0; i < n-1; i++) {
// 找到最小元素的索引
minIndex = i;
for (j = i+1; j < n; j++)
if (arr[j] < arr[minIndex])
minIndex = j;
// 交换找到的最小元素和第i个元素
temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
}
改良版:排一趟可以同时确定最大和最小
其核心思想是:两个指针(begin
,end
)在两边,默认指向的是最小值和最大值。三个指针(mini
,i
,maxi
)从begin
和end
之间遍历。然后互相进行比较,更新下标,找到此趟的最小值和最大值,最后将更新完的mini
和maxi
所指向的值与begin
和end
进行交换,通过begin
和end
固定最小值和最大值,这样一趟下来,待排数组的最小值和最大值就会在数组的头和尾了。
如果大家不理解核心思想也没有关系,看完下面的代码详细讲解帮助你再次理解吧。
//数据交换
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//直接选择排序(优化版:一趟同时确定最大和最小)
// O(N^2)
void SelectSort(int* a, int n) //传入数组的地址和大小
{
int begin = 0, end = n - 1; // 标记待排数列的首尾
while (begin < end) //只要begin和end没有相交,就一直循环
{
int mini = begin, maxi = begin; //因为begin和end在不断向中心靠拢,所以也要在while循环中更新mini和maxi的初始指向
for (int i = begin + 1; i <= end; ++i)//单趟排序
{
if (a[i] > a[maxi])
{
maxi = i;
}
//通过将a[i]的下标赋值给mini和maxi来更新mini和maxi的指向
//保证所指向的数一定是此趟当前遍历到的最大值和最小值
if (a[i] < a[mini])
{
mini = i;
}
}
//上边的for循环,利用mini,i,maxi三个指针就将一趟中的最大值和最小值找到了
//下面通过数据交换函数将mini,i,maxi三个指针找到的最大值和最小值与begin和end进行交换,来固定最大值和最小值
Swap(&a[begin], &a[mini]);
if (begin == maxi) //防止在maxi和begin恰好都指向最大值时,因为mini和begin的交换,导致maxi找不到自己指向的值
maxi = mini;
Swap(&a[end], &a[maxi]);
//然后将两指针向中间靠拢,进行第二趟排序,找数组的第二大和第二小
++begin;
--end;
}
}
直接选择排序的特性总结:
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
2.2.2 堆排序
堆排序是指利用二叉树中的堆积树(堆)这种数据结构所设计的一种排序算法。堆排序是利用向下调整的思想来通过建堆和堆删除达到有序,如果大家对二叉树这种数据结构印象还比较模糊的话,可以点击链接 二叉树复习一下~
向下调整算法
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根结点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
int array[] = {27,15,19,18,28,34,65,49,25,37};
//向下调整——可以将数组恢复成堆的顺序——小根堆
void AdjustDown(HPDataType* a, int n, int parent) //传入数组的地址和数组的总大小还有根的下标
{ //传入根位置的下标是因为需要调整的元素的位置就是根位置
// 先假设左孩子小——利用假设法
int child = parent * 2 + 1; //第一次先找到根节点下面的子节点
while (child < n) // 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; //如果最小的节点都大于父节点了,就说明不需要调了
}
}
}
堆的创建
下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过向下调整算法,把它构建成一个堆。从倒数的第一个非叶子结点的子树开始调整,一直调整到根结点的树,就可以调整成堆。
int a[] = {1,5,3,8,7,6};
图片中的是调整为大根堆,思路是一样的,代码只有些许不同
堆删除
删除堆是删除堆顶的数据,将堆顶的数据跟最后一个数据一换,然后删除数组最后一个数据,再进行向下调
整算法。
//堆顶的删除
void HPPop(HP* php)
{
assert(php);
assert(php->size > 0);
Swap(&php->a[0], &php->a[php->size - 1]);//要想删除堆顶元素,需要先将堆顶元素与数组最后一个元素交换位置
php->size--; //这样可以保持除堆顶以外的元素仍然能保持堆的顺序,可以大大提升效率
AdjustDown(php->a, php->size, 0); //利用向下调整将删除数据之后的数组恢复为堆
}
堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
- 建堆
升序:建大堆
降序:建小堆 - 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
//堆排序 0(N*logN)
void HeapSort(int* a, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
堆排序的特性总结:
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
2.3 交换排序
基本思想
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
2.3.1 冒泡排序
冒泡排序是一种简单的排序算法。顾名思义,冒泡排序就是通过每次比较两个数字,让较大(或较小)的数字像泡泡一样浮到数列的一端的一种比较排序算法。
具体算法描述如下:
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后 的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。
//冒泡排序
// O(N^2) 最坏
// O(N) 最好
void BubbleSort(int* a, int n)
{
for (int j = 0; j < n; j++)
{
// 单趟
int flag = 0;
for (int i = 1; i < n - j; i++)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]);
flag = 1;
}
}
if (flag == 0)
{
break;
}
}
}
冒泡排序的特性总结:
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2) ,最好是O(N)
- 空间复杂度:O(1)
- 稳定性:稳定
- 直接插入排序是优于冒泡排序的
2.3.2 快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
hoare版
完整代码
void QuickSort(int* a, int left, int right)
{
//递归的结束条件——如果下面有小区间优化则这个递归结束条件可有可无
//if (left >= right)
// return;
//小区间优化,不在递归分割排序,减少递归的次数
if((right - left + 1) < 10)
{
//当向下递到小区间的待排值不足10个时,使用插入排序
InsertSort(a + left, right - left + 1);//传入的是小区间的最左和小区间的最右
}
else
{
// 三数取中
int midi = GetMidi(a, left, right); //通过三数取中获得了一个较中的值的下标
Swap(&a[left], &a[midi]); //将最左边的数和较中值进行位置交换
//设最左边的值为key
int keyi = left;
int begin = left, end = right;
while (begin < end) //当begin != end 时,两指针就相遇了
{
// 右边找小
while (begin < end && a[end] >= a[keyi]) //右指针 < key 时跳出循环
{
--end;
}
// 左边找大
while (begin < end && a[begin] <= a[keyi]) //左指针 > key 时跳出循环
{
++begin;
}
//两循环都跳出时,说明右边小值和左边大值都找到了
Swap(&a[begin], &a[end]); //交换两值位置
}
//跳出循环就证明两个指针已经完成任务并相遇
Swap(&a[keyi], &a[begin]); //将相遇点的值与最左边的key值进行交换
keyi = begin; //让key到相遇点上分割子区间
// [left, keyi-1] keyi [keyi+1, right]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
//向子区间递下去
}
}
单趟思想
int begin = left, end = right;
while (begin < end) //当begin != end 时,两指针就相遇了
{
// 右边找小
while (begin < end && a[end] >= a[keyi]) //右指针 < key 时跳出循环
{
--end;
}
// 左边找大
while (begin < end && a[begin] <= a[keyi]) //左指针 > key 时跳出循环
{
++begin;
}
//两循环都跳出时,说明右边小值和左边大值都找到了
Swap(&a[begin], &a[end]); //交换两值位置
}
//跳出循环就证明两个指针已经完成任务并相遇
- 创建两个指针(
begin
和end
)分别指向数组的最左边和最右边 - 通过循环使两指针向中间靠拢遍历,右指针找小于key、左指针找大于key的值,都找到就进行交换。
- 直至两指针相遇退出循环
关于相遇点与key值交换的问题
Swap(&a[keyi], &a[begin]); //将相遇点的值与最左边的key值进行交换
问题一:左边做key,为何右边先走
问题二:相遇位置为什么一定比key小
答:如果是左边做key的情况下,右边先走可以保证相遇位置一定比key小,因为相遇的场景一共有两种,
第一种L遇到R:R先走,停下来,R停下的条件是遇到比key小的值,R停的位置一定比key小,L没有找到大的,遇到了R才停下来。
第二种R遇到L:R先走,找小于key的值,没有找到比key小的,直接跟L相遇了。L停留的位置是上一轮交换的位置,上一轮的交换,把比key小的值,换到L的位置了。
当然,如果让右边做key也可以,只需要左边先走,就可以保证相遇位置一定比key要大了
三数取中优化函数
可避免有序情况下,效率退化
//三数取中优化函数
int GetMidi(int* a, int left, int right) //传入数组的地址和指向头尾的两个指针
{
int midi = (left + right) / 2;
// left midi right
//通过if判断结构,使头中尾三个数的数值进行比较,返回中间值的下标
if (a[left] < a[midi])
{
if (a[midi] < a[right])
{
return midi;
}
else if (a[left] < a[right])
{
return right;
}
else
{
return left;
}
}
else // a[left] > a[midi]
{
if (a[midi] > a[right])
{
return midi;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
挖坑法
挖坑法和hoare版没有太大的区别,所以了解一下就可以了。
前后指针版
void QuickSort(int* a, int left, int right)
{
//递归的结束条件——如果下面有小区间优化则这个递归结束条件可有可无
//if (left >= right)
// return;
//小区间优化,不在递归分割排序,减少递归的次数
if ((right - left + 1) < 10)
{
//当向下递到小区间的待排值不足10个时,使用插入排序
InsertSort(a + left, right - left + 1);//传入的是小区间的最左和小区间的最右
}
else
{
// 三数取中
int midi = GetMidi(a, left, right); //通过三数取中获得了一个较中的值的下标
Swap(&a[left], &a[midi]); //将最左边的数和较中值进行位置交换
//设最左边的值为key
int keyi = left;
// 三数取中
int midi = GetMidi(a, left, right);
Swap(&a[left], &a[midi]);
int keyi = left;
int prev = left; //将数组最左边的位置给prev指针
int cur = prev + 1; //将prev指针后边的位置给cur指针
while (cur <= right) //当cur还在范围内就一直遍历
{
if (a[cur] < a[keyi] && ++prev != cur) //cur指针如果遍历到小于key的值,并且不挨着prev指针,就交换两个指针的值。
Swap(&a[prev], &a[cur]);
cur++;
}//跳出循环就证明cur指针已经走到底了,这时的prev指针左边都是小于key值的,右边都是大于key值的
Swap(&a[prev], &a[keyi]);//将相遇点的值与最左边的key值进行交换
keyi = prev; //让key到prev位置上分割子区间
// [left, keyi-1] keyi [keyi+1, right]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
//向子区间递下去
}
}
单趟思想
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[prev], &a[cur]);
cur++;
}
cur指针在后,prev指针在前,cur指针去找小于key的值,找到之后就停下来,++prev,并交换prev和cur位置的值。 然后重复前面的操作就可以做到,小于key的值往前放,大于key的值往后推。在if的判断条件中,加入了排除cur和prev重复交换的情况。
非递归版
非递归版快排,我们需要借助数据结构—— 栈,来实现快排的递归效果,因为栈是深度优先遍历。如果对栈这个数据结构不清晰的同学,可以点击上方链接进行复习,以便更好地理解非递归版的快排。首先我先给大家讲解一下栈实现递归效果的思路,其次再展示完整代码。
思路
完整代码
//非递归版
void QuickSortNonR(int* a, int left, int right)
{
ST st;
STInit(&st); //对栈进行初始化
//因为栈中存放的是整形结构,所以通过插入两次(区间右值和区间左值),来实现将待排区间压入栈中
STPush(&st, right);
STPush(&st, left);
//循环每走一次,相当于之前快排的一次递归。
while (!STEmpty(&st))
{
//首先取栈顶的待排区间
int begin = STTop(&st);
STPop(&st);
int end = STTop(&st);
STPop(&st);
//将取到的区间传入开排函数中进行单趟排序,并返回排好之后的key值,用来分割子区间
int keyi = PartSort2(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
//如果子区间里还有值,就把子区间右值和子区间左值压入栈中,准备下次循环取出子区间进行排序
if (keyi + 1 < end)
{
STPush(&st, end);
STPush(&st, keyi + 1);
}
if (begin < keyi - 1)
{
STPush(&st, keyi - 1);
STPush(&st, begin);
}
}
STDestroy(&st); //对栈进行销毁
}
// 前后指针版
int PartSort2(int* a, int left, int right)
{
// 三数取中
int midi = GetMidi(a, left, right);
Swap(&a[left], &a[midi]);
int keyi = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[prev], &a[cur]);
cur++;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
快速排序的特性总结:
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
2.4 归并排序
基本思想
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
递归版
void _MergeSort(int* a, int* tmp, int begin, int end)
{
//递归到子区间只有一个数的时候默认有序,就可以返回区间进行归并了
if (begin >= end)
return;
//分解就是将区间一分为二
int mid = (begin + end) / 2;
// 如果[begin, mid][mid+1, end]有序就可以进行归并了
//不是有序就继续向子区间递归,直到递归(分解)成一个数就默认有序了
_MergeSort(a, tmp, begin, mid);
_MergeSort(a, tmp, mid + 1, end);
//走到这说明当前两个区间已经有序了,可以进行归并了
// 归并
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
//通过while循环对两个区间的元素大小进行依次比较,谁小就将元素拷贝到新数组tmp中
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++];
}
//最后将tmp数组中的元素通过memcpy函数替换到原数组中
memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
//+ begin是因为每次归并的区间范围都在变
}
//此函数是为了创造一个tmp数组,以此避免如果直接在归并排序函数中创建tmp数组,会因为向下递归导致数组反复创建的问题
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n); //申请一个能够存放n个整形的数组的空间,并将地址赋给tmp指针
if (tmp == NULL)
{
perror("malloc fail");
return;
}
//使用子函数,对待排数组a进行归并排序
_MergeSort(a, tmp, 0, n - 1);
free(tmp);
tmp = NULL;
}
非递归版
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
// gap每组归并数据的数据个数
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
// [begin1, end1][begin2, end2]
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
printf("[%d,%d][%d,%d] ", begin1, end1, begin2, end2);
// 第二组都越界不存在,这一组就不需要归并
if (begin2 >= n)
break;
// 第二的组begin2没越界,end2越界了,需要修正一下,继续归并
if (end2 >= n)
end2 = n - 1;
int j = i;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
printf("\n");
gap *= 2;
}
free(tmp);
tmp = NULL;
}
归并排序的特性总结:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
2.5 非比较排序
2.5.1 计数排序
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
- 统计相同元素出现次数
- 将统计的结果对照元素数值和数组下标数值相符的位置,将结果放到数组中。
- 利用数组的下标为自然序号排序自动将元素排序好
// 时间复杂度:O(N+range)
// 只适合整数/适合数据范围集中
// 空间复杂度:O(range)
void CountSort(int* a, int n)
{
//先通过for循环遍历比较出待排的最小值和最大值
//这样就可以确定待排数列的范围,从而合理的申请数组空间
int min = a[0], max = a[0];
for (int i = 1; i < n; i++)
{
if (a[i] < min)
min = a[i];
if (a[i] > max)
max = a[i];
}
//根据范围range申请数组空间并进行初始化
int range = max - min + 1;
int* count = (int*)calloc(range, sizeof(int));
if (count == NULL)
{
perror("calloc fail");
return;
}
// 统计元素重复次数
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;
}
// 排序
int j = 0;
for (int i = 0; i < range; i++)
{
while (count[i]--)
{
a[j++] = i + min;
}
}
free(count);
}
计数排序的特性总结:
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(MAX(N,范围))
- 空间复杂度:O(范围)
- 稳定性:稳定