【2】认识O(NlogN)的排序

目录


一、剖析递归行为和递归行为时间复杂度的估算

用递归方法找一个数组中的最大值,系统上到底是怎么做的?

master公式使用:T(N) = a*T(N/b) + O(N^d)

  • log(b,a) > d => 复杂度为O(N^log(b,a))
  • log(b,a) = d => 复杂度为O(N^d*logN)
  • log(b,a) < d => 复杂度为O(N^d)

将p(0,5)不断往下拆分,只有当所有的子节点的值求解出来后,才可以求解出最终的结果。每个节点都依赖自身的子节点来求解得到结果,栈的深度其实就是树的高度。

	public static int getMax(int[] arr) {
		return process(arr, 0, arr.length-1);
	}
	
	//arr[L...R]范围上求最大值
	public static int process(int[] arr, int L, int R) {
		if(L==R)return arr[L]; 
		int mid=L+((R-L)>>1); //中点
		int leftMax=process(arr,L,mid);
		int rightMax=process(arr,mid+1,R);
		return Math.max(leftMax, rightMax);
	}
	

上述实现中,一个母问题拆分为2个规模为N/2的子问题,除了子问题拆分外的复杂度为O(1)。所以上述实现的master公式为:T(N) = 2*T(N/2) + O(1)  。a=2,b=2,log(b,a)=1, d=0。由于log(b,a)>d,所以时间复杂度为O(N)。等效于从左到右遍历数组找最大值。

二、归并排序

1.merge介绍

时间复杂度O(NlogN),空间复杂度O(N)

Merge函数:在p1和p2都不越界的情况下,比较p1和p2指向的数,将小的值copy到help[i]中,然后将指向较小值的指针往后移动(p1++或p2++),同时i++。

最后,p1或p2越界,将没枚举完的剩余部分copy到help数组后面。

若p1越界,将[p2,R]的内容接到help数组后面

若p2越界,将[p1,M]的内容接到help数组后面

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

2.master公式分析时间复杂度

归并往下拆分过程中,将母问题拆分成2个规模为原规模一半的子问题,同时merge只遍历数组一次,为O(N) => 写出归并排序的master公式:T(N) = 2*T(N/2) + O(N)

a=2, b=2,log(b, a)=1, d=1  => log(b, a) == d  => 时间复杂度为O(NlogN)

3.为什么O(N^2)算法不好

O(N^2)的排序算法如选择排序,第一轮比较了N-1次确定放在0位置的数,第二轮比较了N-2次确定放在1位置的数,第三轮比较了N-3次确定放在2位置的数。可见,在上述选数的每一轮过程都是独立的,其中浪费了很多前几轮的比较信息,造成了比较大的浪费。相比之下,O(NlogN)的排序算法充分利用每次的比较信息,即比较行为没有被浪费,比较的信息被用去合并成了更大的有序整体。

4.归并排序的扩展

小和问题和逆序对问题

小和问题:在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。 例子:[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.

  

如上图所示,在合并【1 3 4】和【2 5】的过程中,

比较1和2,此时左组中的值小于右组中的值,发现小和数1。同时右组中还有【2 5】两个数,所以右组存在2个比1大的数,记录2个小和数1。

更新:左组【3 4】 右组【2 5】

比较3和2,此时右组中的值小于右组中的值,没有小和数出现,continue

更新:左组【3 4】 右组【5】

比较3和5,此时左组中的值小于右组中的值,发现小和 数3。同时右组中还有【5】一个数,所以右组存在1个比3大的数,记录1个小和数3。

更新:左组【4】 右组【5】

比较4和5,发现左组中的值小于右组中的值,发现小和数4。同时右组中还有【5】一个数,所以右组存在1个比4大的数,记录1个小和数4。

综上,本轮merge记录得小和数:2个1、1个3、1个4(其余merge过程记录小和数同理,最终将所有记录的小和数相加即为结果)

该方法既不遗漏也不重复:

不遗漏:merge过程中一定会将某个数的范围从单个数拓展成整体

不重复:已经变成一部分(一组)的东西,在这个部份内不会重复产生小和数,仅会在左组中的数比右组中的某个数小时才产生小和数。

排序不能省略:只有通过排序,才可以在O(1)的时间内求得右组中有多少个数比左组中的某个数要大。

 给出小和问题代码如下:

	public static int smallSum(int[] arr) {
		if(arr==null||arr.length<2) {
			return 0;
		}
		return process(arr,0,arr.length-1);
	}
	
	//arr[L...R]既要排好序,也要求小和
	public static int process(int[] arr, int l, int r) {
		if(l==r) {
			return arr[l];
		}
		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]?(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;
	}

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

小和问题是求某个数右边有多少个数比当前数大,而逆序对问题是求某个数右边有多少个数比当前数小,所以原理其实是一样的。

 逆序对问题代码如下:

package class02;

public class Code02_NiXu{
	
	public static int niXu(int[] arr) {
		if(arr==null||arr.length<2) {
			return 0;
		}
		return process(arr,0,arr.length-1);
	}
	
	//arr[L...R]既要排好序,也要求小和
	public static int process(int[] arr, int l, int r) {
		if(l==r) {
			return arr[l];
		}
		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]?(m-p1+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;
	}
	
}

三.快速排序

1. 荷兰国旗问题一

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

实现<=num的数放左侧,>num的数放右侧:维护一个<=的左区间

情况一:若i位置的数值<=num,则将i位置值与<=区的下一个数交换,<=区间向右扩一个,同时位置指针i++

情况二:若i位置的数值>num,则直接i++

例如当前i位置的数值是4,比5小,所以将4与6位置互换,<=区间向右扩,同时i++

实质:<=区域是<=num的数值,<=区域和i之间的数是>num的数值。i在往右走的过程中,<=区域在把>区域推着往右走,当i越界是,<=区域外的部分就是>区域。

2. 荷兰国旗问题二

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

要将>num、=num、<num严格区分:维护一个<num左区间、一个>num右区间

情况一:若i位置数值<num,则i位置和<区域下一个位置交换,<区域右扩,i++

情况二:若i位置数值==num,则i++

情况三:若i位置数值>num,则i位置和>区域下一个位置交换,>区域左扩,i原地

情况一扩完i++是因为<区域的下一个位置的数值是确定的数值,要么是当前i位置数值,要么是==num的数值,不论哪种都没必要比较,直接i++。

情况三扩完i原地是因为>区域的下一个位置的数值是未知的数值,在交换完之后,i位置的数值就是一个未知数值,所以需要停留原地再次将i位置数值与num进行比较。

3.快排不同版本

快排1.0版本,每次选取区间最右侧的数作为基准,然后将<=基准的数放在左区间,>num的数值放在右区间,然后继续在<=基准的左区间和>基准的右区间重复上述过程,直至到最底层时整个数组完全有序。

 

 快排2.0版本,选取区间最右侧的数作为基准,然后将<基准的数放在左边,==基准的数放在中间,>基准的数放在右边,最后将基准与>区域第一个数交换,就得到了三块区域。随后在<基准的区间和>基准的区间递归进行上述操作,直至最终数组完全有序。

 上述两个版本最坏都是O(N^2),例如上述例子[1 2 3 4 5  6 7 8 9],第一次以9作划分,只有左区间没有右区间,第二次以8作为划分,还是只有左区间没有右区间……这样下来时间就是O(N^2)

=>划分值选的很极端就会让算法退化成O(N^2)

 

快排3.0 在待排区间随机选择一个数交换到最后,然后作为划分的基准,这样就做到将基准的选择成为等概率事件 => 在数学上证明,数学上累加的长期期望是O(N*logN) 

	//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);
			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) {
		int less=L-1; // <区域有边界
		int more=R; //>区域左边界
		while(L<more) { //L表示当前数的位置 arr[R] -> 划分值
			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 x, int y) {
		int tmp=arr[x];
		arr[x]=arr[y];
		arr[y]=tmp;
	}

  • 18
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
时间复杂度为O(nlogn)的排序算法有希尔排序,堆排序,快速排序和归并排序。其中归并排序是一种稳定的排序算法,而希尔排序、堆排序和快速排序是不稳定的排序算法。 <span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [python-归并排序算法.docx](https://download.csdn.net/download/qq_43934844/87893705)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [时间复杂度O(nlogn)的排序算法](https://blog.csdn.net/qq_43533956/article/details/123978524)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [时间复杂度为O(nlogn)的排序算法](https://blog.csdn.net/qq_46130027/article/details/129765856)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

看未来捏

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

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

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

打赏作者

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

抵扣说明:

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

余额充值