树形逻辑套路总结

一.决策

  给定一个集合,和一个随机数字,求这个集合是否存在和为此随机数的组合。例如{1,2,3},target=2,可以找到a[1]为2;target=4,可以找到a[0]+a[2]=4;target=0,则找不到元素累加为0。

  按照决策的思想,我们对元素的所有组合是一个0和1的过程。比如在第一个元素开始,我们可以选择加或者不加入等式中,这就有两个分支了:(1)a[0]+....;(2).....。

  整个程序运行就如图一样,是一个树形结构。

    public boolean isCount(int[] nums,int target){
        return decision(nums,0,0,target);
    }
    boolean decision(int[] nums,int i,int count,int target){
    
if(i == nums.length){
       return count == target; }else { return decision(nums,i+1,count+nums[i],target) || decision(nums,i+1,count,target); } }

  由程序可见,它是深度优先的,而深度优先搜索会有很多计算浪费的情况,这个时候就要涉及到剪枝了,怎么剪枝呢,把不需要计算的情况剔除掉。

    boolean decision(int[] nums,int i,int count,int target){
        if(count == target){
            return true;
        }else if(count > target){
            return false;
        }
        if(i == nums.length){
            return count == target;
        }else {
            return decision(nums,i+1,count+nums[i],target) || decision(nums,i+1,count,target);
        }
    }

  当和等于目标随机数的时候,提前结束返回true,而在和大于目标随机数的时候,未来的计算将会是无用的,直接返回false。

  假设target=1,则我们需要把大于1的全部剪掉。

 

  接下来另一个例子,leetcode 17. Letter Combinations of a Phone Number

  将2和3的字符集按顺序组合,这个题是决策思想的另一种应用。设第一个字母a的字符集为a[n],第二个字母b的字符集为b[m]。

  输入ab两个字符,应该出现的结果是i:0->n,j:0->m,a[i]+b[j]。

public List<String> letterCombinations(String digits) {
        String[] letters = {"abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
        List<String> result = new ArrayList<String>();
        createResult(digits,0,letters,"",result);
        return result;
    }
    void createResult(String digits,int i,String[] letters,String letter,List<String> result){
        if(i == digits.length()){
            if(!letter.isEmpty())
                result.add(letter);
            return;
        }else {
            int base = 50;
            int index = (int)digits.charAt(i)-base;
            for(int j=0;j<letters[index].length();j++){
                createResult(digits,i+1,letters,letter+letters[index].charAt(j),result);
            }
        }
    }

  这题不能剪枝了,因为它要的是全部的情况。

二.递推

  在数学的数列中,标志一个数列的规律的称为递推公式,比如等差数列的递推公式为:a[n]-a[n-1]=d,等比数列的递推公式为:a[n] = a[n-1]*q。而递推公式就是一次递归,特别是n为正整数,f为抽象函数的时候。

 

  比如斐波那契数列f(n)=f(n-1)+f(n-2)这个递推公式。计算递推公式通过递归可读性强,但是性能比较差,容易爆栈。

class Solution {
    public int climbStairs(int n) {
        if(n <= 1){
            return 1;
        }else {
            return climbStairs(n-1)+climbStairs(n-2);
        }
    }
}

  输入的n大了就会爆栈,并且时间和很长。

  仔细一看这个递推公式改成循环,性能提升就不止一点点了。

class Solution {
    public int climbStairs(int n) {
        if(n <= 1){
            return 1;
        }else {
            int an_2=1;
            int an_1 = 1;
            int an = 0;
            for(int i=2;i<=n;i++){
                an = an_2 + an_1;
                an_2 = an_1;
                an_1 = an;

            }
            return an;
        }
    }
}

  这道题就是非常受到面试欢迎的题目了,它虽然是easy等级的,但是它为一维动归提供了一些思想基础。

  这道题可以自顶向下分析,比如我走n阶梯,方法有f(n)种,那f(n)是由什么组成的?因为一次可以走一步或者两步,则f(n)应该可以倒着走一步或者两步。倒走一步为f(n-1)种方法,倒走两步为f(n-2)种方法,所以f(n)=f(n-1)+f(n-2),为本题的递推公式。它的初始值很容易就能计算出来,f(0)=0,f(1)=1,f(2)=2,f(3)=f(1)+f(2)=3...

  再看一个递推的应用。

 

  这是一道一维动归的简单题,要求我们不能抢两家连续的,在这种约束下抢最大利益。

  设f(n)为n家抢劫的最大利益,f(n)=max{f(n-1),f(n-2)+A[n]}。很显然如果你要加上A[n]你必须隔开相邻。

  递推的初始值为f(1)=A[1],f(2)=max{A[1],A[2]}。

class Solution {
    public int rob(int[] nums) {
        if(nums == null || nums.length == 0){
            return 0;
        }else if(nums.length == 1){
            return nums[0];
        }else {
            int a1 = nums[0];
            int a2 = Math.max(nums[1],nums[0]);
            int f = Math.max(a1,a2);
            for(int i=2;i<nums.length;i++){
                f = Math.max(a1+nums[i],a2);
                a1 = a2;
                a2 = f;
            }
            return f;
        }
    }

}

三.二分

  二分查找,实际上是一个二叉查找树。二分查找的先决条件,就是这个数组是有序数组。它拥有如下的算法骨架。

int left,right;
while(left < right){
   int medium = (right-left)/2 + left;
   if(target > a[medium]){
       left = medium + 1;
   }else if(target < a[medium]){
       right = medium - 1;
   }else{
       break;
    }    
}    

  有序,但是是重复元素怎么办?二分完了然后中心扩展不就完了。

 

class Solution {
    public int[] searchRange(int[] nums, int target) {
        int left = 0,right = nums.length -1;
        int leftIndex = -1,rightIndex = -1;

        if(nums.length == 0){
            return new int[]{leftIndex,rightIndex};
        }else if(nums.length == 1){
            if(nums[0] == target){
                leftIndex = 0;
                rightIndex = 0;
            }
            return new int[]{leftIndex,rightIndex};
        }

        while(left <= right){
            int mid = left + (right - left)/2;
            if(nums[mid] > target){
                right = mid - 1;
            }else if(nums[mid] < target){
                left = mid + 1;
            }else {
                int i = mid;
                int j = mid;
                while(true){
                    if(i-1 >= 0 && nums[i-1] == target){
                        i--;
                    }else {
                        break;
                    }
                }
                while(true){
                    if(j+1 < nums.length && target == nums[j+1]){
                        j++;
                    }else {
                        break;
                    }
                }
                leftIndex = i;
                rightIndex = j;
                break;
            }
        }
        return new int[]{leftIndex,rightIndex};
    }
}

   下面这个题让我又爱又恨。

  

    public double findMedian(int[] nums1,int[] nums2){
        int n = nums1.length;
        int m = nums2.length;
        int total = n+m;
        if(total %2 != 0){
            return divide(nums1,0,n-1,nums2,0,m-1,(n+m)/2+1);
        }else {
            return 0.5*(divide(nums1,0,n-1,nums2,0,m-1,(n+m)/2) + divide(nums1,0,n-1,nums2,0,m-1,(n+m)/2 + 1));
        }
    }
    double divide(int[] nums1,int start1,int end1,int[] nums2,int start2,int end2,int k){
        int a = end1 - start1 + 1;
        int b = end2 - start2 + 1;
        // 让nums1始终是最短的数组
        if(a > b){
            return divide(nums2, start2, end2, nums1, start1, end1, k);
        }
        // 如果数组nums1已经没有"余额",则直接到nums2取满k
        if(a == 0){
            return nums2[start2 + k-1];
        }
        // 当k减少到1时候,从start1和start2选最小的一个
        if(k == 1){
            return Math.min(nums1[start1],nums2[start2]);
        }
        // 将k分为k/2 以及 k - k/2 两部分
        // 如果 a数组即将走到尽头并且不足k/2,则使用a数组剩余部分
        int addA = Math.min(k/2,a);
        int addB = k - addA;
        // 预计跳转的地方将舍弃小的部分
        if(nums1[start1+addA - 1] > nums2[start2+addB - 1]){
            return divide(nums1,start1,end1,nums2,start2+addB,end2,k-addB);
        }else {
            return divide(nums1,start1+addA,end1,nums2,start2,end2,k-addA);
        }
    }

 

 

四.回溯

  回溯,指的是我从a->b->c状态的时候,从c能回到b状态,然后从b能回到a状态。

  经典案例,全排列的递归算法。算法思路是,将a[i]与k:i->n中随意元素进行位置更换,再返回的时候再换回来,这个换回来的过程就是回溯。

    void perm(int[] nums,int k,int m,List<List<Integer>> result){
        if(k == m){
            List<Integer> rList = new ArrayList<Integer>();
            for(int v :nums){
                rList.add(v);
            }
            result.add(rList);

        }else {
            for(int i=k;i<=m;i++){
                swap(nums,i,k);
                perm(nums,k+1,m,result);
                swap(nums,i,k);
            }
        }
    }

    void swap(int[] nums,int i,int j){
        if(i == j)
            return ;
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

  在for循环里面,先将i与k位置调换,然后进入递归,再递归出来的时候又将i与k调换回来,这是一种恢复状态的行为,成为回溯。

 

  这道题和决策的第一个例子很像,但是它是允许重复元素累加的,这个思维就像返回过来一样。我们最开始的例子是把它加上去,这回我们要把它减掉。

public class CombinationSum {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> result = new ArrayList<>();
        if(candidates == null || candidates.length < 1){
            return result;
        }
        count(candidates,0,target,result,new ArrayList<>());
        return result;
    }
    void count(int[] nums,int i,int target,List<List<Integer>> result ,List<Integer> eachList){
        if(target < 0){
            return ;
        }else if(target == 0){
            result.add(new ArrayList<>(eachList));
            return ;
        }else {
            for(int j=i;j<nums.length;j++){
                eachList.add(nums[j]);
                count(nums,j,target-nums[j],result,eachList);
                eachList.remove(eachList.size()-1);
            }
        }
    }
}

  同样我们使用了回溯的方法,在加入这个元素之后进入递归,递归出来之后去掉这个元素,很遗憾不是很快。

 

posted @ 2019-08-11 16:56  天目山电鳗  阅读(629)  评论(0编辑  收藏  举报