【算法笔记】2.LeetCode-Hot100-双指针专项 原

272 阅读7分钟

1. 移动零(t283)

简单难度,题目示例如下:

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

示例 1:

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

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

暴力解法:从头开始遍历,如果遇到非零元素,将其交换到末尾。

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
       for (int i0; i < nums.size(); i++){
            if (nums[i] == 0){
                for (int j = i + 1; j < nums.size(); j++){
                    if (nums[j] != 0){
                        swap(nums[i], nums[j]);
                        break;
                    }
                }
            }
       }
    }
};

时间复杂度: O(); 空间复杂度:O(1)

更好的解法:使用双指针的思路,左指针指向当前已经处理好的序列的尾部,右指针指向待处理序列的头部。右指针不断向右移动,每次右指针指向非零数,则将左右指针对应的数交换,同时左指针右移。

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int n = nums.size(), left0, right0;
        while (right < n){
            if (nums[right]){
                swap(nums[left], nums[right]);
                left++;
            }
            right++;
        }
    }
};

时间复杂度: O(n); 空间复杂度:O(1)

2. 盛最多水的容器(t11)

中等难度,题目示例如下:

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

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

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

图片

解法思路:这套题的基础思路很简单,用双指针一头一尾。但是指针的移动条件,比较巧妙,通过比较左右两块板的高度,如果一侧较低,就移动。一句话概括:尽量手持大的板,去找更大的板。

class Solution {
public:
    int maxArea(vector<int>& height) {
        int n = height.size(), left = 0, right = n - 1; 
        int area = 0, max_area = 0;
        while (left < right){
            area = min(height[left], height[right]) * (right - left);
            if (area > max_area) max_area = area;
            if (height[left] < height[right]){
                left ++;
            }
            else{
                right --;
            }
        }
        return max_area;
    }
};

时间复杂度: O(n); 空间复杂度:O(1)。

看到题解下面的有条评论说得很精彩,摘录如下:

by Quirky Satoshi4QI: 这个思路我理解了很久。本质上这是一个如何去简化两个for循环的问题。可以这么来理解:假设本身要用

x y作为数组下标分别遍历才能完成所有情况的枚举,那么怎么才能简化呢?我们假设x是从左往右递增的,y是从右往左递减的,那么当x的木板比y的木板短时,说明这一轮的y的遍历提前结束,后面不论y如何向左移动都没用了。 当这一轮结束后,x向右指向一个,就变成了求解同样的一个新问题,只不过求解的范围缩小了而已。 最终xy相遇之后,所有的该遍历的都遍历到了,被简化掉的遍历项目就是我们这个算法的收益。

3. 三数之和(t15)

中等难度,题目示例如下:

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

示例 1:

输入: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:

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

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

解题思路:这题涉及到三元组,初看没想到如何求解,看了题解才清楚思路。这道题采用双指针的思路,核心解决两个问题:1.如何去重?2.双指针如何设置?

具体思路是先对数组进行从小到大排序,遍历每一个元素位置,双指针区间初始设置在该元素后面,通过相邻元素判断来解决去重问题。

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        int n = nums.size();
        sort(nums.begin(), nums.end());
        vector<vector<int>> ans;
        
        for (int i = 0; i < n; i++){
            if (i > 0 && nums[i] == nums[i - 1]) continue;
            
            int l = i + 1, r = n - 1;
            
            while (l < r){
                if (nums[l] + nums[r] + nums[i] == 0){
                    ans.push_back({nums[i], nums[l], nums[r]});
                    l++;
                    r--;
                    while (l < r && nums[l] == nums[l - 1]) l++;
                    while (l < r && nums[r] == nums[r + 1]) r--;
                }
                elseif (nums[l] + nums[r] + nums[i] < 0){
                    l++;
                }
                else {
                    r--;
                }
            }
        }
        return ans;
    }
}

时间复杂度:O(),引入双指针的核心作用就是让原本需要三次循环减少到两次,左右指针往中心靠近的过程,减少了一次循环带来的计算量。

空间复杂度:O(logN)主要来自于排序。

4. 接雨水(t42)

困难难度,题目示例如下:

图片

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

输入: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:

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

解题思路:题目涉及物理模拟,初见比较难,没什么思路,直接看题解,官方提供了三种解题思路。

思路1:动态规划

动态规划的核心就是做问题拆解,因此,可以将问题拆成三个步骤:

  • 1.假设右边无限高,那么接水量只取决于左边,先统计这种情况下每列最多能接多少水。
  • 2.假设左边无限高,那么接水量只取决于右边,再统计这种情况下每列最多能接多少水。
  • 3.最终实际的接水量就是两者最小值 - 本身容器高度。

图片

class Solution {
public:
    int trap(vector<int>& height) {
        int n = height.size();
        if (n == 0) {
            return0;
        }
        vector<int> leftMax(n);
        leftMax[0] = height[0];
        for (int i1; i < n; i++) {
            leftMax[i] = max(leftMax[i - 1], height[i]);
        }

        vector<int> rightMax(n);
        rightMax[n - 1] = height[n - 1];
        for (int i = n - 2; i >= 0; i--) {
            rightMax[i] = max(rightMax[i + 1], height[i]);
        }

        int ans0;
        for (int i0; i < n; i++) {
            ans += min(leftMax[i], rightMax[i]) - height[i];
        }
        return ans;
    }
};

时间复杂度:O(n),共遍历数组三次。

空间复杂度:O(n),额外创建 leftMax 和 rightMax 两个数组。

思路2:单调栈**

单调栈是指栈内元素按照单调顺序排列的栈。例如,单调递减栈,如果遇到比栈顶大的元素,就开始出栈,直到再次保持单调递减。

单调递减栈做这道题很合适,从左往右遍历,当遇到比栈顶还高的柱子时,就说明右边界出现了,可以开始计算中间能接多少水。

图片

class Solution {
public:
    int trap(vector<int>& height) {
        int ans = 0;
        stack<int> stk;
        int n = height.size();
        for (int i = 0; i < n; i++) {
            while (!stk.empty() && height[i] > height[stk.top()]) {
                int top = stk.top();
                stk.pop();
                if (stk.empty()) {
                    break;
                }
                int left = stk.top();
                int currWidth = i - left - 1;
                int currHeight = min(height[left], height[i]) - height[top];
                ans += currWidth * currHeight;
            }
            stk.push(i);
        }
        return ans;
    }
};

时间复杂度:O(n)

空间复杂度:O(n)

思路3:双指针

双指针实际上是对思路1的优化,通过左右两个指针不断往中间靠近,减小循环次数。核心思路是:如果左边更矮,那么当前左边所能接的水只受左边的最大值控制;如果右边更矮,就由右边最大值控制。非常巧妙,边移动边计算。

图片

class Solution {
public:
    int trap(vector<int>& height) {
        int ans = 0;
        int left = 0, right = height.size() - 1;
        int leftMax = 0, rightMax = 0;
        while (left < right) {
            leftMax = max(leftMax, height[left]);
            rightMax = max(rightMax, height[right]);
            if (height[left] < height[right]) {
                ans += leftMax - height[left];
                ++left;
            } else {
                ans += rightMax - height[right];
                --right;
            }
        }
        return ans;
    }
};

时间复杂度:O(n)

空间复杂度:O(1)