目录
1.归并排序图解
2.归并排序(递归版)
3.归并排序(非递归版)
1.归并排序图解
归并排序的核心思想是让左右两边有序的部分进行合并比较排序,具体什么意思呢?分两点:
1.分:左右两边要有序,如何做到左右两边有序呢?看上面的图,当我们将一个数组进行分离,一直分,知道左右两边进行对比的数只有一个,这种情况下肯定有序。
2.治:这个过程就是将左右两边数进行排序,然后排好后再进行重复操作,就将整个数组置为有序了。
我们用代码来加深整个过程的理解。
2.归并排序(递归版)
我们先来讲这个递归版本的代码思想:
我这里传的四个参数分别是:a--数组,begin--头下标,end--尾下标,tmp用来拷贝的数组。
从图中我们不难发现分的过程是一个除以2倍的关系,所以我们用mid来记录接下来递归的左边尾下标和右边头下标,递归终止条件就是头下标大于等于尾下标的时候,接下来我们进行的操作就是定义一下左边的头尾下标和右边的头尾下标,还有一个i用作tmp的下标,接下来就开始比对了,我们进行升序排序,所以我们把小的往tmp里放,循环的结束条件就是左右两边随便一边走完就停止,接下来再把没输完的全部输进去就好,不要忘记我们的tmp充当的是临时拷贝的角色,我们要改动的是数组a,所以我们使用库函数memcpy来将tmp里的数拷贝到a中,这样就模拟了治的过程,也就是说每一趟递归我们a里的值跟上面图片治部分的每一行是一样的。
我们来看看结果:
归并是一种时间复杂度为O(NlogN)的排序算法,而且它的稳定性也比较高。
我们再来感受一下不用递归,归并怎么来写。
3.归并排序(非递归版)
//归并排序 非递归实现 void MergeSortNrec(int* a, int begin, int end, int* tmp) { int gap = 1; while (gap < end+1) { for (int i = 0; i < (end - begin + 1); i += 2*gap) { int begin1 = i, end1 = i + gap-1; int begin2 = i + gap, end2 = i + gap * 2-1; int j = begin1; if (end1 >= end+1|| begin2 >= end+1) { break; } if (end2 >= end+1) { end2 = end; } while (begin1 <= end1 && begin2 <= end2) { if (a[begin1] <= a[begin2]) { tmp[j++] = a[begin1++]; } else { tmp[j++] = a[begin2++]; } } while (begin1 <= end1) { tmp[j++] = a[begin1++]; } while (begin2 <= end2) { tmp[j++] = a[begin2++]; } memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1)); } gap *= 2; } }
非递归版我们采用的逻辑还是一样的,只不过实现形式有很大区别,递归中我们是分治,用递归达到分的目的,这里我们没有递归,怎么办呢,我们可以用循环来实现,这里我就不卖关子了,我们的总体思路是直接到分的最后一步,也就是一个跟一个比的情款,然后再往回去走。
所以,上面代码中的gap的作用就是控制对比元素的个数,一开始一个跟一个对比,每一趟新的循环gap阔两倍,然后治的过程,代码跟我们递归版的是相差无几的,但是这里的end1,begin2,end2要注意有可能会发生越界的情况,所以我们要对他们进行判断,当end1和begin2中的任意一个出现越界情况的话,我们就终止循环,因为光是左边的就已经到尾了,就没必要比右边了(右边压根没数据了);如果是end2越界,说明这个数组长度不是gap的整数倍了,我们就将end2重置为尾下标就行了,保证了不会出现越界的行为。