排序主要有八种,前面6种在之前已经介绍并实现过了。
这是前面的6大牌排序介绍:http://t.csdn.cn/l06fT
归并排序:
基本思想:归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有 序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
下面让我们模拟实现归并排序:
实现的方式是用一个临时数组去存储归并后的数据,然后再拷贝回去,如果直接在原数组去实现的话,就需要进行数据的移动,这样很可能就会导致数据的丢失。归并排序递归实现和二叉树的后序遍历有点像,都是访问都最后,然后出来的左右两组数据之后再递归的根的位置。
void _MergeSort(int* a, int begin, int end, int* tmp) { if (begin >= end) { return; } int mid = (begin+end)/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, (end2 - begin+1) * sizeof(int)); } void MergeSort(int* a, int n) { //创建临时数组 int* tmp = (int*)malloc(n * sizeof(int)); if (tmp == NULL) { perror("malloc fail"); return; } _MergeSort(a, 0, n - 1, tmp); free(tmp); tmp = NULL; }
归并排序并不像快排一样,快排的递归深度为O(N),所以递归的方法对于快排来说用处不大,所以快排通常使用非递归的方式去解决,也就是用队列来模拟递归的过程,对于归并排序来说它的递归深度并不高,是log(N),所以使用递归去实现归并排序也是没有问题。但这里也会实现一下归并排序的非递归方式
归并排序的非递归方式:
对于归并排序的非递归方式,我们不能使用简单的栈或者队列来模拟实现递归的过程,因为递归的过程像后序遍历,但是我们可以使用循环的方式去模拟实现,就像之前所学过的斐波那契数列一样,也可以使用循环的方式直接实现。思路:我们第一次可以一个一个分开,然后两个两个进行归并,然后再4个4个进行归并。当然这其中有些特殊情况,我们需要处理一下。
特殊情况及其出来方式:
1.第一组数据的部分缺失:2.第二组数据全部缺失:
3.第二组数据部分缺失:
以上3种情况都可能出现,可以分开出来,那为什么第二组全部缺失要放在第二组部分缺失前面呢?因为全部缺失是部分缺失的特例,而且它们的处理方式是不一样的,所以必须先单独出来,不然全部缺失的特例就没有办法解决了。
void MergeSortNonR(int* a, int n) { int* tmp = (int*)malloc(n * sizeof(int)); if (tmp == NULL) { perror("malloc fail"); return; } int gap = 1; while (gap < n) { //gap个数据和gap个数据进行排序 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; //第一组数据部分缺失 if (end1 >= n) { break; } //第二组数据全部缺失 if (begin2 >= n) { break; } //第二组数据部分缺失 if (end2 >= n) { //修正一下 end2 = n - 1; } 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; } free(tmp); tmp = NULL; }
归并排序的特性总结:
1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(N) , 使用递归的方式也能较好的解决,因为递归的深度并不高
4. 稳定性:稳定
非比较排序之一计数排序:
非比较排序意思是不使用比较的方式进行排序,其中计数排序是其中比较有用的排序。
但是计数排序有一定的局限性:
首先先介绍计数排序的原理:
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤: 1. 统计相同元素出现次数 2. 根据统计的结果将序列回收到原来的序列中
下面介绍绝对映射和相对映射:
根据上面的分析,我们不难发现,计数排序是有一定的局限性的,就是所排的数据要相对集中,否则浪费的空间就会很多。
实现的方式:
首先我们要先计算出我们要排的最大和最小值,然后算出他们的差值,因为这个是左闭右闭区间,所以要加一才能计算出接下来要开辟数组的大小。然后遍历数组统计出他们分别出现的次数,并保存到tmp数组中,最后把tmp数组的数据进行还原即可。
下面是代码实现:
void CountSort(int* a, int n) { //计数排序首先要找到最大和最小的相减得到新的数组的个数 int max = a[0]; int min = a[0]; for (int i = 0; i < n; i++) { if (a[i] > max) { max = a[i]; } if (a[i] < min) { min = a[i]; } } //因为这里是左闭右闭的区间,所以要加一 int range = max - min + 1; int* tmp = (int*)malloc(range * sizeof(int)); memset(tmp, 0, range * sizeof(int)); for (int i = 0; i < n; i++) { tmp[a[i] - min]++; } //将tmp数组的元素赋值回原数组 int j = 0; for (int i = 0; i < range; i++) { while ((tmp[i])--) { a[j] = i + min; j++; } } free(tmp); tmp = NULL; }
最后总结计数排序:虽然计数排序的时间复杂度是O(N),但是它的使用收到很多的限制,它只适合整形的数据,同时数据之间的差值还要比较小才适用,否则开辟的空间会非常大。
排序算法的总结:
排序中运用最多的是快排。为什么我们要关注排序的稳定性,因为有时的数据要保证数据相对比较有序,例如:在高考都是600分的时候,我们可能就需要按照语文或者其他学科的成绩进行排名。所以我们最好是要保证数据相对稳定。
对于非比较排序来说使用的并不多。