认识O(N * logN)的排序

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


一、master公式

T ( N ) = a ∗ T ( N / b ) + O ( N d ) T(N) = a * T(N/b) + O(N^d) T(N)=aT(N/b)+O(Nd)
T ( N ) T(N) T(N)指母问题的数据量为 N N N级别, T ( N / b ) T(N/b) T(N/b)指子问题的规模为 N / b N/b N/b a a a指子问题被调用的次数, O ( N d ) O(N^d) O(Nd)指调用子问题之外的算法复杂度。

public static int process(int[] arr, int L, int R) {
	if (L == R) {
		return arr[L];
	}
	int mid = L + (R - L)/2;
	int leftMax = process(arr, L, mid);
	int rightMax = process(arr, mid + 1, R);
	return Math.max(leftMax, rightMax);
}

用master公式代入上面的方法:
T ( N ) = 2 ∗ T ( N / 2 ) + O ( 1 ) T(N) = 2 * T(N/2) + O(1) T(N)=2T(N/2)+O(1)

求解master公式的时间复杂度

将master公式的三个参数a、b、d代入以下式子,若
l o g b a < d log_b a < d logba<d O ( N d ) O(N^d) O(Nd)
l o g b a > d log_b a > d logba>d O ( N l o g b a ) O(N^{log_b a}) O(Nlogba)
l o g b a = d log_b a = d logba=d O ( N d ∗ l o g N ) O(N^d * logN) O(NdlogN)

二、归并排序

1、概述

找到一个数组的中点,先让左边的子数组排好序,再让右边的子数组排好序,最后merge两个子数组(可用双指针merge)。

2、代码实例

public class MergeSort {
	
	public static void mergeSort(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;//指向L
		int p2 = M + 1//指向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 (int i = 0; i < help.length; i++) {
			arr[L + i] = help[i];
		}
	}

	//for test
	public static void comparator(int[] arr){
		Arrays.sort(arr);
	}
}

3、分析归并排序时间复杂度

对以上代码分析,得到master公式的三个参数为 a = 2 , b = 2 , d = 1 a = 2, b = 2, d = 1 a=2,b=2,d=1,所以有:
T ( N ) = 2 ∗ T ( N / 2 ) + O ( N ) T(N) = 2 * T(N/2) + O(N) T(N)=2T(N/2)+O(N)
由于 l o g b a = d log_b a = d logba=d,所以归并排序的时间复杂度为 O ( N ∗ l o g N ) O(N * logN) O(NlogN)
额外空间复杂度为 O ( N ) O(N) O(N)

时间复杂度变低的原因

冒泡排序法和选择排序法浪费了大量时间在数的比较上,而归并排序并没有浪费这种比较行为,而是把这种比较变成一个整体有序的部分,再将这个部分与另一个部分merge得到更大的一个有序的部分。

四、归并排序扩展

1、小和问题

问题描述:在一个数组中,每一个数左边比当前数小的数累加起来
如:数组 [ 1 , 3 , 4 , 2 , 5 ] [1, 3, 4, 2, 5] [1,3,4,2,5]的小和为 0 + 1 + ( 1 + 3 ) + 1 + ( 1 + 3 + 4 + 2 ) = 16 0+1+(1+3)+1+(1+3+4+2)=16 0+1+(1+3)+1+(1+3+4+2)=16

代码实现

可以把小和问题理解为当前数x的后面有多少个数a比当前数x大,从头到尾遍历数组,把a * x的结果累加起来,最终得到小和。

public class 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;
		}
		int mid = L + ((R - L) >> 1);//位移运算符优先级小于+-,必须要括号
		return process(arr, 1, mid) + process(arr, mid + 1, R) + merge(arr, 1, 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;//指向L
		int p2 = M + 1//指向M + 1
		int res = 0;
		while(p1 <= M && p2 <= R) {
			res += arr[p1] < arr[p2] ? (R - p2 + 1) * arr[p1] : 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 (int i = 0; i < help.length; i++) {
			arr[L + i] = help[i];
		}
		return res;
	}

	//for test
	public static void comparator(int[] arr){
		Arrays.sort(arr);
	}
}

2、逆序对

在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有的逆序对。

五、快速排序法

荷兰国旗问题

问题一:给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度为 O ( 1 ) O(1) O(1),时间复杂度 O ( N ) O(N) O(N)

问题二:给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度为 O ( 1 ) O(1) O(1),时间复杂度 O ( N ) O(N) O(N)

问题一

从左到右遍历数组arr,把数组分为小于等于区a和大于区b。初始时a区为0。开始遍历数组,数组在位置i的数小于等于num,则小于等于区a的下一个数与arr[i]交换,并且a区长度加一,i++,;若数组在位置i的数大于num,则直接i++。i++越界时停止遍历。

问题二

从左到右遍历数组arr,把数组分为小于区a、等于区b和大于区c。初始时a区在数组最左侧,c区在数组最右侧,长度均为0。
1)arr[i] < num,arr[i]和a区下一个数交换,a区右扩,i++
2)arr[i] == num,i++;
3)arr[i] > num,arr[i]和c区前一个数交换,c区左扩,i不变。
大于区和i相遇时遍历结束。

快速排序1.0

思想

把数组最后一个数设置为num,然后按照问题一的方法遍历数组,让数组左边的数都小于等于num,右边的数都大于num。然后数组左边和右边重复这个过程。

快速排序2.0

思想

把数组最后一个数设置为num,然后按照问题二的方法遍历数组,让数组左边的数都小于num,右边的数都大于num,中间的数等于num。然后数组左边和右边重复这个过程。

快速排序2.0版本比1.0版本效率更高。

复杂度分析

无论是快排1.0还是2.0版本,时间复杂度都是 O ( N 2 ) O(N^2) O(N2)
举例:对数组[1,2,3,4,5,6,7,8]进行排序。每次都需要遍历一次数组。
原因:划分值的选取很偏,导致最差情况。

快速排序3.0

思想

随机在数组选取一个数,把这个数设为num,然后与数组最后一个数交换。这样的选取方法下每种情况(最好、最坏、次好、次坏…)等概率发生。取数学期望后时间复杂度为
O ( N ∗ l o g N ) O(N * logN) O(NlogN)

代码实现

public class QuickSort{
	
	public static void quickSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		quickSort(arr, 0, arr.length - 1);
	}
	
	public static void quickSort(int[] arr, int L, int R) {
		if (L < R) {
			swap(arr, L + (int)(Math.random() * (R - L + 1)), R);
			int[] p = partition(arr, L, R);
			quickSort(arr, L, p[0] - 1); //小于区
			quickSort(arr, p[1] + 1, R); //大于区
		}
	}

	// 这是一个处理arr[1..r]的函数
	// 默认以arr[r]做划分,arr[r] -> p    <p   ==p   >p
	// 返回等于区域(左边界,右边界),所以返回一个长度为2的数组res,res[0] res[1]
	public static int[] partition(int[] arr, int L, int R){
		int less = L - 1;
		int more = R;
		//问题二的三种情况
		while(L < more) {
			if (arr[L] < arr[R]) {
				swap(arr, ++less, L++);
			}
			else if (arr[L] > arr[R]) {
				swap(arr, --more, L);
			}
			else {
				L++;
			}
		}
		swap(arr, more, R);
		return new int[] {less + 1, more};
	}

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

总结

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值