归并排序
归并分类的方法是算法分析中的重要内容,传统的归并排序(递归实现,空间复杂度为O(n))较为适合初学者理解分治法的思路,同时也很好地体现了递归分解的思想。
基本思想
:
(1)将待排序元素分成大致相同的两个子集合
(2)分别对两个子集合进行排序
(3)将排好序的两个子集合并成一个排好序的集合
一、递归法 (传统+插排改进)。
将长度为n的数组,逐次二分递归,分隔为n个自然有序的单个元素,再两两合并,最终合并为有序的整体。
具体 实现代码
如下:
void mergesort(int* a,int low,int high){
int mid = (low + high) / 2;
if (low < right) {
insert_mergesort(a, low, mid);
insert_mergesort(a, mid + 1, high);
merge(a, low, mid, high);
}
}
void merge(int *a,int low ,int mid,int high ) {
int i, j, k=low;
int b[50] = { 0 }; //假设对50个元素非递减排序
i = low;
j = mid + 1;
while (i <= mid && j <= high) {
if (a[i] <= a[j]) {
b[k] = a[i++];
}
else b[k] = a[j++];
k++;
}
if (i > mid) {
for (int t = j; t <= high; t++)
b[k++] = a[t];
}
else
for (int t = i; t <= mid; t++)
b[k++] = a[t];
for (k = low; k <= high; k++)
a[k] = b[k];
}
当子集合的元素很少时,归并算法多数时间消耗在哪里?在递归的处理上!因此,当子集合的元素个数适当少时,采用在小规模集合上能有效工作的排序方法,而非继续划分,以此克服上述缺点带来的时间消耗。
所以,递归法还可以继续改进,当划分的子区间够小时(一般认为<=15个元素),采用插入排序法,能够加快排序过程。但要注意插入排序函数放置的位置!
void insertsort(int *a, int low,int high)
{
for (int i = low + 1; i <= high; i++) {
int tmp = a[i];
int j = i - 1;
while (tmp < a[j] && j >= low ) {
a[j + 1] = a[j];
j--;
}
a[j + 1] = tmp;
}
}
void merge(int *a,int low ,int mid,int high ) {
int i, j, k=low;
int b[50] = { 0 };
i = low;
j = mid + 1;
while (i <= mid && j <= high) {
if (a[i] <= a[j]) {
b[k] = a[i++];
}
else b[k] = a[j++];
k++;
}
if (i > mid) {
for (int t = j; t <= high; t++)
b[k++] = a[t];
}
else
for (int t = i; t <= mid; t++)
b[k++] = a[t];
for (k = low; k <= high; k++)
a[k] = b[k];
}
void insert_mergesort(int* a,int low,int high){
int tmp = high - low;
int mid = (low + high) / 2;
if (tmp>15) {
insert_mergesort(a, low, mid);
insert_mergesort(a, mid + 1, high);
merge(a, low, mid, high);
}
insertsort(a, low, high); //插入排序放置在递归出口处
}
二、非递归。
将单个元素两两配对后,再不断合并的方法,具体流程如下图所示:
如上图中,2^n 个元素排序容易想到合并方法,但是如果是其他情况呢?你能想到下图的合并方法吗?
对于非 2^n 个元素的序列,排序时总是残留一个小于每次合并size的子序列,例如7这个元素。要使得排序函数具有普适性,就要单独把残留项保留下来,在最后一步时,将前面排好的序列(一定含有2^n个元素)与残留项一起合并,得到最终的结果。
具体 实现代码
如下:
//从low到mid是前一个子序列,mid+1到high是后一个子序列
void merge(int* a, int* b, int low, int mid, int high) {
int k = low;
int i = low;
int j = mid+1;
while (i <= mid && j <= high) {
if (a[i] <= a[j]) {
b[k++] = a[i++];
}
else b[k++] = a[j++];
}
if (i > mid) {
for (int t = j; t <= high; t++)
b[k++] = a[t];
}
else
for (int t = i; t <= mid; t++)
b[k++] = a[t];
for (k = low; k <= high; k++)
a[k] = b[k];
}
//从按照划分的子序列长度进行每一轮的合并
void merge_pass(int *a,int *b,int sum,int size) {
int i;
for (i = 0; i <= sum - 2 * size; i += 2 * size)
merge(a, b, i, i + size-1, i + 2 * size - 1);
if (i + size < sum)
merge(a, b, i, i + size-1, sum - 1);
}
//排序函数
void bottom_up_mergesort(int* a,int low,int high) {
int sum = high - low + 1;
int b[50] = { 0 }; //假设对50个元素排序
int size = 1;
int k = low;
while ( size < sum ) {
//当前合并的序列长度小于原序列长度时,需要将两个子序列合并
merge_pass(a,b,sum,size);
//合并之后的子序列长度变为之前的两倍
size *= 2;
};
}
三、自然合并排序法。
自然合并排序是合并排序算法的一种改进。对于初始给定的数组,通常存在多个长度大于1的已自然排好序的子数组段。
例如,若数组a中元素为 {4,8,3,7,1,5,6,2} , 则自然排好序的子数组段有 {4,8},{3,7},{1,5,6},{2} 。
将他们看做一个个元素集合,用merge()函数进行合并最终得到整体有序的元素序列。
按照如上过程,构成更大的排好序的子数组段 {3,4,7,8},{1,2,5,6},继续合并为 {1,2,3,4,5,6,7,8}。