左程云算法课笔记(二)O(NlogN)的排序

认识O(NlogN)的排序

一、归并排序

归并排序图例,就是一个简单的分治思想
在这里插入图片描述
基本操作:

  1. 找出数组的基准值位置mid=(l+r)/2,其中l,r分别表示当前数组的头尾位置
  2. 分别递归调用merge_sort(arr ,l ,mid)和merge(arr,mid+1,r)
  3. 从基准值将数组一分为二
  4. 构造一个临时数组对两个数组进行从小到大的合并
  5. 将临时数组里面的元素放到arr数组里面
//代码很简单清晰,就不解释了
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 i, int R) {
		if(L == R)
			return;
		int mid = L + ((R - L) >> 1);
		process(arr, L, mid);
		preocess(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];
		}
	}
}

用master公式来分析归并排序的时间复杂度:
将数组等分为两部分,b=2,对两部分进行递归,a=2
因此log(b,a) = d = 1,归并排序的时间复杂度为O(N*logN)

例题

小和问题

在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。

例如: [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

如果我们从左至右依次遍历数组求小数和,那么时间复杂度很明显会是O(N^2)
想要优化算法,可以用到归并排序的思想,将时间复杂度变为O(NlogN)

public class SmallSum {
	//求小数和函数
	public static int samllSum(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, 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是小数和,具体的求法见下文
			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(i = 0; i < help.length; i++) {
			arr[l + i] = help[i];
		}
		return res;
	}
}

草,这可太难解释了,让我憋一会

当我们看到题目时,可能第一思路是遍历数组,让每个元素与其左边的数对比,来找出小数,这样操作的时间复杂度为O(N^2)。如果我们换一种思路,同样是遍历数组,但是这次是找每个元素右边有几个数比当前元素大,然后这个个数再乘以当前元素,依次相加之后得到的依然是小数和。

我感觉我在这干说可能也说不明吧,各位直接看视频吧
一小时零四分开始

逆序对问题

各位考研的朋友可能对这个概念很熟悉,老线代人了。

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

这个问题就是上面问题的相反求解,上面是求左边比右边数小的数,这个问题是求左边比右边数大的数。求解过程这里就不解释了。

二、快速排序

荷兰国旗问题

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

设置一个小于区域,从数组的左边开始,对给定的数组进行遍历:

  1. 当数组元素小于num时,将这个元素与小于区域右边的第一个数进行交换,小于区域右移一位,指针后移一位。
  2. 当数组元素等于num时,指针直接右移。
  3. 直到指针越界停止。

(2)给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组中间,大于num的数放在数组右边。要求额外空间复杂度O(1),时间复杂度O(N)
设置一个小于区域,从数组左边开始;设置一个大于区域,从数组右边开始,对给定数组进行遍历:

  1. 当数组元素小于num时,将这个元素与小于区域右边的第一个数进行交换,小于区域右移一位,指针后移一位。
  2. 当数组元素等于num时,指针直接右移。
  3. 当数组元素大于num时,将这个元素与大于区域左边的第一个数进行交换,大于区域左移一位,指针不动(这是因为交换过来的元素还没有被遍历过)。

这两个算法都可以使用快速排序的思想进行:

  1. 把数组范围中的最后一个数作为划分值,然后把数组通过荷兰国旗问题分成三个部分:
    左侧 < 划分值、中间 == 划分值、右值 > 划分值
  2. 对左侧范围和右侧范围,递归执行

分析:

  1. 划分值越靠近两侧复杂度越高;划分值越靠近中间,复杂度越低
  2. 可以轻而易举的举出最差的例子,所以不改进的快速排序时间复杂度为O(N^2)

因此如果想要降低时间复杂度,需要的是在数组中随机找出一个数作为划分值。

  1. 在数组范围中,等概率随机选一个数作为划分值,然后把数组通过荷兰国旗问题分为三个部分:
    左侧 < 划分值、中间 == 划分值、右值 > 划分值
  2. 对左侧范围和右侧范围分别进行递归
  3. 时间复杂度为O(NlogN)
//快速排序代码
public class QuickSort {
	 
	 public static void quickSort(int[] arr) {
		if(arr == null || arr.length < 2) {
			return;
		}
		quickSort(arr, 0, arr.length - 1);
	}
   //arr[l..r]排序
   public static void quickSort(int[] arr, int L, int R) {
		if(L < R) {
			//数组中随机选一个数与最右端数进行交换
			swap(arr, L, (int)(Math.random{} * (R - L + 1)), R);
			//这里数组p只有两个值,表示的是划分之后的等于划分数的左右两个边界的指针p[0],p[1]
			int[] p = partition(arr, L, R);
			quickSort(arr, L, p[0] - 1);	// < 区
			quickSort(arr, p[1] + 1, R);	// > 区
		}
	}
	//这是一个处理arr[l..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) {
		//小于区域边界,从L的左边一位开始
		int less = L - 1;
		//大于区域边界,从R开始
		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);
		//最后整个数组中,等于划分值的左右边界分别为less+1,more
		return new int[] {less + 1, more};
	}
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值