算法通关村第10关——归并排序(黄金)
归并排序(MERGE-SORT)简单来说就是将大的序列先视为若干个比较小的数组,分成几个比较小的结构,然后是利用归并的思想实现的排序方法,该算法采用经典的分治策略(分就是将问题**分(divide)成一些小的问题分别求解,而治(conquer)**则将分的阶段得到的各答案"合"在一起)。
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,就是下侧的满二叉树。比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],这个操作与合并两个有序数组的完全一样,不同的是这里是将数组的两个部分合并。
在看一下遍历时处理元素的过程:
由图的线就可以看出算法的执行过程
写出如下的代码:
- 首先,定义一个mergeSort方法,接收一个数组、起始位置、结束位置和一个临时数组作为参数。
public static void mergeSort(int[] array, int start, int end, int temp[]) {
}
- 在mergeSort方法中,首先判断起始位置是否小于结束位置。如果不满足,则说明只有一个元素或没有元素需要排序,直接返回。
public static void mergeSort(int[] array, int start, int end, int temp[]) {
if(left < right){
return;
}
}
如果起始位置小于结束位置,则将数组一分为二,分别调用mergeSort方法对左半部分和右半部分进行排序。
具体做法是通过计算起始位置和结束位置的中间值,将数组分为两部分,左半部分的起始位置为start,结束位置为middle,右半部分的起始位置为middle+1,结束位置为end。
递归地调用mergeSort方法对左半部分和右半部分进行排序,直到每个子数组都只剩下一个元素或没有元素。
public static void mergeSort(int[] array, int start, int end, int temp[]) {
if(start >= end){
return;
}
mergeSort(array, start, (start + end)/2, temp);
mergeSort(array, (start + end)/2, end, temp);
}
下面用图讲解一下:
初始数组:[6, 3, 2, 1, 4, 5, 8, 7]
第一次分割:
[6, 3, 2, 1] [4, 5, 8, 7]
第二次分割:
[6, 3] [2, 1] [4, 5] [8, 7]
第三次分割:
[6] [3] [2] [1] [4] [5] [8] [7]
接下来,调用merge方法将左右两个已排序的子数组合并起来。merge方法接收一个数组、起始位置、结束位置和一个临时数组作为参数。
public static void mergeSort(int[] array, int start, int end, int temp[]) {
if(start >= end){
return;
}
mergeSort(array, start, (start + end)/2, temp);
mergeSort(array, (start + end)/2, end, temp);
merge(array, start, end, temp);
}
在merge方法中,首先定义三个变量,分别表示左半部分的起始位置left,右半部分的起始位置right,以及临时数组的索引位置index。
static void merge(int[] array, int start, int end, int[] temp) {
}
使用while循环比较左右两个子数组的元素大小,将较小的元素依次放入临时数组中。当其中一个子数组的所有元素都放入临时数组后,跳出循环。
static void merge(int[] array, int start, int end, int[] temp) {
int middle = (start + end) / 2;
int left = start;
int right = middle + 1;
int index = left;
while (left <= middle && right <= end) {
if (array[left] < array[right]) {
temp[index++] = array[left++];
} else {
temp[index++] = array[right++];
}
}
}
如果左半部分还有剩余元素,将其依次放入临时数组中。
如果右半部分还有剩余元素,将其依次放入临时数组中。
while (left <= middle) {
temp[index++] = array[left++];
}
while (right <= end) {
temp[index++] = array[right++];
}
最后,将临时数组中的元素复制回原数组中,从起始位置到结束位置,完成排序。
for (int i = start; i <= end; i++) {
array[i] = temp[i];
}
开始合并过程,将子数组按照顺序合并成有序数组。
第一次合并:
[3, 6] [1, 2] [4, 5] [7, 8]
第二次合并:
[1, 2, 3, 6] [4, 5, 7, 8]
第三次合并:
[1, 2, 3, 4, 5, 6, 7, 8]
最终,得到了一个有序的数组。
在main方法中,我们可以测试mergeSort方法。创建一个测试数组,然后创建一个与测试数组长度相同的临时数组。调用mergeSort方法对测试数组进行排序,并打印结果。
public class MergeSort {
public static void mergeSort(int[] array, int start, int end, int temp[]) {
if (start >= end) {
return;
}
mergeSort(array, start, (start + end) / 2, temp);
mergeSort(array, (start + end) / 2 + 1, end, temp);
merge(array, start, end, temp);
}
public static void merge(int[] array, int start, int end, int[] temp) {
int middle = (start + end) / 2;
int left = start;
int right = middle + 1;
int index = left;
while (left <= middle && right <= end) {
if (array[left] < array[right]) {
temp[index++] = array[left++];
} else {
temp[index++] = array[right++];
}
}
while (left <= middle) {
temp[index++] = array[left++];
}
while (right <= end) {
temp[index++] = array[right++];
}
for (int i = start; i <= end; i++) {
array[i] = temp[i];
}
}
public static void main(String[] args) {
int[] array = {6, 3, 2, 1, 4, 5, 8, 7};
int[] temp = new int[array.length];
mergeSort(array, 0, array.length - 1, temp);
System.out.println(Arrays.toString(array));
}
}
那我们再来写一下力扣的排序数组题目:
912. 排序数组
class Solution {
public int[] sortArray(int[] nums) {
// 创建一个临时数组,用于存储合并过程中的临时结果
int[] temp = new int[nums.length];
// 调用归并排序函数
mergeSort(nums, 0, nums.length-1, temp);
return nums;
}
private void mergeSort(int[] nums, int start, int end, int[] temp) {
if (start < end) { // 当起始位置小于结束位置时,继续拆分数组
int mid = (start + end) >> 1; // 计算中间位置
// 分别对左右两个子数组进行归并排序
mergeSort(nums, start, mid, temp); // 对左半部分进行排序
mergeSort(nums, mid+1, end, temp); // 对右半部分进行排序
// 合并两个有序的子数组
merge(nums, start, mid, end, temp);
}
}
private void merge(int[] nums, int start, int mid, int end, int[] temp) {
int left = start; // 左子数组的起始位置
int right = mid + 1; // 右子数组的起始位置
int index = start; // 临时数组的起始位置
// 比较左右子数组的元素,将较小的元素放入临时数组中
while (left <= mid && right <= end) {
if (nums[left] < nums[right]) {
temp[index++] = nums[left++];
} else {
temp[index++] = nums[right++];
}
}
// 将左子数组中剩余的元素放入临时数组中
while (left <= mid) {
temp[index++] = nums[left++];
}
// 将右子数组中剩余的元素放入临时数组中
while (right <= end) {
temp[index++] = nums[right++];
}
// 将临时数组中的元素复制回原数组中
for (int i = start; i <= end; i++) {
nums[i] = temp[i];
}
}
}