力扣

文章目录

  • 力扣
    • 回溯
      • 10. 正则表达式匹配
      • 17. 电话号码的字母组合
      • 22. 括号生成
      • 39. 组合总和
      • 46. 全排列
      • 47. 全排列II
    • 动态规划
      • 5. 最长回文子串
      • 53. 最大子序和
      • 62. 不同路径
      • 63. 不同路径II
      • 64. 最小路径和
      • 70. 爬楼梯
      • 91. 解码方法
      • 96. 不同的二叉搜索树
      • 95. 不同的二叉搜索树II
      • 120. 三角形最小路径和
      • 139. 单词拆分
      • 152. 乘积最大子序列
      • 279. 完全平方数
      • 343. 整数拆分
      • ==团灭6种股票问题==
      • 121. 买卖股票的最佳时机
      • 122. 买卖股票的最佳时机II
      • 123. 买卖股票的最佳时机III
      • 188. 买卖股票的最佳时机IV
      • 309. 买卖股票的最佳时机含冷冻期
      • 714. 买卖股票的最佳时机含手续费
      • 198. 打家劫舍
      • 213. 打家劫舍II
      • 337. 打家劫舍III
      • 300. 最长上升子序列
      • 416. 分割等和子集(0-1背包问题)
      • 474. 一和零
      • 494. 目标和
      • 322. 零钱兑换(完全背包问题)
      • 518. 零钱兑换II
      • 1143. 最长公共子序列
      • 1371. (未做)每个元音包含偶数次的最长子字符串
  • 高级数据结构
    • 优先队列
      • 剑指offer41. 数据流中的中位数
      • 215. 数组中的第K个最大元素
    • Ordered Map
      • ==TreeMap==
      • 729. 我的日程安排表I
      • 731. 我的日程安排表II
      • 732. 我的日程安排表III
      • 846. 一手顺子
      • 855. 考场就坐
      • 220. 存在重复元素III

力扣

回溯

10. 正则表达式匹配

https://leetcode-cn.com/problems/regular-expression-matching/solution/zheng-ze-biao-da-shi-pi-pei-by-leetcode-solution/

public boolean isMatch(String s, String p) {
    int m = s.length();
    int n = p.length();
    boolean[][] dp = new boolean[m+1][n+1];
    dp[0][0] = true;

    for(int i = 0; i <= m; i++){
        for(int j = 1; j <= n; j++){
            if(p.charAt(j-1) == '*'){
                dp[i][j] = dp[i][j-2];
                if(matches(s,p,i,j-1)){
                    dp[i][j] = dp[i][j] || dp[i-1][j];
                }
            }
            else {
                if(matches(s,p,i,j)){
                    dp[i][j] = dp[i-1][j-1];
                }
            }
        }
    }

    return dp[m][n];
}

public boolean matches(String s, String p, int i, int j){
    if(i == 0) 
        return false;
    if(p.charAt(j-1) == '.')
        return true;
    return s.charAt(i-1) == p.charAt(j-1);
}

17. 电话号码的字母组合

public class telnum {
    char[][] m = new char[][]{
            {},
            {},
            {'a','b','c'},{'d','e','f'},
            {'g','h','i'},{'j','k','l'},{'m','n','o'},
            {'p','q','r','s'},{'t','u','v'},{'w','x','y','z'}
    };

    public List<String> letterCombinations(String str){
        List<String> res = new ArrayList<>();
        if(str.length() == 0)
            return res;
        dfs(str, 0, new StringBuilder(), res);
        return res;
    }
    
    //dfs的递归跑法
    //在进入一个dfs之后,如果不满足条件1,则进入条件2,先add,然后继续dfs
    //如果满足条件1,return之后返回上一个dfs,执行delete
    //如果某一m[]遍历结束,跳回上一层m[]继续dfs
    void dfs(String str, int index, StringBuilder sb, List<String> res){

        //1.截止条件
        if(index == str.length()){
            res.add(sb.toString());
            return;
        }

        //2.候选节点
        for(char c : m[str.charAt(index) - '0']){
            sb.append(c);
            dfs(str, index+1, sb, res);
            sb.deleteCharAt(sb.length() - 1);
        }
    }
}

22. 括号生成

用String

public class GenerateParenthesis {

    public List<String> generateParenthesis(int n){
        char[] p = new char[]{'(',')'};
        int[] pb = new int[]{n,n};

        List<String> res = new ArrayList<>();
        dfs(n, p, pb, "", res);
        return res;
    }

    void dfs(int n, char[] p, int[] pb, String str, List<String> res){

        //截止条件
        if(str.length() == 2 * n){
            res.add(str);
            return;
        }

        //候选节点
        if(pb[0] > 0){
            pb[0]--;
            dfs(n, p, pb, str + p[0], res);
            pb[0]++;
        }
        if(pb[1] > 0 && pb[0] != pb[1]){
            pb[1]--;
            dfs(n, p, pb, str + p[1], res);
            pb[1]++;
        }
    }
}

用StringBuilder

    public List<String> generateParenthesis(int n){
        List<String> res = new ArrayList<>();
        char[] p = new char[]{'(',')'};
        int[] pb = new int[]{n,n};
        dfs(n,p,pb,res,new StringBuilder());
        return res;
    }

    public void dfs(int n, char[] p, int[] pb, List<String> res, StringBuilder sb){

        if(sb.length() == n*2){
            res.add(sb.toString());
            return;
        }

        if(pb[0] > 0){
            pb[0]--;
            dfs(n,p,pb,res,sb.append(p[0]));
            pb[0]++;
            sb.deleteCharAt(sb.length()-1);
        }

        if(pb[1] > 0 && pb[0] != pb[1]){
            pb[1]--;
            dfs(n,p,pb,res,sb.append(p[1]));
            pb[1]++;
            sb.deleteCharAt(sb.length()-1);
        }
    }

39. 组合总和

public class Combin {
    public List<List<Integer>> combinationSum(int[] p, int t){
        List<List<Integer>> res = new ArrayList<>();
        dfs(p, t, new ArrayList<>(), res);
        return res;
    }

    void dfs(int[] p, int t, List<Integer> chain, List<List<Integer>> res){

        //截止条件
        int s = sum(chain);
        if(s >= t){
            if(s == t){
                List<Integer> tmp = new ArrayList<>(chain);
                Collections.sort(tmp);
                if(!res.contains(tmp)){
                    res.add(tmp);
                }
            }
            return;
        }

        //候选节点
        for(int i = 0; i < p.length; i++){
            int c = p[i];
            chain.add(c);
            dfs(p, t, chain, res);
            chain.remove(chain.size() - 1);
        }
    }

    int sum(List<Integer> chain){
        int res = 0;
        for(int i : chain){
            res += i;
        }
        return res;
    }
}

46. 全排列

public class Permute {
    public List<List<Integer>> permute(int[] p){
        List<List<Integer>> res = new ArrayList<>();
        boolean[] pb = new boolean[p.length];
        dfs(p, pb, new ArrayList<>(), res);
        return res;
    }

    void dfs(int[] p, boolean[] pb, List<Integer> chain, List<List<Integer>> res){
        //截止条件
        if(chain.size() == p.length){
            res.add(new ArrayList<>(chain));
            return;
        }

        //候选节点
        for(int i = 0; i < p.length; i++){
            int c = p[i];
            //筛选
            if(!pb[i]){
                chain.add(c);
                pb[i] = true;
                dfs(p, pb, chain, res);
                chain.remove(chain.size() - 1);
                pb[i] = false;
            }
        }
    }
}

47. 全排列II

public class PermuteUnique {

    public List<List<Integer>> permuteUnique(int[] px){
        HashMap<Integer,Integer> m = new HashMap<>();
        for(int i : px){
            m.put(i, m.containsKey(i) ? m.get(i) + 1 : 1);
        }
        int len = m.size();
        int[] p = new int[len];
        int[] pb = new int[len];
        int[] index = new int[1];
        m.forEach((k,v) ->{
            p[index[0]] = k;
            pb[index[0]] = v;
            index[0]++;
        });

        List<List<Integer>> res = new ArrayList<>();
        dfs(px.length, p, pb, new ArrayList<Integer>(), res);
        return res;
    }

    void dfs(int size, int[] p, int[] pb, List<Integer> chain, List<List<Integer>> res){
        //截止条件
        if(chain.size() == size){
            res.add(new ArrayList<>(chain));
            return;
        }

        //候选节点
        for(int i = 0; i < p.length; i++){
            int c = p[i];
            //筛选
            if(pb[i] > 0){
                chain.add(c);
                pb[i]--;
                dfs(size, p, pb, chain, res);
                pb[i]++;
                chain.remove(chain.size() - 1);
            }
        }
    }
}

全排列方法改改也可以用

    public List<List<Integer>> permuteUnique(int[] p){
        List<List<Integer>> res = new ArrayList<>();
        boolean[] pb = new boolean[p.length];
        dfs(p, pb, new ArrayList<>(), res);
        return res;
    }

    void dfs(int[] p, boolean[] pb, List<Integer> chain, List<List<Integer>> res){
        //截止条件
        if(chain.size() == p.length){
            List<Integer> tmp = new ArrayList<>(chain);
            if(!res.contains(tmp)){
                res.add(tmp);
            }
            return;
        }

        //候选节点
        for(int i = 0; i < p.length; i++){
            int c = p[i];
            //筛选
            if(!pb[i]){
                chain.add(c);
                pb[i] = true;
                dfs(p, pb, chain, res);
                chain.remove(chain.size() - 1);
                pb[i] = false;
            }
        }
    }

动态规划

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i5n8i51Z-1596520187453)(/Users/cwq/Documents/全栈工程师的笔记/算法图片/动态规划.png)]

https://leetcode-cn.com/circle/article/lxC3ZB/
https://mp.weixin.qq.com/s/Hew44D8rdXb3pf8mZGk67w
https://labuladong.gitbook.io/algo/di-ling-zhang-bi-du-xi-lie/dong-tai-gui-hua-xiang-jie-jin-jie

5. 最长回文子串

中心扩展法

public String longestPalindrome1(String s) {

    if (s == null || s.length() == 0) {
        return "";
    }
    int strLen = s.length();
    int left = 0;
    int right = 0;
    int len = 1;
    int maxStart = 0;
    int maxLen = 0;

    for (int i = 0; i < strLen; i++) {
        left = i - 1;
        right = i + 1;
        //如果左边与当前i相同,接着向左边找
        while (left >= 0 && s.charAt(left) == s.charAt(i)) {
            len++;
            left--;
        }
        //同理
        while (right < strLen && s.charAt(right) == s.charAt(i)) {
            len++;
            right++;
        }
        //如果i的左右边相同,两边一起找
        while (left >= 0 && right < strLen && s.charAt(right) == s.charAt(left)) {
            len = len + 2;
            left--;
            right++;
        }
        if (len > maxLen) {
            maxLen = len;
            maxStart = left;
        }
        //每次找完len归1,最大值记录在maxLen
        len = 1;
    }
    return s.substring(maxStart + 1, maxStart + maxLen + 1);

}

动态规划

状态方程:p(i,j) = p(i+1,j-1) && (Si == Sj)

public String longestPalindrome2(String s) {
    if (s == null || s.length() < 2) {
        return s;
    }
    int strLen = s.length();
    int maxStart = 0;  //最长回文串的起点
    int maxEnd = 0;    //最长回文串的终点
    int maxLen = 1;  //最长回文串的长度

    boolean[][] dp = new boolean[strLen][strLen];

    for (int r = 1; r < strLen; r++) {
        for (int l = 0; l < r; l++) {
            //这里r-l <= 2 的意思是,如果长度小于2的话,说明其中不存在第二个子串了
            //所以dp[l + 1][r - 1])不能用,这是为了给最小的子串赋值
            if (s.charAt(l) == s.charAt(r) && (r - l <= 2 || dp[l + 1][r - 1])) {
                dp[l][r] = true;
                if (r - l + 1 > maxLen) {
                    maxLen = r - l + 1;
                    maxStart = l;
                    maxEnd = r;

                }
            }

        }

    }
    return s.substring(maxStart, maxEnd + 1);

}

53. 最大子序和

public int maxSubArray(int[] nums) {
    //ans是最大值,sum是遍历时计算值
    int ans = nums[0];
    int sum = 0;
    for(int num : nums){
        //如果上一次的sum是正数,那么这次必须得计算
        //因为下一次有可能得到一个更大的值,之前的最大值保存在ans里了,不用担心
        //如果是负数,那么反正都是负数了,加一个正数或负数,不如直接等于它
        if(sum > 0){
            sum += num;
        }else {
            sum = num;
        }
        ans = Math.max(ans,sum);
    }
    return ans;
}

62. 不同路径

排列组合
math.factorial(m+n-2) / (math.factorial(m-1) · math.factorial(n-1))

动态规划
状态方程:dp[i][j] = dp[i-1][j] + dp[i][j-1]

//时间复杂度O(m*n)
//空间复杂度O(m*n)
public int uniquePaths(int m, int n) {
    int[][] dp = new int[m][n];
    for(int i = 0; i < n; i++) dp[0][i] = 1;
    for(int i = 0; i < m; i++) dp[i][0] = 1;

    for(int i = 1; i < m; i++){
        for(int j = 1; j < n; j++){
            dp[i][j] = dp[i-1][j] + dp[i][j-1];
        }
    }
    return dp[m-1][n-1];
}
//优化为一维数组,空间复杂度为o(n)
public int uniquePaths(int m, int n) {
    int[] dp = new int[n];
    Arrays.fill(dp,1);

    //滚动数组,相当于将数组从上而下滚动,一维变成二维
    //所以dp[j] = dp[j] + dp[j-1]
    //这里dp[j]是之前保存的上一行,相当于dp[i-1][j]
    //同理,dp[j-1]相当于dp[i][j-1]
    for(int i = 1; i < m; i++){
        for(int j = 1; j < n; j++){
            dp[j] = dp[j-1] + dp[j];
        }
    }
    return dp[n-1];
}

63. 不同路径II

public int uniquePathsWithObstacles(int[][] obstacleGrid) {
    int m = obstacleGrid.length, n = obstacleGrid[0].length;
    int[] f = new int[n];
    
    f[0] = obstacleGrid[0][0] == 0 ? 1 : 0;
    
    //从第二列开始计算所以有j-1>=0,但是循环得从j=0开始,防止只有一列
    for(int i = 0; i < m; i++){
        for(int j = 0; j < n; j++){
            if(obstacleGrid[i][j] == 1){
                f[j] = 0;
                continue;
            }
            if(j - 1 >= 0 && obstacleGrid[i][j-1] == 0){
                f[j] += f[j-1];
            }
        }
    }

    return f[n-1];

}

64. 最小路径和

public int minPathSum(int[][] grid) {
    int m = grid.length, n = grid[0].length;
    int[][] dp = new int[m][n];
    int min = 0;

    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];

    for(int i = 1; i < m; i++){
        for(int j = 1; j < n; j++){
            int one = dp[i-1][j] + grid[i][j];
            int two = dp[i][j-1] + grid[i][j];
            int three = Math.min(one,two);
            dp[i][j] = three; 
        }
    }

    return dp[m-1][n-1];
}

70. 爬楼梯

public int climbStairs(int n) {
    int one = 1, two = 2, sum = 0;
    if(n == 1) return 1;
    if(n == 2) return 2;
    for(int i = 3; i <= n; i++){
        sum = one + two;
        one = two;
        two = sum; 
    }
    return sum;
}

91. 解码方法

public class NumDecodings {
    public int numDecodings(String s){
        int n = s.length();
        int[] dp = new int[n];
        if(s.charAt(0) == '0')
            return 0;
        dp[0] = 1;
        for(int i = 1; i < n; i++){
            //如果第i-1个字符与第i个字符匹配在1-26之外
            //那么此字符只能作为单个字符出现,所以dp[i]与dp[i-1]一样
            if(s.charAt(i) != '0')
                dp[i] += dp[i-1];
            //如果第i-1个字符与第i个字符匹配在1-26之内
            //若i<2,说明此时字符串最多到第二个,那么dp[i]只能加1,
            //若i>2,那么相当于在原有方法dp[i-2]的数量上上加两倍
            //尾数接两个单字符一种,接两个字符一种
            if(s.charAt(i-1) == '1' || (s.charAt(i-1) == '2' && s.charAt(i) <= '6')){
                if(i - 2 > 0)
                    dp[i] += dp[i-2];
                else
                    dp[i]++;
            }
        }
        return dp[n-1];
    }
}

96. 不同的二叉搜索树

G(n): 长度为n的序列能构成的不同二叉搜索树的个数。
F(i, n): 以i为根、序列长度为n的不同二叉搜索树个数(1≤i≤n)。

  • G(n) = ∑F(i,n)
  • F(i,n) = G(i−1)⋅G(n−i)
  • G(n) = ∑G(i−1)⋅G(n−i)
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];
}

95. 不同的二叉搜索树II

public List<TreeNode> generateTrees(int n) {
    if (n == 0) {
        return new LinkedList<TreeNode>();
    }
    return generateTrees(1, n);
}

public List<TreeNode> generateTrees(int start, int end) {
    List<TreeNode> allTrees = new LinkedList<TreeNode>();
    if (start > end) {
        allTrees.add(null);
        return allTrees;
    }

    // 枚举可行根节点
    for (int i = start; i <= end; i++) {
        // 获得所有可行的左子树集合
        List<TreeNode> leftTrees = generateTrees(start, i - 1);

        // 获得所有可行的右子树集合
        List<TreeNode> rightTrees = generateTrees(i + 1, end);

        // 从左子树集合中选出一棵左子树,从右子树集合中选出一棵右子树,拼接到根节点上
        for (TreeNode left : leftTrees) {
            for (TreeNode right : rightTrees) {
                TreeNode currTree = new TreeNode(i);
                currTree.left = left;
                currTree.right = right;
                allTrees.add(currTree);
            }
        }
    }
    return allTrees;
}

120. 三角形最小路径和

public int minimumTotal(List<List<Integer>> triangle) {
    int n = triangle.size();
    // dp[i][j] 表示从点 (i, j) 到底边的最小路径和。
    int[][] dp = new int[n + 1][n + 1];
    // 从三角形的最后一行开始递推。
    for (int i = n - 1; i >= 0; i--) {
        for (int j = 0; j <= i; j++) {
            dp[i][j] = Math.min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle.get(i).get(j);
        }
    }
    return dp[0][0];
}

139. 单词拆分

public boolean wordBreak(String s, List<String> wordDict) {
    Set<String> wordDictSet = new HashSet(wordDict);
    boolean[] dp = new boolean[s.length()+1];
    dp[0] = true;

    //每次比较的截止点就是i,比较从0到i是否能匹配到字典
    for(int i = 1; i <= s.length(); i++){
        for(int j = 0; j < i; j++){
            if(dp[j] && wordDictSet.contains(s.substring(j,i))){
                dp[i] = true;
                break;
            }
        }
    }
    return dp[s.length()];
}

152. 乘积最大子序列

由于存在负数,那么会导致最大的变最小的,最小的变最大的。因此还需要维护当前最小值imin,当负数出现时则imax与imin进行交换再进行下一步计算。碰到两次负数时,imax存的乘负数就是最大的。

public int maxProduct(int[] nums) {
    int max = Integer.MIN_VALUE, imax = 1, imin = 1;
    for(int i = 0; i < nums.length; i++){
        if(nums[i] < 0){
            int tmp = imax;
            imax = imin;
            imin = tmp;
        }
        imax = Math.max(imax * nums[i], nums[i]);
        imin = Math.min(imin * nums[i], nums[i]);
        max = Math.max(imax,max);
    }

    return max;
}

279. 完全平方数

public int numSquares(int n){
    int[] dp = new int[n+1];
    dp[0] = 0;

    for(int i = 1; i <= n; i++){
        dp[i] = i;
        for(int j = 1; i - j * j >= 0; j++){
            //dp[i-j*j]+1:其实是i减去这个完全平方数j*j
            //所需要的总数就是dp[减后的数] + 1(这个完全平方数占一个数)
            dp[i] = Math.min(dp[i],dp[i-j*j]+1);
        }
    }
    return dp[n];
}

343. 整数拆分

//数学推导,取3越多越大
public int integerBreak(int n){
    if(n <= 3) return n-1;
    int a = n / 3;
    int b = n % 3;
    if(b == 0) return (int) Math.pow(3,a);
    if(b == 1) return (int) Math.pow(3,a-1)*4;
    return (int) Math.pow(3,a)*2;
}

团灭6种股票问题

https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/solution/yi-ge-tong-yong-fang-fa-tuan-mie-6-dao-gu-piao-wen/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D879vAqi-1596520187455)(/Users/cwq/Documents/全栈工程师的笔记/算法图片/5.png)]

三种状态:rest(保持)、sell、buy
i是第几天的股票,k是可以买卖几次,0和1是状态(是否持有股票)

dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
              max(   选择 rest  ,             选择 sell      )

解释:今天我没有持有股票,有两种可能:
要么是我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有;
要么是我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。

dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
              max(   选择 rest  ,           选择 buy         )

解释:今天我持有着股票,有两种可能:
要么我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票;
要么我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了。

状态方程

dp[-1][k][0] = 0
解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0 。
dp[-1][k][1] = -infinity
解释:还没开始的时候,是不可能持有股票的,用负无穷表示这种不可能。

base case:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -infinity

状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])

121. 买卖股票的最佳时机

public int maxProfit(int[] prices) {
    if(prices.length == 0) return 0;
    int len = prices.length;
    int[][] dp = new int[len][2];
    for(int i = 0; i < len; i++){
        if(i-1 == -1){
            //相当于第一天没有买股票,钱为0
            dp[i][0] = 0;
            //买了股票,钱为-prices[i]
            dp[i][1] = -prices[i];
            continue;
        }
        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 dp[len-1][0];
}

简化

public int maxProfit(int[] prices) {
    if(prices.length == 0) return 0;
    int n = prices.length;
    int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
    for (int i = 0; i < n; i++) {
        // dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
        // dp[i][1] = max(dp[i-1][1], -prices[i])
        //因为只能买卖一次,所以每次买进股票都是-prices[i]
        dp_i_1 = Math.max(dp_i_1, -prices[i]);
    }
    return dp_i_0;
}

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

状态方程

dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
            = max(dp[i-1][k][1], dp[i-1][k][0] - prices[i])

我们发现数组中的 k 已经不会改变了,也就是说不需要记录 k 这个状态了:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
public int maxProfit(int[] prices) {
    if(prices.length == 0) return 0;
    int len = prices.length;
    int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
    for(int i = 0; i < len; i++){
        int tmp = dp_i_0;
        dp_i_0 = Math.max(dp_i_0,dp_i_1+prices[i]);
        //与上题不同,可以买卖无限次,需要记录持续的买进状态
        dp_i_1 = Math.max(dp_i_1,tmp-prices[i]);
    }
    return dp_i_0;
}

123. 买卖股票的最佳时机III

k = 2 和前面题目的情况稍微不同,因为上面的情况都和 k 的关系不太大。要么 k 是正无穷,状态转移和 k 没关系了;要么 k = 1,跟 k = 0 这个 base case 挨得近,最后也没有存在感。

错误示范

int k = 2;
int[][][] dp = new int[n][k + 1][2];
for (int i = 0; i < n; i++)
    if (i - 1 == -1) { /* 处理一下 base case*/ }
    dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
    dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
}
return dp[n - 1][k][0];

其实我们之前的解法,都在穷举所有状态,只是之前的题目中 k 都被化简掉了。比如说第一题,k = 1;所以k = 2时:

public int maxProfit(int[] prices) {
    if(prices.length == 0) return 0;
    int max_k = 2;
    int[][][] dp = new int[prices.length][max_k+1][2];

    for(int i = 0; i < prices.length; i++){
        for(int k = max_k; k >= 1; k--){
            if(i-1 == -1){
                dp[i][k][0] = 0;
                dp[i][k][1] = -prices[i];
                continue;
            }
            dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
            dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
        }
    }

    return dp[prices.length-1][max_k][0];
}
//dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][2][1] + prices[i])
//dp[i][2][1] = max(dp[i-1][2][1], dp[i-1][1][0] - prices[i])
//dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
//dp[i][1][1] = max(dp[i-1][1][1], -prices[i])

public int maxProfit(int[] prices) {
    int dp_i10 = 0, dp_i11 = Integer.MIN_VALUE;
    int dp_i20 = 0, dp_i21 = Integer.MIN_VALUE;
    for (int price : prices) {
        dp_i20 = Math.max(dp_i20, dp_i21 + price);
        dp_i21 = Math.max(dp_i21, dp_i10 - price);
        dp_i10 = Math.max(dp_i10, dp_i11 + price);
        dp_i11 = Math.max(dp_i11, -price);
    }
    return dp_i20;
}

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

一次交易由买入和卖出构成,至少需要两天。所以说有效的限制 k 应该不超过 n/2,如果超过,就没有约束作用了,相当于 k = +infinity。这种情况是买卖股票的最佳时机II。

public int maxProfit(int max_k, int[] prices) {
    int n = prices.length;
    if (max_k > n / 2) 
        return maxProfit2(prices);

    int[][][] dp = new int[n][max_k + 1][2];
    for (int i = 0; i < n; i++) 
        for (int k = max_k; k >= 1; k--) {
            if (i - 1 == -1) {
                dp[i][k][0] = 0;
                dp[i][k][1] = -prices[i];
                continue;
            }
            dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
            dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);     
        }
    return dp[n - 1][max_k][0];
}

public int maxProfit2(int[] prices) {
    int len = prices.length;
    int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
    for(int i = 0; i < len; i++){
        int tmp = dp_i_0;
        dp_i_0 = Math.max(dp_i_0,dp_i_1+prices[i]);
        dp_i_1 = Math.max(dp_i_1,tmp-prices[i]);
    }
    return dp_i_0;
}

309. 买卖股票的最佳时机含冷冻期

每次 sell 之后要等一天才能继续交易。只要把这个特点融入状态转移方程即可:

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])
解释:第 i 天选择 buy 的时候,要从 i-2 的状态转移,而不是 i-1 。
public int maxProfit(int[] prices) {
    if(prices.length == 0) return 0;
    int len = prices.length;
    int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
    int dp_pre_0 = 0;
    for(int i = 0; i < len; i++){
        int tmp = dp_i_0;
        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
        dp_i_1 = Math.max(dp_i_1, dp_pre_0 - prices[i]);
        dp_pre_0 = tmp;
    }
    return dp_i_0;
}

714. 买卖股票的最佳时机含手续费

每次交易要支付手续费,只要把手续费从利润中减去即可。改写方程:

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee)
解释:相当于买入股票的价格升高了。
在第一个式子里减也是一样的,相当于卖出股票的价格减小了。
public int maxProfit(int[] prices, int fee) {
    if(prices.length == 0) return 0;
    int len = prices.length;
    int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
    for(int i = 0; i < len; i++){
        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
        dp_i_1 = Math.max(dp_i_1, dp_i_0 - prices[i] - fee);
    }
    return dp_i_0;
}

198. 打家劫舍

public int rob(int[] nums) {
    if(nums == null || nums.length == 0){
        return 0;
    }

    if(nums.length == 1) return nums[0];

    int[] dp = new int[nums.length];
    dp[0] = nums[0];
    dp[1] = Math.max(nums[0],nums[1]);

    //每次是否要rob看i-1和i-2
    for(int i = 2; i < nums.length; i++){
        dp[i] = Math.max(dp[i-2]+nums[i], dp[i-1]);
    }

    return dp[nums.length-1];
    
}

213. 打家劫舍II

public int rob1(int[] nums){
    if(nums.length == 0) return 0;
    if(nums.length == 1) return nums[0];
    return Math.max(myrob(Arrays.copyOfRange(nums,0,nums.length-1)),
            myrob(Arrays.copyOfRange(nums,1,nums.length)));
}

public int myrob(int[] nums){
    //滚动数组,省了dp
    int pre = 0, cur = 0, tmp;
    for(int num : nums){
        tmp = cur;
        cur = Math.max(pre+num,tmp);
        pre = tmp;
    }
    return cur;
}

337. 打家劫舍III

//暴力递归,要么要爷爷和孙子,要么要儿子
public int rob(TreeNode root){
    if(root == null) return 0;
    int money = root.val;

    if(root.left != null)
        money += rob(root.left.left) + rob(root.left.right);
    if(root.right != null)
        money += rob(root.right.left) + rob(root.right.right);

    return Math.max(money,rob(root.left) + rob(root.right));
}

//当前节点选择偷时,那么两个孩子节点就不能选择偷了
//当前节点选择不偷时,两个孩子节点只需要拿最多的钱出来就行(两个孩子节点偷不偷没关系)
//我们使用一个大小为 2 的数组来表示 int[] res = new int[2] 
//0 代表不偷,1 代表偷
//任何一个节点能偷到的最大钱的状态可以定义为
//当前节点选择不偷:当前节点能偷到的最大钱数 = 左孩子能偷到的钱 + 右孩子能偷到的钱
//当前节点选择偷:当前节点能偷到的最大钱数 = 左孩子选择自己不偷时能得到的钱 + 右孩子选择不偷时能得到的钱 + 当前节点的钱数
public int rob(TreeNode root) {
    int[] result = robInternal(root);
    return Math.max(result[0], result[1]);
}

public int[] robInternal(TreeNode root) {
    if (root == null) return new int[2];
    int[] result = new int[2];

    //存的相当于result
    int[] left = robInternal(root.left);
    int[] right = robInternal(root.right);

    //偷与不偷的两种形式
    result[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
    result[1] = left[0] + right[0] + root.val;

    return result;
}

300. 最长上升子序列

public int lengthOfLIS(int[] nums) {
    if(nums.length == 0) return 0;
    int[] dp = new int[nums.length];
    dp[0] = 1;
    int maxans = 1;
    
    // maxval记录本次循环最大值,dp[j]记录j这个点之前有几个数比他小
    for(int i = 1; i < nums.length; i++){
        int maxval = 0;
        for(int j = 0; j < i; j++){
            if(nums[i] > nums[j]){
                maxval = Math.max(dp[j],maxval);
            }
        }
        dp[i] = maxval + 1;
        maxans = Math.max(dp[i],maxans);
    }
    return maxans;
}

416. 分割等和子集(0-1背包问题)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zS6AW2ju-1596520187456)(/Users/cwq/Documents/全栈工程师的笔记/算法图片/2.png)]

//dp[][],行是nums的len,列是nums和sum的一半target
//只要找到某些数字的和等于target即结束
public boolean canPartition(int[] nums) {
    int len = nums.length;
    if(len == 0) return false;
    int sum = 0;
    for(int num : nums){
        sum += num;
    }
    //和为奇数判错
    if((sum & 1) == 1) return false;

    int target = sum / 2;

    boolean[][] dp = new boolean[len][target+1];
    
    if(nums[0] <= target){
        dp[0][nums[0]] = true;
    }

    for(int i = 1; i < len; i++){
        for(int j = 0; j < target+1; j++){
            dp[i][j] = dp[i-1][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][target];
}
//简单优化
public boolean canPartition(int[] nums) {
    int len = nums.length;
    if(len == 0) return false;
    int sum = 0;
    for(int num : nums){
        sum += num;
    }

    if((sum & 1) == 1) return false;

    int target = sum / 2;

    boolean[][] dp = new boolean[len][target+1];
    //有这句再通过for循环的dp[i][j] = dp[i-1][j]其实已经把第一列设置为true
    dp[0][0] = true;

    if(nums[0] < target) dp[0][nums[0]] = true;
    if(nums[0] == target) return true;

    for(int i = 1; i < len; i++){
        for(int j = 0; j < target+1; j++){
            dp[i][j] = dp[i-1][j];

            if(nums[i] <= j){
                dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]];
            }
        }
        //如果最后一列有值为true,说明已经找到
        if(dp[i][target] == true){
            return true;
        }
    }
    return dp[len-1][target];
}

在“填表格”的时候,当前行只参考了上一行的值,因此状态数组可以只设置 2 行,使用“滚动数组”的技巧“填表格”即可;

实际上连“滚动数组”都不必,在“填表格”的时候,当前行总是参考了它上面一行 “头顶上” 那个位置和“左上角”某个位置的值。因此,我们可以只开一个一维数组,从后向前依次填表即可。

“从后向前” 写的过程中,一旦 nums[i] <= j 不满足,可以马上退出当前循环,因为后面的 j 的值肯定越来越小,没有必要继续做判断,直接进入外层循环的下一层。相当于也是一个剪枝,这一点是“从前向后”填表所不具备的。

//状态数组压缩到一维
public boolean canPartition(int[] nums) {
    int len = nums.length;
    if (len == 0) {
        return false;
    }

    int sum = 0;
    for (int num : nums) {
        sum += num;
    }

    if ((sum & 1) == 1) {
        return false;
    }

    int target = sum / 2;

    boolean[] dp = new boolean[target + 1];
    dp[0] = true;

    if(nums[0] < target) dp[nums[0]] = true;
    if(nums[0] == target) return true;

    for (int i = 1; i < len; i++) {
        for (int j = target; nums[i] <= j; j--) {
            if (dp[target]) {
                return true;
            }

            dp[j] = dp[j] || dp[j - nums[i]];
        }
    }
    return dp[target];
}

474. 一和零

状态转移方程

  • 选择当前字符串:dp[i−1][j−当前字符串使用0的个数][k−当前字符串使用1的个数]+1
  • 不选择当前考虑的字符串,至少是这个数值:dp[i−1][j][k]

不论从后向前还是从前向后,每次选择字符串找dp数组时,都是会查找dp[i−1][j−当前字符串使用0的个数][k−当前字符串使用1的个数],然后加1,再比较能不能要

其实每个dp[i][j][k]位置相当于使用j和k的个数的0、1所能创建字符串的最大值,每次遍历strs其实就是查找选择这个字符串能不能提高这个最大值

public int[] countZeroAndOne(String str){
    int[] cnt = new int[2];
    for(char c : str.toCharArray()){
        cnt[c - '0']++;
    }
    return cnt;
}

public int findMaxForm(String[] strs, int m, int n) {
    int len = strs.length;
    if(strs == null || len == 0) return 0;

    int[][][] dp = new int[len+1][m+1][n+1];

    for(int i = 1; i < len+1; i++){

        int[] cnt = new int[2];
        cnt = countZeroAndOne(strs[i-1]);
        int zero = cnt[0];
        int one = cnt[1];

        for(int j = 0; j <= m; j++){
            for(int k = 0; k <= n; k++){
                //先把上一行抄下来
                dp[i][j][k] = dp[i-1][j][k];
                //如果给的m和n够这个字符串使用,那么计算一下减去这次使用的个数i和j
                //再将上一行的这个位置的个数加一,是否比不加上这个字符串多
                if(j >= zero && k >= one){
                    dp[i][j][k] = Math.max(dp[i-1][j][k], dp[i-1][j-zero][k-one]+1);
                }
            }
        }
    }
    return dp[len][m][n];
}

优化一下空间,并从后向前赋值

public int[] countZeroAndOne(String str){
    int[] cnt = new int[2];
    for(char c : str.toCharArray()){
        cnt[c - '0']++;
    }
    return cnt;
}

public int findMaxForm(String[] strs, int m, int n) {
    int[][] dp = new int[m + 1][n + 1];
    dp[0][0] = 0;
    for (String s : strs) {
        int[] zeroAndOne = countZeroAndOne(s);
        int zeros = zeroAndOne[0];
        int ones = zeroAndOne[1];
        for (int i = m; i >= zeros; i--) {
            for (int j = n; j >= ones; j--) {
                dp[i][j] = Math.max(dp[i][j], dp[i - zeros][j - ones] + 1);
            }
        }
    }
    return dp[m][n];
}

494. 目标和

//递归
int count = 0;
public int findTargetSumWays(int[] nums, int S) {
    calculate(nums,0,0,S);
    return count;
}

public void calculate(int[] nums, int i, int sum, int S){
    if(nums.length == i){
        if(sum == S) count++;
    }else {
        calculate(nums,i+1,sum+nums[i],S);
        calculate(nums,i+1,sum-nums[i],S);
    }
}
//动态规划
public int findTargetSumWays(int[] nums, int S) {
    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];
}

//空间优化
public int findTargetSumWays(int[] nums, int S) {
    int[] dp = new int[2001];
    dp[nums[0] + 1000] = 1;
    dp[-nums[0] + 1000] += 1;
    for (int i = 1; i < nums.length; i++) {
        int[] next = new int[2001];
        for (int sum = -1000; sum <= 1000; sum++) {
            if (dp[sum + 1000] > 0) {
                next[sum + nums[i] + 1000] += dp[sum + 1000];
                next[sum - nums[i] + 1000] += dp[sum + 1000];
            }
        }
        dp = next;
    }
    return S > 1000 ? 0 : dp[S + 1000];
}

322. 零钱兑换(完全背包问题)

https://leetcode-cn.com/problems/coin-change/solution/322-ling-qian-dui-huan-by-leetcode-solution/

public int coinChange(int[] coins, int amount) {
  int max = amount + 1;
  int[] dp = new int[max];
  //如果只有一块钱零钱,那么最大值就是amount,给dp全赋值amount+1
  //那么如果找到比其小的就可填入,dp[0]得赋值0
  Arrays.fill(dp,max);
  dp[0] = 0;

  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];
}

518. 零钱兑换II

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lPO0Sen1-1596520187457)(/Users/cwq/Documents/全栈工程师的笔记/算法图片/3.jpg)]

public int change(int amount, int[] coins) {
    int[] dp = new int[amount+1];
    dp[0] = 1;
    for(int coin : coins){
        for(int x = coin; x <= amount; x++){
            dp[x] += dp[x-coin];
        }
    }
    return dp[amount];
}

1143. 最长公共子序列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3dVw7W8N-1596520187458)(/Users/cwq/Documents/全栈工程师的笔记/算法图片/1.png)]

//贼慢
public int longestCommonSubsequence(String text1, String text2) {
    int[][] dp = new int[text1.length()+1][text2.length()+1];

    for(int i = 1; i <= text1.length(); i++){
        for(int j = 1; j <= text2.length(); j++){
            if(text1.charAt(i-1) == text2.charAt(j-1)){
                dp[i][j] = 1 + dp[i-1][j-1];
            }else {
                dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
            }
        }
    }

    return dp[text1.length()][text2.length()];
}

1371. (未做)每个元音包含偶数次的最长子字符串


高级数据结构

优先队列

剑指offer41. 数据流中的中位数

public class MedianFinder {
    private Queue<Integer> A, B;

    public MedianFinder() {
        // 初始都是小顶堆,保存较大的一半
        A = new PriorityQueue<>(); 
        // 这里用了lambda表达式,调用了内部用比较器,将其变成大顶堆
        // 保存较小的一半
        B = new PriorityQueue<>((x, y) -> (y - x)); 
    }
    public void addNum(int num) {
        if(A.size() != B.size()) {
            //当两个堆元素不等时,需要往小根堆A中加入元素,然后poll出堆顶元素(最小)
            //再加入大根堆B中(保证B中保存的一直是较小的一半)
            A.add(num);
            B.add(A.poll());
        } else {
            //当两个堆元素相等时,需要往大根堆B中加入元素,然后poll出堆顶元素(最大)
            //再加入小根堆A中(保证A中保存的一直是较大的一半)
            B.add(num);
            A.add(B.poll());
        }
    }
    public double findMedian() {
        return A.size() != B.size() ? A.peek() : (A.peek() + B.peek()) / 2.0;
    }
}

215. 数组中的第K个最大元素

注意,是要找数组中第几大的数,升序排序完最后一个数是第一大

public int findKthLargest(int[] nums, int k) {
    //利用优先队列特性直接搞定
    Queue<Integer> queue = new PriorityQueue<Integer>();
    for(int num : nums){
        queue.add(num);
        if(queue.size() > k) queue.poll();
    }
    return queue.peek();
}

Ordered Map

TreeMap

  • Object ceilingKey(Object key):返回大于或等于给定键的最小键,如果没有这样的键则返回null。
  • Object floorKey(Object key):返回小于或等于给定键的最小键,如果没有这样的键则返回null。
  • default V getOrDefault (Object key, V defaultValue):如果map中含有指定的key,就返回该key对应的value,否则使用该方法的第二个参数作为默认值返回。意思是找得到就返回V,找不到返回key对应的value。

729. 我的日程安排表I

class MyCalendar {

    private TreeMap<Integer, Integer> calendar;

    public MyCalendar() {
        calendar = new TreeMap<>();
    }
    
    public boolean book(int start, int end) {
        Integer prev = calendar.floorKey(start);
        Integer next = calendar.ceilingKey(start);
        //因为prev取得是map中key小于等于start的值
        //next取得是map中key大于等于start的值
        //而如果我们要插入一个正确的start和end,必须不能和它重合
        //1. start比map中key都小,所以prev = null,next = map中第一个比start大或等于的key
        //这时start小于map中所有key,要保证end小于map中第一个比start大或等于的key
        //2. prev = map中第一个比start小或等于的key, next = map中第一个比start大或等于的key
        //这时要保证start >= prev的value,end <= next;就是保证start和end在上下界的安全区间内
        //3. prev = map中第一个比start小或等于的key, next = null,跟1同理
        //4. prev = null , next = null 存在的时候说明map为空,直接插入就行
        if((prev == null || calendar.get(prev) <= start) &&
            (next == null || end <= next)){
                calendar.put(start, end);
                return true;
        }
        return false;
    }
}

731. 我的日程安排表II

这次treemap中存的是,每个时间time和其对应的使用次数,如果是start就是从1开始++,如果是end就从-1开始–。用一个计数active来判断是否有三重预定,正常情况下一个start和end相加为0。

如果某次达到了三重预定,那么将其对应的start和end的值分别–和++,如果必要,可remove。

public class MyCalendarTwo {

    private TreeMap<Integer, Integer> delta;

    public MyCalendarTwo() {
        delta = new TreeMap<>();

    }

    public boolean book(int start, int end) {
        delta.put(start, delta.getOrDefault(start, 0) + 1);
        delta.put(end, delta.getOrDefault(end, 0) - 1);

        int active = 0;
        for(int value : delta.values()){
            active += value;
            if(active >= 3){
                delta.put(start, delta.getOrDefault(start, 0) - 1);
                delta.put(end, delta.getOrDefault(end, 0) + 1);

                if(delta.get(start) == 0)
                    delta.remove(start);

                return false;
            }
        }

        return true;
    }
}

732. 我的日程安排表III

与上题差不多,记录一下最大的active就行

public class MyCalendarThree {

    private TreeMap<Integer, Integer> delta;

    public MyCalendarThree() {
        delta = new TreeMap<>();

    }

    public int book(int start, int end) {
        delta.put(start, delta.getOrDefault(start, 0) + 1);
        delta.put(end, delta.getOrDefault(end, 0) - 1);

        int active = 0, ans = 0;
        for(int value : delta.values()){
            active += value;
            if(active > ans) ans = active;
        }

        return ans;
    }
}

846. 一手顺子

先将所有数按照,key:大小;value:数量,的顺序存入treemap,然后遍历map,每次找到一个最小的card,按照card到card+W-1的大小查询map,如果不符合,说明不成立。

public boolean isNStraightHand(int[] hand, int W) {
    int len = hand.length;
    if( len % W != 0) return false;
    TreeMap<Integer, Integer> count = new TreeMap<>();

    for(int card : hand){
        if(!count.containsKey(card))
            count.put(card,1);
        else
            count.put(card,count.get(card)+1);
    }

    while (count.size() > 0){
        int first = count.firstKey();
        for(int card = first; card < first + W; card++){
            if(!count.containsKey(card)) return false;
            int n = count.get(card);
            if(n == 1)
                count.remove(card);
            else
                count.put(card,n-1);
        }
    }

    return true;
}

855. 考场就坐

当我们要调用 leave§ 函数时,我们只需要把有序集合中的 p 移除即可。当我们要调用 seat() 函数时,我们遍历这个有序集合,对于相邻的两个座位 i 和 j,如果选择在这两个座位之间入座,那么最近的距离 d 为 (j - i) / 2,选择的座位为 i + d。除此之外,我们还需要考虑坐在最左侧 0 和最右侧 N - 1 的情况。

public class ExamRoom {

    private TreeSet<Integer> students;
    private int N;

    public ExamRoom(int N) {
        this.N = N;
        students = new TreeSet<>();
    }

    public int seat() {
        int student = 0;

        if(students.size() > 0){

            int dist = students.first();
            Integer prev = null;

            for(int s : students){
                if(prev != null){
                    int d = (s - prev) / 2;
                    if(d > dist){
                        dist = d;
                        student = prev + d;
                    }
                }
                prev = s;
            }

            if(N - 1 - students.last() > dist){
                student = N - 1;
            }
        }

        students.add(student);
        return student;
    }

    public void leave(int p) {
        students.remove(p);
    }
}

220. 存在重复元素III


你可能感兴趣的:(LeetCode中等+)