欢迎来到 Claffic 的博客
“只要有花可开,就不允许生命与黯淡为伍。”
前言:
承接上篇,继续带大家手撕常见排序算法,这次讲剩余的两类:交换排序和归并排序。
注:
这篇博客属于数据结构内容,建议学习完C语言的小伙伴食用~
目录
Part1:交换排序
1.冒泡排序
1.1思想
1.2实现
2.快速排序
2.1Hoare经典版
2.2挖坑版
Part2:归并排序
归并排序
思想
实现
最后总结
冒泡排序相比大家都很熟悉了,这是学习C语言过程当中的第一个排序算法;
它的思想就是一趟一趟走,每一次都找到最大的数字,放到靠后的位置,就像水底冒泡一样,越往上气泡越大。
动图:
//冒泡排序
void BubbleSort(int* a, int n)
{
for (int j = 0; j < n; j++)
{
for (int i = 1; i < n-j; i++)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]);
}
}
}
}
特征分析:
像选择排序,思想易理解,也好实现,适合教学;
时间复杂度:最好:O(N^2) 最坏:O(N^2);
空间复杂度:O(1);
稳定性:稳如老狗
在这里,讲快速排序三种常见的实现方法:Hoare经典版本,挖坑版本,前后指针版本。
因为这个算法是霍尔大佬发明的,所以以他的名字命名,这是最经典的快速排序:
思想:
先找一个参考值 key,定义左右两端的下标/指针 R , L ,向内靠拢;
注意:左边做key,右边先走,可使相遇点比key小;
R 找比 key 值小的位置,L 找比 key 值大的位置;
都找到后,R L 对应位置的数字交换;
R L 相遇,相遇位置对应的数字与 key 交换。
这样就保证了数组左侧数字都小于 key,右侧数字都大于 key 。
到这里还没完呢,可不能保证左右侧数字都是有序的,所以还要对左右两侧进行相同操作,没错,可以用递归。
有点类似与二叉树的分治。
代码实现:
//快排(Hoare)
void QuickSort1(int* a, int left, int right)
{
if (left >= right)
return;
int begin = left, end = right;
int keyi = left;
while (left < right)
{
//右找小
while (left < right && a[right] >= a[keyi])
right--;
//左找大
while (left < right && a[left] <= a[keyi])
left++;
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
keyi = left;
//递归
QuickSort1(a, begin, keyi - 1);
QuickSort1(a, keyi + 1, end);
}
特性分析:
快速排序的综合性能和适用场景都比较好,大多数库里的排序都是快排;
时间复杂度:最好:O(N*logN) 最坏:O(N^2) 平均取O(N*logN);
空间复杂度:O(logN) (递归创建函数栈帧)
稳定性:不稳定
以下另外两种版本与此版本的性能相同,便不再进行此特性分析
这个方式的本质思想是没有变化的,就是把 key 值另外保存起来,形成一个坑位,在数据交换的过程中不断更新坑位:
这种思路下,就不必让 R 优先走了,也可以让 L 优先走,因为最终都能保证相遇点比 key 小,图示默认让 R 优先走;
代码实现:
//快排(挖坑法)
void QuickSort2(int* a, int left, int right)
{
if (left >= right)
return;
int begin = left, end = right;
int key = a[left];
int hole = left;
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;
//递归
QuickSort2(a, begin, hole - 1);
QuickSort2(a, hole + 1, end);
}
特征同霍尔版本
2.3前后指针版
这个版本是最方便,最好理解的;
定义 cur 和 prev 两个下标,对应位置的值与 key 比较:
无论比较的值是大是小,cur 都要++。
代码实现:
//部分快排(前后指针法)
int PartSort3(int* a, int left, int right)
{
int keyi = left;
int key = a[left];
int prev = left;
int cur = left + 1;
while (cur <= right)
{
if (a[cur] < key && ++prev != cur) // 保证 prev 在 cur 的左侧
Swap(&a[cur], &a[prev]);
cur++;
}
Swap(&a[keyi], &a[prev]);//与下标为keyi的交换
keyi = prev;
return keyi;
}
特征同霍尔版本
所谓归并排序,就是采用分治法,将已有的有序子序列合并,得到完全有序的序列,以此类推,最终并得到的就是一个有序的数组。
实现过程中,我们需要一块辅助空间来帮助归并,最先归并好的结果在那块辅助空间当中,最后再把结果拷贝到原数组空间中。
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
return;
// 中分
int mid = (begin + end) / 2;
//[begin,mid] [mid+1,end]
// 递归
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid+1, end, tmp);
//拷贝
int begin1 = begin,begin2 = mid + 1;
int end1 = mid,end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
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 n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
// 这里不能自己递归自己,否则会不断开辟空间
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
特征分析:
归并排序效率不低,但需要开辟一块大小为N的空间,当数据量过大时,可以考虑在磁盘中排序。
时间复杂度:最好:O(N*logN) 最坏:O(N*logN)
空间复杂度:O(N)
稳定性:稳定
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(N^2) | O(N) | O(N^2) | O(1) | 稳定 |
简单选择排序 | O(N^2) | O(N^2) | O(N^2) | O(1) | 不稳定 |
直接插入排序 | O(N^2) | O(N^1.3) | O(N^2) | O(1) | 稳定 |
希尔排序 | O(N*logN)~O(N^2) | O(N*logN) | O(N^2) | O(1) | 不稳定 |
堆排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(1) | 不稳定 |
归并排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(N) | 稳定 |
快速排序 | O(N*logN) | O(N*logN) | O(N^2) | O(logN)~O(N) | 不稳定 |
各排序对比
代码已上传至 我的gitee
拿走不谢~
总结:
到这里,其实排序还并没有完结,我在后期会更新快速排序的优化以及快速排序和归并的非递归实现方式,有点小期待呢~
码文不易
如果你觉得这篇文章还不错并且对你有帮助,不妨支持一波哦