DFS

什么时候使用 used 数组,什么时候使用 begin 变量
排列问题,讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为不同列表时),需要记录哪些数字已经使用过,此时用 used 数组;

  • 排列去除重复: i>0 && cand[i]==cand[i-1] && !used[i-1]

组合问题,不讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为相同列表时),需要按照某种顺序搜索,此时使用 begin 变量。

  • 组合去除重复: i>begin && cand[i] == cand[i-1]

参考模板:

  1. private void dfs(int[] nums, int n, int index, List<Integer> ans, List<List<Integer>> ret) {
  2. if(index == n) {
  3. ret.add(new ArrayList<>(ans)); //值传递转拷贝
  4. }
  5. for(int i=index; i<n; i++) {
  6. if(i>index && nums[i]==nums[i-1]) { //去重
  7. continue;
  8. }
  9. ans.add(nums[i]);
  10. dfs(nums,n,i+1,ans,ret);
  11. ans.remove(ans.size()-1); //回溯
  12. }
  13. }

HashSet去重

  1. HashSet<List<Integer>> hs = new HashSet<>();
  2. dfs() {
  3. if(...){
  4. hs.add(new ArrayList<>(ans));
  5. }
  6. }
  7. Solution() {
  8. List<List<Integer>> ret;
  9. for(List<Integer> L : hs) {
  10. ret.add(L);
  11. }
  12. }

int 反转

普通:

  1. public class Solution {
  2. public int reverseBits(int n) {
  3. int rev = 0;
  4. for (int i = 0; i < 32 && n != 0; ++i) {
  5. rev |= (n & 1) << (31 - i);
  6. n >>>= 1;
  7. }
  8. return rev;
  9. }
  10. }

进阶:

  1. n = (n<<16) | (n>>>16);
  2. n = ((n&0x00ff00ff)<<8) | ((n&0xff00ff00)>>>8);
  3. n = ((n&0x0f0f0f0f)<<4) | ((n&0xf0f0f0f0)>>>4);
  4. n = ((n&0x33333333)<<2) | ((n&0xcccccccc)>>>2);
  5. n = ((n&0x55555555)<<1) | ((n&0xaaaaaaaa)>>>1);
  6. return n;

1的个数

primary:

  1. public int hammingWeight(int n) {
  2. int ret = 0;
  3. while (n!=0) {
  4. if((n&1)==1) ret++;
  5. n=n>>>1;
  6. }
  7. return ret;
  8. }

medium:

  1. public int hammingWeight(int n) {
  2. int ret = 0;
  3. while(n!=0) {
  4. ret++;
  5. n = n&(n-1); //末位置0
  6. }
  7. return ret;
  8. }

high:

  1. public int hammingWeight(int n) {
  2. n = ((n>>>1) & 0x55555555) + (n & 0x55555555);
  3. n = ((n>>>2) & 0x33333333) + (n & 0x33333333);
  4. n = ((n>>>4) & 0x0f0f0f0f) + (n & 0x0f0f0f0f);
  5. n = ((n>>>8) & 0x00ff00ff) + (n & 0x00ff00ff);
  6. n = ((n>>>16) & 0x0000ffff) + (n & 0x0000ffff);
  7. return n;
  8. }

回文类

暴力:

  1. private boolean isPalind(char[] charArray,int left,int right) {
  2. while(left < right) {
  3. if(charArray[left] != charArray[right]) {
  4. return false;
  5. }
  6. left++;
  7. right--;
  8. }
  9. return true;
  10. }

DP:

  1. boolean[][] dp = new boolean[n][n];
  2. for(int right=0; right<n; right++) {
  3. for(int left=0; left<=right; left++) { //left==right 标记自身
  4. //记不住的写法: dp[left][right] = charArray[left]==charArray[right] && (right-left<=2 || dp[left+1][right-1]));
  5. //好理解的写法:
  6. if(charArray[left] != charArray[right]) {
  7. dp[left][right] = false;
  8. } else {
  9. if(right-left < 3) {
  10. dp[left][right] = true;
  11. } else {
  12. dp[left][right] = dp[left+1][right-1];
  13. }
  14. }
  15. }
  16. }

中心扩散:


单调栈

42.接雨水

  1. Deque<Integer> spool = new ArrayDeque<>();
  2. for(int i = 0; i < n; i++) {
  3. while(!spool.isEmpty() && height[i]>height[spool.peekLast()]) {
  4. int cur = spool.pollLast();
  5. if(spool.isEmpty()) break;
  6. int left = spool.peekLast();
  7. int curWidth = i - left - 1;
  8. int curHeight = Math.min(height[i],height[left]) - height[cur];
  9. ret += curWidth * curHeight;
  10. }
  11. spool.addLast(i);
  12. }

删除有序数组重复元素类

80.删除有序数组的重复项Ⅱ
26,27同理
模板, 最多保留k个重复。

  1. class Solution {
  2. public int removeDuplicates(int[] nums) {
  3. return process(nums, 2);
  4. }
  5. int process(int[] nums, int k) {
  6. int u = 0;
  7. for (int x : nums) {
  8. if (u < k || nums[u - k] != x) nums[u++] = x;
  9. }
  10. return u;
  11. }
  12. }

最大数

179.最大数
观察知应先比较第一位数,再比较最后一位数,从大到小排,但有点懵怎么排,感觉做起来会比较复杂
看题解,直接暴力拼接字符串比对大小…
学习了一下sort函数怎么用

  1. class Solution {
  2. public String largestNumber(int[] nums) {
  3. String[] nums2str = new String[nums.length];
  4. String ret;
  5. for (int i = 0; i < nums.length; i++) {
  6. nums2str[i] = Integer.toString(nums[i]); //先全转为String
  7. }
  8. Arrays.sort(nums2str,(s1,s2)->(s2+s1).compareTo(s1+s2)); //比较s2+s1 < s1+s2
  9. if("0".equals(nums2str[0])) {
  10. return "0";
  11. }
  12. return String.join("",nums2str); //依次拼接
  13. }
  14. }

字典树

208.实现Trie(前缀树)
字典树模板,以下代码的search 和 startsWith其实可以提取一个函数出来。

class Trie {
    private Trie[] children;
    private boolean isEnd;
    /** Initialize your data structure here. */
    public Trie() {
        children = new Trie[26];
        isEnd = false;
    }

    /** Inserts a word into the trie. */
    public void insert(String word) {
        Trie node = this;
        for(int i = 0; i < word.length(); i++) {
            char ch = word.charAt(i);
            int index = ch - 'a';
            if(node.children[index] == null) {
                node.children[index] = new Trie();
            }
            node = node.children[index];
        }
        node.isEnd = true;
    }

    /** Returns if the word is in the trie. */
    public boolean search(String word) {
        Trie node = this;
        for(int i = 0; i <word.length(); i++) {
            char ch = word.charAt(i);
            int index = ch - 'a';
            if(node.children[index] == null) {
                return false;
            }
            node = node.children[index];
        }
        return node!=null && node.isEnd;
    }

    /** Returns if there is any word in the trie that starts with the given prefix. */
    public boolean startsWith(String prefix) {
        Trie node = this;
        for (int i = 0; i < prefix.length(); i++) {
            char ch = prefix.charAt(i);
            int index = ch - 'a';
            if(node.children[index] == null) {
                return false;
            }
            node = node.children[index];
        }
        return true;
    }
}

打家劫舍-dp

198,213 打家劫舍
很明显的dp题,状态转移方程考虑抢不抢第n间
213因为绕成圈所以不能同时抢第一间和最后一间,应分为两个范围进行计算
198,打家劫舍Ⅰ:

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if(n==0) return 0;
        if(n==1) return nums[0];
        int pre = 0,cur = 0, tmp;
        for (int i = 0; i < n; i++) {
            tmp = cur;
            cur = Math.max(pre+nums[i], tmp);
            pre = tmp;
        }
        return cur;
    }
}

213,打家劫舍Ⅱ

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if(n==0) return 0;
        if(n==1) return nums[0];
        return Math.max(robRange(Arrays.copyOfRange(nums, 0, nums.length - 1)),
                        robRange(Arrays.copyOfRange(nums, 1, nums.length)));
    }
    private int robRange(int[] nums) {
        int pre = 0,cur = 0,tmp;
        for(int n : nums) {
            tmp = cur;
            cur = Math.max(pre+n, cur);
            pre = tmp;
        }
        return cur;
    }
}

扰乱字符串-dp

87.扰乱字符串
想到了深搜或dp去做,但是一开始拘泥于字符串的形式
看题解恍然只需要划分区间做dp即可。
判断的条件有二: 1. 对应长度必须相等;2. 对应划分字符串中的字母出现频率也是相等的

class Solution {
    public boolean isScramble(String s1, String s2) {
        if(s1.equals(s2)) return true; 
        int n = s1.length();
        if(n != s2.length()) return false; 
        char[] c1 = s1.toCharArray();
        char[] c2 = s2.toCharArray();
        boolean[][][] dp = new boolean[n][n][n+1]; //dp[s1开始位置][s2开始位置][串长度]

        //初始化单个字符情况dp[i][j][1]
        for(int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                dp[i][j][1] = c1[i] == c2[j];
                //c1[i] == c2[j] ? dp[i][j][1] = true : dp[i][j][1] = false;
            }
        }
        //len10 i0 k3 -> 0-2 3 3-9 7
        //len10 j7 k3 -> 0-6 7 7-9 3
        //len7  i4 k4 -> 3-6 4 7-9 3
        //len7  j0 k4 -> 0-2 3 3-6 4
        for(int len = 2; len <= n; len++) { 
            for(int i = 0; i <= n-len; i++) {
                for(int j = 0; j <= n-len; j++) {
                    for(int k = 1; k < len; k++) { //k为试探划分位置
                        boolean stay = dp[i][j][k] && dp[i+k][j+k][len-k]; //两边保持位置不变,比较 ac 与 bd
                        boolean swap = dp[i][j+len-k][k] && dp[i+k][j][len-k]; //左右交换位置,比较 ad 与 bc
                        if(swap || stay) {
                            dp[i][j][len] = true;
                            break;
                        }
                    }
                }
            }
        }
        return dp[0][0][n];
    }
}

实现strStr()-KMP

28.实现strStr()
talk is cheap , show the code.

class Solution {
    public int strStr(String haystack, String needle) {
        if(needle.isEmpty()) return 0;

        int n = haystack.length() , m = needle.length();
        haystack = " " + haystack; //预处理,开头加空格防止j从-1开始处理
        needle = " " + needle;
        char[] hs = haystack.toCharArray();
        char[] nd = needle.toCharArray();

        int[] next = new int[m+1]; //开始构造next数组,next数组记录了匹配串中匹配的前缀后缀位置※
        for(int i = 2,j=0; i <= m; i++) { //构造从I=2开始
            while (j>0 && nd[i]!=nd[j+1]) j = next[j]; //匹配不成功则j跳转到next[j]
            if(nd[i] == nd[j+1]) j++; //匹配成功记录j++
            next[i] = j; 
        }
        //开始匹配
        for(int i=1,j=0; i<=n; i++) {
            while(j>0 && hs[i]!=nd[j+1]) j = next[j]; //匹配不成功跳转next[j]
            if(hs[i] == nd[j+1]) j++; //匹配成功继续走
            if(j==m) return i-m; //匹配完成
        }

        return -1;
    }
}

解码方法-dp

91.解码方法
dp题,当出现子序列选择的时候就可以考虑使用dp解,
这题的状态转移方程还是很明显的。
仅考虑一位时,只需要不为0即可,dp[i] = dp[i-1]
考虑两位时,计数不小于10,不大于26即可。dp[i] = dp[i-2]
一位两位都满足时,dp[i] = dp[i-1] + dp[i-2]

class Solution {
    public int numDecodings(String s) {
        if(s.charAt(0)=='0') return 0;
        int n = s.length();
        int[] dp = new int[n+1];
        dp[0] = 1;
        for (int i = 1; i <= n; i++) {
            if(s.charAt(i-1) != '0') { 
                dp[i] += dp[i-1]; //只检验一位的情况
            }
            if(i>1 && s.charAt(i-2) != '0' && ((s.charAt(i-2) - '0')*10 + (s.charAt(i-1)-'0')) <= 26) {
                dp[i] += dp[i-2]; //检验两位的情况
            }
        }
        return dp[n];
    }
}

进阶的,学习一下使用滚动数组来做空间优化。

class Solution {
    public int numDecodings(String s) {
        if(s.charAt(0)=='0') return 0;
        int n = s.length();
        //int[] dp = new int[n+1];
        int[] dp = new int [3]; //dp[i]只跟dp[i-1]和dp[i-2]相关
        dp[0] = 1;
        for (int i = 1; i <= n; i++) {
            dp[i%3] = 0;
            if(s.charAt(i-1) != '0') {
                dp[i%3] = dp[(i-1)%3];
            }
            if(i>1 && s.charAt(i-2) != '0' && ((s.charAt(i-2) - '0')*10 + (s.charAt(i-1)-'0'))<=26) {
                dp[i%3] += dp[(i-2)%3];
            }
        }
        return dp[n];
    }
}

363.矩形区域不超过K的最大数值和-二维前缀和

矩形区域,数值和,毫无疑问先求个二维前缀和
问题是接下来该怎么查找。
首先肯定是暴力枚举,i,j定位矩形左上角,pq定位右下角

  • 时间复杂度:预处理前缀和数组复杂度为 O(m n)O(mn),查找答案的复杂度为 O(m^2 n^2)。整体复杂度为 O(m^2 * n^2)。
  • 空间复杂度:O(m * n)O(mn)

幸运的是,这题居然暴力能过

class Solution {
    public int maxSumSubmatrix(int[][] matrix, int k) {
        int m = matrix.length, n = matrix[0].length;
        int[][] sum = new int[m+1][n+1];
        for(int i = 1; i <= m; i++) {
            for(int j = 1; j<=n; j++) { //预处理算二维前缀和
                sum[i][j] = matrix[i-1][j-1] + sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1];
            }
        }

        int ret = Integer.MIN_VALUE;
        for(int i = 1; i <= m; i++) {
            for(int j = 1; j <= n; j++) { //(i,j)左上角
                for(int p = i; p<=m; p++) {
                    for(int q=j; q<=n; q++) { //(p,q)右下角
                        int cur = sum[p][q] - sum[p][j-1] - sum[i-1][q] + sum[i-1][j-1]; //计算所求
                        if(cur <= k) {
                            ret = Math.max(ret, cur);
                        }
                    }
                }
            }
        }

        return ret;
    }
}

进阶的..看不太明白..
【宫水三叶】优化枚举的基本思路 & 将二维抽象成一维 & 最大化「二分」效益 & 空间优化


897.递增顺序搜索树

记录一下O(1)空间的Morris中序遍历解法
Morris基本代码:

 public void morris(TreeNode root) {
        TreeNode cur=root,pre=null;
        while(cur!=null){
            pre=cur.left;              
            if(pre!=null){
                while(pre.right!=null&&pre.right!=cur){
                    pre=pre.right;
                }
                if(pre.right==null){
                    pre.right=cur;
                    cur=cur.left;
                    continue;
                }else{
                    pre.right=null;
                }
            }
            cur=cur.right;
        } 
    }

本题题解:

class Solution {
    public TreeNode increasingBST(TreeNode root) {
        TreeNode cur=root,pre=null;
        TreeNode dummy=new TreeNode(0),tail=dummy;
        while(cur!=null){
            pre=cur.left;
            if(pre!=null){
                while(pre.right!=null&&pre.right!=cur){
                    pre=pre.right;
                }
                if(pre.right==null){
                    pre.right=cur;
                    cur=cur.left;
                    continue;
                }else{
                    cur.left=null;
                }
            }
            tail.right=cur;
            tail=cur;
            cur=cur.right;
        }
        return dummy.right;
    }
}

1011.在D天内送达包裹的能力-二分

二分查找,每天的载重肯定在单个最大包裹重量到所有包裹重量和这个区间内,所以要对这个区间进行二分查找。

class Solution {
    public int shipWithinDays(int[] weights, int D) {
        int left = Arrays.stream(weights).max().getAsInt(); //单个最大包裹
        int right = Arrays.stream(weights).sum(); //所有包裹
        while (left < right) {
            int mid = (left + right) / 2;
            int need = 1,cur = 0; //need表示需要的天数,cur表示当前重量
            for(int w : weights) {
                if(cur + w > mid) {
                    ++need;
                    cur = 0;
                }
                cur += w;
            }
            if(need > D) {
                left = mid + 1; //如果当前负重mid无法如期完成,则加大负载
            } else {
                right = mid; //反之
            }
        }
        return left;
    }
}

633.平方数之和-双指针

用最暴力的做法就是两个数依此枚举到sqrt(),那么优化方法就是枚举一个,另一个数用sqrt(c-a*a)判断是否能满足。

class Solution {
    public boolean judgeSquareSum(int c) {
        for (long a = 0; a * a <= c; a++) { //防止溢出,使用long
            double b = Math.sqrt(c - a * a); 
            if (b == (int) b) {
                return true;
            }
        }
        return false;
    }
}

另一种思路就是两个数的区间都在0-sqrt(c),那就用双指针,如果结果比c大了就左移右指针,比 c小就右移左指针。
时间复杂度和上面的枚举一样是O(√c).

class Solution {
    public boolean judgeSquareSum(int c) {
        int i = 0;
        int j = (int)Math.sqrt(c);
        while(i<=j) {
            int ret = i*i + j*j;
            if(ret == c) {
                return true;
            } else if(ret < c) {
                i++;
            } else {
                j--;
            }
        }
        return false;
    }
}

还有一种解法是依据于数学,学习一下就好,下次我还是想不到哈哈。
费马平方和:奇质数能表示为两个平方数之和的充分必要条件是该质数被 4 除余 1 。
即:当且仅当一个自然数的质因数分解中,满足 4k+3 形式的质数次方数均为偶数时,该自然数才能被表示为两个平方数之和。
因此我们对 c 进行质因数分解,再判断满足 4k+3 形式的质因子的次方数是否均为偶数即可。

public class Solution {
    public boolean judgeSquareSum(int c) {
        for (int i = 2, cnt = 0; i * i <= c; i++, cnt = 0) {
            while (c % i == 0 && ++cnt > 0) c /= i;
            if (i % 4 == 3 && cnt % 2 != 0) return false;
        }
        return c % 4 != 3;
    }
}

403.青蛙过河-DP || 记忆化搜索

规律如下:

  1. 能否跳到下个石头上取决于当前所处石头位置和上次跳跃的距离。
    1. 因此有状态转移方程: dp[i][k] = dp[j][k-1] || dp[j][k] || dp[j][k+1]
    2. i表示本次跳跃的落点,j是能从哪个石头起跳, k = stone[i] - stone[j] 上次跳跃的距离
  2. 那么可以看出,初始条件dp[0][0]为true,青蛙不用跳就已经站在第0个石头上, k 的距离一定不大于 stone[i] - stone[i-1]
    1. 预处理:如果有 stone[i] - stone[i-1] > i ,青蛙肯定跳不到第i块石头,也肯定到不了终点。
    2. 因为每次起跳距离至多增加1,石头编号至少增加1,所以有跳跃m次, LeetCode 每日一题 - 图1.
      class Solution {
      public boolean canCross(int[] stones) {
        int n = stones.length;
        boolean[][] dp = new boolean[n][n];
        dp[0][0] = true;
        for (int i = 1; i < n; i++) {  //预处理
            if(stones[i] - stones[i-1] > i) {
                return false;
            }
        }
        for (int i = 1; i < n; i++) {
            for(int j = i-1; j >= 0; j--) { //寻找上次能起跳的位置
                int k = stones[i] - stones[j]; 
                if( k > j+1 ) {
                    break;
                }
                dp[i][k] = dp[j][k-1] || dp[j][k] || dp[j][k+1];
                if( i==n-1 && dp[i][k]) { 
                    return true;
                }
            }
        }
        return false;
      }
      }
      

137.只出现一次的数字-二进制

以前做过的题是其他数字出现两次,那就一遍全部异或就剩下了单次出现的数字,但是这题显然..不会这么简单,在想了半天的异或没想法后,先暴力试了一下,还是过了的

class Solution {
    public int singleNumber(int[] nums) {
        HashMap<Integer,Integer> map  = new HashMap<Integer, Integer>();
        for(int num : nums) {
            map.put(num,map.getOrDefault(num,0)+1);
        }
        for(Map.Entry<Integer, Integer> entry : map.entrySet()) {
            if(entry.getValue() == 1) {
                return entry.getKey();
            }
        }
        return -1;
    }
}

然后看看题解学习一下大佬的做法和思路,
第一种方法是依此确定每一个二进制位,非答案的每一个元素都出现3次,那么每个二进制位的和模3即为答案的该位值

答案的第 ii 个二进制位就是数组中所有元素的第 ii 个二进制位之和除以 33 的余数。

不过因为如此来确定每一位的,那就需要保证32都有效,因此需要注意使用的语言对「有符号整数类型」和「无符号整数类型」有没有区分,如果没有区分的语言需要对最高位进行特殊判断。
时间复杂度: LeetCode 每日一题 - 图2,其中logC = 32
空间复杂度:LeetCode 每日一题 - 图3

class Solution {
    public int singleNumber(int[] nums) {
        int ans = 0;
        for (int i = 0; i < 32; ++i) {
            int total = 0;
            for (int num: nums) {
                total += ((num >> i) & 1);
            }
            if (total % 3 != 0) {
                ans |= (1 << i);
            }
        }
        return ans;
    }
}

第二种方法是数字电路设计..
膜拜大佬orz,这都咋想到的..
时间复杂度:LeetCode 每日一题 - 图4
空间复杂度:LeetCode 每日一题 - 图5

class Solution {
    public int singleNumber(int[] nums) {
        int a = 0, b = 0;
        for (int num : nums) {
            int aNext = (~a & b & num) | (a & ~b & ~num), bNext = ~a & (b ^ num);
            a = aNext;
            b = bNext;
        }
        return b;
    }
}