102. 二叉树的层次遍历

https://leetcode-cn.com/problems/binary-tree-level-order-traversal/description/

题目

给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。

例如:

给定二叉树:  [3,9,20,null,null,15,7],


    3
   / \
  9  20
    /  \
   15   7
   

返回其层次遍历结果:

[
  [3],
  [9,20],
  [15,7]
]

解题思路:

层次遍历,没啥好说的,就是本题需要将层次区分开来。因此传统的方法,需要做一些修订。用一个队列保存当前层次的所有节点。

每一轮遍历的时候,依次出队所有的节点,存入结果,并遍历出队的所有节点,如果有子节点则存入队列,等待下一轮遍历。

也就是有两层的遍历,用一个临时的队列,保存中间结果

代码:

public List<List<Integer>> levelOrder(TreeNode root) {
    Queue<TreeNode> queue = new ArrayDeque<>();
    if (root != null) {
        queue.add(root);
    }
    List<List<Integer>> result = new ArrayList<>();
    while(!queue.isEmpty()){
        List<Integer> ans = new ArrayList<>();
        Queue<TreeNode> tempQueue = new ArrayDeque<>(queue);
        while (!tempQueue.isEmpty()){
            TreeNode node = tempQueue.poll();
            ans.add(node.val);
        }
        tempQueue = new ArrayDeque<>(queue);
        queue.clear();
        while(!tempQueue.isEmpty()){
            TreeNode node = tempQueue.poll();
            if (node.left != null) {
                queue.add(node.left);
            }
            if (node.right != null) {
                queue.add(node.right);
            }
        }
        result.add(ans);
    }
    return result;
}
2019/02/01 15:24 下午 posted in  LeetCode

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

https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/description/

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
示例 2:

输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
     注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
     因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:

输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

解题思路:

dp题。尝试写出状态转移方程。

设s[i],第i天股票的价格

1) dp[i], 到第i天,最优的股票收益。

dp[i] = max {max{ s[i] - s[j] + dp[j - 1]} j = 1...i - 1,

dp[i - 1]代表不卖,以及s[i] < s[j]的情况,算法复杂度O(n2)

代码:

public int maxProfit(int[] prices) {
    if (prices.length == 0){
        return 0;
    }
    int[] dp = new int[prices.length];
    for (int i = 1 ; i < prices.length; i++){
        int max = dp[i - 1];
        for (int j = 0; j < i; j++){
            int temp = prices[i] - prices[j];
            temp = temp < 0 ? 0 : temp;
            if (j > 0) {
                if (temp > 0) {
                    temp += dp[j - 1];
                } else {
                    temp = dp[j - 1];
                }
            }
            if (temp > max){
                max = temp;
            }
        }
        dp[i] = max;
    }
    return dp[prices.length - 1];
}
2019/02/01 16:53 下午 posted in  LeetCode

1349. 参加考试的最大学生数

给你一个 m * n 的矩阵 seats 表示教室中的座位分布。如果座位是坏的(不可用),就用 '#' 表示;否则,用 '.' 表示。

学生可以看到左侧、右侧、左上、右上这四个方向上紧邻他的学生的答卷,但是看不到直接坐在他前面或者后面的学生的答卷。请你计算并返回该考场可以容纳的一起参加考试且无法作弊的最大学生人数。

学生必须坐在状况良好的座位上。
 

示例 1:

输入:seats = [["#",".","#","#",".","#"],
              [".","#","#","#","#","."],
              ["#",".","#","#",".","#"]]
输出:4
解释:教师可以让 4 个学生坐在可用的座位上,这样他们就无法在考试中作弊。 

示例 2:

输入:seats = [[".","#"],
              ["#","#"],
              ["#","."],
              ["#","#"],
              [".","#"]]
输出:3
解释:让所有学生坐在可用的座位上。

示例 3:

输入:seats = [["#",".",".",".","#"],
              [".","#",".","#","."],
              [".",".","#",".","."],
              [".","#",".","#","."],
              ["#",".",".",".","#"]]
输出:10
解释:让学生坐在第 1、3 和 5 列的可用座位上。

提示:

  • seats 只包含字符 '.' 和'#'
  • m == seats.length
  • n == seats[i].length
  • 1 <= m <= 8
  • 1 <= n <= 8

2. 结题思路

2.1. 思路过程

  1. 当前行的状态只和前一行的状态有关。
  2. 为了表示前一行的状态,加上题目矩阵规模小于8,可以使用状态压缩。
  3. 一个简便方法,采用位运算快速进行移位,以及有效状态的判断。

2.2. 细节考虑

  1. 位运算,通过左移,右移一位,可以快速判断,当前的状态是否合法。
  2. 将合法的座位,映射为0。坏掉的座位,映射为1,与当前状态与(&),可以快速判断当前状态是否合法

3. 代码

public int maxStudents(char[][] seats) {
    int m = seats.length;
    int n = seats[0].length;
    int[][] dp = new int[m + 1][1 << n];
    int[] tmp = new int[m];
    int sum;
    for (int i = 0; i < m; i++) {
        sum = 0;
        for (char c : seats[i]) {
            sum = sum << 1;
            if (c == '#') {
                sum += 1;
            }
        }
        tmp[i] = sum;
    }
    for (int i = m - 1; i >= 0; i--) {
        for (int j = 0; j < 1 << n; j++) {
            if ((j & (j << 1)) == 0 && (j & (j >> 1)) == 0 && (j & tmp[i]) == 0) {
                for (int k = 0; k < 1 << n; k++) {
                    if ((k & (j << 1)) == 0 && (k & (j >> 1)) == 0) {
                        dp[i][j] = Math.max(dp[i][j], Integer.bitCount(j) + dp[i + 1][k]);
                    }
                }
            }
        }
    }
    int result = 0;
    for (int i = 0; i < 1 << n; i++) {
        result = Math.max(0, dp[0][i]);
    }
    return result;
}

其他解法

mark一下,学习:
https://leetcode-cn.com/problems/maximum-students-taking-exam/solution/er-fen-tu-zui-da-du-li-ji-by-lightcml/

2020/02/11 17:01 下午 posted in  LeetCode

169. 求众数

https://leetcode-cn.com/problems/majority-element/description/


给定一个大小为 n 的数组,找到其中的众数。众数是指在数组中出现次数大于  ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在众数。

示例 1:

输入: [3,2,3]
输出: 3
示例 2:

输入: [2,2,1,1,1,2,2]
输出: 2

解题思路:

  我的思路是,排序,然后逐一比对。直到找到众数为止。但是直觉告诉我应该有更优雅的方式。

  这是一道求众数的问题,有很多种解法,其中我感觉比较好的有两种,一种是用哈希表,这种方法需要O(n)的时间和空间,另一种是用一种叫摩尔投票法 Moore Voting,需要O(n)的时间和O(1)的空间,比前一种方法更好。这种投票法先将第一个数字假设为众数,然后把计数器设为1,比较下一个数和此数是否相等,若相等则计数器加一,反之减一。然后看此时计数器的值,若为零,则将当前值设为候选众数。以此类推直到遍历完整个数组,当前候选众数即为该数组的众数。不仔细弄懂摩尔投票法的精髓的话,过一阵子还是会忘记的,首先要明确的是这个叼炸天的方法是有前提的,就是数组中一定要有众数的存在才能使用,下面我们来看本算法的思路,这是一种先假设候选者,然后再进行验证的算法。我们现将数组中的第一个数假设为众数,然后进行统计其出现的次数,如果遇到同样的数,则计数器自增1,否则计数器自减1,如果计数器减到了0,则更换当前数字为候选者。这是一个很巧妙的设定,也是本算法的精髓所在,为啥遇到不同的要计数器减1呢,为啥减到0了又要更换候选者呢?首先是有那个强大的前提存在,一定会有一个出现超过半数的数字存在,那么如果计数器减到0了话,说明目前不是候选者数字的个数已经跟候选者的出现个数相同了,那么这个候选者已经很weak,不一定能出现超过半数,我们选择更换当前的候选者。那有可能你会有疑问,那万一后面又大量的出现了之前的候选者怎么办,不需要担心,如果之前的候选者在后面大量出现的话,其又会重新变为候选者,直到最终验证成为正确的众数,佩服算法的提出者啊。

2019/02/13 15:46 下午 posted in  LeetCode

189. 旋转数组

给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。

示例 1:

输入: [1,2,3,4,5,6,7] 和 k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]
示例 2:

输入: [-1,-100,3,99] 和 k = 2
输出: [3,99,-1,-100]
解释: 
向右旋转 1 步: [99,-1,-100,3]
向右旋转 2 步: [3,99,-1,-100]
说明:

尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。
要求使用空间复杂度为 O(1) 的原地算法。

解题思路:

  关键在于O(1)的算法。如果是O(n)的话,方法就太多了。这里就不说了。主要说一下O(1)的做法。

  采用翻转字符的方法,思路是先把前n-k个数字翻转一下,再把后k个数字翻转一下,最后再把整个数组翻转一下:

1 2 3 4 5 6 7
4 3 2 1 5 6 7
4 3 2 1 7 6 5
5 6 7 1 2 3 4

2019/02/13 15:48 下午 posted in  LeetCode

258. 各位相加


给定一个非负整数  num,反复将各个位上的数字相加,直到结果为一位数。

示例:

输入: 38
输出: 2 
解释: 各位相加的过程为:3 + 8 = 11, 1 + 1 = 2。 由于 2 是一位数,所以返回 2。
进阶:
你可以不使用循环或者递归,且在 O(1) 时间复杂度内解决这个问题吗?

解题思路:

普通的方法都会解,这里就不重复了。说下O(1)的解法。

这里用到 一个很巧妙的算法。

根据以上,可以得出,快速求解,就是将数字不断减去9,直到不能减为止即为正解。

那么不断减去9的过程,可以化为--> mod 9

代码如下:

class Solution {
    public int addDigits(int num) {
        if (num == 0){
            return num;
        }
        num %= 9;
        return num == 0 ? 9 : num;
    }
}
2019/02/13 16:45 下午 posted in  LeetCode

32. 最长有效括号

https://leetcode-cn.com/problems/longest-valid-parentheses/description/

给定一个只包含 '(' 和 ')' 的字符串,找出最长的包含有效括号的子串的长度。

示例 1:

输入: "(()"
输出: 2
解释: 最长有效括号子串为 "()"
示例 2:

输入: ")()())"
输出: 4
解释: 最长有效括号子串为 "()()"

代码:

public int longestValidParentheses(String s) {
    int[] dp = new int[s.length()];
    int max = 0;
    for (int i = 1; i < s.length(); i++){
        if (s.charAt(i) == '('){
            dp[i] = 0;
        }else{
            if (dp[i - 1] != 0) {
                int index = i - dp[i - 1] - 1;
                if (index >= 0 && s.charAt(index) == '(') {
                    dp[i] = dp[i - 1] + 2;
                    if (index - 1 >= 0 ){
                        dp[i] += dp[index - 1];
                    }
                }
            }
            if (s.charAt(i - 1) == '('){
                if (i >= 2) {
                    dp[i] = dp[i] > dp[i - 2] + 2 ? dp[i] : dp[i - 2] + 2;
                }else{
                    dp[i] = 2;
                }
            }
            if (max < dp[i]){
                max = dp[i];
            }
        }
    }
    return max;
}
2019/01/21 00:04 上午 posted in  LeetCode

39. 组合总和

https://leetcode-cn.com/problems/combination-sum/description/

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

candidates 中的数字可以无限制重复被选取。

说明:

所有数字(包括 target)都是正整数。
解集不能包含重复的组合。

示例 1:

输入: candidates = [2,3,6,7], target = 7,
所求解集为:
[
  [7],
  [2,2,3]
]
示例 2:

输入: candidates = [2,3,5], target = 8,
所求解集为:
[
  [2,2,2,2],
  [2,3,3],
  [3,5]
]

题目分析:

  1. 一看到这道题,就感觉要用递归来做,应该是回溯算法。百度证实无误。
  2. 然后自己还没有生写过回溯。想了一会儿,结果不对,和正确解法做了一下比较,修改了一下自己的算法。标答是C++的,在java上需要修改一下,一些特别的地方要处理一下。

直接上代码:

/**
 * 核心算法
 * @param candidates 数组
 * @param target 当前递归子问题需要计算的target
 * @param start 开始查找的index
 * @param result 当前递归的result数组
 * @param ans 最后的答案
 */
private void findOne(int[] candidates, int target, int start, List<Integer> result, List<List<Integer>> ans){
    if (target == 0){
        List<Integer> list = new ArrayList<>(result); //这里需要新开一个数组,否则会一直复用这个对象,导致结果不对
        ans.add(list);
        return;
    }else if(target < candidates[start]){
        return; //不符合的结果,不处理。
    }else{
        for (int i = start; i < candidates.length; i++){
            result.add(candidates[i]);
            findOne(candidates, target - candidates[i], i, result, ans);
            result.remove(result.size() - 1);
            //这里有点像树的遍历,这里就是要一个节点和不要一个节点的分支。然后可以重复使用元素,所以递归子问题,仍然从i开始。
        }
    }
}

public List<List<Integer>> combinationSum(int[] candidates, int target) {
    List<List<Integer>> ans = new ArrayList<>();
    Arrays.sort(candidates); //题目没有说排好序的数组,所以这里要先拍个序
    findOne(candidates, target, 0, new ArrayList<>(), ans);
    return ans;
}
2019/01/21 00:06 上午 posted in  LeetCode

448. 找到所有数组中消失的数字

题目描述

给定一个范围在 1 ≤ a[i] ≤ n ( n = 数组大小 ) 的 整型数组,数组中的元素一些出现了两次,另一些只出现一次。

找到所有在 [1, n] 范围之间没有出现在数组中的数字。

您能在不使用额外空间且时间复杂度为O(n)的情况下完成这个任务吗? 你可以假定返回的数组不算在额外空间内。

示例:

输入:
[4,3,2,7,8,2,3,1]

输出:
[5,6]

思路分析

关键点在于不用额外空间,时间复杂度为O(n)。这两点限制了使用排序算法,或者用额外空间来记录。

那么,不能用额外空间,就只能用原有的数组空间了。这里有一个技巧就是用数组的下标也可以存储信息。因为数据的范围是在n之内,因此数组内出现的数字都是合法的下标。基于这个前提,本题有一个很巧妙的做法。详见代码。

代码

public List<Integer> findDisappearedNumbers(int[] nums) {
        List<Integer> result = new ArrayList<>();
        for (int i = 0; i < nums.length; i++){
            //将数组中存放的值作为坐标,进行二次访问。目标是将值设置为相反数(保证是负数)。那么没有变化的位置就是缺失的值
            nums[Math.abs(nums[i])-1] = - Math.abs(nums[Math.abs(nums[i])-1]);
        }
        for (int i = 0; i < nums.length; i++){
            if (nums[i] > 0){
                result.add(i);
            }
        }
        return result;
    }
2019/03/12 20:18 下午 posted in  LeetCode

55. 跳跃游戏

给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个位置。

示例 1:

输入: [2,3,1,1,4]
输出: true
解释: 从位置 0 到 1 跳 1 步, 然后跳 3 步到达最后一个位置。
示例 2:

输入: [3,2,1,0,4]
输出: false
解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。

解题思路:

本道题用贪心。贪心的策略很巧妙。第一次独立没有想出来。
贪心的策略:

  1. 每一步都选择最大的可能去接近结果。
  2. 那么遍历一遍,如果最大的结果超过了最后一个位置。则说明最后一个位置是可达的,也就是返回true。

贪心的巧妙在于,我们不在意到底是如何选择的,只在乎是否可达。

代码:

 public boolean canJump(int[] nums) {
    int result = 0;
    if (nums.length == 1){ //注意边界,最后一个台阶的值是没有意义的。因此只有一个台阶的时候,永远是true
        return true;
    }
    for (int i = 0; i < nums.length; i++){
        if (i > result || (nums[i] == 0 && i == result)){
            break;
        }
        result = Math.max(result, nums[i] + i);
    }
    return result >= nums.length - 1;
}
2019/01/28 16:04 下午 posted in  LeetCode

72. 编辑距离

题目

给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

插入一个字符
删除一个字符
替换一个字符
示例 1:

输入: word1 = "horse", word2 = "ros"
输出: 3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
示例 2:

输入: word1 = "intention", word2 = "execution"
输出: 5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')

解题思路

  1. 先作弊了,看到要用dp。思考许久还是没有想法。
    解析一下自己的错误思路。
    看到有三种变换方式,有一点不知道该怎么处理。

  2. 看了网上的解题报告,写下自己理解后的东西。

    首先,确认dp的含义
    word1记为s1, word2记为s2
    dp[i][j] = s1 从0...i, s2从0...j 两个字符串的编辑距离。所以dp的大小应该是dp[s1.length + 1][s2.length + 1].

  • 转换公式
    计算dp[i][j]有三种变化方式

    1. dp[i - 1][j], 由于对比dp[i][j],s1少了一个,所以要insert一个,编辑距离 + 1
    2. dp[i][j - 1] 由于对比dp[i][j],s1少了一个,所以要delete一个,编辑距离 + 1
    3. dp[i - 1][j - 1],对比s1[i - 1] 和 s2[j - 1]的情况,如果相等,编辑距离不变,否则需要+1(替换)

    从上文可以看出,三种变换方式都有了。

    接下来处理边界条件

    dp[0][i] = i和dp[i][0] = i,分别代表增加i个,和删除i个。

代码

 public int minDistance(String word1, String word2) {
    int[][] dp = new int[word1.length() + 1][word2.length() + 1];
    for (int i = 0; i <= word1.length(); i++) {
        dp[i][0] = i;
    }
    for (int j = 0; j <= word2.length(); j++) {
        dp[0][j] = j;
    }
    for (int i = 1; i <= word1.length(); i++) {
        for (int j = 1; j <= word2.length(); j++) {
            int a = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
            int cost = word1.charAt(i - 1) == word2.charAt(j - 1) ? 0 : 1;
            dp[i][j] = Math.min(a, dp[i - 1][j - 1] + cost);
        }
    }
    return dp[word1.length()][word2.length()];
}
2019/02/01 11:46 上午 posted in  LeetCode