算法学习-----分治法

分治法

分治法的概念

	先分(将问题抽象法)
	后解 (将一个小问题一个个具体求解)
	合(通过更新,比较等方式将子问题的解合并成一个原问题的解)

分治法的经典例子

分法的实验(学校)

最大子序和

代码
    package 分治法;
    public class solution {
        /*
         * 问题描述
         * 1.用分治算法求解最大子段和问题。要求算法的时间复杂度不超过 O(nlogn)。
        最大子段和问题描述:给定由 n 个整数(可能为负整数)组成的序列 a1, a2,„, an, 
        求该序列形如j k i k a的子段和的最大值。当所有整数均为负整数时定义其最大子段和为 0。
         */
        /*
         * 解决办法,思考如何分解,分解成每一个子问题
         * 			然后通过这几个子问题,递归,求出总问题的答案
         */
        //分解,左边一半最大,右边一半最大,或者中间最大
        public int Divid(int[] nums) {
             return CountMinQue(nums,0,nums.length-1);	
        }
        //分解成三部分递归方程,求解每个子问题,这个函数抽象成 “求这一段的最大子序和,先不要想怎么实现”
        public int  CountMinQue(int []nums,int left,int right){
            if(left==right)
                return nums[left]>0?nums[left]:0;
            else {
                int mid=(left+right)/2;
                int leftMax=CountMinQue(nums, left, mid);
                int RightMax=CountMinQue(nums, mid+1, right);/*思考了很久,为啥上面这两个函数跟下面的是不一样的,解析见思路*//*上面两个是抽象的,下面的是具体实现,求解*/
                int MidMax=getMidMax(nums, left, right, mid);
                return Math.max(RightMax, Math.max(leftMax, MidMax));	/*这里是合,谁大就返回谁*/
            }
        }
        /*也是求这一段最大子序和的具体实现*/
        public int getMidMax(int []nums,int left,int right,int mid) {//这个是这个算法的重点,理解好中间的概念!
            int LeftNum=0;
            int RightNum=0;
            int LeftMax=nums[mid];
            int RightMax=nums[mid+1];
            //从中间开始计算左边的最大
            for(int i=mid;i>=left;i--) {
                LeftNum+=nums[i];     /*这里的for循环+LeftNum,保证了该子序号是连续的*/
                LeftMax=Math.max(LeftNum, LeftMax);
            }
            //从中间开始计算右边的最大
            for(int i=mid+1;i<=right;i++) {
                RightNum+=nums[i];	  /*这里的for循环+RightNum,保证了该子序号是连续的*/
                RightMax=Math.max(RightNum, RightMax);
            }
            return LeftMax+RightMax;

        }
    }
/*测试用例

	-1 -1 -2 -3(全是负数)    0

	1 2 -1 -1(左边最大)      3

	-1 -1 1 2(右边最大)      3

	-1 2 1 -1 (中间最大)     3
*/
问题 与 思考
  • 如何求最大子序和(方法总结)

    1. 先分

      ​ (其实是先将问题抽象化,譬如该题,最大子序和可能在左边,可能在右边,或者中间,定义一个抽象的函数(不是那种抽象)“求某一段的最大子序列”。然后这个可以作为递归方程式。)

    2. 具体思考如何求解

      ​ 求左边和求右边实质是求中间最大子序和,我们思考如何解决这个小问题:如何具体求解某一段的最大子序和?这里求解问题的核心和关键所在。

    3. 合,通过子问题的结果,不断更新,最后得到最终问题的结果。

  • 为什么求左边和求右边和求中间的函数不一样?

  • 算法学习-----分治法_第1张图片

    • 实质上求左边/右边,实质上就是用求中间的函数求的

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gLiHLMjY-1647878604690)(C:\Users\xie\AppData\Roaming\Typora\typora-user-images\image-20220321090308547.png)]

      • 原因(从最小的子问题分析)

        • 当分解成只有一个的时候,左边右边中间,所以此时用一个求中间函数就可以了//实际就是它本身

        ​ eg:2---------->2

        • 当有2个的时候,左边的最大值,是上一层的最大值。右边的最大值,是上一层的最大值。所以此时就是从中间开始求,然后比较中间,左,右,就可以了。

          eg:2 | 3 ------------->5

        • 当有3个的时候,左边的最大值是上一层返回的最大值,右边的数也是他本身,也是最大的。所以此时就是从中间开始求,然后比较中间,左,右,就可以了。

          eg:2 3 |-6 -------------------->5

        • 以此类推,实际上,左边和右边的结果不用求,都是上一层返回的结果(上一层的最大子序列和),只需要求该层中间的最大子序和,然后比较中间,左边,右边就可以了。同时要注意,左边和右边严格上来说不是不用求,他们其实是上一层最大子序和返回的(用求中间的函数得到的)。

  • 如何保证了该子序列是连续的?

    用了for从中间向两边拓展,每拓展一次,LeftSum都在加/减,所以保证了LeftSum一直都在更新,所以该结果是连续的

分治法的例题

最大子序列和

​ leetcode53最大子数组和

​ 面试题16.17连续数列

​ 这两题与上面的最大子序列和一模一样,就不写总结了。

/*最大子数组和,唯一不同就是负数也要算*/
class Solution{
    public int maxSubArray(int[] nums) {
       return max(nums,0,nums.length-1);
   }
   public int max(int[] nums,int left,int right){
       if(left==right)
           return nums[left];
       else{
           int mid=(left+right)/2;
           int LeftSum=max(nums,left,mid);
           int RightSum=max(nums,mid+1,right);
           int MidSum=Mid(nums,left,right,mid);
           return Math.max(LeftSum,Math.max(RightSum,MidSum));
       }
   }
   public int Mid(int []nums,int left,int right,int mid){
       int LSum=0;
       int RSum=0;
       int LeftSum=nums[mid];
       int RightSum=nums[mid+1];
       for(int i=mid;i>=left;i--){
           LSum+=nums[i];
           LeftSum=Math.max(LeftSum,LSum);
       }
       for(int i=mid+1;i<=right;i++){
           RSum+=nums[i];
           RightSum=Math.max(RightSum,RSum);
       }
       return LeftSum+RightSum;
   }
}

多数元素

leetcode169多数元素

class Solution {
    public int majorityElement(int[] nums) {
        return divid(nums,0,nums.length-1);
    }
    //作用:分别求出在某个区间的众数(左边,右边)
    public int divid(int []nums,int left,int right){
        if(left==right){
            return nums[left];
        }
        else{
            int mid=(left+right)/2;
            int Left=divid(nums,left,mid);//左边的众数
            int Right=divid(nums,mid+1,right);//右边的众数
            if(Left==Right)//如果两边的众数都一样,说明这个数就是众数
                return Left;
            int LeftCount=helpAll(nums,left,right,Left);
            int RightCount=helpAll(nums,left,right,Right);
            return LeftCount>RightCount?Left:Right;
        }
    }
    public int helpAll(int nums[],int left,int right,int temp){
        int count=0;
        for(int i=left;i<=right;i++){
            if(temp==nums[i])
                count++;
                System.out.println(nums[i]);
        }
        return count;
    }
}

数组中的第k个最大元素

​ [leetcode215数组中的第k个最大元素](215. 数组中的第K个最大元素)

class Solution {
    public int findKthLargest(int[] nums, int k) {
        //快排,归并排序
        QuickSort(nums,0,nums.length-1,k);
        for(int i=0;i<nums.length;i++){//0 1
            System.out.println(nums[i]);
        }
        return nums[nums.length-k];//直接返回一个第K大的元素
    }
    //用归并排序,先找一个基准,然后比基准大的数放基准的左边
    //比基准小的数放基准的右边
    public void QuickSort(int[] nums,int left,int right,int k){
        if(left<right){
            int mid=Quick(nums,left,right);
        if(mid==nums.length-k)
            return ;
            QuickSort(nums,left,mid-1,k);
            QuickSort(nums,mid+1,right,k);
        }
        else
            return ;
    }
    public int Quick(int[] nums,int left,int right){//找一次基准你
    System.out.printf("左边"+left);
        System.out.println("一开始的"+left);
        // System.out.println("基准元素"+p);
         int p=nums[left];
        while(left<right){
           //
            while(nums[right]>=p&&left<right){//比基准大的话,放在基准的左边
                // System.out.println(left);
                //这里错了
                right--;
            }
             nums[left]=nums[right];
             while(nums[left]<=p&&left<right){//比基准大的话,放在基准的左边
               left++;
                // System.out.println("lll"+right);
            }
            nums[right]=nums[left];
             
        }
        //出来之后,left等于right
       nums[left]=p;
        return left;
    }
}

Pow(x,n)

Pow(x,n)

数值的整数次方

class Solution{
    public double myPow(double x, int n){
        return n>=0?PowCount(x,n):1.0/PowCount(x,-n);
    }
    //这里是算幂的
    public double PowCount(double x,int n){
        if(n==0)
            return 1;
        if(n==1)
            return x;
        else if(n%2==0){
            double y=PowCount(x,n/2);
            return y*y;
        }
        else {
            double y=PowCount(x,n/2);
            return y*y*x;
        }
    }
}

x的平方根

x的平方根

class Solution {
    //分治法练习
    public int mySqrt(int x) {
        if(x==0)
            return 0;
        if(x==1)//为啥还要考虑这种情况?他边界,0,x我还是好乱,救命,二分查找还是好乱啊
            return 1;
       int left=-1;//这里是那个视频的讲解
       int right=x+1;//这里是那个视频的讲解
       while(left+1!=right){
           int mid=(left+right)/2;
            if(mid>x/mid)
                right=mid;//这里是那个视频的讲解
            else if(mid<x/mid)
                left=mid;//这里是那个视频的讲解
            else if(mid==x/mid)
                return mid;
            // System.out.println(mid);
       }
       return left;
    }
}

二叉搜索树的后序遍历

二叉搜索数的后序遍历

class Solution {
    public boolean verifyPostorder(int[] postorder) {
        return verify(postorder,0,postorder.length-1);
    }
    public boolean verify(int []postorder,int left,int right){

        int record=left;
        for(;record<=right;record++){
            if(postorder[record]>postorder[right])
                break;
        }//找到一组左子树的,思考,要是没有呢?
        for(int i)//如果右子树比左子树小,则返回FALSE;
        verify(postorder,left,record-1);
        verify(postorder,record,right-1);
    }
}

求众数

思路

​ 同最大子序和,可以在左边,可能在右边,可能在中间。

但是这样找的方法需要众数比较集中,所以先将众数进行排序。

代码

先排序

/*归并排序*/
package 分治法;
//归并排序
public class MergeSort {
	//
	//先分
	//后合并
	
	//1.先传入一个数组
	public void MergeNums(int[] nums) {
		DividNums(nums, 0, nums.length-1);
	}
	//分,然后递归
	public void DividNums(int[] nums,int left,int right) {
		if(left==right) {
			//分到只有一个元素,这里还没有写,等会认真思考
			return ;
		}
		else {
			//大于等于一个元素
			int mid=(left+right)/2;
			DividNums(nums, left, mid);
			DividNums(nums, mid+1, right);
			Merge(nums,left,mid,right);//这里的关键是归并,前面的重点是中间的求和
		}
	}
	public void Merge(int[] nums,int left,int mid,int right) {
		//将左边的数组和右边的数组合起来
		//创造一个数组装他们拍完序的数组
		int[] MyNew=new int[right-left+1];
		//还要建立两个指针,用来移动位置
		int PointerLeft=left;
		int PointerRight=mid+1;
		int Point=0;
		while(PointerLeft<=mid&&PointerRight<=right) {
			if(nums[PointerLeft]<=nums[PointerRight]) {
				MyNew[Point]=nums[PointerLeft];
				Point++;
				PointerLeft++;
			}
			else {
				MyNew[Point]=nums[PointerRight];
				Point++;
				PointerRight++;
			}	
		}
		while(PointerLeft<mid+1) {
			MyNew[Point]=nums[PointerLeft];
			Point++;
			PointerLeft++;
		}
		while(PointerRight<=right) {
			MyNew[Point]=nums[PointerRight];
			Point++;
			PointerRight++;
		}
		for(int i = 0;i<MyNew.length;i++) {
			nums[left+i]=MyNew[i];//找了很久的bug,原来在这里!!!!!
		}
	}

}

后分治

package 分治法;

public class MyMode {
	int ModeValue;//众数的值
	int ModeCount;//众数的次数
	int Myleft;//因为这两个一直变,所以定义成全局变量
	int Myright;
	//
	public void FindMode(int[] nums) {
		ResultMyNode(nums, 0,nums.length-1);
		System.out.println("众数是"+ModeValue+"重数是"+ModeCount);
	}
    /*先分 ,将问题抽象化,要不众数在左边,要不在右边,要不在中间*/
    /*这里先算众数在中间,这样的话,先分出左边,中间,右边,并且要是左边或者右边的子问题比中间的众数小,那更加不用求了,可以节省时间*/
	public void ResultMyNode(int nums[],int left,int right) {
		split(nums, left, right);//从中间分开两半先
		//算出他的长度
//		int len=Myright-Myleft-1;//这里的重数
		if(ModeCount<=Myleft-left+1) {//说明中间的重数小,开始求左边的重数
			ResultMyNode(nums,left,Myleft);
		}
		if(ModeCount<=nums.length-Myright) {//说明中间的重数小,开始求右边的重数
			ResultMyNode(nums,Myright,nums.length-1);
		}	
	}
	//这里是具体求众数,从中间两边出发,统计众数的个数,并且边界在移动(原来的边界在中间)。当遇到不等的时候,跳出循环,更新左边界和右边界,同时这个左边界作为左边数组的右边界,右边界作为右边数组的左边界。继续求解子问题*/
	public void split(int nums[],int left,int right) {//扩大左边的和扩大右边的
		//前提:有序的数组
		int mid=(left+right)/2;
		int recover;
		for(recover=mid;recover>=left;recover--) {
			if(nums[recover]!=nums[mid])
				break;//找到不等的值,然后跳出来,找到子问题的边界(left,right)
		}
		this.Myleft=recover;
		for(recover=mid+1;recover<=right;recover++) {
			if(nums[recover]!=nums[mid])
				break;
		}
		this.Myright=recover;
		if(Myright-Myleft-1>this.ModeCount) {
			this.ModeCount=Myright-Myleft-1;
			this.ModeValue=nums[mid];
		}
		if(Myright-Myleft-1==this.ModeCount&&nums[mid]>this.ModeValue) {
			
			this.ModeCount=Myright-Myleft-1;
			this.ModeValue=nums[mid];
		}
	}
}

/*测试用例
	1 1 1 2 3(众数在左边)   众数1 重数3
	2 3 1 1 1(众数在右边)	众数1 重数3
	2 1 1 1 3(众数在中间)	众数1 重数3
	2 2 1 1 3(众数有多个,选值大的那一个)	众数2 重数2
*/
问题 与 思考
  1. 如何求众数?

    通过不断维护以下四个变量。

    int ModeValue;//众数的值
    int ModeCount;//众数的次数
    int Myleft;//因为这两个一直变,所以定义成全局变量
    int Myright;
    

    (此过程,不断地分解递归,不断更新这四个值,或者不更新)

    ​ 通过MyLeft和Myright(边界)不断地移动,可以求出某个数的重数。

    ​ 若该数的重数大,更新众数的值。//另外这里有个规则,要是重数一样,谁众数的值大,谁就当众数

    ​ 否则,原众数及重数不变。

    ​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-blnI0r3I-1647878101820)(C:\Users\xie\AppData\Roaming\Typora\typora-user-images\image-20220321130732032.png)]

测试用例
1 1 1 2 3(众数在左边) 众数1 重数3
2 3 1 1 1(众数在右边) 众数1 重数3
2 1 1 1 3(众数在中间) 众数1 重数3
2 2 1 1 3(众数有多个,选值大的那一个) 众数2 重数2
*/


#### 问题 与 思考

1. 如何求众数?

   通过不断维护以下四个变量。

   ```java
   int ModeValue;//众数的值
   int ModeCount;//众数的次数
   int Myleft;//因为这两个一直变,所以定义成全局变量
   int Myright;

(此过程,不断地分解递归,不断更新这四个值,或者不更新)

​ 通过MyLeft和Myright(边界)不断地移动,可以求出某个数的重数。

​ 若该数的重数大,更新众数的值。//另外这里有个规则,要是重数一样,谁众数的值大,谁就当众数

​ 否则,原众数及重数不变。
算法学习-----分治法_第2张图片
以上内容是3-21所总结的学习动态规划内容,不过学校实验的求子序和要下标,迟点再解出这个问题,切记。
不是很完整,继续完善。

你可能感兴趣的:(Java,数据结构,算法)