欢迎大家参观我的新网站,悦来
前面我们了解了鸡尾酒排序和桶排序,今天来学习归并排序,首先了解一下什么是归并排序:归并排序(合并排序)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(分而治之)的一个非常典型的应用将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,使再子序列段间有序看不明白,没关系,用图来直观展示:
首先准备一列待排序的数,将这一列待排序的数就行分割,分为左右两部分,我们这样考虑: 将左右两边的数进行排序,我们比较左右两边最小的数,就可以确定整列数中的最小数,一次这样进行比较次小数,就可以得到从小到大排列的全部数,这是典型的分治思想.将一个打的问题拆分为小的问题,我们这里要解决的是全部数的排序,分解为了左边排序,右边排序,再比较的分问题. 经过递归操作,就得到了上图中的步骤.
我们再来看一下动图:
是否明白了归并排序的原理了呢,就是分而治之,再归并统一.
下面我们看一下Java实现的归并排序代码:
private static void sort(int[] array) {
int[] resultArr = new int[array.length];
merge(0, array.length-1, resultArr, array);
System.out.println(Arrays.toString(resultArr));
}
private static void merge(int left, int right, int[] resultArr, int[] array) {
if (left >= right) {
return;
}
// 中间分割点
int middle = (left + right) / 2;
// 左边起点与终点
int leftStart = left;
int leftEnd = middle;
// 右边起点与终点
int rightStart = middle + 1;
int rightEnd = right;
System.out.println(leftStart+"---"+leftEnd);
// 左边再次分割
merge(leftStart, leftEnd, resultArr, array);
// 右边再次分割
merge(rightStart, rightEnd, resultArr, array);
// 进行比较归并
int i = left;
while (leftStart <= leftEnd && rightStart <= rightEnd) {
resultArr[i++] = array[leftStart] < array[rightStart] ? array[leftStart++] : array[rightStart++];
}
while (leftStart <= leftEnd) {
resultArr[i++] = array[leftStart++];
}
while (rightStart <= rightEnd) {
resultArr[i++] = array[rightStart++];
}
for (i = left; i <= right; i++) {
array[i] = resultArr[i];
}
}
大家可以看出来,是用递归实现的,有一些地方要稍微讲解一下
下面有三个while循环,第一个看判断条件,可以看出来是当左右两边有任意一边没有比较结束,就进行比较
第二个while循环是判断如果右边已经比较结束,则把左边数据依次赋值给结果数组
第三个while循环是判断左边比较已经结束,则把右边数据依次赋值给结果数组,经过三次循环比较,就可以将左右两边的数据比较完全.
还有一个for循环,看样子是把结果数组又赋值给了原数组,其实是因为,结果数组存放的是左右两边比较之后的结果,需要把比较之后的结果重新赋值给原数组,进行下一次的比较,不然的话,原数组永远不会变,比较也就没有了任何意义.
举个列子:
比如: 4 6 7 1
我们要首先比较4 和 6得到4 6,比较7 和 1得到1 7,这使得结果数组是4 6 1 7,原数组是4 6 7 1,下一次进行比较的时候,如果原数组不变,就变成了比较 4和7,6和1了,会得到错误结果4 7 1 6,而我们希望比较的是,4和1,6和7,所以需要把上一次的结果数组替换掉原数组,就得到了正确结果1 4 6 7.
我们不用递归用迭代来实现一下,代码如下:
private static void sort2(int[] array) {
int[] result = new int[array.length];
// 分割之后左右两边的数据长度
for (int i = 1; i < array.length; i *= 2) {
// 遍历左右两边的数据块
for (int j = 0; j < array.length; j += i * 2) {
int middle = (j + i) < array.length ? j + i : array.length;
int left = j; // 记录最左边的下标
int leftBegin = j;//左边数据快左始下标
int leftEnd = middle;//左边数据快结束下标
int rightBegin = middle;//右边数据快起始下标
//右边数据快结束下标
int rightEnd = (j + 2 * i) < array.length ? (j + 2 * i) : array.length;
while (leftBegin < leftEnd && rightBegin < rightEnd) {
result[left++] = array[leftBegin] < array[rightBegin] ? array[leftBegin++] : array[rightBegin++];
}
while (leftBegin < leftEnd) {
result[left++] = array[leftBegin++];
}
while (rightBegin < rightEnd) {
result[left++] = array[rightBegin++];
}
}
int[] temp = array;
array = result;
result = temp;
}
result = array;
System.out.println(Arrays.toString(result));
}
迭代版与递归版的一点不同是,递归进行了数据分割,先排左边,再排右边,最后归并,迭代使用了数据块长度来进行左右分割的,并且是左右排序同时进行,所以迭代中的for循环更新原数组,在这里就变成了数组之间的直接赋值,因为上一步的结果数组是所有的左右数据块排序之后的.
在Java的Arrays.sort()排序函数中,进行了优化,不同的数组,不同的长度选用的排序算法也不一样,如果小于47则使用插入排序,如果数组长度小于286的时候而大于47,优先使用快速排序,而且还有判断数组是否已经接近排好顺序,数组是否高度结构化等等条件,在真实项目中直接用就可以,一般来说比你自己写的要高效得多,但我们还是要知道这些算法的具体实现.