Leetcode-3--递归、DFS、回溯

递归

24 两两交换链表

因为直接去递归next会改变顺序,所以可以每两个结点下去递归
还有链表的那一套~

143 重排链表

这题可以有不是递归的方法 借助辅助数组另外使用双指针

 public void reorderList(ListNode head) {
        if(head ==null || head.next ==null) return ;
        List<ListNode> nodes = new ArrayList<>();
        ListNode cur = head;
        while(cur !=null){
            nodes.add(cur);
            cur = cur.next;
        }
        int i =0;
        int j = nodes.size()-1;
        while(i<j){
            nodes.get(i).next = nodes.get(j);
            i++;
            if(i >= j){
                break;
            }
            nodes.get(j).next = nodes.get(i);
            j--;
        }
        nodes.get(i).next = null;      
    }

递归方法相当于中间段处理好了,返回了这个头结点对应的tail。穿起来。返回外层head 的tail.
递归的重点在于每次都是返回tail

public void reorderList(ListNode head) {
        if(head==null ||head.next==null||head.next.next==null) return ;
        int len = 0;
        ListNode cur = head;
        while(cur!=null){
            len++;
            cur = cur.next;
        }
        reverse(head,len);
    }
    public ListNode reverse(ListNode head,int len){
        if(len ==1){
            ListNode tail = head.next;
            head.next = null;
            return tail;
        }
        if(len==2){
            ListNode tail = head.next.next;
            head.next.next = null;
            return tail;
        }
        ListNode tail = reverse(head.next,len-2);
        ListNode post = tail.next;
        tail.next = head.next;
        head.next = tail;
        return post;
    }

98 验证二叉搜索树

integer 和int的取值范围一样,为什么int这里会报错呢?

 public boolean isValidBST(TreeNode root) {
        return isBST(root, null, null);
    }
    public boolean isBST (TreeNode root, Integer lower, Integer upper){
        if(root == null) return true;
        int val = root.val;
        if(lower != null && val <= lower) return  false;
        if(upper!= null && val >= upper) return false;

        if(! isBST(root.left, lower, val)) return false;
        if(!isBST(root.right, val,upper)) return false;
         return true;
    }

17

至今都还每写对,仍然有问题;

96 不同的二叉搜索树

//1- k-1 作为左子树,k+1 -n作为右子树,最后等于左子树的个数 * 右子树的个数
     //缺少抽象提取信息的能力
    public int numTrees(int n) {
        if(n ==1 || n==0) return 1;
        int res =0;
        for(int i =1;i<=n;i++){
            //其实无所谓递归的数具体是几,只是希望得到这几个数能组成的二叉搜索树的个数
            res += numTrees(i-1) * numTrees(n-i);
        }//看这里,对前面状态的依赖,重复的计算----->DP正合适
        return res;
    }

DP方法

//s得再想象一下
     //缺少抽象提取信息的能力
    public int numTrees(int n) {
        if(n ==1 || n==0) return 1;
        int[] dp = new int[n+1];
        //状态是数字为i时有多少种状态
        dp[0] = 1;
        dp[1] = 1;
        for(int i = 2;i<=n;i++){
           for(int j =1;j<=i;j++){
               dp[i] += dp[j-1] * dp[i-j];
           }
        }
        return dp[n];
    }

二叉树展开成链表

class Solution {
    public void flatten(TreeNode root) {
       List<TreeNode> list = new ArrayList<TreeNode>();
        preorderTraversal(root, list);
        int size = list.size();
        for (int i = 1; i < size; i++) {
            TreeNode prev = list.get(i - 1), curr = list.get(i);
            prev.left = null;
            prev.right = curr;
        }
    }

    public void preorderTraversal(TreeNode root, List<TreeNode> list) {
        if (root != null) {
            list.add(root);
            preorderTraversal(root.left, list);
            preorderTraversal(root.right, list);
        }
    }
    //非递归的先序遍历

}

我想要的递归:

//本来只要dfs遍历就可以,但这里要求原地
 //理解什么是原地,就是不能去新建一棵树,要在这棵树的基础上改
 // 这才是我想要的代码,完美递归,其实仔细想想应该能想到的,,可惜了
class Solution {
    public void flatten(TreeNode root) {
        if(root == null) return ;
        flatten(root.left);
        TreeNode r = root.right;
        root.right = root.left;
        root.left = null;
        while(root.right!=null){
            root = root.right;
        }
        flatten(r);
        root.right = r;
    }

}

回溯

组合、排列、子集、路径(求各种可能的情况的这种题)都可以用回溯。

回溯 其实就是DFS搜索的过程,只是DFS 特指这种用在树上的搜索。
另一个角度理解,回溯其实就是在生成一个搜索树,所以其实回溯和dfs思想一样,可以理解为dfs是一种特殊的回溯。。

link.
这里的回溯讲的很清楚:
解决一个回溯问题,实际上就是一个决策树的遍历过程。一般来说,需要解决三个问题:

  • 路径:也就是已经做出的选择。
  • 选择列表:也就是你当前可以做的选择。
  • 结束条件:也就是到达决策树底层,无法再做选择的条件。

我们所使用的框架基本就是:

LinkedList result = new LinkedList();
public void backtrack(路径,选择列表){
    if(满足结束条件){
        result.add(结果);
    }
    for(选择:选择列表){
        做出选择;
        backtrack(路径,选择列表);
        撤销选择;
    }
}

其中最关键的点就是:在递归之前做选择,在递归之后撤销选择。

22 括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

 List<String> res = new LinkedList<>();
   public List<String> generateParenthesis(int n) {
      dfs(new StringBuilder(),0,0,n);
      return res; 
    }
    public void dfs(StringBuilder s, int left, int right,int n){
        if(right>left || left>n) return ;
        if(s.length()== 2*n){
            res.add(s.toString());
            return ;
        }
        s.append("(");
        //注意这里的坑,不能用++,那是赋值
        dfs(s,left+1,right,n);
        s.deleteCharAt(s.length()-1);
        s.append(")");
        dfs(s,left,right+1,n);
        s.deleteCharAt(s.length()-1);
    }

39 组合总和

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

//不能重复,记得剪

    List<List<Integer>> res = new LinkedList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);
        dfs(candidates, 0,target,0, new LinkedList<Integer>());
        return res;
    }
    //注意剪枝的方式,就是每次都只考虑当前点以及之后的点,因为前面的在当前点考虑就重复了
    
    public void dfs(int[] nums,int index, int target, int sum, LinkedList<Integer> cur){
        if(sum > target) return ;
        if(sum == target){
        //注意这里不能直接添加,因为引用类型,后面还会改变
            res.add(new ArrayList(cur));
            return;
        }
        for(int i = index; i< nums.length;i++){
          cur.add(nums[i]);
          dfs(nums, i,target,sum+nums[i],cur);
          cur.removeLast();
            }
        }

40 组合总和

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用一次。

List<List<Integer>> res = new LinkedList<>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        //与39的区别是,1 有重复数字 2 每个数字只能使用一次
        //两种list都有Collection参数的构造函数
        Arrays.sort(candidates);
        dfs(candidates,0,0,target,new LinkedList<Integer>());
        return res;
    }
    public void dfs(int[] nums, int index,int sum,int target, LinkedList<Integer> cur){
        if(sum > target) return;
        if(sum == target){
            res.add(new LinkedList(cur));
            return ;
        }   
        for(int i = index;i< nums.length;i++){
           if(sum > target) return ;
           if(sum == target){
               res.add(new LinkedList(cur));
               return ;
           }
           //i>0 这样剪枝把下一层相同的数剪了,这是不对的,重复的是同一层的相等的数
           if(i>index && nums[i]==nums[i-1])  continue;
           cur.add(nums[i]);
           dfs(nums,i+1, sum+nums[i], target, cur);
           cur.removeLast();
       }

    }

216 组合总和

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

这题其实相当于一个九叉树的遍历了,只是为了不取重复数字而遍历当前位置的后面的数字。

class Solution {
    List<List<Integer>> res = new LinkedList<>();
    public List<List<Integer>> combinationSum3(int k, int n) {
        //注意这里写起来有点别扭
        dfs(n,k,1,0,new LinkedList<Integer>());

        return res;
    }
    public void dfs(int n, int k, int curNum, int curSum, LinkedList<Integer> tmp){
        if(curSum > n) return ;
        if(curSum== n && tmp.size()==k ){
             res.add(new LinkedList(tmp));
             return ;
        }
        //为了去重,可以规定每个数只能搭配它后面的
        for(int i = curNum; i<= 9;i++){
            tmp.add(i);
            dfs(n,k,i+1,curSum+i,tmp);
            tmp.removeLast();
        }
        
    }
}

46 排列

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

List<List<Integer>> res = new ArrayList<>();
   public List<List<Integer>> permute(int[] nums) {
        dfs(new LinkedList<Integer>(),nums);
        return res;
    }
    public void dfs(LinkedList<Integer> cur, int[] nums){
        if(cur.size()== nums.length){
            res.add(new ArrayList(cur));
        }
        for( int i =0;i< nums.length;i++){
            if(cur.contains(nums[i])) continue;
            cur.add(nums[i]);
            dfs(cur,nums);
            cur.removeLast();
        }
    }

47 全排列 ⭐️

要注意剪枝方法和辅助数组法
nums[i]==nums[i-1] && ! used[i-1] 说明这种情况已经被考虑过了。可以理解为重复的元素在同一层的情况,肯定是会重复的。

//这题在写的时候,就是不知道每次加数进去的时候数字用没用过,因为有重复也不能用contains判断了
    //所以直接每次都全遍历,单用used数组标志用没用过
    //不知道怎么处理,肯定是缺东西了,加辅助数组,临时变量都是方法
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> permuteUnique(int[] nums){  
        Arrays.sort(nums);
        boolean[] used = new boolean[nums.length];
        dfs(new LinkedList(), nums, used);
        return res;
    }
    public void dfs(LinkedList<Integer> cur, int[] nums, boolean[] used ){
        if(cur.size()== nums.length){
            res.add(new ArrayList(cur));
            return ;
        }
        for(int i=0;i<nums.length;i++){
            if(used[i]){
                continue;
            }
            if(i>0 && nums[i]==nums[i-1] && ! used[i-1]){
                continue;
            }
            used[i] = true;
            cur.add(nums[i]);
            dfs(cur,nums,used);
            used[i] = false;
            cur.removeLast();
        }
    }

剑指 offer 字符串排列

class Solution {
    //完全可以按照47的思路来
   List<String> res = new LinkedList<>();
    char[] c;
    public String[] permutation(String s) {
        c = s.toCharArray();
        Arrays.sort(c);
        boolean[] used = new boolean[c.length];
        dfs(c,new StringBuilder(),used);
        return res.toArray(new String[res.size()]);
    }
    void dfs(char[] c, StringBuilder sb,boolean[] used) {
        if(sb.length() ==  c.length){
            res.add(new String(sb));
            return;
        }
        for(int i = 0; i < c.length; i++){
            if(used[i]){
                continue;
            }
            if(i > 0 && c[i] == c[i-1] && !used[i-1]){
                continue;
            }
            used[i] = true;
            sb.append(c[i]);
            dfs(c,sb,used);
            used[i] = false;
            sb.delete(sb.length()-1,sb.length());
        }


        
    }
    
}
class Solution {
    //组合好想,因为从前往后选就行了
    //但是排列是在某个位置选完后,后面的位置要从没选过的元素中选择
    //没选过的元素位置不固定啊
    //这里的交换。。。不懂
   List<String> res = new LinkedList<>();
    char[] c;
    public String[] permutation(String s) {
        c = s.toCharArray();
        dfs(0);
        return res.toArray(new String[res.size()]);
    }
    void dfs(int x) {
        if(x == c.length - 1) {
            res.add(String.valueOf(c)); // 添加排列方案
            return;
        }
        HashSet<Character> set = new HashSet<>();
        for(int i = x; i < c.length; i++) {
            if(set.contains(c[i])) continue; // 重复,因此剪枝
            set.add(c[i]);
            swap(i, x); // 交换,将 c[i] 固定在第 x 位 
            dfs(x + 1); // 开启固定第 x + 1 位字符
            swap(i, x); // 恢复交换
        }
    }
    void swap(int a, int b) {
        char tmp = c[a];
        c[a] = c[b];
        c[b] = tmp;
    }
}

131分割回文串

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。

class Solution {
    List<List<String>> res = new LinkedList<>();
    public List<List<String>> partition(String s) {
        //if(s==null||s.length()==1) return true;
        dfs(s, 0, new LinkedList());
        return res;
    }
    public void dfs(String s, int start, LinkedList<String> cur){
        if(start == s.length()){
            res.add(new LinkedList<>(cur));
            return;
        }
        for(int i = start; i< s.length(); i++){
            if(!isParalism(s,start,i)){
                continue;
            }
            cur.addLast(s.substring(start,i+1));
            dfs(s, i+1, cur);
            cur.removeLast();
        }
    }
    public boolean isParalism(String c, int start, int end){
        if(end==start) return true;
        while(start<end){
            if(c.charAt(start)!= c.charAt(end)){
                return false;
            }
            start++;
            end--;
        }
        return true;
    }
}

78 子集

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

//这个题不会做的点是不知道什么时候可以结束递归啊
    //什么时候当前结果结束,可以放在结果集中?
    List<List<Integer>> res = new LinkedList<>();
    int len =0;
    public List<List<Integer>> subsets(int[] nums) {
        // res.add(new LinkedList<>());
        dfs(nums,0,0,new LinkedList<>());
        return res;  
    }
    public void dfs(int[] nums, int len, int index, LinkedList<Integer> cur){
        if(cur.size() == len){
            res.add(new LinkedList<>(cur));
        }
        for(int i = index;i<nums.length;i++){
            cur.add(nums[i]);
            dfs(nums, len+1,i+1,cur);
            cur.removeLast();
        }
    }

416. 分割等和子集

1 暴力思路

//自己的思路:找到所有的子集分割的情况,然后判断相等的
  //关键是怎么找到所有情况的dfs也写不出来,因为没读懂隐含的条件
  //两个等和子集,说明每个子集的和都是 nums 数组和的一半
  //超时但却思路完整的代码
  public boolean canPartition(int[] nums) {
      if(nums.length==1) return false;
      int total = 0;
      for(int num: nums) total += num;
      if(total % 2 != 0) return false;
      //接下来的过程就是找 total/2的子集了;
      if(dfs(nums, 0, total /2)) return true;
      return false;
  }
  public boolean dfs(int[] nums, int index, int cur){
      if(cur==0) return true;
      for(int i = index;i<nums.length;i++){
          if(cur-nums[i]<0) return false;
          if(dfs(nums,i+1, cur-nums[i])) return true;
          //这其实也是回溯,但是因为参数传cur,所以值传到下一层但这层的值没有改变
      }
      return false;
  }

2 暴力会超时,经过剪枝叶,可以ac

//子集中可以有重复数字,但是这个重复不应该在同一层中,否则后面的这个重复的就属于多考虑的了
    //于是那个问题又出现了,就是怎么去掉同一层重复的,而保留下一层重复
    public boolean canPartition(int[] nums) {
        Arrays.sort(nums);
        if(nums.length==1) return false;
        int total = 0;
        for(int num: nums) total += num;
        if(total % 2 != 0) return false;
        //接下来的过程就是找 total/2的子集了;
        if(dfs(nums, 0, total /2)) return true;
        return false;
    }
    public boolean dfs(int[] nums, int index, int cur){
        if(cur==0) return true;
        for(int i = index;i<nums.length;i++){
            //控制一下index,就可以保证只在这一层控制重复的问题
            if(i-1 >= index && nums[i]==nums[i-1]) continue;
            if(cur-nums[i]<0) return false;
            if(dfs(nums,i+1, cur-nums[i])) return true;
            //这其实也是回溯,但是因为参数传cur,所以值传到下一层但这层的值没有改变
        }
        return false;
    }

3 背包 动态规划

 //01背包wenti
    //dp[i][j]表示从数组的 [0, i] 这个子区间内挑选一些正整数,每个数只能用一次,使得这些数的和恰好等于 j。
    public boolean canPartition(int[] nums) {
        //zhuangtai, hang 
        int len = nums.length;
        int total = 0;
        for(int num: nums) total += num;
        if(total % 2 != 0) return false;
        boolean [][] dp = new boolean [len][total /2 + 1];
        if(nums[0]<total/2) dp[0][nums[0]] = true;
        for(int i = 1;i<len;i++){
            for(int j= 0;j<=total/2;j++){
                if(nums[i] == j){
                    dp[i][j] = true;
                    continue;
                }
                if(nums[i] <j){
                    dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]];
                }
            }
        }
        return dp[len-1][total/2];
    }

N皇后问题

搜索问题都是这样做的,有一个map, 走过的点全都打上标记。遇到标记了就说明不能走,也就是每次走没有标记的位置(可能还有些别的条件,像这里),所以主要是遍历的方式,每一次要做啥,做完去哪里?那里走到头发现不通,咋回去?

对于 决策问题 也可以这样做,相当于搜索的是一棵树了。

class Solution {
    //要想不相互攻击,每行每列每个斜着的线都只能有一个皇后
    List<List<String>> res = new LinkedList<>();
    public List<List<String>> solveNQueens(int n) {
        char[][] maps = new char[n][n];
        for(char[] map : maps) Arrays.fill(map,'.');
        LinkedList<String> cur = new LinkedList<String>();
        dfs(maps, 0, cur);
        return res; 
    }
    public void dfs(char[][] maps, int row,LinkedList<String> cur){
        if(row == maps.length ){
            res. add(new LinkedList<>(cur));
            return;
        }
        int n = maps[row].length;
        for(int i =0; i< n;i++){
            if(!isValid(maps, row, i)){
                continue;
            }
            maps[row][i] = 'Q';
            cur.add(new String(maps[row]));
            dfs(maps, row+1,cur);
            maps[row][i] = '.';
            cur.removeLast();
        }
    }
    public boolean isValid(char[][] maps, int row,int col){
        //检查这一列,以及这个点的左上和右上,因为是遍历着往下填,所以不需要考虑对角线的下半部分
        //列
        for(int i =0;i<maps.length;i++){
            if(maps[i][col] == 'Q') {
                return false;
            }
        }
        //右上方
        for(int i = row-1, j = col+1; i>=0 && j<maps[row].length;i--,j++){
            if(maps[i][j] == 'Q'){
                return false;
            }
        }
        //左上方
        for(int i = row-1, j= col-1;i>=0 && j>=0; i--,j--){
            if(maps[i][j] == 'Q'){
                return false;
            }
        }
        return true;

    }
}

面试题 08.08. 有重复字符串的排列组合

class Solution {
    LinkedList<String> res = new LinkedList<String>();
    public String[] permutation(String S) {
        //先思考一下,读懂题目的隐藏条件才行
        //有重复 意味着 如果直接搜索列举排列所有的字符串,会有重复的,
        //所以 得剪枝,方法跟之前的相同, 剪掉的是重复的,所以 得先把 字符串 按字典序 排序
        char[] s = S.toCharArray();
        Arrays.sort(s);
        boolean[] used = new boolean[s.length];
        dfs(s, 0, new StringBuilder(),used );
        //注意list转String的方式,直接toArray得到的是object类型的
        return res.toArray(new String[res.size()]);
    }
    public void dfs(char[] s, int index, StringBuilder cur,boolean[] used ){
        if(cur.length() == s.length){
            res.add(cur.toString());
            return ;
        }
        for(int i = 0; i<s.length; i++){
            if(!used[i]){
                if(i>0 && s[i] == s[i-1] && !used[i-1] ){
                    continue;
                }

                 cur.append(s[i]);
                 used[i] = true;
                 dfs(s, i+1, cur,used);
                 cur.deleteCharAt(cur.length()-1);
                 used[i] = false;
            }
        }
    }
}

硬币兑换

class Solution {
     //硬币是可以重复使用的
    //一下就想到了做法,却在写代码时卡了几次
    public int coinChange(int[] coins, int amount) {
        if(coins ==null||coins.length ==0) return -1;
        //各个和所需要的最小硬币数目
        int[] dp = new int[amount+1];
        //注意这里没必要全都填最大整数,只要填amount+1,除非全用1 ,这是最小的情况了
        Arrays.fill(dp, amount+1);
        dp[0] = 0;
        for(int i=1;i<=amount;i++){
            for(int j = 0;j< coins.length;j++){
                if(i-coins[j] >=0 && dp[i-coins[j]] != amount+1){
                    dp[i] = Math.min(dp[i],dp[i-coins[j]]+1);
                }
            }
        }
        return dp[amount] == amount+1 ? -1: dp[amount];
    }
    //dfs
    //硬币是可以重复使用的
    //dfs想法也很自然的,就是每次选一个数,下一次递归时就递归一个当前需要减掉选的这个数
    //dfs只能写void,不能写带返回值的,因为没剪枝,所以会超时,相当于暴力递归了
    int min = Integer.MAX_VALUE;
    public int coinChange(int[] coins, int amount) {
        if(coins==null||coins.length==0) return -1 ;
         dfs(coins, amount, 0);
         return min == Integer.MAX_VALUE? -1:min;
    }
    public void  dfs( int[] coins, int amount, int res){
        if(amount < 0) return ;
        if(amount == 0){
            min = Math.min(res, min);
            return ;
        }
        for(int i=0;i<coins.length;i++){
            dfs(coins,amount-coins[i],res+1);
        }

    }
}

74 单词搜索

要尽量减少 dfs的次数,一开始没有及时返回,没有进行判断,会超时

class Solution {
    //DFS,
    // 矩阵中每个点都可以作为开始结点
    // 用过的点不能再用,所以要加标记,还要在回溯回来的过程中取消标记
    //超时怎么办,就尽量减少递归次数
    boolean res = false;
    public boolean exist(char[][] board, String word) {
        if(board == null || word == null) return false;
        char[] chars = word.toCharArray();
        int len = chars.length;
        int m = board.length;
        int n = board[0].length;
        int[][] used = new int[m][n];
        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                //不需要所有的都遍历,只要找到一个true就可以立即返回
                if(board[i][j] == chars[0]){
                   dfs(board,chars,0,i,j,used);
                }
                if(res){
                    return true;
                }

            }
        }
        return false;
    }
    public void dfs(char[][] board, char[] chars, int index,int i, int j,int[][] used){
        if(index == chars.length){
            res = true;
            return ;
        }
        if(i<0 || i>board.length-1|| j<0 || j>board[0].length-1) return ;
        if(used[i][j] == 1) return ;
        if(board[i][j] !=  chars[index]) return ;
        used[i][j] = 1;
        //这里也要随时判断来减少递归的次数
        dfs(board,chars,index+1,i+1,j,used); 
        if(res) return;
        dfs(board,chars,index+1,i-1,j,used);
        if(res) return;
        dfs(board,chars,index+1,i,j+1,used);
        if(res) return;
         dfs(board,chars,index+1,i,j-1,used);
         if(res) return;
        used[i][j] = 0;
    }
}

剑指 Offer 55 - II. 平衡二叉树

判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。

这题的点有两个,一是递归时是要知道左右子树的高度的
二是:如何融合既需要高度又需boolean的递归;

public boolean isBalanced(TreeNode root) {

    return dfs(root)==-1? false:true;
}
public int dfs(TreeNode root){
    if(root == null) return 0;
    int left = dfs(root.left);
    if(left == -1) return -1;
    int right = dfs(root.right);
    if(right == -1) return -1;
    return Math.abs(left-right) >1?-1 : Math.max(left,right)+1;
}

面试题 08.08. 有重复字符串的排列组合

class Solution {
    LinkedList<String> res = new LinkedList<String>();
    public String[] permutation(String S) {
        //先思考一下,读懂题目的隐藏条件才行
        //有重复 意味着 如果直接搜索列举排列所有的字符串,会有重复的,
        //所以 得剪枝,方法跟之前的相同, 剪掉的是重复的,所以 得先把 字符串 按字典序 排序
        char[] s = S.toCharArray();
        Arrays.sort(s);
        boolean[] used = new boolean[s.length];
        dfs(s, 0, new StringBuilder(),used );
        //注意list转String的方式,直接toArray得到的是object类型的
        return res.toArray(new String[res.size()]);
    }
    public void dfs(char[] s, int index, StringBuilder cur,boolean[] used ){
        if(cur.length() == s.length){
            res.add(cur.toString());
            return ;
        }
        for(int i = 0; i<s.length; i++){
            if(!used[i]){
                if(i>0 && s[i] == s[i-1] && !used[i-1] ){
                    continue;
                }

                 cur.append(s[i]);
                 used[i] = true;
                 dfs(s, i+1, cur,used);
                 cur.deleteCharAt(cur.length()-1);
                 used[i] = false;
            }
        }
    }
}

93 复原IP

给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。

有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。

class Solution {
    //一个经典的回溯,枚举所有的可能的分割情况
    //不合理的直接返回, 合理的直到分割了四个数了,就加进去
    //思路好懂,但是代码不好写,
    // 难在 一: 合理怎么判断
    //二:对于每个位置的数,要考虑它下一步的情况。。应该把哪些列为下一步要考虑的???
    //对于每一个数字,最多往后考虑三位,如果已有三位,要加点,,,只有一位的也可以加点啊,还要考虑0开头不对
    //但其实这样只会增加代码的实现的复杂度, 可以直接往后,组成的数字不合理就停

    List<String> res = new LinkedList<String>();
    int SegCount = 4;
    int[] segments = new int[SegCount];
    public List<String> restoreIpAddresses(String s) {
        int len = s.length();
        if(len < 4 || len > 12) return new ArrayList<String>();
        dfs(s, 0, 0);
        return res; 
    }
    public void dfs(String s, int cur, int segmentId){
        //合理的要加进去的情况,数组满且字符串用完
      if(segmentId == SegCount){
          if(cur == s.length()){
              StringBuilder sb = new StringBuilder();
              for(int i = 0; i < SegCount; i++){
                  sb.append(segments[i]);
                  if(i != SegCount-1){
                      sb.append('.');
                  }
              }
              res.add(sb.toString());
          }
          return;
      }
      //虽然数组没填满,但字符串用完了的情况
      if(cur == s.length()){
          return;
      }
      //开始为0的情况
      if(s.charAt(cur) == '0'){
          segments[segmentId] = 0;
          dfs(s, cur+1, segmentId+1);
      }
      //普通的情况,就是以当前坐标往后尽可能的弄各种数字
      int countNum = 0;
      for(int i = cur; i < s.length(); i++){
          countNum = countNum * 10 + (s.charAt(i) - '0');
          //countNum只要是合理的,就可以继续往下递归
          if(countNum > 0 && countNum <= 0xFF){
              segments[segmentId] = countNum;
              dfs(s, i+1, segmentId+1);
          }else{
              break;
          }
      }
    }
}

动态规划

动态规划算法是一种空间换时间的策略。我的理解本质上动态规划还是一个决策过程,每一步都会有选择,并且这个选择会对后面的结果产生影响。满足无后效性和最优子结构。

无后效型:如果给定 某一状态,则在这一阶段以后的发展过程 不受 这一状态之前的各种状态的影响。
最优子结构:大问题的最优解可以由小问题的最优解推出。
问题引入

自上而下分析DP
DP问题的核心是状态转移方程,如何得到这个方程呢?
可以从结果状态出发,看结果状态是如何由前一个或前几个状态得到。(做题时注意分析)。一般需要分类讨论各种可能出现的情况。

DP实施
在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。也就是说仍然从头开始考虑所有的可能性,但是只保留那部分有可能成为最优解的。
考虑DP问题的步骤:

状态 ,一般是所求是什么,什么就是状态;
状态转移方程 ,自上而下分析;
状态初始化 ,可以直接得到的状态,或者状态转移方程的边界值;
输出,返回值,状态转移矩阵出口处理;
考虑是否能进行状态压缩,即压缩状态转移矩阵。
最终解决DP问题需要按照状态转移方程,从初始化的状态开始打表格。
必要时从状态转移方程中看是否需要多设置一行或者一列哨兵,可以避免很多边界值的讨论。

1. 线性DP

Given a target find minimum (maximum) cost / path / sum to reach the target.

routes[i] = min(routes[i-1], routes[i-2], … , routes[i-k]) + cost[i];所以可以到达当前状态的值中最小的加上这个状态的值。(参考上面的Q点)

53. 最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
1 状态:到达当前位置的最大连续子数组和;
2 状态转移: 对于任一个状态,要么等于前面的最大和加它本身,要么等于它自己,

//dp[0]初始化为nums[0]的值,或者最小int
for(int i=1;i<nums.length;i++){
            dp[i] = Math.max(dp[i-1]+ nums[i],nums[i]);
            res = dp[i] > res? dp[i]:res;
        }

64. 最小路径和

给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
每次只能向下或者向右移动一步。
最暴力的做法,就是搜索出所有的左上到右下的路径,并求路径和最小的那个(DFS)

二维矩阵的DP
DP矩阵为一个二维数组,从左上角开始填充,出口为右下角。

public int minPathSum(int[][] grid) {
      int m = grid.length;
      int n = grid[0].length;
      if (m == 0 || n == 0) {
          return 0;
      }
      // 初始化
      int[][] dp = new int[m][n];
      dp[0][0] = grid[0][0];
      //也算初始化,因为最上面的一行只能从左边得到
      for (int i = 1; i < m; i++) {
          dp[i][0] = dp[i - 1][0] + grid[i][0];
      }
      //最左边的一列只能从上面得到
      for (int j = 1; j < n; j++) {
          dp[0][j] = dp[0][j - 1] + grid[0][j];
      }
      //dp过程
      for (int i = 1; i < m; i++) {
          for (int j = 1; j < n; j++) {
              dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
          }
      }
      //出口
      return dp[m - 1][n - 1];

  }
优化
不使用额外的空间,直接在原矩阵上修改。因为dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j],前面的值修改后后面不会再使用到。
public int minPathSum(int[][] grid) {
     for(int i = 0; i < grid.length; i++) {
         for(int j = 0; j < grid[0].length; j++) {
             if(i == 0 && j == 0) continue;
             else if(i == 0)  grid[i][j] = grid[i][j - 1] + grid[i][j];
             else if(j == 0)  grid[i][j] = grid[i - 1][j] + grid[i][j];
             else grid[i][j] = Math.min(grid[i - 1][j], grid[i][j - 1]) + grid[i][j];
         }
     }
     return grid[grid.length - 1][grid[0].length - 1];
 }

DFS

剑指 Offer 55 - II. 平衡二叉树
判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。

这题的点有两个,一是递归时是要知道左右子树的高度的
二是:如何融合既需要高度又需boolean的递归;

public boolean isBalanced(TreeNode root) {
        
        return dfs(root)==-1? false:true;
    }
    public int dfs(TreeNode root){
        if(root == null) return 0;
        int left = dfs(root.left);
        if(left == -1) return -1;
        int right = dfs(root.right);
        if(right == -1) return -1;
        return Math.abs(left-right) >1?-1 : Math.max(left,right)+1;
    }

面试题 08.08. 有重复字符串的排列组合

class Solution {
    LinkedList<String> res = new LinkedList<String>();
    public String[] permutation(String S) {
        //先思考一下,读懂题目的隐藏条件才行
        //有重复 意味着 如果直接搜索列举排列所有的字符串,会有重复的,
        //所以 得剪枝,方法跟之前的相同, 剪掉的是重复的,所以 得先把 字符串 按字典序 排序
        char[] s = S.toCharArray();
        Arrays.sort(s);
        boolean[] used = new boolean[s.length];
        dfs(s, 0, new StringBuilder(),used );
        //注意list转String的方式,直接toArray得到的是object类型的
        return res.toArray(new String[res.size()]);
    }
    public void dfs(char[] s, int index, StringBuilder cur,boolean[] used ){
        if(cur.length() == s.length){
            res.add(cur.toString());
            return ;
        }
        for(int i = 0; i<s.length; i++){
            if(!used[i]){
                if(i>0 && s[i] == s[i-1] && !used[i-1] ){
                    continue;
                }

                 cur.append(s[i]);
                 used[i] = true;
                 dfs(s, i+1, cur,used);
                 cur.deleteCharAt(cur.length()-1);
                 used[i] = false;
            }
        }
    }
}

动态规划

自上而下分析DP

DP问题的核心是状态转移方程,如何得到这个方程呢?
可以从结果状态出发,看结果状态是如何由前一个或前几个状态得到。(做题时注意分析)。一般需要分类讨论各种可能出现的情况。

DP实施

在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。也就是说仍然从头开始考虑所有的可能性,但是只保留那部分有可能成为最优解的。
考虑DP问题的步骤:

  1. 状态 ,一般是所求是什么,什么就是状态;
  2. 状态转移方程 ,自上而下分析;
  3. 状态初始化 ,可以直接得到的状态,或者状态转移方程的边界值;
  4. 输出,返回值,状态转移矩阵出口处理;
  5. 考虑是否能进行状态压缩,即压缩状态转移矩阵。
    最终解决DP问题需要按照状态转移方程,从初始化的状态开始打表格。
    必要时从状态转移方程中看是否需要多设置一行或者一列哨兵,可以避免很多边界值的讨论

1. 线性DP

Given a target find minimum (maximum) cost / path / sum to reach the target.

routes[i] = min(routes[i-1], routes[i-2], … , routes[i-k]) + cost[i];所以可以到达当前状态的值中最小的加上这个状态的值。(参考上面的Q点)

53. 最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
1 状态:到达当前位置的最大连续子数组和;
2 状态转移: 对于任一个状态,要么等于前面的最大和加它本身,要么等于它自己,

//dp[0]初始化为nums[0]的值,或者最小int
for(int i=1;i<nums.length;i++){
            dp[i] = Math.max(dp[i-1]+ nums[i],nums[i]);
            res = dp[i] > res? dp[i]:res;
        }

64. 最小路径和

给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
每次只能向下或者向右移动一步。
最暴力的做法,就是搜索出所有的左上到右下的路径,并求路径和最小的那个(DFS)

  1. 二维矩阵的DP
    DP矩阵为一个二维数组,从左上角开始填充,出口为右下角。
public int minPathSum(int[][] grid) {
      int m = grid.length;
      int n = grid[0].length;
      if (m == 0 || n == 0) {
          return 0;
      }
      // 初始化
      int[][] dp = new int[m][n];
      dp[0][0] = grid[0][0];
      //也算初始化,因为最上面的一行只能从左边得到
      for (int i = 1; i < m; i++) {
          dp[i][0] = dp[i - 1][0] + grid[i][0];
      }
      //最左边的一列只能从上面得到
      for (int j = 1; j < n; j++) {
          dp[0][j] = dp[0][j - 1] + grid[0][j];
      }
      //dp过程
      for (int i = 1; i < m; i++) {
          for (int j = 1; j < n; j++) {
              dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
          }
      }
      //出口
      return dp[m - 1][n - 1];

  }
  1. 优化
    不使用额外的空间,直接在原矩阵上修改。因为dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j],前面的值修改后后面不会再使用到。
public int minPathSum(int[][] grid) {
     for(int i = 0; i < grid.length; i++) {
         for(int j = 0; j < grid[0].length; j++) {
             if(i == 0 && j == 0) continue;
             else if(i == 0)  grid[i][j] = grid[i][j - 1] + grid[i][j];
             else if(j == 0)  grid[i][j] = grid[i - 1][j] + grid[i][j];
             else grid[i][j] = Math.min(grid[i - 1][j], grid[i][j - 1]) + grid[i][j];
         }
     }
     return grid[grid.length - 1][grid[0].length - 1];
 }

120 三角形最小路径和

给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。

相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。

public int minimumTotal(List<List<Integer>> triangle) {
        if(triangle == null || triangle.size()==0) return 0;
        int m = triangle.size();
        int n = triangle.get(m-1).size();
        //dp 矩阵,状态就是每个位置对应的最小路径值
        int[][] dp = new int[m][n];
        //初始化
        dp[0][0] = triangle.get(0).get(0);
        int res = Integer.MAX_VALUE;
        //遍历,注意是三角形,每一列的数等于行号
        for(int i = 1; i< m; i++){
            for(int j = 0; j <= i; j++){
                //先处理两种特殊情况
                if(j == 0){
                    dp[i][j] = dp[i-1][j] + triangle.get(i).get(j);
                }else if(j == i){
                    dp[i][j] = dp[i-1][j-1] + triangle.get(i).get(j);
                }else{
                    dp[i][j] = Math.min(dp[i-1][j],dp[i-1][j-1]) + triangle.get(i).get(j);
                }
            }
        }
        //出口
        for(int i= 0;i < n;i++){
            res = res<dp[m-1][i] ? res:dp[m-1][i];
        }
        return res;
    }

空间优化,使用o(n)的空间复杂度。也就是二维dp数组压缩成一维的过程。因为更新每个值时需要用到上一行的两个值,所以需要两个临时变量来存。

 int prev = 0, cur;
     for (int i = 1; i < triangle.size(); i++) {
         //对每一行的元素进行推导
         List<Integer> rows = triangle.get(i);
         for (int j = 0; j <= i; j++) {
             cur = dp[j];
             if (j == 0) {
                 // 最左端特殊处理
                 dp[j] = cur + rows.get(j);
             } else if (j == i) {
                 // 最右端特殊处理
                 dp[j] = prev + rows.get(j);
             } else {
                 dp[j] = Math.min(cur, prev) + rows.get(j);
             }
             prev = cur;
         }
     }

152. 乘积最大子数组

221 最大正方形

在一个由 0 和 1 组成的二维矩阵内,找到只包含 1 的最大正方形,并返回其面积(好像那个找有个岛的问题哦,不过那个是dfs)。

首先 ,可以有暴力方法,每增加一行或一列判断值是否全为1。
动态规划:dp(i,j) 表示以 (i, j) 为右下角,且只包含 11 的正方形的边长最大值。动态规划中,当前状态要由前面的状态确定,此时需要考虑三个状态,而不是仅仅考虑对角线的,详情可以看这个。

 public int maximalSquare(char[][] matrix) {
        if(matrix == null || matrix.length==0 ||matrix[0].length==0) return 0;
        int res = 0;
        // 注意状态是什么
        int row = matrix.length;
        int col = matrix[0].length;
        int[][] dp = new int[row][col];
        //初始化
        
        for(int i = 0;i<col;i++) {
            dp[0][i] = matrix[0][i] ==1 ? 1:0;
        }
        for(int j = 0;j<row;j++){
            dp[j][0] = matrix[j][0]==1 ? 1:0;
        }
                
        for(int i = 1;i<row;i++){
            for(int j = 1;j<col;j++){
                if(matrix[i][j] =='1'){
                    dp[i][j] = Math.min(dp[i-1][j], Math.min(dp[i-1][j-1],dp[i][j-1])) +1;
                }
                 res = Math.max(res,dp[i][j]);
            }
        }
        //状态是以该点为右下角的最大的边长,返回值应该平方
        return res*res;

    }

279. 完全平方数
这题不太明白,贪心的做法也还没看
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

很直接的思路是暴力枚举所有比n小的完全平方数,并枚举所有的组合选小的,但是这题没给n的范围,也就是会有n很大的情况,暴力会超时。

对于n,考虑所有的n-k的情况,k为小于n的完全平方数,这样就变成了递归:numSquares(n) = min(numSquares(n-k) + 1) ∀k∈square numbers
但仍然有超时的问题,因为这个过程存在着大量的重复计算。因此可以考虑使用DP算法。

需要提前计算所有小于等于n的完全平方数。
状态:1-n 每个的数的最小 个数。

public int numSquares(int n) {
    int dp[] = new int[n + 1];
    Arrays.fill(dp, Integer.MAX_VALUE);
    // bottom case
    dp[0] = 0;

    // pre-calculate the square numbers.
    int max_square_index = (int) Math.sqrt(n) + 1;
    int square_nums[] = new int[max_square_index];
    for (int i = 1; i < max_square_index; ++i) {
      square_nums[i] = i * i;
    }

    for (int i = 1; i <= n; ++i) {
      for (int s = 1; s < max_square_index; ++s) {
        if (i < square_nums[s])
          break;
        dp[i] = Math.min(dp[i], dp[i - square_nums[s]] + 1);
      }
    }
    return dp[n];
    }

322. 零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

public int coinChange(int[] coins, int amount) {
        //状态
        int[] dp = new int[amount+1];
        Arrays.fill(dp, amount+1);
        //初始化
        dp[0] = 0;
        //dp过程
        for (int i = 1; i <= amount; i++) {
            for (int j = 0; j < coins.length; j++){
               if (coins[j] <= i) {
                dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
            }
        }
        }
        //出口为什么是这个?
        return dp[amount] > amount? -1:dp[amount];
    }

2 达到目标的不同方式总数

Given a target find a number of distinct ways to reach the target.
解法:Sum all possible ways to reach the current state.
routes[i] = routes[i-1] + routes[i-2], … , + routes[i-k]

Generate sum for all values in the target and return the value for the target.

for (int i = 1; i <= target; ++i) {
   for (int j = 0; j < ways.size(); ++j) {
       if (ways[j] <= i) {
           dp[i] += dp[i - ways[j]];
       }
   }
}
return dp[target]

70 爬楼梯(青蛙跳台阶,斐波那契)

递归解法会超时!!!
注意考虑边界n和不符合要求的n

 public int climbStairs(int n) {
        if(n<2) return n;
        //1 什么是状态,n个台阶,爬到每个台阶时的方法数
        int[] dp = new int[n];
        //2 状态初始化
        dp[0] = 1;
        dp[1] = 2;
        //3 状态转移
        for(int i =2;i<n;i++){
            dp[i] = dp[i-1] + dp[i-2];
        }
        //dp出口
        return dp[n-1];   
    }

62. 不同路径

这个题有点像T64,只不过64还要计算最小路径和。

 public int uniquePaths(int m, int n) {
        //状态
        int[][] dp = new int[n][m];
        //初始化,只能向下向右,所以第一行第一列可以初始化
        for(int i = 0;i<n; i++){
            dp[i][0] = 1;
        }
        for(int j =0; j<m; j++){
            dp[0][j] = 1;
        }
        //dp 过程
        for(int i = 1;i<n ;i++){
            for(int j = 1;j<m;j++){
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        //出口
        return dp[n-1][m-1];
    }

63. 不同路径 II

T62只能向右或向下走,这题不仅向下向右,还添加了障碍物。

public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        //状态
        int[][] dp = new int[m][n];
        //初始化
        boolean flag = false;
        for(int i=0;i<n;i++){
           if(obstacleGrid[0][i]==1) flag = true;
           if(flag==true) dp[0][i] = 0;
           else dp[0][i] = 1;
        }
        flag = false;
         for(int i=0;i<m;i++){
           if(obstacleGrid[i][0]==1) flag = true;
           if(flag==true) dp[i][0] = 0;
           else dp[i][0] = 1;
        }
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                if(obstacleGrid[i][j] == 1) dp[i][j] = 0;
                else dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }

96. 不同的二叉搜索树

377. 组合总和 Ⅳ

这题直接想用回溯做,但是回溯的话肯定会有重复的问题,所以必须要剪枝。既然有重复,可以考虑记忆化搜索,也就是DP。

public int combinationSum4(int[] nums, int target) {
       //状态,就是题目中说的 和为目标正整数的组合的数目,目标从1到n;
       int[] dp = new int[target+1];
       //初始化,这里的dp[0]不是 目标是0,而是一个标志,因为用到0的时候,说明nums中出现过当前这个目标数字
       //所以设置为1
       dp[0] = 1;
       //
       for(int i =1;i<=target;i++){
           for(int num : nums){
               if(i-num >= 0){
               dp[i] += dp[i-num];
               }
           }
       }
       return dp[target];
    }

更细节上来说,这是一种背包问题:

常见的背包问题有1、组合问题。2、True、False问题。3、最大最小问题。分为三类。
1、组合问题:377. 组合总和 Ⅳ, 494. 目标和 , 518. 零钱兑换 II
2、True、False问题:139. 单词拆分 416. 分割等和子集
3、最大最小问题: 474. 一和零 322. 零钱兑换

416. 分割等和子集

494 目标和

给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

public int findTargetSumWays(int[] nums, int S) {
       //状态 dp[i][j] 表示用数组中的前 i 个元素,组成和为 j 的方案数
       int[][] dp = new int[nums.length][2001];
        dp[0][nums[0] + 1000] = 1;
        dp[0][-nums[0] + 1000] += 1;
        for (int i = 1; i < nums.length; i++) {
            for (int sum = -1000; sum <= 1000; sum++) {
                if (dp[i - 1][sum + 1000] > 0) {
                    dp[i][sum + nums[i] + 1000] += dp[i - 1][sum + 1000];
                    dp[i][sum - nums[i] + 1000] += dp[i - 1][sum + 1000];
                }
            }
        }
        return S > 1000 ? 0 : dp[nums.length - 1][S + 1000];
    }

2–子序列问题

300. 最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。

public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        //状态,以数组中每个位置为结尾的子串的 最长上升子序列
        int[] dp = new int[n];
        //初始化
        Arrays.fill(dp, 1);
        for(int i=1;i<n;i++){
            for(int j=0; j<i; j++){
                if(nums[i] > nums[j]){
                    dp[i] = Math.max(dp[i], dp[j]+1);
                }
            }
        }
        int res = 0;
        for(int i=0; i<n;i++){
           res = Math.max(dp[i],res);
        }
        return res;
    }

674 最长连续递增序列
给定一个未经排序的整数数组,找到最长且连续的的递增序列。
滑动窗口 和 动态规划 都可以做。
滑动窗口做法:

public int findLengthOfLCIS(int[] nums) {
        int ans = 0, anchor = 0;
        for (int i = 0; i < nums.length; ++i) {
            if (i > 0 && nums[i-1] >= nums[i]) anchor = i;
            ans = Math.max(ans, i - anchor + 1);
        }
        return ans;
    }

DP做法:

public int findLengthOfLCIS(int[] nums) {
        if(nums==null || nums.length==0) return 0;
        //状态
        int[] dp = new int[nums.length];
        //初始化
        dp[0] = 1;
        for(int i=1; i< nums.length;i++){
            if(nums[i] > nums[i-1]){
                dp[i] = dp[i-1]+1;
            }else{
                dp[i] = 1;
            }
        }
        int res = 0;
        for(int i=0;i<nums.length;i++){
            res = dp[i]>res?dp[i]:res;
        }
        return res;
    }

T300和T674 可以对比得到连续和非连续的区别,不连续的要对位置i 前面所有的进行判断,而连续的只考虑前一个。

673. 最长递增子序列的个数
给定一个未排序的整数数组,找到最长递增子序列的个数。
???充满疑问的一道题
128 最长连续序列
给定一个未排序的整数数组,找出最长连续序列的长度。
要求算法的时间复杂度为 O(n)。

这题可以dp,但是排序后顺着找就可以,没必要dp呀。

public int longestConsecutive(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        Arrays.sort(nums);
        int n = nums.length;
        int max = 1, cur = 1;
        for (int i = 1; i < n; i++) {
            if (nums[i] != nums[i - 1]) {
                if (nums[i - 1] + 1 == nums[i]) cur++;
                else {
                    max = Math.max(max, cur);
                    cur = 1;
                }
            }
        }
        return Math.max(max, cur);
    }

3 区间DP

Given a set of numbers find an optimal solution for a problem considering the current number and the best you can get from the left and right sides.

Get the best from the left and right sides and add a solution for the current

position.for(int l = 1; l<n; l++) {
   for(int i = 0; i<n-l; i++) {
       int j = i+l;
       for(int k = i; k<j; k++) {
           dp[i][j] = max(dp[i][j], dp[i][k] + result[k] + dp[k+1][j]);
       }
   }
}
return dp[0][n-1]

96. 不同的二叉搜索树

给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?

给定一个有序序列 1 … n,为了根据序列构建一棵二叉搜索树。我们可以遍历每个数字 i,将该数字作为树根,1 … (i-1) 序列将成为左子树,(i+1) … n 序列将成为右子树。于是,我们可以递归地从子序列构建子树(也就是区间dp)。

G(n): 长度为n的序列的不同二叉搜索树个数。

F(i, n): 以i为根的不同二叉搜索树个数。

对于根 i 的不同二叉搜索树数量 F(i, n)F(i,n),是左右子树个数的笛卡尔积(组合)

public int numTrees(int n) {
        int[] G = new int[n + 1];
        G[0] = 1;
        G[1] = 1;
        for (int i = 2; i <= n; ++i) {
           for (int j = 1; j <= i; ++j) {
               //合并了两个公式
               G[i] += G[j - 1] * G[i - j];
            }
        }
    return G[n];
  }

1130. 叶值的最小代价生成树

  public int mctFromLeafValues(int[] arr) {
         int n = arr.length;
            //这道题的核心是:
            //要知道中序遍历就决定了arr数组(0...n-1)里的第k位元素的所有左边元素(包括它自己)都在左子树里,
            //而其右边元素都在右子树里
            //而此时左右两边子树分别选出最大值的乘积就是此时的根,也就是题目中说的非叶节点
            //所以我们可以假定从i到j位,最小和可能是:此刻k位左右两边元素中最大值的乘积 + 子问题k左边(i,k)的最小值
            // + 子问题k位右边(k+1,j)的最小值
            //即:dp[i][j]=min(dp[i][j], dp[i][k] + dp[k+1][j] + max[i][k]*max[k+1][j])
            //这道题跟leetcode1039一个套路
            //求arr从i到j之间的元素最大值, 保存在max[i][j]中
            //这道题i和j是可以相等的
            int[][] max = new int[n][n];
            for (int j=0;j<n;j++) {
                int maxValue = arr[j];
                for (int i=j;i>=0;i--) {
                    maxValue = Math.max(maxValue, arr[i]);
                    max[i][j] = maxValue;
                }
            }
            int[][] dp = new int[n][n];
            for (int j=0; j<n; j++) {
                for (int i=j; i>=0; i--) {
                    //k是i到j之间的中间某个值,i<=k<j
                    int min = Integer.MAX_VALUE;
                    for (int k=i; k+1<=j; k++) {
                       min = Math.min(min,dp[i][k] + dp[k+1][j] + max[i][k]*max[k+1][j]);
                       dp[i][j] = min;
                    }
                }
            }
            return dp[0][n-1];
    }

== 312. 戳气球==

4 字符串DP

Given two strings s1 and s2, return some result.
Most of the problems on this pattern requires a solution that can be accepted in O(n^2) complexity.

// i - indexing string s1
// j - indexing string s2
for (int i = 1; i <= n; ++i) {
   for (int j = 1; j <= m; ++j) {
       if (s1[i-1] == s2[j-1]) {
           dp[i][j] = /*code*/;
       } else {
           dp[i][j] = /*code*/;
       }
   }
}

If you are given one string s the approach may little vary.

for (int l = 1; l < n; ++l) {
   for (int i = 0; i < n-l; ++i) {
       int j = i + l;
       if (s[i] == s[j]) {
           dp[i][j] = /*code*/;
       } else {
           dp[i][j] = /*code*/;
       }
   }
}

解码方法

class Solution {
   //这题倒是想到了动态方程,但是分类讨论情况太多,看看人家是怎么整合的
   //情况很多的时候与其考虑去掉不可以的,不如直接考虑能用的,剩下的一起处理
   //char转int ,char-'0'
   //每个数字,要么自己作为一个数字 映射到一个字符,要么和它前面的数字组合成一个1到26 的数字
   public int numDecodings(String s) {
       if(s == null || s.length()==0) return 0;
       if(s.charAt(0)=='0') return 0;
       int len  =s.length();
       int[] dp = new int[len+1];
       dp[0] = 1;//初始化没明白
       dp[1] = 1;
       s = " " +s;
       for(int i=2;i<=len;i++){
           //
           //if(s.charAt(i)<1||s.charAt(i)>)
           boolean flag = false;
           // 先考虑单个数的qingk
           if(s.charAt(i)!='0') {
               dp[i] += dp[i-1];
               flag =true;
           }
           //进一步考虑
           if(s.charAt(i-1)!='0' && (s.charAt(i-1)-'0')* 10+ s.charAt(i)-'0' <=26 &&  (s.charAt(i-1)-'0')* 10+ s.charAt(i)-'0' > 0 ){
               dp[i] += dp[i-2];
               flag = true;
           }
           //两种都不满足就是不符合要求
           if(!flag) return 0;
       }
       return dp[len];
   }
}

4-- 最长子字符串/序列

1143 最长公共子序列 ⭐️
大部分比较困难的字符串问题都和这个问题一个套路

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。

状态:二维DP数组,dp[i][j] 表示第一个字符串到 i 位置,第二个字符串到 j 位置的, 最长公共子串
初始化:让索引为 0 的行和列表示空串,dp[0][…] 和 dp[…][0] 都应该初始化为 0;

public int longestCommonSubsequence(String text1, String text2) {
        //状态,以当前位置为结尾的两个子串的最长公共子序列的长度
        int len1 = text1.length();
        int len2 = text2.length();
        //一般为了考虑空串,多加一行一列
        int[][] dp = new int[len1+1][len2+1];
        //初始化
        for(int i=0;i<=len1;i++){
            dp[i][0] = 0;
        }
        for(int i=0; i<=len2;i++){
            dp[0][i] = 0;
        }
        //注意这里的dp转移,子序列问题的状态转移都差不多
        for(int i=1;i <= len1; i++){
            for(int j=1; j<= len2; j++){
                if(text1.charAt(i-1) == text2.charAt(j-1))
                   dp[i][j] = dp[i-1][j-1] + 1;
                else
                   dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
            }
        }
        return dp[len1][len2];
        }

72 编辑距离⭐️
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

public int minDistance(String word1, String word2) {
        int n = word1.length();
        int m = word2.length();
        // 有一个字符串为空串
        if (n * m == 0)
           return n + m;
        //状态,word1的i位置到word2的j位置的最小次数
        int[][] dp = new int[n+1][m+1];
        for(int i=0;i<= n;i++){
            dp[i][0] = i;
        }
        for(int i=0;i<=m;i++){
            dp[0][i] = i;
        }
        //dp[i][j-1]替换A串i位置,dp[i-1][j]替换B串j位置,
        //要么就在dp[i-1][j-1]的基础上考虑,若相等就不用再修改了
        for(int i = 1;i<=n;i++){
            for(int j = 1;j<=m;j++){
                int tmp = dp[i-1][j-1];
                if(word1.charAt(i-1)!= word2.charAt(j-1))
                   tmp = tmp+1;
                dp[i][j] = Math.min(dp[i][j-1]+1,Math.min(dp[i-1][j]+1,tmp));
            }
        }
        return dp[n][m];
    }

10. 正则表达式匹配
115 不同的子序列
给定一个字符串 S 和一个字符串 T,计算在 S 的子序列中 T 出现的个数。
当 S[j] == T[i] , dp[i][j] = dp[i-1][j-1] + dp[i][j-1] :
有两种情况
匹配时最后一个字符为s[j](那么我们前j-1个字符只需要匹配T的前j-1个字符就行了,即dp[i-1][j-1])
或者不是(那么由s[j-1]前面出现过的T[i]作为最后一个字符了,即dp[i][j-1])
状态转移方程没搞懂

public int numDistinct(String s, String t) {
        int[][] dp = new int[t.length() + 1][s.length() + 1];
        for (int j = 0; j < s.length() + 1; j++) dp[0][j] = 1;
        for (int i = 1; i < t.length() + 1; i++) {
            for (int j = 1; j < s.length() + 1; j++) {
                if (t.charAt(i - 1) == s.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1] + dp[i][j - 1];
                else dp[i][j] = dp[i][j - 1];
            }
        }
        return dp[t.length()][s.length()];
    }

1092. 最短公共超序列
712. 两个字符串的最小ASCII删除和

4–回文系列

647. 回文子串
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被计为是不同的子串。

public int countSubstrings(String s) {
        if(s == null || s.equals("")){
            return 0;
        }
        int n = s.length();
        int res = n;
        //状态 从i到j是不是回文串,所以只填上三角矩阵即可
        boolean[][] dp = new boolean[n][n];
        //初始化
        for(int i = 0;i<n;i++) dp[i][i] = true;
        //dp[i][j] =  dp[i+1][j-1];所以从右下角开始更新
        for(int i= n-1; i>=0;i--){
            for(int j = i+1;j<n;j++){
                if(s.charAt(i)==s.charAt(j)){
                    //特殊情况,因为直接用状态可能会有得不到偶数中心的情况
                    if(j-i==1) dp[i][j] = true;
                    else dp[i][j] = dp[i+1][j-1];
                }else{
                    dp[i][j] = false;
                }
                if(dp[i][j]) res++;
            }
        }
        return res;
    }

5 最长回文子串
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
DP做法跟647很像

//像673一样,找到所有的回文子串,并在这个过程中更新最大子串的长度,以及开始和结束的位置
    public String longestPalindrome(String s) {
        if(s==null || s.length() < 2) return s;
        int n = s.length();
        boolean[][] dp = new boolean[n][n];
        int max_len = 1;
        int start = 0, end = 0;
        //初始化
        for(int i = 0;i<n;i++) dp[i][i] = true;
        for(int i = n-1;i>=0;i--){
            for(int j = i+1;j< n;j++){
                if(s.charAt(i)==s.charAt(j)){
                    if(j-i==1) dp[i][j] = true;
                    else dp[i][j] = dp[i+1][j-1];
                }else{
                    dp[i][j] = false;
                }
                if(dp[i][j] && j-i+1 > max_len){
                    max_len = j-i+1;
                    start = i;
                    end = j;
                }
            }
        }
        return s.substring(start,end+1);
    }

516 最长回文子序列
给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。

public int longestPalindromeSubseq(String s) {
        //跟子串做法相似
        if(s == null) return 0;
        int n = s.length();
        if(n==1) return 1;

        int[][] dp = new int[n][n];
        for(int i = 0;i<n;i++) dp[i][i] = 1;

        int res = 0;
        for(int i=n-1;i>=0;i--){
            for(int j = i+1;j<n;j++){
                if(s.charAt(i)==s.charAt(j)){
                    dp[i][j] = dp[i+1][j-1]+2;
                }else{
                //????
                    dp[i][j] = Math.max
                    (dp[i+1][j],dp[i][j-1]);
                }
            }
        } 
        return dp[0][n-1];

    }

5 取舍决定(股票套题)

Given a set of values find an answer with an option to choose or ignore the current value.

// i - indexing a set of values
// j - options to ignore j values
for (int i = 1; i < n; ++i) {
   for (int j = 1; j <= k; ++j) {
       dp[i][j] = max({dp[i][j], dp[i-1][j] + arr[i], dp[i-1][j-1]});
       dp[i][j-1] = max({dp[i][j-1], dp[i-1][j-1] + arr[i], arr[i]});
   }
}

198 打家劫舍

 public int rob(int[] nums) {
        if(nums==null||nums.length==0) return 0;
        int n = nums.length;
        //状态
        int[] dp = new int[n];
        dp[0] = nums[0];
        if(n>1) dp[1] = Math.max(nums[1],nums[0]);
        for(int i = 2;i<n;i++){
            dp[i] = Math.max(dp[i-2]+nums[i],dp[i-1]);
        }
        return dp[n-1];
    }

213. 打家劫舍 II
与198的区别在于,这里加了一个条件,房子是首尾相连的。
可以看成是两个单列, 一个第一个置为0,另一个最后一个置为0,取最大的。

public int rob(int[] nums) {
        if(nums==null||nums.length==0) return 0;
        if(nums.length==1) return nums[0];
        //左闭右开哦
        return Math.max(robrange(Arrays.copyOfRange(nums, 0, nums.length - 1)),
                                robrange(Arrays.copyOfRange(nums,1,nums.length)));
    }
    public int robrange(int[] nums){
        int pre=0,cur =0,tmp;
        for(int num:nums){
            tmp = cur;
            cur = Math.max(pre + num, cur);
            pre = tmp;
        }
        return cur;
    }

打家劫舍3

class Solution {
    //什么叫 两个直接相连的房子在 同一天 晚上 被打劫 ,这句话可以转化成什么?
    //选了根节点,两个子节点就都不能选了,选了子节点, 子节点的字节点又不能选了
    //有点像DP感觉,按层遍历即可
    //但是又是二叉树,是不是可以按DFS来做???(觉得DFS不可)
    //最终: DFS,后序优先遍历, DP
    //DP的状态方程也有可能是依赖于后面的状态,那就从后往前遍历就行了,数组是从大下标到小下标,树是后序遍历
    //树型DP,遍历的过程不同
    //这题还有点像那个最大乘积,就是得用两个DP数组
    public int rob(TreeNode root) {
        //考虑某一个点,选它就等于 左右都不选的最大值,不选它就等于 左右选不选都有可能的最大值
        Map<TreeNode, Integer> f = new HashMap<TreeNode,Integer>();
        Map<TreeNode, Integer> g = new HashMap<TreeNode,Integer>();
        dfs(root,f,g);
        return Math.max(f.getOrDefault(root,0) ,g.getOrDefault(root,0));
    }
    public void dfs(TreeNode root, Map<TreeNode,Integer> f,Map<TreeNode,Integer> g){
        if(root ==null) return ;
        dfs(root.left,f,g);
        dfs(root.right,f,g);
        f.put(root, root.val + g.getOrDefault(root.left,0) + g.getOrDefault(root.right,0));
        g.put(root, Math.max(f.getOrDefault(root.left,0),g.getOrDefault(root.left,0))+ Math.max(f.getOrDefault(root.right,0),g.getOrDefault(root.right,0)));
    }
}

121. 买卖股票的最佳时机
解法一:
对每一个位置,记录当前位置对应的最低价,然后计算赚的钱数。

 public int maxProfit(int[] prices) {
        if(prices==null || prices.length==0) return 0;
        int min = Integer.MAX_VALUE;
        int maxprofit = 0;
        for(int i = 0;i<prices.length;i++){
            if(prices[i]>min){
                if((prices[i]-min)>maxprofit) maxprofit = prices[i]-min;
            }else{
                min = prices[i];
            }
        }
        return maxprofit;
    }

解法二 动态规划
状态 dp[i][j] 表示:在索引为 i 的这一天,用户手上持股状态为 j 所获得的最大利润;
dp[0][1] 代表购入了股票,dp[0][0] 代表买入又卖出; 状态 0 特指:“卖出股票以后不持有股票的状态”,请注意这个状态和“没有进行过任何一次交易的不持有股票的状态”的区别

 public int maxProfit(int[] prices) {
        if(prices==null || prices.length==0) return 0;
        //每一天持股和不持股对应的收益
        int[][] dp = new int[prices.length][2];
        //初始化
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for(int i=1;i<prices.length;i++){
            //考虑都能由哪些状态得到
            //要么没改变,要么卖出
            dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+ prices[i]);
            dp[i][1] = Math.max(dp[i-1][1],-prices[i]);
        }
        return Math.max(dp[prices.length-1][0],dp[prices.length-1][1]);
    }

122. 买卖股票的最佳时机 II
暴力搜索
贪心:
贪的方式是只要第二天比今天有增长,就考虑买入卖出。这样就可以得到总体的最大利润了。

public int maxProfit(int[] prices) {
        int res = 0;
        for(int i=1;i<prices.length;i++){
            if((prices[i]-prices[i-1])>0){
                res += prices[i]-prices[i-1];
            }
        }
        return res;
    }

动态规划:

public int maxProfit(int[] prices) {
        int len = prices.length;
        if (len < 2) {
            return 0;
        }

        // cash:持有现金
        // hold:持有股票
        // 状态数组
        // 状态转移:cash → hold → cash → hold → cash → hold → cash
        int[] cash = new int[len];
        int[] hold = new int[len];

        cash[0] = 0;
        hold[0] = -prices[0];

        for (int i = 1; i < len; i++) {
            // 这两行调换顺序也是可以的
            cash[i] = Math.max(cash[i - 1], hold[i - 1] + prices[i]);
            hold[i] = Math.max(hold[i - 1], cash[i - 1] - prices[i]);
        }
        return cash[len - 1];
    }

==123买卖股票的最佳时机 III ==
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

public int maxProfit(int[] prices) {
        int len = prices.length;
        if (len < 2) {
            return 0;
        }

        // dp[i][j] ,表示 [0, i] 区间里,状态为 j 的最大收益
        // j = 0:什么都不操作
        // j = 1:第 1 次买入一支股票
        // j = 2:第 1 次卖出一支股票
        // j = 3:第 2 次买入一支股票
        // j = 4:第 2 次卖出一支股票

        // 初始化
        int[][] dp = new int[len][5];
        dp[0][0] = 0;
        dp[0][1] = -prices[0];

        // 3 状态都还没有发生,因此应该赋值为一个不可能的数
        for (int i = 0; i < len; i++) {
            dp[i][3] = Integer.MIN_VALUE;
        }

        // 状态转移只有 2 种情况:
        // 情况 1:什么都不做
        // 情况 2:由上一个状态转移过来
        for (int i = 1; i < len; i++) {
            // j = 0 的值永远是 0
            dp[i][0] = 0;
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
            dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
            dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
            dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
        }
        // 最大值只发生在不持股的时候,因此来源有 3 个:j = 0 ,j = 2, j = 4
        return Math.max(0, Math.max(dp[len - 1][2], dp[len - 1][4]));
    }

188. 买卖股票的最佳时机 IV

6 背包问题

377. 组合总和 Ⅳ

416. 分割等和子集

494 目标和

贪心

122. 买卖股票的最佳时机 II

比较

位运算

位运算符:NOT,AND 和 XOR

  • XOR:异或运算(^);两个数转为二进制,然后从高位开始比较,如果相同则为0,不相同则为1。
    两个相同的数异或得0,两个不同的数异或为1,0与任何数异或都得任何数。(因此可以用于检测出现奇数次的数)
  • AND :与运算(&);两个数都转为二进制,然后从高位开始比较,如果两个数都为1则为1,否则为0。
  • NOT:如果位为0,结果是1,如果位为1,结果是0.

136. 只出现一次的数字
首先,对于这种只出现一次(或者说找重复的问题)可以通过HashSet或者HashMap来解决,但有时会要求不使用额外的空间。

318. 最大单词长度乘积

进制运算

67. 二进制求和

  • 库函数直接求,先把字符串转为二进制整数,相加,然后转二进制字符串
Integer.toBinaryString(Integer.parseInt(a, 2) + Integer.parseInt(b, 2));

算法的时间复杂度为 O(N+M)。但有一个问题在 Java 中,该方法受输入字符串 a 和 b 的长度限制。字符串长度太大时,不能将其转换为 Integer,Long 或者 BigInteger 类型。

33 位 1,不能转换为 Integer。65 位 1,不能转换为 Long500000001 位 1,不能转换为 BigInteger。
因此,为了适用于长度较大的字符串计算,应该使用逐比特位的计算方法。

Integer常用函数:

  1. ParseInt(String s) //将基本类型转换成字符串,s必须是数字型字符串
  2. parseInt(String s, int radix) //将字符串s按照radix进行转换相应的进制数,然后运行的结果都是以十进制的形式打印
  3. Integer.toString(int src, int radix); //将int整数按进制转换
  4. Integer.toString(int src) // 将int转字符串 除此之外:任何类型+"" 变成String类型(推荐使用)
  5. int max = Integer.MAX_VALUE;
    int min = Integer.MIN_VALUE;
  6. 十进制转不同的进制:
    Integer.toBinaryString(int src); //二进制
    Integer.toOctalString(int src); //八进制
    Integer.toHexString(int src);//十六进制
  • 逐位相加
    按位 计算 和 以及 进位即可;
     public String addBinary(String a, String b) {
          int m = a.length(); int n = b.length();
          if(n > m) return addBinary(b,a);
          int carry = 0; int j = n-1;
          StringBuilder sb = new StringBuilder();
          for(int i = m-1; i>-1;i--){
              if(a.charAt(i)=='1') ++carry;
              if(j>=0 && b.charAt(j--)=='1') ++carry;
    
              if( carry %2 ==1) sb.append('1');
              else sb.append('0');
    
              carry = carry/2;
          }
          //不要忘记最前面一位
          if(carry ==1) sb.append('1');
          //因为append是填在后面的,所以最后要翻转
          sb.reverse();
          return sb.toString();
      }
    

这种解法与 2. 两数相加 的一样,都是一个进位,从后往前加;

  • 位运算

交换两数值的三种方法

Hot100

最大子序和

class Solution {
    //一个错的答案,没想清楚状态,状态应该是以当前位置为 结尾的  子数组的和
    //所以,要返回的自然是整个dp数组的最大值
    //记得填充到子数组乘积那个题
    public int maxSubArray(int[] nums) {
        if(nums.length == 1) return nums[0];
        int n = nums.length;
        int[] dp = new int[n];
        //
        int res=  nums[0];
        dp[0] = nums[0];
        for(int i = 1;i<n;i++){
            dp[i] = Math.max((dp[i-1] + nums[i]), nums[i]);
            if(dp[i] > res) res = dp[i];
        }
        return res;
    }
}

152 乘积最小子数组

可以对比最大子序和,但这里的因为有负数不可以直接在 前一个 和当前 单个元素中取最大的;
自己的想法是动态规划,但是每个位置都记录两个值,一个是最大,一个是最小,然后用最大和最小的都与这个数字相乘,取大的那个
这里虽然是连续的但却用1维的,跟字符串还是不同哦。。可以考虑一下为啥不同

//子数组 连续 所以这个值要么是前一个位置传过来的,要么是重新计算的
    //因为有正负,所以除了保留最大值还要保留最小值
    public int maxProduct(int[] nums) {
        int len = nums.length;
        if(len==1) return nums[0];
        int[] max = new int[len];
        int[] min = new int[len];
        //初始化
        max[0] = nums[0];
        min[0] = nums[0];
        for(int i =1;i<len;i++){
            max[i] = Math.max(max[i-1] * nums[i], Math.max(nums[i], min[i-1] * nums[i] ));
            min[i] = Math.min(min[i-1] * nums[i], Math.min(nums[i],max[i-1]* nums[i]));
        }
        int res = Integer.MIN_VALUE;
        for(int i =0;i<len;i++){
            res = res > max[i] ? res: max[i];
        }
        return res;
    }

169 多数元素

 //众数,排序返回中间位置的元素即可
     public int majorityElement(int[] nums) {
        Arrays.sort(nums);
        return nums[nums.length/2];
    }

560. 和为K的子数组

给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。
暴力法:

//思路是滑动窗口,若小于k则向右扩数组,但有小数,不可
    //想了DP,DFS, 滑动窗口,都不可,那就暴力吧
    public int subarraySum(int[] nums, int k) {
        int res = 0;
        for(int start = 0;start < nums.length; start++){
            int cur = 0;
            for(int end = start;end < nums.length; end++){
                cur += nums[end];
                if(cur == k) res++;
            }
        }
        return res;
    }

前缀和法:

public int subarraySum(int[] nums, int k) {
        int res = 0;
        HashMap<Integer,Integer> hash = new HashMap<>();
        hash.put(0,1);
        int preSum = 0;
        for(int i = 0;i< nums.length; i++){
            preSum += nums[i];
            if(hash.containsKey(preSum-k)){
                res += hash.get(preSum - k);
            }
            hash.put(preSum, hash.getOrDefault(preSum,0)+1);  
        }
        return res;
    }

70 组合

给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。

class Solution {
    //wei le bu chongfu,jiu meici dou cong zhe ge weizhi wanghou digui 
    List<List<Integer>> res = new LinkedList<>();
    public List<List<Integer>> combine(int n, int k) {
        for(int i = 1;i<=n;i++){
            LinkedList<Integer> now = new LinkedList<>();
            now.add(i);
            dfs(now,i, n,k);
        }
        return res;
    }
    public void dfs(LinkedList<Integer> cur, int index, int n, int k){
        if(cur.size() == k){
            res.add(new LinkedList(cur));
            return ;
        }
        if(index > n) return ;
        for(int i = index+1; i<= n;i++){
            cur.add(i);
            dfs(cur, i, n,k);
            cur.removeLast();
        }
    }
}

226 翻转二叉树

public TreeNode invertTree(TreeNode root) {
        if(root == null) return root;
        TreeNode left = root.left;
        root.left  = invertTree(root.right);
        root.right = invertTree(left);
        return root;
    }

416. 分割等和子集

5 最长回文子串

class Solution {
    //1 搜索出所有的回文字符串,然后取最长的
    //2 从下往上的过程,把搜索的过程逆过来(dp)
    //超时的代码
    int[] res = {-1,-1,-1};
    public String longestPalindrome(String s) {
        if(s==null||s.length()==0) return s;
        for(int i = 0;i<s.length();i++){
            for(int j = i;j<s.length();j++){
               if(! isParalism(s,i,j)){
                   continue;
               }
                if(j-i+1 > res[0]){
                    res[0]  =j-i+1;
                    res[1] = i;
                    res[2] = j;
                }
            }
        }
        return s.substring(res[1],res[2]+1);
    }
    public boolean isParalism(String s, int l, int r){
        if(l == r) return true;
        while(l<=r){
            if(s.charAt(l)!= s.charAt(r)){
                return false;
            }
        }
        return true;
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值