目录
一、归并排序
归并排序非递归
归并排序特性总结
二、计数排序
计数排序特性总结
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用 。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。动态演示:实现思路:
对于归并思想我想大家都不陌生,在我前面的博文—“链表OJ经典题浅刷”中第四题“合并两个有序链表”中用到的就是归并的思想,感兴趣大家可以去看一下:链表OJ经典题浅刷< 1 >(看完不再害怕链表题)_Fan~Fan的博客-CSDN博客
归并无非就是将两个有序的数据量合并在一起,假设我们给定两个有序数组,要想将两个有序数组合并在一起,我们需要创建一个第三方数组。两个有序数组从下标0开始依次比较数据量的大小,小的放入第三方数组中,一直比较直到两个有序数组的数据都放入第三方数组中。
对于两个有序数组的合并我们可以用到归并思想,那对于一个无序数组的排序,我们如何用到归并的思想呢?这里我们需要用到分治,我们可以将数组分为两份,两份分为四份,四份分为八份.....直到分到一份一份的。然后进行归并,两个一份归并成一个两份,两个两份归并成一个四份,两个四份归并成一个八份.....一直归并直到整体有序。
画图演示:
下面我们先来打印一下其分解递归过程:
✍代码展示:
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, end1 = mid; int begin2 = mid + 1, end2 = end; int index = begin; while (begin1 <= end1 && begin2 <= end2) { if (a[begin1] < a[begin2]) { tmp[index++] = a[begin1++]; } else { tmp[index++] = a[begin2++]; } } //如果begin2先走完,将begin1后面的元素拷贝到第三方数组 while (begin1 <= end1) { tmp[index++] = a[begin1++]; } //如果begin1先走完,将begin2后面的元素拷贝到第三方数组 while (begin2 <=end2) { tmp[index++] = a[begin2++]; } //归并结束,把tmp数组里面归并的值拷贝到原数组中 memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int)); } //归并排序 void MergeSort(int* a, int n) { //开辟第三方数组 int* tmp = (int*)malloc(sizeof(int) * n); assert(tmp); _MergeSort(a, 0, n - 1, tmp); free(tmp); }
基本思想:
在我们快排中,递归改为非递归需要借助栈,而归并排序直接用循环即可。
在递归版本中我们要把数组分解成最小数据单位然后进行合并,而在非递归版本中我们直接把它看成最小数据单位然后不断扩大间距进行归并。
下面我们来打印一下伪代码:
上述情况是在理想状态下可行,也就是我们的数组的长度为2的次方倍,倘若数组的长度不为2的次方倍,那就会出现问题。接下来我们来看一下长度为6时的情况:
下面我们来总结一下可能出现的越界的情况:
1、end1越界:
end1越界,begin2和end2也越界。
2、begin2越界:
begin2越界,end2也越界。
✍代码展示:
// 归并排序非递归实现 void MergeSortNonR(int* a, int n) { int* tmp = (int*)malloc(sizeof(int) * n); assert(tmp); int gap = 1; while (gap < n) { //间距为gap为一组,两两归并 for (int i = 0; i < n; i += 2 * gap) { int begin1 = i, end1 = i + gap - 1; int begin2 = i + gap, end2 = i + 2 * gap - 1; //end1越界,进行修正 if (end1 >= n) { end1 = n - 1; } //begin2越界,第二个区间不存在 if (begin2 >= n) { begin2 = n; end2 = n - 1; } //begin2不越界,end2越界,修正下end2 if (begin2 < n && end2 >= n) { end2 = n - 1; } int index = i; while (begin1 <= end1 && begin2 <= end2) { if (a[begin1] < a[begin2]) { tmp[index++] = a[begin1++]; } else { tmp[index++] = a[begin2++]; } } while (begin1 <= end1) { tmp[index++] = a[begin1++]; } while (begin2 <= end2) { tmp[index++] = a[begin2++]; } } memcpy(a,tmp,n*sizeof(int)); gap *= 2; } free(tmp); }
:归并排序的缺点在于需要O(N)的空间复杂度,归并排序所要解决的更多的是在磁盘中的外排序问题。
内排序和外排序:
在排序中我们分为两种排序—内排序和外排序
内排序:数据量比较少,在内存中直接进行排序。
外排序:数据量比较多,从磁盘中获取数据排序。
对于我们前面所讲到的排序,其只适用于内排序。而对于归并排序,其既适用于内排序又适用于外排序,要解决数据量较大的排序,我们就只能使用归并排序进行解决。
假如我们要对10亿个整数进行排序,10亿个整数大概占据内存空间4G,而我们只有1G的运行内存,数据量太大,无法直接加载到内存,这就导致内排序无法解决此类问题,而对于归并排序,我们可以将这4G文件等分为4等份,然后分别读到内存进行归并排序,排完后读写回磁盘文件。
:时间复杂度:O(N*logN)
:空间复杂度:O(N)
:稳定性:稳定
基本思想:
计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。操作步骤:1. 统计相同元素出现次数2. 根据统计的结果将序列回收到原来的序列中动态演示:实现思路:假如我们要对下面的数组进行计数排序:计数排序的核心步骤就是数组中数字的值为多少,我们就把该值映射到新开辟数组的下标上。 我们看到上面数组中的最大值为9,那我们就需要开辟10个空间才能保证数组中的值都能够得到映射。我们遍历原数组,根据数组元素的多少在新开辟的数组中进行映射。上述方法即是哈希的绝对映射思想,此方法所存在的一个大的问题就是空间复杂度过大,对于小的数据还可以,如果我们对数据{ 10000,5000,6546,8888,7778}进行排序,我们就需要开辟10001个空间的数组,得不偿失。并且数组前5000个空间并没有值进行映射,这就导致了大量空间的浪费,因此针对较大的数据,我们用相对映射思想。
相对映射:此时新数组下标范围为[ 0,max-min ],映射到下标的位置为原数组的值-原数组min值。
相对映射相比较绝对映射避免了空间的大量浪费。
✍代码展示:
//计数排序 void CountSort(int* a, int n) { 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]; } //求新数组范围 int range = max - min + 1; //开辟新数组 int* countA = (int*)malloc(sizeof(int) * range); assert(countA); //把新开辟的数组都初始化为0 memset(countA, 0, sizeof(int) * range); //计数 for (int i = 0; i < n; i++) { countA[a[i] - min]++; } //排序 int j = 0; for (int i = 0; i < range; i++) { while (countA[i]--) { a[j++] = i + min; } } free(countA); }
:计数排序在数据范围集中时,效率高,但是适用范围及场景都很有限。
:时间复杂度:O(N+range)
:空间复杂度:O(range)
:稳定性:稳定