归并排序
1. 算法概述
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
2. 算法原理
归并操作(merge),也叫归并算法,指的是将两个顺序序列合并成一个顺序序列的方法。如:设有数列{6,202,100,301,38,8,1}
- 初始状态:6,202,100,301,38,8,1
- 第一次归并后:{6,202},{100,301},{8,38},{1},比较次数:3;
- 第二次归并后:{6,100,202,301},{1,8,38},比较次数:4;
- 第三次归并后:{1,6,8,38,100,202,301},比较次数:4;
- 总的比较次数为:3+4+4=11;
- 逆序数为14
归并操作的工作原理如下:
- 第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤3直到某一指针超出序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
3. 动图演示
4. 代码实现
从上述动图中,我们可以看到,第一次归并,相邻2个元素为一组(认为单个元素为有序集合),进行归并排序,使其局部有序;第二次归并相邻两个有序集合为一组,进行归并排序。
首先我们先实现两个有序集合如果实现归并,看图如下
因为我们是基于一个数组,将其虚拟拆分为多个有序集合,相邻两个有序集合进行合并,则只需要3个下标就能确定两个集合:第一个有序集合的开始下标 left,第二个有序集合的开始下标 right(同时也是第一个有序集合的结束下标),第二个有序集合的结束下标 end。将上述代码修改后,如下:
/**
* 将两个有序表合并和一个有序表
*
* @param arr
* @param left:第一个有序集合的起始下标
* @param right:第二个有序集合的起始下标,同时也是第一个有序集合的结束下标
* @param end:第二个有序集合的结束下标
*/
private static void merge(int[] arr, int left, int right, int end) {
//创建一个临时数组,用于存储两个有序集合归并后的数据
int[] temp = new int[end - left + 1];
int i = left, j = right, tempIndex = 0;
//遍历第一个有序集合,进行排序
while (i < right && j <= end) {
//当第一个有序集合的元素 <= 第二个集合的第一个元素,则证明 i 最小,补填到temp中
if (arr[i] <= arr[j]) {
temp[tempIndex] = arr[i];
//遍历第一个有序集合下一个元素
i++;
} else {
//第一个有序集合的元素 > 第二个集合的第一个元素,则证明 j 最小,补填到temp中
temp[tempIndex] = arr[j];
//将第二个有序集合的临时下标后移一位,用第一个有序集合的i元素继续和第二个有序集合的元素进行比较
j++;
}
//临时数组下标++
tempIndex++;
}
//第二个集合元素 < 第一个集合元素,剩下第一个集合的元素未补填到temp中,则依次补填
while (i < right) {
temp[tempIndex] = arr[i];
i++;
tempIndex++;
}
//第二个集合元素 >= 第一个集合元素,剩下第二个集合的元素未补填到temp中,则依次补填
while (j <= end) {
temp[tempIndex] = arr[j];
j++;
tempIndex++;
}
//将归并排序好的临时结合,替换至原数组相对位置
//API参数说明: 数据源,数据源开始下标,目标源,目标源开始下标,替换元素个数
System.arraycopy(temp, 0, arr, left, temp.length);
}
归并排序代码如下:
@Test
public void sort() {
int[] arr = new int[]{10, 1, 4, 3, 7, 5, 6, 9, 8, 2};
log.info("排序前:{}", JSON.toJSONString(arr));
//要从 1 开始,即单个元素认为是一个有序集合,开始进行归并
mergeSort(arr, 1);
log.info("排序后:{}", JSON.toJSONString(arr));
}
/**
* @param arr
* @param len:有序集合长度
*/
public static void mergeSort(int[] arr, int len) {
/* 本次归并后,有序集合的长度,是原有序集合长度的2倍
* 假设本次第一次归并,应当将相邻的两个元素为一组(单个元素认为有序),进行排序,有序集合的长度为2 2的1次方
* 假设本次第二次归并,应当将相邻的两个有序集合为一组(上次归并后有序集合长度为2),进行排序,有序集合的长度为4 2的2次方
* 假设本次第三次归并,应当将相邻的两个有序集合为一组(上次归并后有序集合长度为4),进行排序,有序集合的长度为8 2的3次方
* thisLen << 1 为 2的len次方
*/
int thisLen = len << 1;
//中间值
int mid = arr.length / thisLen;
//如果有序数组长度>数组长度一半,则表示已经排序完成,此时mid为0.
if (mid == 0) {
return;
}
//两个相邻的有序集合为一组,进行归并,并排序。总是从原集合的第一个元素开始
for (int i = 0; i < mid; ++i) {
/*
* i * length 一致向后循环进行排序
* 当len=1,则thisLen=2,那么{0},{1}为一组,那么下一组为{2},{3}
* 第一组
* 第一个有序集合开始下标符合:left = i(0) * thisLen(2) = 0;{0}
* 第二个有序集合开始下标符合:right = left(0) + len(1) = 1;{1}(第一有序集合开始下标+有序集合长度)
* 结束下标符合:left(0) + thisLen(2) - 1; 1(第一个有序集合开始下标 + 合并后整组长度 -1)
* 第二组
* 第一个有序集合开始下标符合:left = i(1) * thisLen(2) = 2;{2}
* 第二个有序集合开始下标符合:right = left(2) + len(1) = 3;{3}(第一有序集合开始下标+有序集合长度)
* ...
* 当len=2,则thisLen=4,那么{0,1},{2,3}为一组,那么下一组为{4,5},{6,7}
* 第一组
* 第一个有序集合开始下标符合:left = i(0) * thisLen(4) = 0;{0,1}
* 第二个有序集合开始下标符合:right = left(0) + len(2) = 2;{2,3}
* 第二组
* 第一个有序集合开始下标符合:left = i(1) * thisLen(4) = 4;{4,5}
* 第二个有序集合开始下标符合:right = left(4) + len(2) = 2;{6,7}
*
* 由此可见,每一个有序集合开始下标为 left= i * length
*/
//每组内第一个有序集合开始下标
int left = i * thisLen;
//即是第一个有序集合结束下标,又是第二个有序集合开始下标
int right = left + len;
//第二个有集合结束下标
int end = left + thisLen - 1;
//将相邻两个有序集合归并成一组(一个新的大的有序集合)
merge(arr, left, right, end);
}
/*
* right为0则表示整个数组长度被length整除,没有剩余未排序元素
* 取模运算。7 % 4 等价于 7 & 3
* 如果==0,则表示被整除,没有未参与归并的元素
*/
int remainder = arr.length & (thisLen - 1);
if (remainder != 0) {
/*
* 将未参与归并的数据,再次进行归并排序,将剩下未参与归并的元素和倒数前一个有序集合归并
* 倒数第一个前一个有序集合
* 结束下标:arr.length - 余数(remainder)(同时也是未参与归并排序元素集合的开始下标)
* 开始下标:arr.length - 余数(remainder) - 合并后有序集合的长度(thisLen)
*/
int left = arr.length - remainder - thisLen;
int right = arr.length - remainder;
int end = arr.length - 1;
//将剩下未参与归并的元素和倒数前一个有序集合归并
merge(arr, left, right, end);
}
//递归执行下一趟归并排序
mergeSort(arr, thisLen);
}