文章

hot100双指针题解

283. 移动零

问题描述

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

示例 1:

1
2
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]

示例 2:

1
2
输入: nums = [0]
输出: [0]

提示:

  • $1 <= nums.length <= 10^4$
  • $-2^{31} <= nums[i] <= 2^{31} - 1$

进阶:你能尽量减少完成的操作次数吗?

题解

以下是 移动零 的 C++ 解法,采用双指针方法来实现高效的原地操作。


解题思路

  1. 双指针方法:
    • 使用两个指针,一个 slow 指针用于记录非零元素的插入位置,另一个 fast 指针用于遍历数组。
    • fast 指向非零元素时,将其移动到 slow 指针的位置,然后移动 slow 指针。
  2. 补零:
    • 遍历完成后,从 slow 指针开始,将剩余位置填充为零。

C++ 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <vector>
using namespace std;

void moveZeroes(vector<int>& nums) {
    int slow = 0; // 慢指针,用于记录插入非零元素的位置

    // 将所有非零元素移动到数组的前面
    for (int fast = 0; fast < nums.size(); ++fast) {
        if (nums[fast] != 0) {
            nums[slow] = nums[fast];
            ++slow;
        }
    }

    // 将剩余的位置填充为0
    for (int i = slow; i < nums.size(); ++i) {
        nums[i] = 0;
    }
}

int main() {
    vector<int> nums = {0, 1, 0, 3, 12};

    moveZeroes(nums);

    // 输出结果
    for (int num : nums) {
        cout << num << " ";
    }
    cout << endl;

    return 0;
}

代码解析

  1. 双指针遍历:
    • fast 指针用于遍历整个数组。
    • slow 指针仅在 nums[fast] 为非零时向前移动,并将当前值覆盖到 slow 的位置。
  2. 填充零:
    • 在所有非零元素移动完成后,从 slow 开始到数组末尾的位置,统一填充为零。

时间复杂度

  1. 遍历数组:
    • 遍历数组一次将非零元素移动到前面,时间复杂度为 $O(n)$。
    • 第二次遍历填充零,时间复杂度也是 $O(n)$。
  2. 整体时间复杂度:
    • $O(n)$。

空间复杂度

  • 使用了固定的指针变量,没有额外的空间开销。
  • 空间复杂度为 $O(1)$。

示例运行

输入:

1
nums = [0, 1, 0, 3, 12]

输出:

1
[1, 3, 12, 0, 0]

进阶优化

  • 双指针方法已经是线性时间复杂度,并且没有使用额外的空间,符合题目要求的高效解法。

11. 盛最多水的容器

问题描述

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0)(i, height[i])

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

说明:你不能倾斜容器。

示例 1:

img

1
2
3
输入:[1,8,6,2,5,4,8,3,7]
输出:49 
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

示例 2:

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

提示:

  • n == height.length
  • $2 <= n <= 10^5$
  • $0 <= height[i] <= 10^4$

题解

以下是 盛最多水的容器 的 C++ 解法,采用双指针法实现 O(n)O(n) 的时间复杂度。


解题思路

  1. 双指针法:
    • 使用两个指针,left 指向数组的起始位置,right 指向数组的末尾位置。
    • 计算当前两个指针构成容器的容量:area = min(height[left], height[right]) * (right - left)
    • 移动指针:
      • 总是移动较小值对应的指针,因为容器的高度由较短的那一边决定。
      • 移动后可能找到更大的容量。
  2. 终止条件:
    • leftright 指针重合时,结束遍历。

C++ 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int maxArea(vector<int>& height) {
    int left = 0, right = height.size() - 1; // 初始化双指针
    int maxArea = 0; // 用于记录最大面积

    while (left < right) {
        // 计算当前的面积
        int currentArea = min(height[left], height[right]) * (right - left);
        maxArea = max(maxArea, currentArea); // 更新最大面积

        // 移动指针,选择较短的一边
        if (height[left] < height[right]) {
            ++left;
        } else {
            --right;
        }
    }

    return maxArea;
}

int main() {
    vector<int> height = {1, 8, 6, 2, 5, 4, 8, 3, 7};
    cout << "Max Area: " << maxArea(height) << endl;
    return 0;
}

代码解析

  1. 双指针遍历:
    • 指针 leftright 分别从数组的两端开始向中间移动。
    • 每次计算以当前两个指针为边界的容器容量。
  2. 更新最大值:
    • 使用 maxArea = max(maxArea, currentArea) 更新最大容器容量。
  3. 移动指针:
    • 移动较短的一边,因为只有增加高度才有可能增加容量。

时间复杂度

  1. 双指针遍历:
    • 每次移动一个指针,总共最多遍历 $n$ 次。
    • 时间复杂度为 $O(n)$。

空间复杂度

  • 只使用了固定的变量,空间复杂度为 $O(1)$。

示例运行

输入:

1
height = [1, 8, 6, 2, 5, 4, 8, 3, 7]

输出:

1
49

解释:

在索引 1 和索引 8 处的两条线构成的容器,能够盛水的最大容量为 min⁡(8,7)×(8−1)=49\min(8, 7) \times (8 - 1) = 49。


总结

  • 双指针法 是解决此类问题的高效方法,具有线性时间复杂度。
  • 此方法通过每次移动较短边来优化搜索空间,并保持正确性。

15. 三数之和

问题描述

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

示例 1:

1
2
3
4
5
6
7
8
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。

示例 2:

1
2
3
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。

示例 3:

1
2
3
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。

提示:

  • 3 <= nums.length <= 3000
  • $-10^5 <= nums[i] <= 10^5$

题解

以下是 三数之和 的 C++ 解法,利用排序和双指针方法解决,时间复杂度为 O(n2)O(n^2)。


解题思路

  1. 排序:
    • 首先将数组排序,以便后续使用双指针查找和为 0 的组合。
  2. 固定一个数,双指针寻找:
    • 遍历数组,固定一个数 nums[i]
    • 使用双指针在排序后的数组中寻找两个数,使它们的和等于 -nums[i]
    • 左指针 left 从当前元素之后开始,右指针 right 从数组末尾开始。
  3. 避免重复:
    • 跳过重复的数字,确保每个三元组唯一。
  4. 调整指针:
    • 如果当前三数之和小于 0,移动左指针。
    • 如果大于 0,移动右指针。
    • 如果等于 0,记录结果,并调整指针以跳过重复值。

C++ 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

vector<vector<int>> threeSum(vector<int>& nums) {
    vector<vector<int>> result;
    sort(nums.begin(), nums.end()); // 排序数组

    for (int i = 0; i < nums.size(); ++i) {
        // 跳过重复的数字
        if (i > 0 && nums[i] == nums[i - 1]) {
            continue;
        }

        int left = i + 1;
        int right = nums.size() - 1;

        while (left < right) {
            int sum = nums[i] + nums[left] + nums[right];

            if (sum < 0) {
                ++left; // 总和小于0,左指针右移
            } else if (sum > 0) {
                --right; // 总和大于0,右指针左移
            } else {
                // 找到一个三元组
                result.push_back({nums[i], nums[left], nums[right]});

                // 跳过重复的数字
                while (left < right && nums[left] == nums[left + 1]) {
                    ++left;
                }
                while (left < right && nums[right] == nums[right - 1]) {
                    --right;
                }

                // 移动指针
                ++left;
                --right;
            }
        }
    }

    return result;
}

int main() {
    vector<int> nums = {-1, 0, 1, 2, -1, -4};

    vector<vector<int>> result = threeSum(nums);

    // 输出结果
    for (const auto& triplet : result) {
        cout << "[";
        for (int num : triplet) {
            cout << num << " ";
        }
        cout << "]" << endl;
    }

    return 0;
}

代码解析

  1. 排序:
    • 排序后的数组可以通过双指针高效查找,确保结果有序且便于去重。
  2. 双指针查找:
    • 每次固定一个数,剩下的问题变成两数之和,使用双指针解决。
  3. 去重:
    • 遍历时,跳过当前元素及指针位置的重复值,避免重复的三元组。
  4. 指针移动:
    • 当找到一个三元组后,继续移动指针以查找新的组合。

时间复杂度

  1. 排序:
    • 时间复杂度为 $O(n \log n)$。
  2. 双指针:
    • 外层遍历 $n$ 次,内层查找最多遍历 $n$ 次,因此双指针部分复杂度为 $O(n^2)$。
  3. 总复杂度:
    • $O(n^2)$。

空间复杂度

  • 排序使用的空间复杂度为 $O(\log n)$,其余为常数级额外空间。
  • 总空间复杂度为 $O(\log n)$。

示例运行

输入:

1
nums = [-1, 0, 1, 2, -1, -4]

输出:

1
2
[-1, -1, 2]
[-1, 0, 1]

总结

  • 该方法通过排序和双指针高效解决了三数之和问题。
  • 去重逻辑保证了结果的唯一性。

42. 接雨水

问题描述

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例 1:

img

1
2
3
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 

示例 2:

1
2
输入:height = [4,2,0,3,2,5]
输出:9

提示:

  • n == height.length
  • $1 <= n <= 2 * 10^4$
  • $0 <= height[i] <= 10^5$

题解

以下是 接雨水 的 C++ 解法,采用双指针方法,时间复杂度为 O(n)O(n)。


解题思路

  1. 问题核心:
    • 雨水能够被接住的条件是当前柱子两侧存在更高的柱子,雨水量等于当前柱子高度和两侧柱子高度的最小值之间的差值。
  2. 双指针法:
    • 使用两个指针 leftright,分别从数组两端向中间移动。
    • 使用两个变量 leftMaxrightMax,记录当前左侧和右侧的最高柱子高度。
    • 根据 leftMaxrightMax的较小值决定哪侧可以确定雨水量:
      • 如果 leftMax < rightMax,移动 left 指针并更新 leftMax
      • 如果 rightMax <= leftMax,移动 right 指针并更新 rightMax
  3. 计算雨水量:
    • 对于每个柱子,雨水量为 min(leftMax, rightMax) - height[i]

C++ 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <vector>
using namespace std;

int trap(vector<int>& height) {
    int left = 0, right = height.size() - 1;
    int leftMax = 0, rightMax = 0;
    int totalWater = 0;

    while (left < right) {
        if (height[left] < height[right]) {
            // 更新左侧最高柱子高度
            if (height[left] >= leftMax) {
                leftMax = height[left];
            } else {
                totalWater += leftMax - height[left];
            }
            ++left; // 移动左指针
        } else {
            // 更新右侧最高柱子高度
            if (height[right] >= rightMax) {
                rightMax = height[right];
            } else {
                totalWater += rightMax - height[right];
            }
            --right; // 移动右指针
        }
    }

    return totalWater;
}

int main() {
    vector<int> height = {0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1};

    cout << "Trapped Rainwater: " << trap(height) << endl;

    return 0;
}

代码解析

  1. 初始化指针和变量:
    • leftright 指向数组两端。
    • leftMaxrightMax 记录当前左侧和右侧的最高柱子高度。
  2. 双指针移动:
    • 比较 height[left]height[right]
      • height[left] < height[right],可以确定左侧的雨水量,因为左侧的最大高度 leftMax 不可能被右侧的柱子改变。
  3. 累积雨水量:
    • 每次移动指针时,更新对应的最大高度,并累积当前柱子的雨水量。

时间复杂度

  1. 遍历数组:
    • 每个指针最多遍历整个数组一次,时间复杂度为 $O(n)$。
  2. 总复杂度:
    • $O(n)$。

空间复杂度

  • 使用了固定数量的变量,空间复杂度为 $O(1)$。

示例运行

输入:

1
height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]

输出:

1
6

解释:

  • 图中蓝色部分为能够接住的雨水,总量为 6。

总结

  • 双指针法是一种高效解决接雨水问题的方法,时间复杂度为 $O(n)$,空间复杂度为 $O(1)$。
  • 通过动态更新 leftMaxrightMax,确保计算的雨水量始终正确。
本文由作者按照 CC BY 4.0 进行授权