前言
作者:小蜗牛向前冲
名言:我可以接受失败,但我不能接受放弃
如果觉的博主的文章还不错的话,还请点赞,收藏,关注支持博主。如果发现有问题的地方欢迎❀大家在评论区指正。
目录
一 归并排序
1 归并排序的递归形式
2 非递归实现归并排序
3 归并排序的总结
二 非比较排序
三 排序大总结
这里我们将继续为大家分享归并排序和非比较排序,我相信大家在上期中对冒泡排序和快速排序有了比较深刻的理解。
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤
归并排序动态演示
对于归并来说,其实就是将二个有序序列和并未一个有序序列,如果说快速排序像是二叉树的前序遍历的话,归并排序就像二叉树的后序遍历。归并排序的思路就是不断取小数尾插就可以了,这里我们就要用到递归的思想了,要不断将要排序的序列分为左右二个子区间,直到每个区间中只剩余一个元素(一个元素相当有序),就停止向下递归,开始返回归并(取小的尾插),注意这里我们要借助一个临时数组。
我们拿上面这个例子来理解一下,首先将序列分为左右二个区间,左区间(10 6 7 1),右区(3 9 4 2)
,我们在以左区间来举例,又可以分解为左右区间,左区间(10 6),右区间(7 1),直到分解为一个元素时就不在分解,取小的尾插到新数组tmp中,完后在拷贝回原数组,经过层层递归和返回,就可以形成二个有序序列(1 6 7 10)和(2 3 4 9)在归并就可以形成一个有序序列。
下面我们用代码实现他:
void _MergeSort(int* a, int begin, int end, int* tmp)
{
//左右区间不可在分,递归停止
if (begin >= end)
return;
//分区间[begin,mid].[mid+1,end]
int mid = begin + (end - begin) / 2;
//递归左区间
_MergeSort(a,begin, mid, tmp);
//递归右区间
_MergeSort(a, mid + 1, end, tmp);
int begin1 = begin, end1 = mid;
int begin2 = mid + 1,end2 = end;
//标记交换空间tmp的下标
int i = begin;
//归并二个有序序列,取小的尾插
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, (end - begin + 1) * sizeof(int));
}
//递归实现归并
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);//为归并开辟临时空间
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
//调用归并函数
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
注意:
1 这里我们要注意在归并的时候区分好各个区间,因为我们还要将tmp中的元素拷贝回原数组,所以memcpy的传的目标指针应该是a+begin,源头指针是tmp+bgin。
2 我们在归并二组序列后,要记得把大的序列剩余的元素直接尾插到归并的新的序列中。
上面我们用递归实现了归并排序,我们可以很容易的看出归并排序是一个完全二叉树,当数据为10亿的时候递归深度才30,也就是说归并排序几乎不会因为递归而栈溢出,但我们为了提高自己的思维能力,我们不妨试试用非递归实现归并排序。
我们这里的非递归形式并不适合用栈来实现,为什么这么说呢?因为我们在进行序列归并时,区间的begin或者end可能使用多次,像斐波那契数列那样通过前面区间求后面区间。那么我们这里还是借用开辟一个动态的数组来解决。这么我们还是要回到递归中说的思路,在二个有序序列取小数尾插到新的序列中;所以我们有个变量gap来标明进行排序的每次组的元素个数,然后定义循环,让二组数据进行归并,归并完成后我们在跟新gap*=2;让其进行跟大的数组进行归并,直到gap 我们这种方式只适合归并数据个数为2^n,但数组数据个数不满足这个条件数组就会发生越界的情况: 当n为奇数的时候 当n为偶数的时候 那么我们就要根据这些越界情况经行修正: 可能发生越界的三种情况: 1 第一组越界,这里我们只要考虑end1越界的情况,为什么这里说呢?因为begin1是不可能越界的(j 2 第二组全部越界,即是第二组的begin2越界了,也就是说要只剩余一组,这里我们不归并直接break返回. 3 第二组部分越界,即end2越界了,这时候我们只有修正一下end2就行了end2 = n-1; 这里我们根据越界的情况去修正我们的代码: 归并排序的特性总结: 思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤 1. 统计相同元素出现次数 对于计数排序的思路来说,思路是非常简单的,我们采用相对映射的思路,首先遍历数组,找到最大值max和最小值min,开辟一个数组CountArr大小为max-min+1。然后在遍历一边原数组,但是我们这边要稍微处理一下,将原数组的值-min,这样求到的值就可以于CountArr的下标对于,统计好下标出现的次数。完成后,我们就可以根据下标进行排序:从CountArr中依次取出数据到原数组中,这里我们要注意加上min恢复数据。 思路图
动图演示 代码实现: 注意点: 1 计数排序只能排序整形,而不能排序浮点型和字符串。 2 计数排序适合排数据相对集中的数据。 计数排序的特性总结: 在这里我们重点总结一下各个排序的时间复杂度和稳定性: 直接插入排序 时间复杂度在数组中的元素全部是逆序时的情况最差O(n²),而最好的情况便是顺序了O(n),只要遍历就可以。 该排序是非常稳定的,二个相同的数据的相对位置不会因为排序而改变,所以他是稳定的。 希尔排序 时间复杂度,该排序的时间复杂度是非常难求的,最坏情况无疑是全部逆序排序O(n²),但对于最好的情况我们姑且认为是O(n^1.3). 该排序是非常不稳定的,为什么这么说,因为该排序每次都要让gap步调相同的元素进行预排序,这极大可能是会打断相同排序的相对位置。 直接选择排序 这个排序就没事好说了总之就是稳的一批。 堆排序 这个排序的效率还是挺高的时间复杂度为O(n*logn),但我们要注意的是建堆就破坏了他的稳定性,所以说他是不稳定的。 冒泡排序 这个也是稳的很,不过我们在进行优化后,在最好的情况,数组是有序时,时间复杂度为O(n),但是他的平均情况基本就是O(n²) 快速排序 这个排序不得不说厉害,当数组都是有序的情况他们的复杂度是最坏的O(n²),在正常情况下都是O(n*logn),我们可以认为遇事不决就可以用快速排序。这里因为快速排序实现的三种方法都要改变相对位置,所以说他是不稳定的。 归并排序 该排序时间复杂度就为O(n*logn),但是我们要注意该排序要开额外的空间,所以他的空间复杂为O(logn)~O(n),这也使得他排序使用场景仅限于内存中,在磁盘中就显的用起来很不方便了,有点挺好的就是非常稳定。 排序方法 平均情况 最好情况 最坏情况 空间复杂度 稳定性 直接插入排序 O(n²) O(n) O(n²) O(1) 稳定 希尔排序 O(n*logn)~O(n²) O(n^1.3) O(n²) O(1) 不稳定 直接选择排序 O(n²) O(n²) O(n²) O(1) 不稳定 堆排序 O(n*logn) O(n*logn) O(n*logn) O(1) 不稳定 冒泡排序 O(n²) O(n) O(n²) O(1) 稳定 快速排序 O(n*logn) O(n*logn) O(n²) O(logn)~O(n) 不稳定 归并排序 O(n*logn) O(n*logn) O(n*logn) O(n) 稳定 其实排序还有其他的,像基数排序,外排序, 桶排序等,但这些排序不常用感兴趣的小伙伴可以自己去了解一下。//非递归实现归并排序
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (NULL == tmp)
{
perror("malloc fail");
return;
}
int gap = 1;//定义gap来标记要排序的组中有多少个元素
while (gap < n)
{
//gap个数据进行归并
for (int j = 0;j < n;j += 2 * gap)
{
//归并取小数进行归并
int begin1 = j, end1 = j + gap - 1;
int begin2 = j + gap, 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++];
}
}
//取大的数组,插入到tmp数组后
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;
}
}
//非递归实现归并排序
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (NULL == tmp)
{
perror("malloc fail");
return;
}
int gap = 1;//定义gap来标记要排序的组中有多少个元素
while (gap < n)
{
//gap个数据进行归并
for (int j = 0;j < n;j += 2 * gap)
{
//归并取小数进行归并
int begin1 = j, end1 = j + gap - 1;
int begin2 = j + gap, end2 = j + 2 * gap - 1;
//第一组越界
if (end1 >= n)
{
break;//直接返回
}
//第二组部分越界
if (begin2 >= n)
{
break;//直接返回
}
//第二组部分越界
if (end2 >= n)
{
//修正end2
end2 = n - 1;
}
int i = j;
//归并二个有序序列,取小的尾插
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 + j, tmp + j, (end2 - j + 1) * sizeof(int));
}
gap *= 2;
}
}
3 归并排序的总结
1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(N)
4. 稳定性:稳定二 非比较排序
2. 根据统计的结果将序列回收到原来的序列中 // 时间复杂度:O(N+range)
// 空间复杂度:O(range)
// 适合数据范围集中,也就是range小
// 只适合整数,不适合浮点数、字符串等
void CountSort(int* a, int n)
{
int max = a[0], min = a[0];
int i = 0;
//找最大数和最小数
for (i = 0;i < n;i++)
{
if (a[i] > max)
{
max = a[i];
}
if (a[i] < min)
{
min = a[i];
}
}
int range = max - min + 1;//求出新数组要开辟的大小
//这里我们要注意,一定要把开辟的数组都初始化为0
int* CountArr = (int*)calloc(rang, sizeof(int));
if (NULL == CountArr)
{
perror("calloc fail");
exit(-1);
}
//将原数组中的值相对映射到CountArr中,并在各下标出现的次数
for (i = 0;i < n;i++)
{
CountArr[a[i] - min]++;
}
int j = 0;
//CountArr数组进行排序
for (i = 0;i < rang;i++)
{
while (CountArr[i]--)
{
a[j] = i + min;//加上min恢复原数据
++j;
}
}
free(CountArr);
CountArr = NULL;
}
1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
2. 时间复杂度:O(N+range)
3. 空间复杂度:O(range)
4. 稳定性:稳定三 排序大总结