稳定性
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次 序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排 序算法是稳定的;否则称为不稳定的。
简单来说就是:两个数据的相对顺序排序完之后没有发生变化,排序算法就是稳定的;反之,则不稳定
一、归并排序
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有 序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
简单来说就是:取小的数据尾插到新数组,再将新数组数据对应拷贝回原来数组
至于为什么不用链表尾插我们等下写出代码来就知道了
归并排序核心步骤:
上图是为了我们更容易理解归并排序而画出来的,真正的过程应该是下面的图: 解析:tmp就是我们新开辟的数组。我们要将数据进行排序,归并排序的前提是原数据左半部分和右半部分都是有序的。如果没有序就进行递归处理,每次在中间位置截取开来,分为左子区间和右子区间两个部分(函数栈帧销毁后可以重复调用,所以不会栈溢出,所以空间复杂度最大为O(N)),依次递归下去。当递归到区间只有一个元素的时候(一次递归的左右区间相同),该元素就是有序的,递归返回有序数据到上一层之后开始进行归并
递归方法
按照上面解析我们通过递归展开图来看看
代码:
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = (end + begin) / 2;
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
数据: 递归展开图: 当我们到达最后一层都只有一个数据的时候,返回开始归并:
if (begin >= end)
return;
int mid = (end + begin) / 2;
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
int begin1 = begin; int end1 = mid;
int begin2 = mid + 1; int 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, (end - begin + 1) * sizeof(int));
归并排序动图
特性
归并排序的特性总结:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
有了上面的了解,再深入研究归并排序的非递归方法
归并排序非递归方法
我们先来看看情况1:给出的数据个数是2的整数倍 这种情况我们先来解决,然后再来看看其他的情况
那么我们可以定义一个gap,gap依次翻倍增加。 每次两组两组比较,每组gap个数据。
代码:
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail\n");
exit(-1);
}
int gap = 1;
while (gap < n)
{
for (int j = 0; j < n; j += 2 * gap)
{
int begin1 = j; int end1 = j + gap - 1;
int begin2 = j + gap; int end2 = j + 2 * gap - 1;
int i = j;
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, tmp, (n) * sizeof(int));
gap *= 2;
}
}
free(tmp);
tmp = NULL;
}
可以看到当数据个数是2的整数倍的时候,我们可以通过归并排序非递归的方法完成排序
但是如果数据个数是奇数或者不是2的整数倍呢? 如下图:
因为我们是按整数倍去算的,所以这两种情况下下标会越界:
所以我们在遇到这种情况是一定要就行判断的,然后采取修改措施
我们来看看情况分为哪一些: 这里begin1不可能越界,因为begin1=j<n 所以只用考虑三种情况:
特殊的三种情况
这里就是为什么不推荐使用整体拷贝,因为每一种特殊情况都要进行修改边界。所以我们每归并一个边界就拷贝回去一个边界
情况1:end1越界
if (end1 >= n)
{
break;
}
如果end1越界了,那么就将begin1不进行归并了,直接拷贝回原数组
情况2:begin2和end2越界
if (begin2 >= n)
{
break;
}
如果begin2和end2越界了,那么就将begin1和end1不进行归并了,直接拷贝回原数组
情况3:end2越界
if (end2 >= n)
{
end2 = n - 1;
}
如果end2越界了,那么让end2与begin2相等,也就是使右区间只有一个元素
注意 !!!
这里不能直接将边界全部修改为 n-1 如果下标原本是【8,11】【12,15】 现在被修改成为【8,8】【8,8】
本来是【12,15】是越界不存在的空间,结果修改成【8,8】就表示区间存在了,平白无故多出来数据与前面数据就行归并。所以不能全部修改为n-1,要修改为一个不存在的区间,比如【9,8】这样的
归并排序的完整代码
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = (end + begin) / 2;
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
int begin1 = begin; int end1 = mid;
int begin2 = mid + 1; int 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, (end - begin + 1) * sizeof(int));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail\n");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail\n");
exit(-1);
}
int gap = 1;
while (gap < n)
{
for (int j = 0; j < n; j += 2 * gap)
{
int begin1 = j; int end1 = j + gap - 1;
int begin2 = j + gap; int end2 = j + 2 * gap - 1;
if (end1 >= n)
{
break;
}
if (begin2 >= n)
{
break;
}
if (end2 >= n)
{
end2 = n - 1;
}
printf("[%d,%d] [%d,%d]", begin1, end1, begin2, end2);
int i = j;
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 + j, tmp + j, (end2 - j + 1) * sizeof(int));
}
gap *= 2;
printf("\n");
}
free(tmp);
tmp = NULL;
}
二、计数排序
思想
计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
操作步骤:
- 统计相同元素出现次数
- 根据统计的结果将序列回收到原来的序列中
特殊情况处理:相对映射
应用场景
适合范围相对集中的数据就行处理
我们来看看代码:
void CountSort(int* a, int n)
{
int max = a[0];
int min = a[0];
for (int i = 0; i < n; ++i)
{
if (max < a[i])
max = a[i];
if (min > a[i])
min = a[i];
}
int range = max - min + 1;
int* p = (int*)malloc(sizeof(int) * range);
if (p == NULL)
{
perror("malloc fail\n");
}
memset(p, 0, sizeof(int) * range);
for (int i = 0; i < n; ++i)
{
p[a[i] - min]++;
}
int j = 0;
for (int i = 0; i < range; ++i)
{
while (p[i]--)
{
a[j++] = i + min;
}
}
}
计数排序的特性总结
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(MAX(N,范围))
- 空间复杂度:O(范围)
- .稳定性:稳定
排序算法复杂度及稳定性分析
总结
其实排序算法还有很多的,比如:基数排序,桶排序,鸡尾酒排序…我们学不完的,所以只需要掌握主要使用的排序方法和思维即可。到目前为止,初阶数据结构就结束了,接下来我会慢慢更新c++的内容,包括后面的高阶数据结构和stl之类的内容。希望大家多多支持!我们下期见!
|