今天看了尚硅谷的数据结构和算法的归并排序课程。虽然最后理解了,但是感觉讲解的并不详细,有些需要自己领悟,所以决定写一篇保姆式教学笔记,希望能帮到大家。
归并排序主要分为三个部分理解
1.分解数组
有如下数组[8,4,5,7,1,3,6,2]
分一次: [8,4,5,7] [1,3,6,2]
分两次: [8,4] [5,7] [1,3] [6,2]
分三次: [8] [4] [5] [7] [1] [3] [6] [2]
第三次分解后,数组都只有一个数字,这样就把问题简单化了
分治法:先分后治
拆分代码为
public static void mergeSort(int[] arr, int left, int right) {
//确定分界处,mid为左数组的最后一个值的索引
int mid = (left + right) / 2;
//当数组长度为一(一个数)时,结束递归(递归出口,不能分了)
if (left < right) {
//左递归
mergeSort(arr, left, mid, temp);
//右递归
mergeSort(arr, mid + 1, right, temp);
}
在理解递归时要注意:1.要把每个递归方法看作一个个独立的个体
2.递归到出口后就会回溯到上一级递归方法,继续执行上一级递归的
剩余代码
所以分解数组执行的顺序为
2.元素排序
子序列排序完毕后,我们就需要对分解后的子序列进行排序
每个子序列的排序是重复的过程:
这里以序列 [8,4,5,7] 和 [1, 3, 6, 2]为例:
拆分代码为
public static void merge(int[] arr, int left, int right, int mid, int[] temp) {
//定义指针
int l = left;//左数组的头指针
int j = mid + 1;//右数组的头指针
int t = 0;//中转数组赋值控制指针
//一. 比较两数组的元素值
//只要左数组和右数组任意一个的指针后移越界,说明比较赋值完毕
while (l <= mid && j <= right) {
//如果右数组的j索引值大于左数组l索引的值,就把l的值赋给中转数组的t。然后把l指针和t指针后移
if (arr[j] > arr[l]) {
temp[t] = arr[l];
l++;
t++;
}
//如果右数组的j索引值小于左数组l索引的值,就把j的值赋给中转数组的t。然后把j指针和t指针后移
else {
temp[t] = arr[j];
j++;
t++;
}
}
//二.将指针未越界数组的剩下值填充到中转数组中,左数组和右数组均有可能
//因为递归操作,每次左右数组都是排序完成的,所以剩下的值也是排序好的
while (l <= mid) {
temp[t] = arr[l];
l++;
t++;
}
while (j <= right) {
temp[t] = arr[j];
j++;
t++;
}
//三.将中转数组copy给arr数组
//注意:此时并非将中转数组的所有值全copy过去(提高效率),而是将每次递归操作合并后的数组copy到arr的相应位置
int templeft = left;
t = 0;
while (templeft <= right) {
arr[templeft] = temp[t];
templeft++;
t++;
}
}
有很多小伙伴不理解最后为什么不直接将中转数组copy给原数组,因为直接copy会产生许多不必要的赋值操作,可以参考下图理解也可以debug分析数据。
可以发现,前三次归并,临时数组的0元素,后三次归并临时数组的[4,5,7,8]元素都是
不需要copy的元素
3.合并数组
合并数组的操作实际上在第二步代码的最后一步操作就完成了,但是程序的执行顺序并非先分解数组再排序合并,而是随递归的回溯,逐步分解合并。
最后代码为:
public class MergeSort {
public static void main(String[] args) {
//原始数组
int[] arr = {8, 4, 5, 7, 1, 3, 6, 2};
//中转数组
int[] temp = new int[arr.length];
//排序
mergeSort(arr, 0, arr.length - 1, temp);
//打印数组
System.out.print(Arrays.toString(arr));
}
//分而合方法
public static void mergeSort(int[] arr, int left, int right, int[] temp) {
//确定分界处,mid为左数组的最后一个值的索引
int mid = (left + right) / 2;
//当数组长度为一(一个数)时,结束递归(递归出口,不能分了)
if (left < right) {
//左递归
mergeSort(arr, left, mid, temp);
//右递归
mergeSort(arr, mid + 1, right, temp);
/*
测试得:左递归和右递归的顺序不会影响程序结果
只不过是合并的顺序相反
*/
//合并
merge(arr, left, right, mid, temp);
}
}
public static void merge(int[] arr, int left, int right, int mid, int[] temp) {
//定义指针
int l = left;//左数组的头指针
int j = mid + 1;//右数组的头指针
int t = 0;//中转数组赋值控制指针
//一. 比较两数组的元素值
//只要左数组和右数组任意一个的指针后移越界,说明比较赋值完毕
while (l <= mid && j <= right) {
//如果右数组的j索引值大于左数组l索引的值,就把l的值赋给中转数组的t。然后把l指针和t指针后移
if (arr[j] > arr[l]) {
temp[t] = arr[l];
l++;
t++;
}
//如果右数组的j索引值小于左数组l索引的值,就把j的值赋给中转数组的t。然后把j指针和t指针后移
else {
temp[t] = arr[j];
j++;
t++;
}
}
//二.将指针未越界数组的剩下值填充到中转数组中,左数组和右数组均有可能
//因为递归操作,每次左右数组都是排序完成的,所以剩下的值也是排序好的
while (l <= mid) {
temp[t] = arr[l];
l++;
t++;
}
while (j <= right) {
temp[t] = arr[j];
j++;
t++;
}
//三.将中转数组copy给arr数组
//注意:此时并非将中转数组的所有值全copy过去(提高效率),而是将每次递归操作合并后的数组copy到arr的相应位置
int templeft = left;
t = 0;
while (templeft <= right) {
arr[templeft] = temp[t];
templeft++;
t++;
}
}
}
可以发现:长度为n的数组归并的次数为n-1次,整个排序的时间复杂度为O(nlogn),归并排序采取空间换时间的方式,排序速度要比冒泡,交换,选择,插入快。