3.归并排序与随机快排

归并排序
递归实现
  1. 整体是递归,左边排好序+右边排好序+merge让整体有序
  2. 让其整体有序的过程里用了排外序方法
  3. 利用master公式来求解时间复杂度

分析:申请一个与原数组长度相同的空间,定义一个做指针和右指针,谁小拷贝谁,如果发某个指针越界,则将另一部分全部剩余元素加到新的数组里,最后将所有数据覆盖原来的数组

非递归实现

递归方法和非递归方法没有本质区别,递归方法只不过是左右两个组无限划分,一直二分下去。然后左部分变有序,右部分有序,再merge让整体有序。

非递归方法无非就是我们规定好了它每回mergeSize的大小,左组多大,右组同样推那么多,然后每次乘2,一直达到N或者超过N的规模

分析:给定一个数组,一开始k=2,代表响铃两个数merge有序,如1,2。左部分为1,右部分为2,如果后面不够一组,不管它。(k代表的是一组的大小,所以左部分和右部分规模都是k/2)

package com.harrison.three;

//归并排序:递归方法实现和非递归方法实现
public class Code01_MergeSort {
    
    
	//递归方法实现
	public static void mergeSort1(int [] arr) {
    
    
		if(arr==null || arr.length<2) {
    
    
			return ;
		}
		process(arr, 0, arr.length-1);
	}
	//将左部分和右部分都排好序
	public static void process(int [] arr,int L,int R) {
    
    
		if(L==R) {
    
    //数组只有一个数,天然有序
			return;
		}
		int mid=L+((R-L)>>1);//找到终点位置
		process(arr, L, mid);
		process(arr, mid+1, R);
		merge(arr, L, mid, R);
	}
	//让整体有序
	public static void merge(int [] arr,int L,int M,int R) {
    
    
		int [] help=new int[R-L+1];//申请一个辅助数组,长度和原数组相等
		int i=0;//辅助数组的专用指针
		int p1=L;
		int p2=M+1;
		while(p1<=M && p2<=R) {
    
    //左指针和右指针都不越界的情况下,谁小拷贝谁
			help[i++]=arr[p1]<=arr[p2]?arr[p1++]:arr[p2++];
		}
		//左指针和右指针只会有一个越界,因为一次只让移动一个指针
		while(p1<=M) {
    
    
			help[i++]=arr[p1++];
		}
		while(p2<=R) {
    
    
			help[i++]=arr[p2++];
		}
		//最后拷贝数组
		for(i=0; i<help.length; i++) {
    
    
			arr[L+i]=help[i];
		}
	}
	
	//非递归方法实现
	public static void mergeSort2(int[] arr) {
    
    
		if(arr==null || arr.length<2) {
    
    
			return;
		}
		int N=arr.length;
		int mergeSize=1;//当前有序的,左组长度
		while(mergeSize<N) {
    
    //执行logN次
			int L=0;
			while(L<N) {
    
    
				//L...M左组长度为mergeSize
				int M=L+mergeSize-1;
				if(M>=N) {
    
    //剩下的数不够一组,肯定有序
					break;
				}
				//正常情况下右组长度也为mergeSize,不够的话长度为N-1
				int R=Math.min(M+mergeSize, N-1);
				merge(arr, L, M, R);
				L=R+1;//下个左组的起始点
			}
			if(mergeSize>N/2) {
    
    //防止越界,产生不可预知的错误
				break;
			}
			mergeSize <<=1;
		}
	}
	
	//产生随机数组,长度随机且值也随机
	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;
	}
	
	//复制随机数组
	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;
		System.out.println("测试开始!");
		for(int i=0; i<testTime; i++) {
    
    
			int[] arr1=generateRandomArray(maxSize, maxValue);
			int[] arr2=copyArray(arr1);
			mergeSort1(arr1);
			mergeSort2(arr2);
			if(!isEqual(arr1, arr2)) {
    
    
				System.out.println("出错啦!");
				printArray(arr1);
				printArray(arr2);
				break;
			}
		}
		System.out.println("测试结束!");
	} 
}

总结:选择排序、冒泡排序、插入排序时间复杂度都为O(N^2),因为存在大量浪费比较的行为;而归并排序没有浪费比较的行为,因为每一次比较都以有序的形式固定下来了。

数组小和

在一个数组中,一个数左边比它小的数的总和,叫数的小和,所有数的小和累加起来,叫数组小和,求数组小和,可以利用归并排序

分析:

  • 左组的数 < 右组的数:产生小和,拷贝左组的数,产生几个呢?右组范围上一共有几个数比左组数大就产生几个
  • 左组的数 >= 右组的数:不产生小和,拷贝右组的数
package com.harrison.three;

//在一个数组中,一个数左边比它小的数的总和,叫数的小和,所有数的小和累加起来,
//叫数组小和,求数组小和,可以利用归并排序
public class Code02_SmallSum {
    
    
	public static int smallSum(int[] arr) {
    
    
		if (arr == null || arr.length < 2) {
    
    
			return 0;
		}
		return process(arr, 0, arr.length - 1);
	}

	public static int process(int[] arr, int L, int R) {
    
    
		if (L == R) {
    
    // 数组只有一个数,没有小和
			return 0;
		}
		// L<R
		int mid = L + ((R - L) >> 1);
		return process(arr, L, mid) + process(arr, mid + 1, R) + merge(arr, L, mid, R);
	}

	public static int merge(int[] arr, int L, int M, int R) {
    
    
		int[] help = new int[R - L + 1];
		int i = 0;
		int p1 = L;
		int p2 = M + 1;
		int res = 0;
		while (p1 <= M && p2 <= R) {
    
    
			res += arr[p1] < arr[p2] ? arr[p1] * (R - p2 + 1) : 0;
			help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
		}
		while (p1 <= M) {
    
    
			help[i++] = arr[p1++];
		}
		while (p2 <= R) {
    
    
			help[i++] = arr[p2++];
		}
		for (i = 0; i < help.length; i++) {
    
    
			arr[L+i] = help[i];
		}
		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;
	}

	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 ? "Nice!" : "Fucking fucked!");
	}
}
快速排序
partition

给定一个数组arr,和一个整数num,请把小于等于num的数放在数组左边,大于num的数放在右边。

分析:设计一个<=区,该区域一开始在数组左侧,只用一个变量记录它的右边界,所以一开始<=区在下标为-1的位置,然后开始遍历数组(0,1,2…N-1)

遍历规则:

  1. i位置的数[i] <= num:把当前数和<=区的下一个数交换,然后<=区向右扩一个位置,当前数跳到下一个(i++)
  2. [i] > num:什么也不干,直接i++

题目升级:将数组分为三段,[< | = | >] ——经典的荷兰国旗问题,小于部分和大于部分可以无序。

分析:设计一个>区,往左扩,<区,往右扩,i位置从左往右遍历,必然会中三中逻辑:

  1. [i] == num:直接i++
  2. [i] < num:[i]与<区的下一个数(或者说右一个数)交换,然后<区右扩一个,i++
  3. [i] > num:[i]与>区的左一个数交换,然后>区左扩,i停在原地

什么时候停?i和>区的左边界撞上的时候

快速排序
  1. 快排1.0版本:以arr[R]做划分值,进行一次partition搞定一个数,然后左侧范围区递归,以左侧范围的arr[R]做划分值,右侧同理,周而复始
  2. 快排2.0版本:与1.0一样,但是一次能搞定等于arr[R]的一批数
  3. 快排3.0版本:随机选一个数(如[i]位置的数),将其与arr[R]交换之后,以arr[R]做划分值。就等同于随机选一个数拿它做划分值,剩下的所有过程跟快排2.0一样
package com.harrison.three;

//
public class Code03_PartitionAndQuickSort {
    
    
	public static void swap(int[] arr, int i, int j) {
    
    
		int temp = arr[i];
		arr[i] = arr[j];
		arr[j] = temp;
	}

	// arr[L...R]上,以arr[R]位置的数做划分值:[<=arr[R] >arr[R]
	public static int partition(int[] arr, int L, int R) {
    
    
		if (L > R) {
    
    
			return -1;
		}
		if (L == R) {
    
    
			return L;
		}
		int lessEqual = L - 1;// <=区的下一个数
		int index = L;// 当前位置的数
		while (index < R) {
    
    
			if (arr[index] <= arr[R]) {
    
    
				swap(arr, index, ++lessEqual);
			}
			index++;
		}
		swap(arr, ++lessEqual, R);
		return lessEqual;
	}

	// arr[L...R]玩荷兰国旗问题的划分,以arr[R]做划分值
	// <arr[R] ==arr[R] >arr[R]
	public static int[] netherlandsFlag(int[] arr, int L, int R) {
    
    
		if (L > R) {
    
    
			return new int[] {
    
     -1, -1 };
		}
		if (L == R) {
    
    // [L...R]上只有一个数
			return new int[] {
    
     L, R };
		}
		int less = L - 1;// <区右边界
		int more = R;// >区左边界
		int index = L;// i位置
		while (index < more) {
    
    // 当前位置[i]不能和>区左边界装上
			if (arr[index] == arr[R]) {
    
    
				index++;
			} else if (arr[index] < arr[R]) {
    
    
				swap(arr, index++, ++less);
			} else {
    
    
				swap(arr, index, --more);
			}
		}
		// [L...less less+1...more-1 more...R]
		// [L...less less+1..........more more+1...R]
		swap(arr, more, R);
		return new int[] {
    
     less + 1, more };
	}

	// 快速排序1.0版本
	public static void quickSort1(int[] arr) {
    
    
		if (arr == null || arr.length < 2) {
    
    
			return;
		}
		process1(arr, 0, arr.length - 1);
	}

	public static void process1(int[] arr, int L, int R) {
    
    
		if (L >= R) {
    
    
			return;
		}
		int M = partition(arr, L, R);
		process1(arr, L, M - 1);
		process1(arr, M + 1, R);
	}

	// 快速排序2.0版本
	public static void quickSort2(int[] arr) {
    
    
		if (arr == null || arr.length < 2) {
    
    
			return;
		}
		process1(arr, 0, arr.length - 1);
	}

	public static void process2(int[] arr, int L, int R) {
    
    
		if (L >= R) {
    
    
			return;
		}
		int[] equalArea = netherlandsFlag(arr, L, R);// 一次搞定一批==区域的数
		process1(arr, L, equalArea[0] - 1);
		process1(arr, equalArea[0] + 1, R);
	}

	// 快速排序3.0版本
	public static void quickSort3(int[] arr) {
    
    
		if (arr == null || arr.length < 2) {
    
    
			return;
		}
		process1(arr, 0, arr.length - 1);
	}

	public static void process3(int[] arr, int L, int R) {
    
    
		if (L >= R) {
    
    
			return;
		}
		swap(arr, L+(int)(Math.random()*(R-L+1)), R);
		int[] equalArea = netherlandsFlag(arr, L, R);// 一次搞定一批==区域的数
		process1(arr, L, equalArea[0] - 1);
		process1(arr, equalArea[0] + 1, R);
	}

	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;
	}

	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);
			int [] arr3=copyArray(arr1);
			quickSort1(arr1);
			quickSort2(arr2);
			quickSort3(arr3);
			if(!isEqual(arr1, arr2) || !isEqual(arr2, arr3)) {
    
    
				succeed=false;
				break;
			}
		}
		System.out.println(succeed?"Nice!":"Oops!");
	}
}

随机快排时间复杂度分析:

  1. 划分值越靠近中间,性能越好,越靠近两边,性能越差
  2. 随机选一个数进行划分的目的就是让好情况和坏情况都变成概率事件
  3. 把每一种情况都列出来,会有每种情况下的时间复杂度,但概率都是1/N
  4. 把每一种情况都考虑,时间复杂度就是这种概率模型下的长期期望
  5. 时间复杂度O(N*logN)和额外空间复杂度O(logN)都是这么来的

快排为什么要用递归运行,因为要记录每一个中点的位置或划分点的位置,递归空间就是每次记录划分值的空间,这是避免不掉的。

猜你喜欢

转载自blog.csdn.net/weixin_44337241/article/details/120803578