算法与数据体系课笔记之-4. 归并排序

思维导图链接

算法与数据结构思维导图

参考左程云算法课程

4.归并排序和几个经典题目分析 总览

在这里插入图片描述

题目1:归并排序法的实现

题目描述:
  • 实现归并排序法,分别用递归法和非递归法
代码实现:
递归实现:
package class04;

import java.util.Arrays;

/**
 * 
 * @author LiuS
 *    归并排序多用于对象排序,可以稳定排序
 * java库中,TimSort是改进的MergeSort,小于某值,用二分插入排序
 * https://www.cnblogs.com/sunshuyi/p/12680918.html
 *  小数据用插入原因是,深度已经不是主要影响因素,新建数组,两两比较复杂度会高于插入过程
 */
public class Code01_MergeSort {
	// 小于此阀值,使用插入排序
	private static final int MIN_MERGE = 32;
	// 递归实现
	public static <E extends Comparable<E>> void sort1(E[] arr) {
		if(arr == null || arr.length < 2) return;
		
		// 递归前,先将辅助数组准备好,减少每次递归重新开辟空间的消耗
		E[] temp = Arrays.copyOf(arr, arr.length); // temp小标从0开始
		
		// 递归函数
		sort1(arr, 0, arr.length - 1, temp);
	}
	private static <E extends Comparable<E>>void sort1(E[] arr, int left, int right, E[] temp) {
		// 1. 递归终止条件,代码优化,小数据用插入
		if(right - left <= MIN_MERGE) {
			insertionSort(arr, left, right);
			return; // 千万别忘记返回了
		}
		
		// 2. 处理当前层
		int mid = left + ((right - left) >> 1);
		
		// 3. 将较大问题转为较小问题
		sort1(arr, left, mid, temp);
		sort1(arr, mid + 1, right, temp);
		
		// 4. 现场恢复,将较小问题组合成较大问题
		// 代码优化,排除左边均小于右边的情况
		if(arr[mid].compareTo(arr[mid + 1]) > 0) {
			merge(arr, left, mid, right, temp);
		}
	}
	
	private static <E extends Comparable<E>>void merge(
			E[] arr, int left, int mid, int right, E[] temp) {
		// 1. 先定义好指针
		int pL = left; // 左区间指针
		int pR = mid + 1;// 右区间指针
		int t = left;  // 临时数组指针
		
		// 2. 合并区间
		// 先处理区间不越界情况
		while(pL <= mid && pR <= right) {
			temp[t ++] = arr[pL].compareTo(arr[pR]) <= 0 
					? arr[pL ++] : arr[pR ++];
		}
		//若区间越界,要么pL越界了,要么pR越界了
		while(pL <= mid) {
			temp[t ++] = arr[pL ++];
		} 
		while(pR <= right) {
			temp[t ++] = arr[pR ++];
		}
		
		// 3. 将temp数组元素copy回原数组指定区间位置
		t = left;
		while(left <= right) {
			arr[left ++] = temp[t ++];
		}
		// 也可以用库函数
//		System.arraycopy(temp, left, arr, left, right - left + 1);
	}
	
	// 插入排序法
	private static <E extends Comparable<E>>void insertionSort(E[] arr, int left, int right) {
		for(int i = left; i <= right; i ++) {
			E target = arr[i];
			int j = i;
			for(; j - 1 >= left; j --) {
				if(arr[j - 1].compareTo(target) > 0) {
					arr[j] = arr[j - 1];
				} else {
					break;
				}
			}
			arr[j] = target;
		}
		
	}
}

非递归实现:
// 非递归实现
	public static <E extends Comparable<E>> void sort2(E[] arr) {
		if(arr == null || arr.length < 2) return;
		
		E[] temp = Arrays.copyOf(arr, arr.length);		
		int N = arr.length; // 边界条件
		// 1. 定义步长,即每次merge要合并的左右区间长度,mid-left+1
		int mergeSize = 1;  
		
		while(mergeSize < N) { // 步长不能越界
		// 2. 定义好要处理的左区间起始指针,右区间尾指针
			int pL = 0; // 要处理的左区间起始指针
			while(pL < N) { // 指针要依次往右推进,不能越界
				int mid = pL + mergeSize - 1; // 左区间最后一个元素
				if(mid >= N) break; // 说明左组不够或没有右组,无需再与右组合并
				// 右区间最后一个元素,可能越界,若越界就是区间最后一个元素
				int pR = Math.min(mid + mergeSize, N - 1);
		// 3. 将划分好的左右区间合并
				merge(arr, pL, mid, pR, temp);
				
				pL = pR + 1; // 依次处理下个要合并的左右区间
			}
			
		// 4. 维护mergeSize,依次处理更大的区间问题
			// 防止数据范围是否溢出,即N在数据范围的边缘。mergeSize太靠近,*2会越界
			if(mergeSize > N / 2) {
				break; // 等于N/2时,还要处理,再扩大两倍,总的区间长度一定不小于N,所有数一定处理完了
			}
			mergeSize <<= 1;
		}		
	}
代码测试:
public static void main(String[] args) {
		int n = 1000000;
		Integer[] arr = ArrayGenerator.generateRandomArray(n, n);
		Integer[] arr1 = Arrays.copyOf(arr, arr.length);
		SortingHelper.sortTime("MergeSort1", arr);
		SortingHelper.sortTime("MergeSort2", arr1);
	}
MergeSort1, n = 1000000 : 0.334222 s
MergeSort2, n = 1000000 : 0.282146 s

题目2:求数组小和

题目描述:
  • 在一个数组中,一个数左边比它小的数的总和,叫数的小和,所有数的小和累加起来,叫数组小和。求数组小和。
    例子: [1,3,4,2,5]
    1左边比1小的数:没有
    3左边比3小的数:1
    4左边比4小的数:1、3
    2左边比2小的数:1
    5左边比5小的数:1、3、4、 2
    所以数组的小和为1+1+3+1+1+3+4+2=16
代码实现:
import java.util.Arrays;

public class Code02_SmallSum {
	public static int smallSum(int[] arr) {
		if (arr == null || arr.length < 2) {
			return 0;
		}
		int[] temp = Arrays.copyOf(arr, arr.length);
		return smallSum(arr, 0, arr.length - 1, temp);
	}

	// arr[left..right]既要排好序,也要求小和返回
	// 所有区间小数和累加,即,左区间,右区间,以及左右合并的大区间
	private static int smallSum(int[] arr, int left, int right, int[] temp) {
		if (left >= right) {
			return 0; // 注意返回的结果是小数和,只有一个元素,为0
		}

		int mid = left + ((right - left) >> 1);

		// 将所有区间统计的小数和累加,最终就是数组中所有数的小数和
		return smallSum(arr, left, mid, temp) 
				+ smallSum(arr, mid + 1, right, temp)
				+ merge(arr, left, mid, right, temp);
	}

	// merge不仅排序,排序是为了更好统计
	// 还要统计出merge后的小数和
	private static int merge(int[] arr, int left, int mid, int right, int[] temp) {
		// 同样,先定义好指针
		int pL = left; // 左区间指针,从左往右推
		int pR = mid + 1; // 右区间指针,也是从左往右推
		int t = left; // 辅助新数组下标指针
		int res = 0; // 统计的小数和

		// 传统的merger过程,只不过要增加统计功能
		// 注意,左右相等时,与稳定排序不同,要放右边数,这样才能统计出左边数组成的所有小数和
		while (pL <= mid && pR <= right) {
			if (arr[pL] < arr[pR]) {
				res += arr[pL] * (right - pR + 1); // pR右边数一定也大于pL
				temp[t++] = arr[pL++]; // 处理左边的下个元素
			} else {
				temp[t++] = arr[pR++]; // 左边>=右边,当前左边数不组成小数和
			}
		}
		while (pL <= mid) { // 右边已经遍历完,即左边剩下的数都比右边大
			temp[t++] = arr[pL++];
		}
		while (pR <= right) { // 左边已经遍历完,即右边剩下数都比左边大
			temp[t++] = arr[pR++]; // res无需再统计,遍历左边数时已经统计完了
		}
		
		// copy新数组
		System.arraycopy(temp, left, arr, left, right - left + 1);
		return res;
	}
代码测试
// 比较器---暴力算法求小数和
	public static int comparator(int[] arr) {
		if (arr == null || arr.length < 2) {
			return 0;
		}
		int res = 0;
		for (int i = 1; i < arr.length; i++) {
			for (int j = 0; j < i; j++) {
				res += arr[j] < arr[i] ? arr[j] : 0;
			}
		}
		return res;
	}

	// 随机数组生成器
	public static int[] generateRandomArray(int maxSize, int maxValue) {
		int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
		for (int i = 0; i < arr.length; i++) {
			arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
		}
		return arr;
	}

	// 辅助函数---copy数组
	public static int[] copyArray(int[] arr) {
		if (arr == null) {
			return null;
		}
		int[] res = new int[arr.length];
		for (int i = 0; i < arr.length; i++) {
			res[i] = arr[i];
		}
		return res;
	}

	// 辅助函数---判断相等
	public static boolean isEqual(int[] arr1, int[] arr2) {
		if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
			return false;
		}
		if (arr1 == null && arr2 == null) {
			return true;
		}
		if (arr1.length != arr2.length) {
			return false;
		}
		for (int i = 0; i < arr1.length; i++) {
			if (arr1[i] != arr2[i]) {
				return false;
			}
		}
		return true;
	}

	//  辅助函数---打印数组
	public static void printArray(int[] arr) {
		if (arr == null) {
			return;
		}
		for (int i = 0; i < arr.length; i++) {
			System.out.print(arr[i] + " ");
		}
		System.out.println();
	}

	// 代码测试
	public static void main(String[] args) {
		int testTime = 500000;
		int maxSize = 100;
		int maxValue = 100;
		boolean succeed = true;
		for (int i = 0; i < testTime; i++) {
			int[] arr1 = generateRandomArray(maxSize, maxValue);
			int[] arr2 = copyArray(arr1);
			if (smallSum(arr1) != comparator(arr2)) {
				succeed = false;
				printArray(arr1);
				printArray(arr2);
				break;
			}
		}
		System.out.println(succeed ? "scuccess!" : "error!");
	}
scuccess!

题目3:求所有的逆序对

题目链接
题目描述:
  • 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。
    输入一个数组,求出这个数组中的逆序对的总数。
    示例:
    输入: [7,5,6,4]
    输出: 5
代码实现:
package class04;

import java.util.Arrays;

public class Code03_ReversePair {
	// 本质上是求左边的数比当前的数大的个数有多少
	// 因只需求个数,不求累加结果,以右边为基点即可
	// 也可以以左边为基点,需要merge从右往左,相等时先处理右边,则稳定性不能保证
	// 若本题求大数和,必须以左边为基点,与求小数和逻辑类似
	public int reversePairs(int[] nums) {
		if(nums == null || nums.length < 2) return 0;
		
		int[] temp = Arrays.copyOf(nums, nums.length);
		return reversePairs(nums, 0, nums.length - 1, temp);
	}

	// 递归实现排序,并返回逆序对个数,即大数个数
	// 同样,所有区间个数累加,即,左区间,右区间,以及左右合并的大区间
	private int reversePairs(int[] nums, int left, int right, int[] temp) {
		if(left >= right) return 0;
		
		int mid = left + ((right - left) >> 1);
		
		return reversePairs(nums, left, mid, temp)
				+ reversePairs(nums, mid + 1, right, temp)
				+ (nums[mid] > nums[mid + 1] ? merge(nums, left, mid, right, temp) : 0);
	}

	// 以右边区间元素为基点,从左往右推
	private int merge1(int[] nums, int left, int mid, int right, int[] temp) {
		int pL = left;
		int pR = mid + 1;
		int t = left;
		int res = 0;
		
		while(pL <= mid && pR <= right) {
			if(nums[pL] <= nums[pR]) { // 不构成逆序对
				temp[t ++] = nums[pL ++];
			} else { // 左大于右,左区间之后的元素与右边当前元素都构成逆序对
				res += mid - pL + 1;
				temp[t ++] = nums[pR ++];
			}
		}
		while(pL <= mid) temp[t ++] = nums[pL ++];
		while(pR <= right) temp[t ++] = nums[pR ++];
		
		System.arraycopy(temp, left, nums, left, right - left + 1);
		return res;
	}

	// 以左边区间元素为基点,从右往左推
	private int merge(int[] nums, int left, int mid, int right, int[] temp) {
		int pL = mid;   // 左指针为左区间最后一个元素
		int pR = right; // 右指针为右区间最后一个元素
		int t = right;  // 从右往左merge
		int res = 0;
		
		// merge从右往左,从大往小填
		while(pL >= left && pR >= mid + 1) {
			if(nums[pL] > nums[pR]) { // 左大,有逆序对,且右边往mid靠近的数都比左小
				res +=  pR - mid; // 即此时左边的a是所有那些数的大数,若是求和,a*Na
				temp[t --] = nums[pL --]; 
			} else {  // 左小,没有逆序对,相等也将右边元素先填入temp中            			 
				temp[t --] = nums[pR --];
			}
		}
		while(pL >= left) temp[t --] = nums[pL --];
		while(pR >= mid + 1) temp[t --] = nums[pR --];
		
		System.arraycopy(temp, left, nums, left, right - left + 1);
		return res;
	}
}

题目4:求数组中的大两倍对数量

题目描述:
  • 在一个数组中,
    对于每个数num,求有多少个后面的数 * 2 依然<num,求总个数

    比如:[3,1,7,0,2]
    3的后面有:1,0
    1的后面有:0
    7的后面有:0,2
    0的后面没有
    2的后面没有
    所以总共有5个

代码实现:
package class04;

import java.util.Arrays;

public class Code04_BiggerThanRightTwice {
	public static int biggerTwice(int[] arr) {
		if (arr == null || arr.length < 2)
			return 0;

		int[] temp = Arrays.copyOf(arr, arr.length);
		return biggerTwice(arr, 0, arr.length - 1, temp);
	}

	private static int biggerTwice(int[] arr, int left, int right, int[] temp) {
		if (left >= right)
			return 0;

		int mid = left + ((right - left) >> 1);

		// 注意,不能对merge条件判断,若是负数,左小于右,但右乘2就小于左了
		return biggerTwice(arr, left, mid, temp) 
				+ biggerTwice(arr, mid + 1, right, temp)
				+ merge2(arr, left, mid, right, temp); 
	}

	// 统计和排序
	// 以左基点进行统计
	private static int merge(int[] arr, int left, int mid, int right, int[] temp) {
		int pL = left;
		int pR = mid + 1;
		int t = left;
		int res = 0;

		// 1. 统计
		while (pL <= mid) {
			while (pR <= right && arr[pL] > (arr[pR] * 2)) { // 找到第一个不是的为止
				pR++;   // 找第一个不满足的位置
			}
			// 目前囊括进来的数,是从[mid+1, pR)
			res += pR - mid - 1;
			pL++;
		}

		// 2. merge排序
		pL = left; // 变量复原
		pR = mid + 1;
		while (pL <= mid && pR <= right) {
			temp[t++] = arr[pL] <= arr[pR] ? arr[pL++] : arr[pR++];
		}
		while (pL <= mid)
			temp[t++] = arr[pL++];
		while (pR <= right)
			temp[t++] = arr[pR++];

		System.arraycopy(temp, left, arr, left, right - left + 1);
		return res;
	}

	// 以右基点进行统计
	private static int merge2(int[] arr, int left, int mid, int right, int[] temp) {
		int res = 0;
		int pL = left;
		int pR = mid + 1;
		int t = left;
		
		// 1. 统计
		// 目前囊括进来的数,是从[pL, mid]
		while(pR <= right) {
			while(pL <= mid && arr[pL] <= arr[pR] * 2) { // 等于2倍不满足
				pL ++;	// 找第一个满足的位置
			}
			res += mid - pL + 1; // 左:0.1.2.3.4,3满足条件,有两个
			pR ++;
		}
		
		// 2. merge排序
		pL = left; // 变量复原
		pR = mid + 1;
		while (pL <= mid && pR <= right) {
			temp[t++] = arr[pL] <= arr[pR] ? arr[pL++] : arr[pR++];
		}
		while (pL <= mid)
			temp[t++] = arr[pL++];
		while (pR <= right)
			temp[t++] = arr[pR++];

		System.arraycopy(temp, left, arr, left, right - left + 1);
		return res;
	}
}

代码测试
// 比较器----暴力算法
	public static int comparator(int[] arr) {
		int ans = 0;
		for (int i = 0; i < arr.length; i++) {
			for (int j = i + 1; j < arr.length; j++) {
				if (arr[i] > (arr[j] << 1)) {
					ans++;
				}
			}
		}
		return ans;
	}

	// 比较器---随机数组生成
	public static int[] generateRandomArray(int maxSize, int maxValue) {
		int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
		for (int i = 0; i < arr.length; i++) {
			arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) ((maxValue + 1) * Math.random());
		}
		return arr;
	}

	// 辅助函数,copy
	public static int[] copyArray(int[] arr) {
		if (arr == null) {
			return null;
		}
		int[] res = new int[arr.length];
		for (int i = 0; i < arr.length; i++) {
			res[i] = arr[i];
		}
		return res;
	}

	// 辅助函数---打印数组
	public static void printArray(int[] arr) {
		if (arr == null) {
			return;
		}
		for (int i = 0; i < arr.length; i++) {
			System.out.print(arr[i] + " ");
		}
		System.out.println();
	}

	// 代码测试
	public static void main(String[] args) {
		int testTime = 500000;
		int maxSize = 5;
		int maxValue = 100;
		System.out.println("测试开始");
		for (int i = 0; i < testTime; i++) {
			int[] arr1 = generateRandomArray(maxSize, maxValue);
			int[] arr2 = copyArray(arr1);
			int res1 = biggerTwice(arr1);
			int res2 = comparator(arr2);
			if (res1 != res2) {
				System.out.println("Oops!");
				printArray(arr1);
				System.out.println("my: " + res1);
				printArray(arr2);
				System.out.println("to: " + res2);
				break;
			}
		}
		System.out.println("测试结束");
	}
测试开始
测试结束

题目5:区间和的个数

题目链接
题目描述:
  • 给你一个整数数组 nums 以及两个整数 lower 和 upper 。
    求数组中,值位于范围 [lower, upper] (包含 lower 和 upper)之内的 区间和的个数 。

    区间和 S(i, j) 表示在 nums 中,位置从 i 到 j 的元素之和,包含 i 和 j (i ≤ j)。
    例如:
    输入:nums = [-2,5,-1], lower = -2, upper = 2
    输出:3
    解释:存在三个区间:[0,0]、[2,2] 和 [0,2] ,对应的区间和分别是:-2 、-1 、2

代码实现:
package class04;
/**
 *    求数组中所有区间的和在指定区间内的个数
 * @author ls
 *
 */
public class Code05_CountOfRangeSum {
	public int countRangeSum(int[] nums, int lower, int upper) {
		if(nums == null || nums.length == 0) return 0;
		
		long[] temp = new long[nums.length]; // 对前缀和数组排序
		
		// 1. 建立preSum前缀和数组,数据有可能越界,转为long型
		long[] preSum = new long[nums.length];
		preSum[0] = nums[0];
		for(int i = 1; i < nums.length; i ++) {
			preSum[i] = preSum[i - 1] + nums[i];
		}
		
		return countRangeSum(preSum, 0, nums.length -1, lower, upper, temp);
	}

	// 2. 递归实现,转为求preSum中[x-upper,x-lower]问题
	private int countRangeSum(long[] preSum, int left, int right, int lower, int upper, long[] temp) {
		if(left == right) { // 分到只有一个元素时,要判断其是否满足指标
			return preSum[left] <= upper && preSum[left] >= lower ? 1 : 0;	
		}
		
		int mid = left + ((right - left) >> 1);
		
		// 累加上各个区间统计的结果
		return countRangeSum(preSum, left, mid, lower, upper, temp)
				+ countRangeSum(preSum, mid + 1, right, lower, upper, temp)
				+ merge(preSum, left, mid, right, lower, upper, temp);
	}

	// 3. merge中统计满足新指标的个数
	private int merge(long[] preSum, int left, int mid, int right, int lower, int upper, long[] temp) {
		// 先统计
		int res = 0;
		int pR = mid + 1; // 右区间起始指针,依次往右推进
		int windowsL = left; // 左区间窗口首指针
		int windowsR = left; // 左区间窗口尾指针
		// [windowsL, windowsR),左闭右开,尾指针不满足条件,初始区间为空
		while(pR <= right) {
			// 计算好新的指标[x-upper, x-lower]
			long min = preSum[pR] - upper;
			long max = preSum[pR] - lower;
			while(windowsR <= mid && preSum[windowsR] <= max) {
				windowsR ++;	// windowsR满足时一直往右推,找到第一个不满足的为止
			}                   // windowsR前一个数满足,本身不满足
			while(windowsL <= mid && preSum[windowsL] < min) {
				windowsL ++;	// windowsL不满足时一直往右推,找到第一个满足的为止
			}                   // windowsL当前数满足>=min,跳出循环
			res += windowsR - windowsL; //[14,14)不满足时为0
			pR ++;
		}
		
		// 再排序
		int t = left;
		int pL = left;
		pR = mid + 1;
		while(pL <= mid && pR <= right) {
			temp[t ++] = preSum[pL] <= preSum[pR] ?
					preSum[pL ++] : preSum[pR ++];
		}
		while(pL <= mid) temp[t ++] = preSum[pL ++];
		while(pR <= right) temp[t ++] = preSum[pR ++];
		System.arraycopy(temp, left, preSum, left, right - left + 1);
		return res;
	}
	
    // 测试
	public static void main(String[] args) {
		int[] nums = {-2, 5, -1};
		int lower = -2;
		int upper = 2;
		System.out.println(new Code05_CountOfRangeSum().countRangeSum(nums, lower, upper));  // 3
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值