【恋上数据结构】归并排序 + LeetCode真题

经典的十大排序算法!
在这里插入图片描述

前言

务必看一下这个:排序算法前置知识+代码环境准备

当上面的内容都准备好以后,那就开始归并排序吧!

归并排序

1945年由约翰·冯·诺伊曼(John von Neumann)首次提出。

执行流程

  • ① 不断地将当前序列平均分割成 2 个子序列
    直到不能再分割(序列中只剩 1 个元素)
  • ② 不断地将 2 个子序列合并成一个有序序列
    直到最终只剩下 1 个有序序列

在这里插入图片描述

序列分割-divide

sort(0, array.length);
-----------------------------------------------------
/**
 * 对 [begin, end) 范围的数据进行归并排序
 */	
private void sort(int begin, int end){
	if(end - begin < 2) return; // 至少要2个元素
	
	int mid = (begin + end) >> 1;
	sort(begin, mid); // 归并排序左半子序列
	sort(mid, end);	// 归并排序右半子序列
	merge(begin, mid, end); // 合并整个序列
}

序列合并-merge

合并到新序列

将两个序列合并的思路为:左序列和右序列的中元素挨个比较,将较小的放入新序列中,最后新序列中的元素必然升序。

下图中 li,ri 分别代表指向左、右序列的元素索引,ai 为新序列(合并后的序列)的元素索引
【li】代表左序列 li 位置的元素,【ri】代表右序列 ri 位置的元素,【ai】为新序列 ai 位置的元素

  • 第一轮:【li】 < 【ri】,【li】放入新数组,【ai】=【li】,li++; ai++;
  • 第二轮:【li】 > 【ri】,【ri】放入新数组,【ai】=【ri】,ri++; ai++;
  • 第三轮:【li】 < 【ri】,【li】放入新数组,【ai】=【li】,li++; ai++;
  • 第四轮:左序列已经遍历完毕,直接将右序列剩余元素放入新序列,得到新序列(升序)。

在这里插入图片描述

原地合并-merge

将两个序列合并时,不一定要合并到新空间,可以合理的利用原空间实现原地合并

例如:

  • array的左半部分[begin, mid),备份到 leftArray 中;
  • 然后将 leftArray 视为左子序列arrary的右半部分[mid, end] 视为右子序列
  • 将左子序列和右子序列合并到 array 中。

在这里插入图片描述

merge 过程:

  • li < ri
    array[ai] = leftArray[li];
    li++,ai++;
  • li >= ri
    array[ai] = array[ri];
    ri++,ai++;

对序列 { 3, 8, 6, 10 } 进行归并排序:
在这里插入图片描述

对序列 { 3, 6, 8, 10 } 进行归并排序:左子序列先遍历结束,那就归并结束。
在这里插入图片描述
对序列 { 8, 10, 3, 6 } 进行归并排序:右子序列先结束,则将左边剩余全部放入。
在这里插入图片描述

原地合并-merge-实现

/**
 * 将 [begin, mid) 和 [mid, end) 范围的序列合并成一个有序序列
 */
private void merge(int begin, int mid, int end){
	int li = 0, le = mid - begin; // 左边数组(基于leftArray)
	int ri = mid, re = end;	// 右边数组(array)
	int ai = begin; // array的索引
	
	// 备份左边数组到leftArray
	for(int i = li; i < le; i++){
		leftArray[i] = array[begin + i];
	}
	
	// 如果左边还没有结束	
	while(li < le){ // li == le 左边结束, 则直接结束归并
		if(ri < re && cmp(array[ri], leftArray[li]) < 0){ // cmp改为<=0会失去稳定性
			array[ai++] = array[ri++]; // 右边<左边, 拷贝右边数组到array
		}else{
			array[ai++] = leftArray[li++]; // 左边<=右边, 拷贝左边数组到array
		}
	}
}

归并排序完整代码

/**
 * 归并排序
 */
@SuppressWarnings("unchecked")
	public class MergeSort <T extends Comparable<T>> extends Sort<T> {
	private T[] leftArray;
	
	@Override
	protected void sort() {
		// 准备一段临时的数组空间, 在merge操作中使用
		leftArray = (T[])new Comparable[array.length >> 1];
		sort(0, array.length);
	}
	
	/**
	 * 对 [begin, end) 范围的数据进行归并排序
	 */	
	private void sort(int begin, int end){
		if(end - begin < 2) return; // 至少要2个元素
		
		int mid = (begin + end) >> 1;
		sort(begin, mid); // 归并排序左半子序列
		sort(mid, end);	// 归并排序右半子序列
		merge(begin, mid, end); // 合并整个序列
	}
	
	/**
	 * 将 [begin, mid) 和 [mid, end) 范围的序列合并成一个有序序列
	 */
	private void merge(int begin, int mid, int end){
		int li = 0, le = mid - begin; // 左边数组(基于leftArray)
		int ri = mid, re = end;	// 右边数组(array)
		int ai = begin; // array的索引
		
		// 备份左边数组到leftArray
		for(int i = li; i < le; i++){
			leftArray[i] = array[begin + i];
		}
		
		// 如果左边还没有结束
		while(li < le){ // li == le 左边结束, 则直接结束归并
			if(ri < re && cmp(array[ri], leftArray[li]) < 0){ // cmp改为<=0会失去稳定性
				array[ai++] = array[ri++]; // 右边<左边, 拷贝右边数组到array
			}else{
				array[ai++] = leftArray[li++]; // 左边<=右边, 拷贝左边数组到array
			}
		}
	}

}

生成 20000 个取值在[1, 10000] 的随机数进行排序:
在这里插入图片描述

复杂度与稳定性

归并排序花费的时间递推式

  • T(n) = 2 ∗ T(n/2) + O(n)
  • T(1) = O(1)
  • T(n) / n = T(n/2)/(n/2) + O(1)

根据递推式计算复杂度
令 Sn = T(n) / n

  • S(1) = O(1)
  • Sn = S(n/2) + O(1) = S(n/4) + O(2) = S(n/8) + O(3) = S(n/2k) + O(k) = S(1) + O(logn) = O(logn)
  • Tn = n ∗ Sn = O(nlogn)

由于归并排序总是平均分割子列,所以

  • 最好、最坏时间复杂度都 O(nlogn)
  • 归并排序属于稳定排序
  • 归并排序的空间复杂度是 O(n/2 + logn) = O(n)
    n / 2 用于临时存放左侧数组, logn 是因为递归调用

常见的递推式与复杂度

以后遇到复杂的时间复杂度计算,写出递归式直接看这张表即可。
在这里插入图片描述

LeetCode真题

88. 合并两个有序数组

题目地址:88. 合并两个有序数组

题目:

给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 num1 成为一个有序数组。

说明:

  • 初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。
  • 你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。

示例:

输入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6],       n = 3

输出: [1,2,2,3,5,6]

思路一:从前往后合并

原地合并,nums1 从头往后合并,将 nums2 元素插入到 nums1 时需要将挪动元素,效率较低

class Solution {

    public void merge(int[] nums1, int m, int[] nums2, int n) {
    	int li = 0, ri = 0, ai = 0, m2 = m; // 备份一下m    	
    	while(ri < n){ //nums2遍历完则直接结束
    		if(nums1[li] <= nums2[ri] && li < m2){
    			li++;
    			ai++;
    		}else{ // nums1[li] >= nums2[ri]
    			// 要令ri指向元素插入到ai, 首先将 ai到ai+li往后移1位
    			for(int i = m+n-1; i > ai; i--){
    				nums1[i] = nums1[i - 1];
    			}
    			nums1[ai++] = nums2[ri++];
                li++;
                m2++;
    		}
    	}
    }
    
}

在这里插入图片描述

思路二:从后往前合并

在思路一的基础上,将从前往后合并变为从后往前合并

原地合并,nums1 从后往前合并,将 nums2 元素放入 nums1 时无需移动元素,效率较高

class Solution {

	public void merge(int[] nums1, int m, int[] nums2, int n) {
		 int len = m + n; //从后往前移, 需要合并后的总长度
		 for(int i = len - 1; i >=0; i--){
			 if(m>0 && n>0 && nums1[m-1] > nums2[n-1]  || n==0){
				 nums1[i] = nums1[--m];
			 }else{
				 nums1[i] = nums2[--n];
			 }
		 }		 
	 }
	 
 }

在这里插入图片描述

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

萌宅鹿同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值