40.组合总和II
思路:与上一题类似,区别在于有重复数字,要考虑去重的情况。去重首先要排序,使重复数字排列在一块。当遇到重复数字时,将它作为一个组合,只可从左边开始选。当这个组合中的前一个数字没有被选择时,这个数字也不能选择。假设有【2,2,2】,我们的只尝试【】,【0】,【0,1】,【0,1,2】这几种情况,去除了【0,2】和【1,2】的情况,因为这两种情况与【0,1】的情况一样,都包含了两个2。递归过程中,若当前位置的元素被选中了,则考虑下一个元素要不要选(不管它是重复的还是不重复的),若当前位置的元素没有被选中,则跳过重复的元素。
代码:
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
List<Integer> current = new ArrayList<>();
Arrays.sort(candidates);
combinationSum2(candidates, 0, target, res, current);
return res;
}
private void combinationSum2(int[] candidates, int start, int target, List<List<Integer>> res, List<Integer> current) {
if (target == 0) {
res.add(new ArrayList<>(current));
return;
}
if (start == candidates.length) {
return;
}
int nextIndex = start + 1;
while (nextIndex < candidates.length) {
if (candidates[nextIndex] == candidates[start]) {
nextIndex ++;
} else {
break;
}
}
combinationSum2(candidates, nextIndex, target, res, current);
if (target >= candidates[start] && (start == 0 || candidates[start] != candidates[start - 1] || current.get(current.size() - 1) == candidates[start])) {
current.add(candidates[start]);
combinationSum2(candidates, start + 1, target -candidates[start], res, current);
current.remove(current.size() - 1);
}
}
时间复杂度:O(2n)
空间复杂度:O(m*C),其中m为解的个数,而C为最长解中的元素个数
41.缺失的第一个正数
思路:设数组长度为N,则答案一定在1~N+1中。我们只需要关注1~N这些数出现的情况即可,即可以直接用原来的数组来进行标记。但是我们又不希望在标记的过程中出现丢失信息的情况。假设数组长度为4,数组为【4,2,3,1】,如果我们只是简单的从前往后遍历,当遍历到第一个数字4时,我们希望标记它已经出现过,因此我们需要标记第四个位置。但是此时,第四个位置的1表示1已经出现过,如果我们直接更改,会丢失这个信息。因此,我们在标记时,对需要标记的位置取反,这样就不会丢失信息了。当我们用第一个4来标记第四个位置的元素时,数组变为【4,3,2,-1】,此时当我们再遍历到第四个元素时,我们通过-1可知4出现过并且已经完成标记,1出现了,不知道有没有进行过标记,因此我们再次对第一个位置标记,最后的结果为【-4,-3,-2,-1】,此时可知缺失的第一个正数为N+1即5。取反标记有一个问题,是遍历时遇到原本就是负数的数字,会错误标记,为了避免这种情况,我们首先遍历一遍数组,把会产生影响的负数变成对我们结果无影响的数字,即位于(1~N)之外的数字,同时又为了避免混淆,我们使用(N+1)。然后再去遍历数组,此时负数的影响已经消失。标记完成后,最后再从头扫描数组,遇到第一个正数时,说明这个位置应该出现的正数没有出现,即为缺失的第一个正数。
代码:
public int firstMissingPositive(int[] nums) {
for (int i = 0; i < nums.length; i++) {
if (nums[i] <= 0) {
nums[i] = nums.length + 1;
}
}
for (int i = 0; i < nums.length; i++) {
int num = nums[i] > 0 ? nums[i] : -nums[i];
if (num <= nums.length && nums[num - 1] > 0) {
nums[num - 1] = -nums[num - 1];
}
}
for (int i = 0; i < nums.length; i++) {
if (nums[i] > 0) {
return i + 1;
}
}
return nums.length + 1;
}
时间复杂度:O(N)
空间复杂度:O(1),所有操作均在原本的数组中进行修改
42.接雨水
思路1:动态规划。每一个位置能存储的雨水,取决于其左侧与右侧的最高位置。因此使用两个数组分别追踪每一个位置所对应的左右两边最高位置,然后再遍历数组,计算每个位置可以存储的雨水量。
代码:
public int trap(int[] height) {
int[] leftMax = new int[height.length], rightMax = new int[height.length];
leftMax[0] = height[0];
rightMax[height.length - 1] = height[height.length - 1];
for (int i = 1; i < height.length; i++) {
leftMax[i] = Math.max(height[i], leftMax[i - 1]);
}
for (int i = height.length - 2; i >= 0; i--) {
rightMax[i] = Math.max(height[i], rightMax[i + 1]);
}
int res = 0;
for (int i = 0; i < height.length; i++) {
res += Math.min(leftMax[i], rightMax[i]) - height[i];
}
return res;
}
时间复杂度:O(N)
空间复杂度:O(N)
思路2:单调栈。每个地方积水一定是遇到了左右侧有比这个地方高的位置出现。我们可以利用单调栈来保存左侧高度信息,当遇到了右侧高度时,逐步计算左侧梯度中每层的水量,直到遇到左侧比当前位置高,或者栈为空。
代码:
public int trap(int[] height) {
Deque<Integer> stack = new LinkedList<>();
stack.push(0);
int res = 0;
for (int i = 1; i < height.length; i++) {
while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
int top = stack.pop();
if (stack.isEmpty()) {
break;
}
int left = stack.peek();
res += (Math.min(height[left], height[i]) - height[top]) * (i - left - 1);
}
stack.push(i);
}
return res;
}
时间复杂度:O(N)
空间复杂度:O(N)
思路3:双指针。我们注意到在动态规划过程中,保存leftMax与rightMax时,只与当前位置的高度和前一个max值有关,因此我们可以用两个max值来保存max,利用两个指针来标记当前位置,以获取当前位置的高度。若LeftMax小,则说明height[left]一定能小于height[right],因此先计算left位置的积水。反之,则计算right位置的积水,然后推进指针。
代码:
public int trap(int[] height) {
if (height.length == 1) {
return 0;
}
int left = 0, right = height.length - 1, leftMax = height[left], rightMax = height[right], res = 0;
while (left <= right) {
if (leftMax < rightMax) {
res += leftMax - height[left++];
if (left < height.length) {
leftMax = Math.max(leftMax, height[left]);
}
} else {
res += rightMax - height[right--];
if (right >= 0) {
rightMax = Math.max(rightMax, height[right]);
}
}
}
return res;
}
时间复杂度:O(N)
空间复杂度:O(1)