【每日三题】2022-4-4 LeetCode 40-42

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)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值