Java解leetcode,助力面试之中等10道题(七)
第377题 组合总和 Ⅳ
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
示例 1:
输入 | 输出 |
---|---|
nums = [1,2,3], target = 4 | 7 |
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
示例 2:
输入 | 输出 |
---|---|
nums = [9], target = 3 | 0 |
解题思路
本题使用动态规划,通过计算从1到目标值中所有的组合数,最后推算出目标值的组合数。
代码
// 组合总和 Ⅳ:
class Solution {
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1];
dp[0] = 1;
for (int i = 1; i <= target; i++) {//计算从1到目标值每个数的组合数
for (int num : nums) {//遍历数组
if (num <= i) {
dp[i] += dp[i - num];
}
}
}
return dp[target];
}
}
时间复杂度为O(target×n),n为数组长度,target为目标值
空间复杂度为O(target
第378题 有序矩阵中第 K 小的元素
给你一个 n x n 矩阵 matrix ,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。
请注意,它是 排序后 的第 k 小元素,而不是第 k 个 不同 的元素。
示例 1:
输入 | 输出 |
---|---|
matrix = [[1,5,9],[10,11,13],[12,13,15]], k = 8 | 13 |
解释:矩阵中的元素为 [1,5,9,10,11,12,13,13,15],第 8 小元素是 13
示例 2:
输入 | 输出 |
---|---|
matrix = [[-5]], k = 1 | -5 |
解题思路
本题使用二分法求解,先判断中间数的位置与k的关系,然后不断缩小范围找到第k个位置。
代码
// 有序矩阵中第 K 小的元素:二分查找
class Solution {
public int kthSmallest(int[][] matrix, int k) {//二分查找
int n = matrix.length;
int left = matrix[0][0];
int right = matrix[n - 1][n - 1];
while (left < right) {
int mid = left + ((right - left) >> 1);
if (check(matrix, mid, k, n)) {//判断函数,如果中间数的位置大于等于k,则将right放在中间位,否则将left放在中间位加1上
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
public boolean check(int[][] matrix, int mid, int k, int n) {//查找中间数属于第几个位置,然后比较与k的大小
int i = n - 1;
int j = 0;
int num = 0;
while (i >= 0 && j < n) {
if (matrix[i][j] <= mid) {
num += i + 1;
j++;
} else {
i--;
}
}
return num >= k;
}
}
时间复杂度为O(nlog(r−l)),n为数组长度,log(r-l)为二分法的时间复杂度
空间复杂度为O(1)
第406题 根据身高重建队列
假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。
请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。
示例 1:
输入 | 输出 |
---|---|
people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]] | [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] |
解释:
编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。
编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。
编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。
编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。
编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。
示例 2:
输入 | 输出 |
---|---|
people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]] | [[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]] |
解题思路
本题使用贪心算法,首先比较数组中各数第一个位置,如果相等,则按从小到大排,如果不相等,则比较第二个数,按从小到大排。
代码
// 根据身高重建队列:贪心
class Solution {
public int[][] reconstructQueue(int[][] people) {
Arrays.sort(people, new Comparator<int[]>() {
public int compare(int[] person1, int[] person2) {
if (person1[0] != person2[0]) {//如果前后数值不相等
return person2[0] - person1[0];//则按数值从小到大排
} else {
return person1[1] - person2[1];//如果前后数相等,则按第二个数从小到大排
}
}
});
List<int[]> ans = new ArrayList<int[]>();
for (int[] person : people) {//遍历加入结果
ans.add(person[1], person);
}
return ans.toArray(new int[ans.size()][]);
}
}
时间复杂度为O(
n
2
n^2
n2),n为数组长度
空间复杂度为O(logn)
第413题 等差数列划分
如果一个数列至少有三个元素,并且任意两个相邻元素之差相同,则称该数列为等差数列。
数组 A 包含 N 个数,且索引从0开始。数组 A 的一个子数组划分为数组 (P, Q),P 与 Q 是整数且满足 0<=P<Q<N 。
如果满足以下条件,则称子数组(P, Q)为等差数组:
元素 A[P], A[p + 1], …, A[Q - 1], A[Q] 是等差的。并且 P + 1 < Q 。
函数要返回数组 A 中所有为等差数组的子数组个数。
示例 1:
输入 | 输出 |
---|---|
1, 3, 5, 7, 9 | 3 |
示例 2:
输入 | 输出 |
---|---|
1, 1, 2, 5, 7 | 0 |
解题思路
使用动态规划不断判断每三个数之间是否为等差数列
代码
// 等差数列划分:动态规划
public class Solution {
public int numberOfArithmeticSlices(int[] A) {
int dp = 0;
int sum = 0;
for (int i = 2; i < A.length; i++) {
if (A[i] - A[i - 1] == A[i - 1] - A[i - 2]) {//判断3个数,两两之间差值是否一样,然后不断遍历
dp = 1 + dp;//dp统计当前是否为等差数列
sum += dp;//sum统计整个包括多少个等差数列
} else
dp = 0;
}
return sum;
}
}
时间复杂度为O(n),n为数组长度
空间复杂度为O(1)
第416题 分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入 | 输出 |
---|---|
nums = [1,5,11,5] | true |
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入 | 输出 |
---|---|
nums = [1,2,3,5] | false |
解释:数组不能分割成两个元素和相等的子集。
解题思路
本题使用动态规划,计算数组各数的总和,然后判断数组中是否有几个数相加等于数组总和的一半。
代码
// 分割等和子集:动态规划
class Solution {
public boolean canPartition(int[] nums) {
int n = nums.length;
if (n < 2) {
return false;
}
int sum = 0, maxNum = 0;
for (int num : nums) {//遍历数组计算数组各数总和以及最大数
sum += num;
maxNum = Math.max(maxNum, num);
}
if (sum % 2 != 0) {//总和为奇数,不符合
return false;
}
int target = sum / 2;
if (maxNum > target) {//最大数超过总和一半,不符合
return false;
}
boolean[] dp = new boolean[target + 1];
dp[0] = true;
for (int i = 0; i < n; i++) {
int num = nums[i];
for (int j = target; j >= num; --j) {//从总和的一半开始找是否有符合的数
dp[j] |= dp[j - num];
}
}
return dp[target];
}
}
时间复杂度为O(n×target),n为数组长度,target为数组各数总和的一半
空间复杂度为O(target)
第417题 太平洋大西洋水流问题
给定一个 m x n 的非负整数矩阵来表示一片大陆上各个单元格的高度。“太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界。
规定水流只能按照上、下、左、右四个方向流动,且只能从高到低或者在同等高度上流动。
请找出那些水流既可以流动到“太平洋”,又能流动到“大西洋”的陆地单元的坐标。
示例 1:
输入 | 输出 |
---|---|
[[1,2,2,3,5],[3,2,3,4,4],[2,4,5,3,1],[6,7,1,4,5],[5,1,1,2,4]] | [[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] |
解题思路
先将第一行和第一列的位置标记为可流向太平洋,将最后一行和最后一列标记为可流向大西洋,然后在递归判断各行各列是否比前一个值大,如果前一个值标记为1,且当前遍历的值比前一个值大,则也标记为1.最后判断流向太平洋和大西洋是否都为1.
代码
// 太平洋大西洋水流问题:DFS
public List<List<Integer>> pacificAtlantic(int[][] matrix) {
if (matrix.length == 0 || matrix[0].length == 0) {
return new ArrayList<>();
}
int m = matrix.length;
int n = matrix[0].length;
int[][] pacific = new int[m][n];
int[][] atlantic = new int[m][n];
//从海洋边界开始
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 || j == 0) {//使用DFS判断第一行和第一列是否能流到太平洋
dfs(matrix, pacific, i, j, matrix[i][j]);
}
if (i == m - 1 || j == n - 1) {//判断最后一行和最后一列是否能流到大西洋
dfs(matrix, atlantic, i, j, matrix[i][j]);
}
}
}
List<List<Integer>> res = new ArrayList<>();//将可行的解加入结果中
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (pacific[i][j] == 1 && atlantic[i][j] == 1) {
res.add(Arrays.asList(i, j));
}
}
}
return res;
}
private void dfs(int[][] matrix, int[][] aux, int i, int j, int pre) {
//判断边界
if (i < 0 || j < 0 || i > matrix.length - 1 || j > matrix[0].length - 1
//已经流到过了
|| aux[i][j] == 1
//不能流动
|| matrix[i][j] < pre) {
return;
}
aux[i][j] = 1;
dfs(matrix, aux, i - 1, j, matrix[i][j]);//递归四个方向
dfs(matrix, aux, i + 1, j, matrix[i][j]);
dfs(matrix, aux, i, j - 1, matrix[i][j]);
dfs(matrix, aux, i, j + 1, matrix[i][j]);
}
时间复杂度为O(mn),m,n为矩阵的行和列
空间复杂度为O(mn)
第437题 路径总和 III
给定一个二叉树,它的每个结点都存放着一个整数值。
找出路径和等于给定数值的路径总数。
路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
二叉树不超过1000个节点,且节点数值范围是 [-1000000,1000000] 的整数。
示例 1:
输入 | 输出 |
---|---|
root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8 | 3 |
解释:和等于 8 的路径有:1. 5 -> 3
2. 5 -> 2 -> 1
3. -3 -> 11
解题思路
本题使用递归来求解,先建立二叉树,存放前缀和以及出现次数,然后不断递归每一个前缀和加上左右子节点的值是否满足目标值,每有一个满足的,则计算前缀和的次数
代码
// 路径总和 III:递归
class Solution {
public int pathSum(TreeNode root, int sum) {
Map<Integer, Integer> prefixSumCount = new HashMap<>();//key是前缀和, value是大小为key的前缀和出现的次数
prefixSumCount.put(0, 1);
return recursionPathSum(root, prefixSumCount, sum, 0);//递归
}
private int recursionPathSum(TreeNode node, Map<Integer, Integer> prefixSumCount, int target, int currSum) {
if (node == null) {
return 0;
}
int res = 0;
currSum += node.val;//当前路径上的和
res += prefixSumCount.getOrDefault(currSum - target, 0);
// 更新路径上当前节点前缀和的个数
prefixSumCount.put(currSum, prefixSumCount.getOrDefault(currSum, 0) + 1);
res += recursionPathSum(node.left, prefixSumCount, target, currSum);//递归判断左右节点是否有满足目标值得节点
res += recursionPathSum(node.right, prefixSumCount, target, currSum);
prefixSumCount.put(currSum, prefixSumCount.get(currSum) - 1);//剪枝
return res;
}
}
时间复杂度为O(n),n为二叉树节点数
空间复杂度为O(n)
第445题 两数相加 II
给你两个 非空 链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。
你可以假设除了数字 0 之外,这两个数字都不会以零开头。
示例 1:
输入 | 输出 |
---|---|
(7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4) | 7 -> 8 -> 0 -> 7 |
解题思路
使用栈的特性,后进先出,利用两个数,一个数存储两链表出栈后的数加上进位的结果,然后对10去余,另一个是进位,相加结果除以10,直到遍历完两个链表。
代码
// 两数相加 II:栈
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
Deque<Integer> stack1 = new LinkedList<Integer>();
Deque<Integer> stack2 = new LinkedList<Integer>();
while (l1 != null) {
stack1.push(l1.val);
l1 = l1.next;
}
while (l2 != null) {
stack2.push(l2.val);
l2 = l2.next;
}
int carry = 0;
ListNode ans = null;
while (!stack1.isEmpty() || !stack2.isEmpty() || carry != 0) {
int a = stack1.isEmpty() ? 0 : stack1.pop();
int b = stack2.isEmpty() ? 0 : stack2.pop();
int cur = a + b + carry;
carry = cur / 10;
cur %= 10;
ListNode curnode = new ListNode(cur);
curnode.next = ans;
ans = curnode;
}
return ans;
}
}
时间复杂度为O(max(m,n)),m,n为两链表的长度
空间复杂度为O(m+n)
第451题 根据字符出现频率排序
给定一个字符串,请将字符串里的字符按照出现的频率降序排列。
示例 1:
输入 | 输出 |
---|---|
“tree” | “eert” |
示例 2:
输入 | 输出 |
---|---|
“cccaaa” | “cccaaa” |
示例 3:
输入 | 输出 |
---|---|
“Aabb” | “bbAa” |
解题思路
本题使用桶排序,先将各字母及其出现次数存入哈希表中,然后再按出现次数划分桶,最后将哈希表中的字母放入桶内,再从大到小输出到结果中。
代码
// 根据字符出现频率排序:桶排序
class Solution {
public String frequencySort(String s) {
Map<Character, Integer> map = new HashMap<Character, Integer>();
int maxFreq = 0;
int length = s.length();
for (int i = 0; i < length; i++) {
char c = s.charAt(i);
int frequency = map.getOrDefault(c, 0) + 1;//用哈希表存储各字符及其出现次数
map.put(c, frequency);
maxFreq = Math.max(maxFreq, frequency);//计算出现次数最多的字符
}
StringBuffer[] buckets = new StringBuffer[maxFreq + 1];//建立桶
for (int i = 0; i <= maxFreq; i++) {//按出现次数不同,建立桶
buckets[i] = new StringBuffer();
}
for (Map.Entry<Character, Integer> entry : map.entrySet()) {
char c = entry.getKey();
int frequency = entry.getValue();
buckets[frequency].append(c);//将哈希表中的字符存入桶中
}
StringBuffer sb = new StringBuffer();
for (int i = maxFreq; i > 0; i--) {
StringBuffer bucket = buckets[i];
int size = bucket.length();
for (int j = 0; j < size; j++) {
for (int k = 0; k < i; k++) {
sb.append(bucket.charAt(j));//加入各桶中的字母加入到结果中
}
}
}
return sb.toString();
}
}
时间复杂度为O(n+k),n为字符串长度,k为字符串包含的不同字符个数
空间复杂度为O(n+k)
第452题 用最少数量的箭引爆气球
在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。
一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。
示例 1:
输入 | 输出 |
---|---|
points = [[10,16],[2,8],[1,6],[7,12]] | 2 |
解释:对于该样例,x = 6 可以射爆 [2,8],[1,6] 两个气球,以及 x = 11 射爆另外两个气球
示例 2:
输入 | 输出 |
---|---|
points = [[1,2],[3,4],[5,6],[7,8]] | 4 |
示例 3:
输入 | 输出 |
---|---|
points = [[1,2],[2,3],[3,4],[4,5]] | 2 |
解题思路
代码
// 用最少数量的箭引爆气球:贪心
class Solution {
public int findMinArrowShots(int[][] points) {
if (points.length == 0) {
return 0;
}
Arrays.sort(points, new Comparator<int[]>() {//排序
public int compare(int[] point1, int[] point2) {
if (point1[1] > point2[1]) {
return 1;
} else if (point1[1] < point2[1]) {
return -1;
} else {
return 0;
}
}
});
int pos = points[0][1];
int ans = 1;
for (int[] balloon: points) {//遍历数组
if (balloon[0] > pos) {//如果下一个数的起始大于当前数的结尾,则将pos标记为下一数的结尾,然后将结果值自增,否则继续遍历
pos = balloon[1];
++ans;
}
}
return ans;
}
}
时间复杂度为O(nlogn),n为数组长度
空间复杂度为O(logn)