归并排序概述:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
归并操作实现思路
所谓归并操作就是将所有分割后的序列按照顺序合并一起。使用分治的思想解决问题。
归并操作的实现思路就是先将所有元素进行分割成子序列,直到不能在分割,再使每个子序列有序,再使子序列段间有序。
将已有序的子序列合并,得到完全有序的序列;
所有合并操作完成后便实现了归并排序。
例如:
该图片可以很清楚的看出,先将无规则元素全部进行分割,直到形成单个元素。
此时开始合并,先是合并两个元素,确保两个元素有序。
再合并4个元素,确保4个元素有序。
重复合并操作直到完成所有所有元素的合并,解决问题。
因此其实现步骤总结有三步:
- 分割所有元素,直到元素成单个分布。
- 从单个开始合并元素,合并的过程确保合并元素有序。
- 重复第二步操作,直到所有元素合并完成。
总的来说,归并排序的思路是先分后合。
代码实现:
在分析图中可以很清晰看出来,用递归的做法就能解决本分割后再回来合并的元素标号问题。
因此采用递归的思路来解决问题。
首先明确实现需要走的三个步骤:
- 分割所有元素,直到元素成单个分布。
- 从单个开始合并元素,合并的过程确保合并元素有序。
- 重复第二步操作,直到所有元素合并完成。
我们根据以上步骤一步一步来实现代码:
1 . 分割所有元素,直到元素成单个分布。
//通过递归的方式来分割数组,分割到元素成单个后不再分割
//这里我们用两个下标变量来定义元素区间的左右边界,当元素区间左右边界重合时说明只有一个元素
public static void mergeSort1(int beginIndex,int endIndex){
if (beginIndex == endIndex){ //左右区间重合,说明只有一个元素,此时退出递归
return ;
}
int mid = (beginIndex + endIndex) / 2; //分割区间,用每次分割区区间的中点为分隔线
int rightIndex = mid + 1; //记录右边区间开始边界,这里将中间分割线向右移动一个下标,来定义右边区间开始线,中间分割线作为左边区间的结束线
mergeSort1(beginIndex ,mid ); //开始分割左边
mergeSort1(rightIndex,endIndex); //开始分割右边
}
2 . 从单个开始合并元素,合并的过程确保合并元素有序。
由于递归的特点,在分割完后会退回到上一次分割的位置,因此在执行完所有分割操作后,接下来的代码就是合并操作的代码,也就是从单个开始合并元素的代码。
对于合并操作基本思路是:
-
建立一个新数组,用来合并归并后排序的元素。
-
由于此时返回的子序列已经有序了,因此从两个子序列的第一个元素开始遍历,寻找符合要求的元素,并且放入新数组中。
例如:
此时合并的子序列如图。
假如我们要构建升序序列,此时从两个子序列的首个元素开始遍历寻找符合要求的元素。
此时符合要求的第一个元素就是最大的元素,因此将4元素放入新数组。
此后再将4的遍历指针向后移,重复此操作,若出现了某个序列遍历完了之后,将另一个序列直接放入新数组的末端。
此时右边已经遍历完成,将左边的所有剩余元素放到新数组的最后:
合并完成后,再将新数组的值复制到原来的数组的对应位置上。
直接上代码:
public static void mergeSort(int beginIndex, int endIndex) {
if (beginIndex == endIndex) {
return;
}
int resIndex = beginIndex; //定义一个开始标志,用于在合并的最后记录新数组应该复制的位置
int length = endIndex - beginIndex + 1;
int mid = (beginIndex + endIndex) / 2;
int rightIndex = mid + 1;
mergeSort(beginIndex, mid);
mergeSort(rightIndex, endIndex);
/* 以上是分割的代码
* -------------------------------------------------------------------------------------
* 以下是合并的代码
*/
boolean flag = false; //定义一个标志位,用于区分哪个区域元素先遍历完
int tempArr[] = new int[length]; //定义一个新数组用于存放
int tempIndex = 0; //定义一个临时下表用于新数组存放的元素位置
while (beginIndex <= mid) { //开始遍历
if (arr[beginIndex] > arr[rightIndex]) { //如果左边元素大于右边元素,左边元素放入新数组并且向后移动
tempArr[tempIndex++] = arr[beginIndex++];
} else { //如果右边元素大于左边元素,右边元素放入新数组并且向后移动
if (rightIndex + 1 <= endIndex) { //做一个边界处理
tempArr[tempIndex++] = arr[rightIndex++];
} else { //若到达边界,标志位设置为true,此时标志着右边遍历先结束
tempArr[tempIndex++] = arr[rightIndex++];
flag = true; //若此时右边先遍历完,将标志位设置一下
break;
}
}
}
if (flag) { //根据标志位信息,将剩余的区域元素放到新数组最后
while (beginIndex <= mid) {
tempArr[tempIndex++] = arr[beginIndex++];
}
} else {
while (rightIndex <= endIndex) {
tempArr[tempIndex++] = arr[rightIndex++];
}
}
for (int i = 0; i < tempIndex; i++) { //将新数组复制到合并的区间中
arr[resIndex++] = tempArr[i];
}
}
由于遍历的特点,经过以上代码后,合并便完成,不再赘述。
测试代码:
public static void main(String[] args) {
int arr[] = {1, 23, 456, 48, 897, 456, 12, 456, 789, 165, 32132, 456, 897, 156, 56, 41, 2, 4, 6, 8, 7};
mergeSort(arr,0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
运行结果:
复杂度:
时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 是否稳定 |
---|---|---|---|---|
O(n Log n) | O(n Log n) | O(n Log n) | O(n) | 是 |