排序算法-08归并排序

归并排序

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);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值