(算法很美)查找和排序第三部分

有关快排和归并的一些题目

一、分治模式的完美诠释:归并排序
1、归并排序主要要解决合并的问题

	//归并排序
	//将数组A的q到r进行归并排序
	public static void part1(int[] A,int p,int r){
		if(p<r){
			int mid=p+((r-p)>>1);//加减的优先级高于左右移
		    part1(A,p,mid);//对左边进行归并排序
		    part1(A,mid+1,r);//对右边进行归并排序
		    merge(A,p,mid,r);//将两遍合并起来
		}
	}
	
	//合并函数
	private static void merge(int[] A,int p,int mid,int r){
		//先把A中的数据拷贝到helper中
		int[] helper=new int[A.length];
		for(int i=0;i<A.length;i++){
			helper[i]=A[i];
		}
		int left=p;//左侧队伍的头部指针,指向待比较元素
        int right=mid+1;//右侧队伍头部指针,指向待比较元素
        int current=p;//原数组的指针,指向待填入数据的位置
        while(left<=mid&&right<=r){
        	if(helper[left]<helper[right]){
        		A[current++]=helper[left];
        		left++;
        	}else{
        		A[current++]=helper[right];
        		right++;
        	}
        }
        if(left<=mid){
        	while(left<=mid){
        		A[current++]=helper[left++];
        	}
        }else if(right<=r){
        	while(right<=r){
        		A[current++]=helper[right++];
        	}
        }
	}

二、快排和归并排序的一些题目
1、调整数组顺序使奇数位于偶数前面(并没有说奇数偶数要有序)
算法思想:用归并的合并思想或者快排的双向扫描法

    //调整数组顺序使奇数位于偶数前面(并没有说奇数偶数要有序)
	public static void part2(int[] A){
		//利用归并排序中合并的方法
		int[] B=new int[A.length];
		for(int i=0;i<A.length;i++){
			B[i]=A[i];
		}
		int left=0;//指向A数组左端
		int right=A.length-1;//指向A数组右端
		int current=0;//指向B数组
		while(left<=right){
			if(B[current]%2==0){
				A[right--]=B[current++];
			}else{
				A[left++]=B[current++];
			}
		}
	}

2、最快速率求出乱序数组中第k小的元素
第一种解法:先排序再找
第二种解法:按照快排的分区思想,按照三点中值法找出主元吗,进行一次分区,此时主元所在的位置就是排序后主元的位置,将k主元的序号对比,如果k比较小说明所找的数在主元的左侧,如果k比较大,那么所找的数就在主元右侧。

	//最快速率求出乱序数组中第k小的元素(注意小标和第几个之间的关系)
	//期望时间复杂度为o(n),最差情况是o(n^2)
	public static int part3(int[] A,int p,int r,int k){
		int q=partition(A,p,r);//获取主元的下标
	    int qk=q-p+1;//计算主元排第几
	    if(qk==k) return A[q];
	    else if(qk<k){
	    	return part3(A,q+1,r,k-qk);//注意这里排第k-qk个
	    }else{
	    	return part3(A,p,q-1,k);
	    }
		
	}
	
	//快排中的双向扫描法
	public static int partition(int[] A,int p,int r){
		int mid=(p+r)/2;
		int temp;
		if(((A[p]>A[mid])&&(A[mid]>A[r]))||((A[p]<A[mid])&&(A[mid]<A[r]))){
			temp=A[p];
			A[p]=A[mid];
			A[mid]=temp;
		}else if(((A[p]>A[r])&&(A[mid]<A[r]))||((A[p]<A[r])&&(A[mid]>A[r]))){
			temp=A[p];
			A[p]=A[r];
			A[r]=temp;
		}
		int left=p+1;
		int right=r;
		while(left<=right){
			if((A[left]<=A[p])&&(left<=right)){
				left++;
			}
			if((A[right]>A[p])&&(left<=right)){
				right--;
			}
			if(left<right){
				temp=A[left];
			    A[left]=A[right];
			    A[right]=temp;
			}
		}
		temp=A[right];
		A[right]=A[p];
		A[p]=temp;
		return right;
	}

3、超过一半的数字:数组中有一个数字出现的次数超过了数组长度的一半,找出这个数字。
法一:先排序,所找的数字一定在中间出现。(改变了原数组)
法二:用第二题中的解法,将k设置为A.length/2(改变了原数组)
法三:从右向左扫描数组,记下扫描的数值和出现次数,如果下一个与上一个相同,则次数加一;若下一个与上一个数值不同,次数清零。最后剩下的数值就是出现次数最多的。(不改变原数组)(很重要的一种方法)
第三种方法的实践:寻找发帖水王
有一个人不仅喜欢自己发帖,还会回复其他ID 发的每一个帖子。传闻水王发帖数目已经超过了贴子总数的一半。如果你有一个当前论坛上所有帖子(包括回帖)的列表,其中帖子作者的ID也在表中,你能快速地找出这个传说中的水王吗。

	//超过一半的数字(法一)
	public static void part41(int[] A){
		Arrays.sort(A);
		System.out.print(A[A.length/2]);
	}
	
	//超过一半的数字(法二) 消除法(不同的元素相互抵消,最后肯定会留下出现次数最多的元素)
	public static void part42(int[] A){
		//候选数,先定位第一个元素
		int candidate=A[0];
		int times=1;
		for(int i=1;i<A.length;i++){
			if(times==0){
				candidate=A[i];
				times=1;
				continue;//***
			}
			if(candidate==A[i]){
				times++;
			}else{
				times--;
			}
		}
		System.out.print(candidate);
	}


4、正好占数组一半的数字
算法思想:仍然用消除法,先统计数组中最后一个元素出现的次数,如果等于一半,那么该数字就是要找的数字;如果不足一半,那么在消除的时候就不用带上这个数字,那么最后剩下的就是要找的数字。(去除最后一个元素后,问题就和3中的法三一样了)

5、最小可用ID:在非负数组(乱序)中找到最小的可分配 id(从1开始编号),数据量1000000(即找到最小的缺席ID)
法一:先进行排序,再找第一个缺席的ID
法二:借助辅助空间,出现过得ID的位置设置为1,未出现的是0,最后找到第一个0即可。
法三:比较位于中间位置的值与其应该对应的序号,如果相同的话,说明左边没有空缺的ID,那么就在右边重复相同的动作;如果不相同的话,那么左侧一定有缺失的ID,在左边进行相同的操作。
三、算法题的五种解法
方法一:举例法
先列举出一些具体的例子,看看是否能发现其中的一般规则。
方法二:模式匹配法(见多识广)
将现有问题与相似问题作类比,看看是否能通过修改相关问题的解法来解决新问题。
方法三:简化推广法
分多步走。首先,我们会修改某个约束条件,比如数据类型或数据量,从而简化这个问题。接着,我们转而处理这个问题的简化版本。最后,一旦找到解决简化问题的算法,我们就可以基于这个问题进行推广,并试着调整简化版问题的解决方案,让它适用于这个问题的复杂版本。
方法四:简单构造法
先从基本的情况(比如n=1)来解决问题,一般只需记下正确的结果。得到n=2的情况。接下来,有了n=1和n=2的结果,就可以试着解决n=3的情况了。(实际上是一种递归的算法)
方法五:数据结构头脑风暴
快速过一下数据结构的列表,然后逐一尝试各种数据结构,一旦找到合适的数据结构,很多问题就能迎刃而解了。

    //找最小可用ID,nlogn
	public static int part51(int[] A){
		Arrays.sort(A);//nlogn
		int i=0;
		while(i<A.length){
			if(i+1!=A[i])
				return i+1;
			i++;
		}
		return i+1;
	}
	
	//找最小可用方ID法二,借用辅助空间,n
	public static int part52(int[] A){
		int[] helper=new int[A.length+1];//这样让下表值与对应的值相同
		for(int i=0;i<A.length;i++){
			if(A[i]<helper.length){//大于的一定不是最小的,即舍弃掉对结果无用的部分
				helper[A[i]]=1;//有这个ID
			}
		}
		for(int i=1;i<helper.length;i++){
			if(helper[i]==0){
				return i;
			}
		}
		return helper.length;
	}
	
	//找最小可用ID法三:(分区,递归) n
	public static int part53(int[] A,int l,int r){
		if(l>r){
			return l+1;
		}
		int midIndex=l+(r-l)>>1;//中间下标
		int q=part3(A,l,r,midIndex-l+1);//找到中间位置元素的值
		int t=midIndex+1;//期望值
		if(q==t){//左侧紧密
			return part53(A,midIndex+1,r);
		}else{
			return part53(A,l,midIndex-1);
		}
	}

6、合并有序数组(不额外创建数组):给定两个排序后的数组A和B,其中A末端有足够的缓冲空间容纳B,编写一个方法,将B合并入A并排序。
算法思想:设置三个指针,其中两个分别指向两个数组的末尾,最后一个指针current指向加上B数组后A数组的最后一个元素的位置,将A数组指针的值与B数组指针的值比较,较大的移到current所指的位置上,然后该指针前移。

	//合并有序数组(不用额外空间)
	//n是A数组有效数的长度
	public static void part6(int[] A,int[] B,int n){
		int a=n-1;//指向A数组的末端
		int b=B.length-1;//指向B数组的末端
		int current=n+B.length-1;//指向最终数组的末端
		while(a>=0&&b>=0){
			if(A[a]>B[b]){
				A[current--]=A[a--];
			}else{
				A[current--]=B[b--];
			}
		}
		if(a<0){//将B数组中剩下的元素全部移到A数组中
			while(b>=0){
				A[current--]=B[b--];
			}
		}
	}

7、逆序对的个数:一个数列,如果左边的数大,右边的数小,则称这两个数为一个逆序对(左边的大右边的小)。求出一个数列中有多少个逆序对。
法一:设置两个指针,一个指针从头到尾定位元素,另一个指针从第一个指针所指的位置开始向后扫描,发现比第一个指针指向的元素小的,逆序对的指数就加一。
法二:(较优化的算法)在归并排序的合并算法中,只要将右侧队伍中的元素放在当前的位置,逆序对的数量就增加左侧队列中数的个数。(交换不影响逆序对的数量)

    //方法二的实现
    static int number=0;  //逆序对数,全局变量
	public static void part1(int[] A,int p,int r){
		if(p<r){
			int mid=p+((r-p)>>1);//加减的优先级高于左右移
		    part1(A,p,mid);//对左边进行归并排序
		    part1(A,mid+1,r);//对右边进行归并排序
		    part7(A,p,mid,r);//将两遍合并起来
		}
	}
		private static void part7(int[] A,int p,int mid,int r){
		//先把A中的数据拷贝到helper中
		int[] helper=new int[A.length];
		for(int i=0;i<A.length;i++){
			helper[i]=A[i];
		}
		int left=p;//左侧队伍的头部指针,指向待比较元素
        int right=mid+1;//右侧队伍头部指针,指向待比较元素
        int current=p;//原数组的指针,指向待填入数据的位置
        while(left<=mid&&right<=r){
        	if(helper[left]<helper[right]){
        		A[current++]=helper[left++];
        	}else{//这种情况下,逆序对数要增加
        		A[current++]=helper[right++];
        		number+=mid-left+1;//加上左边队列的元素个数
        	}
        }
        //看左边有没有到头。右边没到头也没关系
        while(left<=mid){
        	A[current++]=helper[left++];
        }
	}

三、算法题的五种解法
方法一:举例法
先列举出一些具体的例子,看看是否能发现其中的一般规则。
方法二:模式匹配法(见多识广)
将现有问题与相似问题作类比,看看是否能通过修改相关问题的解法来解决新问题。
方法三:简化推广法
分多步走。首先,我们会修改某个约束条件,比如数据类型或数据量,从而简化这个问题。接着,我们转而处理这个问题的简化版本。最后,一旦找到解决简化问题的算法,我们就可以基于这个问题进行推广,并试着调整简化版问题的解决方案,让它适用于这个问题的复杂版本。
方法四:简单构造法
先从基本的情况(比如n=1)来解决问题,一般只需记下正确的结果。得到n=2的情况。接下来,有了n=1和n=2的结果,就可以试着解决n=3的情况了。(实际上是一种递归的算法)
方法五:数据结构头脑风暴
快速过一下数据结构的列表,然后逐一尝试各种数据结构,一旦找到合适的数据结构,很多问题就能迎刃而解了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值