🔥 LeetCode 热题 HOT 100(81-90)
337. 打家劫舍 III
思路:后序遍历 + 动态规划
推荐题解:树形 dp 入门问题(理解「无后效性」和「后序遍历」)
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int rob(TreeNode root) {
int[] temp = steal(root);
return Math.max(temp[0], temp[1]);
}
//返回以当前结点为根节点的二叉树的根节点偷或不偷所能获得的最大值
private int[] steal(TreeNode node) {
if (node == null) {
return new int[] {0, 0};
}
int[] left = steal(node.left);
int[] right = steal(node.right);
//dp[0]表示当前节点不被偷,dp[1]表示偷
int[] dp = new int[2];
//当前不被偷,那么子结点偷或不偷都可以,取最大的和即可
dp[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
//当前结点被偷,子结点都不能偷
dp[1] = node.val + left[0] + right[0];
return dp;
}
}
338. 比特位计数
思路: n & (n - 1)
表示将 n 最后一位 1 置为 0:
class Solution {
public int[] countBits(int num) {
int[] res = new int[num + 1];
for (int i = 0; i <= num; i++) {
res[i] = hamming(i);
}
return res;
}
private int hamming(int n) {
int cnt = 0;
//每次将最后一个1置0
while (n != 0) {
n = n & (n - 1);
cnt++;
}
return cnt;
}
}
动态规划:
任何一个数的汉明重量等于将其最后一位 1 置 0 的数的汉明重量加1,可考虑用动态规划。
class Solution {
public int[] countBits(int num) {
//每个数的汉明重量
int[] dp = new int[num + 1];
//base case
dp[0] = 0;
for (int i = 1; i <= num; i++) {
dp[i] = dp[i & (i - 1)] + 1;
}
return dp;
}
}
347. 前 K 个高频元素
思路一:快速排序的应用,和第K大元素类似
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
int len = map.size();
int[][] freq = new int[len][2];
int i = 0;
for (int key : map.keySet()) {
freq[i][0] = key;
freq[i][1] = map.get(key);
i++;
}
int index = 0;
int left = 0, right = len - 1;;
int pos = len - k;
while (true) {
index = partition(freq, left, right);
if (index == pos) {
break;
} else if (index < pos) {
left= index + 1;
} else if (index > pos) {
right = index - 1;
}
}
int[] res = new int[k];
int cnt = 0;
for (int j = pos; j < len; j++) {
res[cnt++] = freq[j][0];
}
return res;
}
private Random random = new Random(47);
private int partition(int[][] nums, int left, int right) {
int rand = left + random.nextInt(right - left + 1);
swap(nums, left, rand);
int pivot = nums[left][1];
int temp = nums[left][0];
while (left < right) {
while (left < right && pivot <= nums[right][1]) {
right--;
}
if (left < right) {
swap(nums, left, right);
left++;
}
while (left < right && pivot > nums[left][1]) {
left++;
}
if (left < right) {
swap(nums, left, right);
right--;
}
}
nums[left][1] = pivot;
nums[left][0] = temp;
return left;
}
private void swap(int[][] nums, int i, int j) {
int[] temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
思路二:优先队列(大顶堆)
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
PriorityQueue<Integer> pqueue = new PriorityQueue<>((e1, e2) -> map.get(e2) - map.get(e1));
for (int key : map.keySet()) {
pqueue.offer(key);
}
int[] res = new int[k];
for (int i = 0; i < k; i++) {
res[i] = pqueue.poll();
}
return res;
}
}
394. 字符串解码
思路:栈,思路简单,关键在于字符串拼接顺序的细节问题。
class Solution {
public String decodeString(String s) {
Deque<String> stack = new LinkedList<>();
for (int i = 0; i < s.length(); i++) {
String str = s.substring(i, i + 1);
if (str.equals("]")) {
//拼接 [] 之间的字符,这里得到的是逆序,不用反转
StringBuilder strSB = new StringBuilder();
while (!stack.peek().equals("[")) {
strSB.append(stack.pop());
}
//弹出 [
stack.pop();
//拼接 [ 之前的重复次数
StringBuilder reTimesSB = new StringBuilder();
while (!stack.isEmpty() && isDigit(stack.peek())) {
reTimesSB.append(stack.pop());
}
//根据重复次数拼接字符串,反转后转为整型
int reTimes = Integer.parseInt(reTimesSB.reverse().toString());
StringBuilder sb = new StringBuilder();
while (reTimes > 0) {
sb.append(strSB);
reTimes--;
}
//新字符串入栈
stack.push(sb.toString());
} else {
stack.push(str);
}
}
StringBuilder res = new StringBuilder();
while (!stack.isEmpty()) {
res.append(stack.pop());
}
//由于之前的字符拼接都是逆序的,反转后再返回
return res.reverse().toString();
}
//首字符是否为数字
private boolean isDigit(String str) {
char ch = str.charAt(0);
return ch >= '0' && ch <= '9';
}
}
399. 除法求值
406. 根据身高重建队列
416. 分割等和子集
推荐题解:「手画图解」416.分割等和子集 | 记忆化递归 思路详解
思路一:dfs
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for (int num : nums) {
sum += num;
}
if (sum % 2 == 1) {
return false;
}
int target = sum / 2;
return dfs(nums, 0, 0, target);
}
private boolean dfs(int[] nums, int index, int sum, int target) {
//base case
if (nums.length == index) {
if (sum == target) {
return true;
} else {
return false;
}
}
//对于任意一个数,可与选或者不选
return dfs(nums, index + 1, sum + nums[index], target) ||
dfs(nums, index + 1, sum, target);
}
}
用例超时:
[100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,99,97]
思路二:dfs + 备忘录
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for (int num : nums) {
sum += num;
}
if (sum % 2 == 1) {
return false;
}
int target = sum / 2;
return dfs(nums, 0, 0, target);
}
// 备忘录:也可用一个二维数组,一维表示元素和sum,一维表示当前索引index
private Map<String, Boolean> map = new HashMap<>();
private boolean dfs(int[] nums, int index, int sum, int target) {
if (nums.length == index) {
if (sum == target) {
return true;
} else {
return false;
}
}
//描述一个子问题的两个变量是 sum 和 index,组成 key 字符串
String key = sum + "&" + index;
if (map.containsKey(key)) {
return map.get(key);
}
boolean ret = dfs(nums, index + 1, sum + nums[index], target) ||
dfs(nums, index + 1, sum, target);
map.put(key, ret);
return ret;
}
}
思路三:所有 target 绝对值大于 元素总和 或 元素总和 不为 偶数 显然不能拆分;问题等价于能否找到一组元素的和为所有元素总和的一半。(01背包问题:可选物品有限)
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for (int num : nums) {
sum += num;
}
if (sum % 2 == 1) {
return false;
}
//背包容量
int target = sum / 2;
//物品个数
int len = nums.length;
//状态:dp[m][n]表示背包容量为 m,有 前 n 个物品( [0, n-1]共n个 )时能否装满背包
boolean[][] dp = new boolean[target + 1][len + 1];
// base case
// dp[0][0] = true, 不选即满
// dp[0][i] = true, i >= 1; 表示容量为0,有1个及以上个物品时可认为不选即满,固可以填满
// dp[i][0] = false, i >= 1; 表示容量 大于等于 1时,没有物品可选则无法填满
for (int i = 0; i < len + 1; i++) {
dp[0][i] = true;
}
for (int i = 1; i < target + 1; i++) {
for (int j = 1; j < len + 1 ; j++) {
// 如果不能装下前j个物品中的最后一个
if (i < nums[j - 1]) {
dp[i][j] = dp[i][j - 1];
// 能装下,可以选择装或者不装
} else if (i >= nums[j - 1]) {
dp[i][j] = dp[i][j - 1] || dp[i - nums[j - 1]][j - 1];
}
}
}
return dp[target][len];
}
}
437. 路径总和 III
提示:路径并非是一条向下的直线,可以是折线。如root = [10,5,-3,3,2,null,11,3,0,null,1], targetSum = 8
中[5, 3]
和[5, 3, 0]
都符合条件。
推荐题解:对前缀和解法的一点解释
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int pathSum(TreeNode root, int targetSum) {
pathSum(root, 0, targetSum);
return cnt;
}
private int cnt = 0;
private Map<Integer, Integer> map = new HashMap<>() {
{
//前缀树为0的结点个数是一个
put(0, 1);
}
};
// 前序遍历 + 回溯
private void pathSum(TreeNode node, int prefixSum, int targetSum) {
if (node == null) {
return;
}
// 当前结点的前缀和
prefixSum += node.val;
// 和当前结点前缀和 之差 为targetSum 的(父、祖先)结点即可满足条件
cnt += map.getOrDefault(prefixSum - targetSum, 0);
// 选择,使得子结点可以使用当前结点的前缀和
map.put(prefixSum, map.getOrDefault(prefixSum, 0) + 1);
pathSum(node.left, prefixSum, targetSum);
pathSum(node.right, prefixSum, targetSum);
// 撤销选择,使得兄弟结点无法使用当前结点的前缀和
map.put(prefixSum, map.get(prefixSum) - 1);
}
}
438. 找到字符串中所有字母异位词
思路:滑动窗口
class Solution {
public List<Integer> findAnagrams(String s, String p) {
//记录窗口中目标字符的出现次数
Map<Character, Integer> window = new HashMap<>();
//记录目标子串中每个字符出现的次数
Map<Character, Integer> need = new HashMap<>();
for (int i = 0; i < p.length(); i++) {
char ch = p.charAt(i);
need.put(ch, need.getOrDefault(ch, 0) + 1);
}
//记录已经匹配的目标字符数
int match = 0;
//窗口区间,左闭右开
int left = 0, right = 0;
List<Integer> res = new LinkedList<>();
while (right < s.length()) {
//窗口扩张(右边界右移)
char rightChar = s.charAt(right);
right++;
//如果当前字符是目标字符
if (need.containsKey(rightChar)) {
window.put(rightChar, window.getOrDefault(rightChar, 0) + 1);
//当 window 中 rightChar 的个数 小于等于 need 中 rightChar 的数量时
if (window.get(rightChar).compareTo(need.get(rightChar)) <= 0) {
match++;
}
}
//当窗口中已经包含所有目标字符
while(match == p.length()) {
//如果此时窗口大小刚好等于目标字符串长度,说明窗口内容刚好为目标字符串的异位词
if (right - left == p.length()) {
res.add(left);
}
//收缩窗口(左边界右移),直到window不再包含所有目标字符
char leftChar = s.charAt(left);
left++;
if (need.containsKey(leftChar)) {
if (window.get(leftChar).compareTo(need.get(leftChar)) <= 0) {
match--;
}
window.put(leftChar, window.get(leftChar) - 1);
}
}
}
return res;
}
}
448. 找到所有数组中消失的数字
思路一:使用map
记录各个元素出现的频次,然后依次获取 [1, n] 出现的频次,频次为0则说明没有出现。此时空间复杂度为O(n)
。面试时我们应该询问面试官 时间 和 空间复杂度 要求之后再确定具体使用的方法。
class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
Map<Integer, Integer> map = new HashMap<>();
List<Integer> res = new LinkedList<>();
for (int i : nums) {
map.put(i, map.getOrDefault(i, 0) + 1);
}
for (int i = 1; i <= nums.length; i++) {
if (!map.containsKey(i)) {
res.add(i);
}
}
return res;
}
}
思路二:将所有元素放置在其值减1的下标位置。如:1 应该放置在 0 位置,3 应该放置在 2 位置,这样一轮处理后所有 值 与 下标 差值不为1 的 下标加1 即为未出现的元素。思路来自:剑指 Offer 03. 数组中重复的数字
class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
int len = nums.length;
for (int i = 0; i < len; i++) {
// 如果元素的 值 和 下标 不匹配,则将其交换至对的位置
while (nums[i] - 1 != i) {
// 如果发现待交换的两个元素相同则跳过
if (nums[i] == nums[nums[i] - 1]) {
break;
}
swap(nums, i, nums[i] - 1);
}
}
List<Integer> res = new LinkedList<>();
for (int i = 0; i < len; i++) {
// 值 与 下标 不对应,下标加1 即为未出现的元素
if (nums[i] - 1 != i) {
res.add(i + 1);
}
}
return res;
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}