【LeetCode】数组精选17题——双指针、滑动窗口、前缀和

目录

快慢指针:

1. 移动零(简单)

2. 复写零(简单)

对撞指针:

1. 两数之和 II - 输入有序数组(中等)

2. 三数之和(中等)

3. 有效三角形的个数(中等)

4. 四数之和(中等)

5. 盛最多水的容器(中等)

滑动窗口:

1. 长度最小的子数组(中等)

2. 将 x 减到 0 的最小操作数(中等)

3. 乘积小于 K 的子数组(中等)

4. 最大连续1的个数 III(中等)

5. 水果成篮(中等)

前缀和:

1. 寻找数组的中心下标(简单)

2. 和为 K 的子数组(中等)

3. 连续数组(中等)

4. 二维区域和检索 - 矩阵不可变(中等)

5. 矩阵区域和(中等)


双指针是一种常用的算法技巧,通常用于线性数据结构(数组、字符串、链表),使用两个指针扫描。指针并不是指C语言中的指针,而是指能定位数据结构中某个数据的手段,在数组中指的是下标。

双指针的形式:

  • 快慢指针:两个指针从一端向另一端移动,但移动速度不同
  • 对撞指针:两个指针从两端向中间移动,直到相遇或错开

滑动窗口是一种特殊的双指针,两个指针构造一个窗口,用于解决子数组的问题。右边界右移为进窗口,左边界右移为出窗口。

正数数组中子数组的和或积,就可以用滑动窗口解决。如果数组元素不全为正数,求子数组的和不能用滑动窗口,因为当数组中有负数时在子数组中添加数字不一定能增加数组之和,从子数组中删除数字也不一定能减少子数组之和。这时就需要用到前缀和思想。

【LeetCode】数组精选17题——双指针、滑动窗口、前缀和_第1张图片

快慢指针:

1. 移动零(简单)

数组分块问题,即根据一种划分方式,将数组分成两块,可以使用双指针解决。

【LeetCode】数组精选17题——双指针、滑动窗口、前缀和_第2张图片

class Solution {
public:
    void moveZeroes(vector& nums) {
        int n = nums.size();
        int slow = -1; // 指向非0元素的最后一个
        int fast = 0;  // 指向待处理元素的第一个

        while (fast < n)
        {
            if (nums[fast] == 0) // fast扫描到0
            {
                fast++;
            }
            else // fast扫描到非0
            {
                swap(nums[++slow], nums[fast]);
                fast++;
            }
        }
    }
};

2. 复写零(简单)

如果从前往后原地复写0的话,会导致后面的元素被覆盖。例如,[2,0,5,1]    ->    [2,0,0,1]

所以本题采用从后往前复写0,首先要找到原数组从哪里截断。

【LeetCode】数组精选17题——双指针、滑动窗口、前缀和_第3张图片

当fast = n - 1时,说明数组给待处理元素剩的位置只有1个了,这个位置就是留给slow的,不管arr[slow]是0还是非0。将arr[slow]填到数组最后一个位置,然后从slow - 1开始从后往前复写。

当fast = n时,说明数组给待处理元素已经不剩位置了,并且fast一定是从n - 2位置走两步到n,所以上一步的slow一定指向0,然后写了2个0,到达数组末尾。将0填到数组最后一个和倒数第二个位置,然后从slow-2开始从后往前复写。

【LeetCode】数组精选17题——双指针、滑动窗口、前缀和_第4张图片

class Solution {
public:
    void duplicateZeros(vector& arr) {
        int n = arr.size();
        int slow = 0;
        int fast = 0;
        // 找到截断点
        while (fast < n - 1)
        {
            // slow指向待处理元素的第一个
            // fast表示,如果发生复写,待处理元素的第一个的真正的位置
            if (arr[slow])
            {
                fast++;
            }
            else
            {
                fast +=2;
            }
            slow++;
        }
        // 判断fast,然后让slow指向待处理元素的第一个,fast表示待处理位置的第一个
        if (fast == n - 1)
        {
            arr[n - 1] = arr[slow];
            slow--;
            fast--;
        }
        else
        {
            arr[n - 1] = arr[n - 2] = 0;
            slow -= 2;
            fast -= 3;
        }
        // 从后往前复写
        while (slow >= 0)
        {
            if (arr[slow])
            {
                arr[fast] = arr[slow];
                fast--;
            }
            else
            {
                arr[fast] = arr[fast - 1] = 0;
                fast -= 2;
            }
            slow--;
        }
    }
};

对撞指针:

1. 两数之和 II - 输入有序数组(中等)

对撞指针的应用之一就是求排序数组的两个数字之和。如果和 < 目标值,左指针++,如果和 > 目标值,右指针--,直到和 == 目标值,就找到了那两个数。

class Solution {
public:
    vector twoSum(vector& numbers, int target) {
        int n = numbers.size();
        int left = 0;
        int right = n - 1;
        while (left < right)
        {
            if (numbers[left] + numbers[right] < target)
            {
                left++;
            }
            else if (numbers[left] + numbers[right] > target)
            {
                right--;
            }
            else
            {
                return { ++left,++right }; // 因为下标从1开始
            }
        }
        return { -1,-1 };
    }
};

2. 三数之和(中等)

和两数之和类似,如果是排序数组,那么固定nums[i],用对撞指针从i的后面找nums[left] + nums[right] == -nums[i]。

答案不能有重复,所以要在找到一种答案后,跳过重复的元素。

class Solution {
public:
    vector> threeSum(vector& nums) {
        int n = nums.size();
        sort(nums.begin(), nums.end());
        vector> ans;
        
        for (int i = 0; i < n - 2; )
        {
            int left = i + 1;
            int right = n - 1;
            while (left < right)
            {
                if (nums[left] + nums[right] < -nums[i])
                {
                    left++;
                }
                else if (nums[left] + nums[right] > -nums[i])
                {
                    right--;
                }
                else
                {
                    ans.push_back({ nums[i],nums[left++],nums[right--] });
                    // 去重left
                    while (left < right && nums[left] == nums[left - 1])
                    {
                        left++;
                    }
                    // 去重right
                    while (left < right && nums[right] == nums[right + 1])
                    {
                        right--;
                    }
                }
            }
            i++;
            // 去重i
            while (i < n - 2 && nums[i] == nums[i - 1])
            {
                i++;
            }
        }
        return ans;
    }
};

3. 有效三角形的个数(中等)

三角形的判定:任意两边之和大于第三边。

只要两条较短边>最长边,就可以推出任意两边之和大于第三边。

和三数之和类似,先给数组排序,固定最长边nums[i],用对撞指针从i的前面找nums[left] + nums[right] > nums[i]。

一旦向右移动left到某个位置时nums[left] + nums[right] > nums[i],就不需要再向右移动left了。因为只要保持right不动,[left, right)这个下标区间对应的任意一个元素,加上nums[right],都一定比nums[i]大。此时两个指针之间有多少个数字,就找到了多少组两条较短边>最长边。

答案可以有重复,不用跳过重复的元素。

class Solution {
public:
    int triangleNumber(vector& nums) {
        int n = nums.size();
        sort(nums.begin(), nums.end());
        int ans = 0;

        for (int i = n - 1; i >= 2; i--)
        {
            int left = 0;
            int right = i - 1;
            while (left < right)
            {
                if (nums[left] + nums[right] > nums[i])
                {
                    ans += right - left;
                    right--;
                }
                else
                {
                    left++;
                }
            }
        }
        return ans;
    }
};

4. 四数之和(中等)

先排序,固定一个数nums[a],再利用三数之和的解法找到三个数nums[b] nums[c] nums[d],使

nums[b] + nums[c] + nums[d] == target - nums[a]。

答案不能有重复,所以要在找到一种答案后,跳过重复的元素。

class Solution {
public:
    vector> fourSum(vector& nums, int target) {
        int n = nums.size();
        sort(nums.begin(), nums.end());
        vector> ans;
        
        for (int i = 0; i < n - 3; ) // 固定nums[i]
        {
            for (int j = i + 1; j < n - 2; ) // 固定nums[j]
            {
                int sum1 = nums[i] + nums[j];
                int left = j + 1;
                int right = n - 1;
                while (left < right)
                {
                    int sum2 = nums[left] + nums[right];
                    if (sum2 < (long long)target - sum1)
                    {
                        left++;
                    }
                    else if (sum2 > (long long)target - sum1)
                    {
                        right--;
                    }
                    else
                    {
                        ans.push_back({ nums[i],nums[j],nums[left++],nums[right--] });
                        // 去重left
                        while (left < right && nums[left] == nums[left - 1])
                        {
                            left++;
                        }
                        // 去重right
                        while (left < right && nums[right] == nums[right + 1])
                        {
                            right--;
                        }
                    }
                }
                j++;
                // 去重j
                while (j < n - 2 && nums[j] == nums[j - 1])
                {
                    j++;
                }
            }
            i++;
            // 去重i
            while (i < n - 3 && nums[i] == nums[i - 1])
            {
                i++;
            }
        }
        return ans;
    }
};

5. 盛最多水的容器(中等)

假设左板的下标是left,右板的下标是right,两板与x轴构成的矩形的面积为:

S = (right - left) * min(height[left], height[right])

显然,矩形的面积由底边宽度和短板长度共同决定。

如果改变短板,向内收窄一格,底边宽度-1,短板的长度可能会变大,矩形的面积可能变大。

如果改变长板,向内收窄一格,底边宽度-1,由于短板决定高度,长板再怎么改变也没有用,矩形的面积一定变小。

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

滑动窗口:

1. 长度最小的子数组(中等)

进窗口后判断子数组的和是否>= target,如果找到了和>= target的子数组,更新结果,然后出窗口,缩小子数组的长度,直到子数组的和 < target。

class Solution {
public:
    int minSubArrayLen(int target, vector& nums) {
        int n = nums.size();
        int left = 0;
        int right = 0;
        int sum = 0;
        int ans = INT_MAX;
        while (right < n)
        {
            // 进窗口
            sum += nums[right];
            // 判断子数组的和是否>=target
            while (sum >= target && left <= right)
            {
                // 更新结果
                ans = min(ans, right - left + 1);
                // 出窗口
                sum -= nums[left++];
            }
            // 更新右边界
            right++;
        }
        return ans == INT_MAX ? 0 : ans;
    }
};

2. 将 x 减到 0 的最小操作数(中等)

问题可以理解成:求和为x的最左端子数组和最右端子数组的最小长度。

把问题转化成:求和为total - x的子数组的最大长度(total为数组所有元素的和)。

这道题nums[i]的取值范围是:1 <= nums[i] <= 104,求正数数组中子数组的和,显然用滑动窗口。

进窗口后判断子数组的和是否> total - x,如果已经> total - x了,要出窗口,减小子数组的和,直<= total - x。然后判断子数组的和到底是不是== total - x,如果找到了和== total - x的子数组,更新结果。

class Solution {
public:
    int minOperations(vector& nums, int x) {
        int n = nums.size();
        int total = 0;
        for (auto& e : nums)
        {
            total += e;
        }

        int left = 0;
        int right = 0;
        int sum = 0;
        int ans = -1;
        while (right < n)
        {
            // 进窗口
            sum += nums[right];
            // 判断子数组的和是否>total-x
            while (sum > total - x && left <= right)
            {
                // 出窗口
                sum -= nums[left++];
            }
            // 找到和为total-x的子数组,更新结果
            if (sum == total - x)
            {
                ans = max(ans, right - left + 1);
            }
            // 更新右边界
            right++;
        }
        return ans == -1 ? ans : n - ans;
    }
};

3. 乘积小于 K 的子数组(中等)

进窗口后判断子数组的积是否>= k,如果已经 >= k了,要出窗口,减小子数组的积,直到< k。这时得到的子数组就是满足条件的子数组,并且,只要保持右边界不动,向右移动左边界形成的所有子数组的积就一定< k。此时左右边界之间有多少个元素,就找到了多少个乘积< k的子数组。

class Solution {
public:
    int numSubarrayProductLessThanK(vector& nums, int k) {
        int n = nums.size();
        int left = 0;
        int right = 0;
        int prod = 1;
        int ans = 0;
        while (right < n)
        {
            // 进窗口
            prod *= nums[right];
            // 判断子数组的积是否>=k
            while (prod >= k && left <= right)
            {
                // 出窗口
                prod /= nums[left++];
            }
            // 更新结果
            ans +=  right - left + 1;
            // 更新右边界
            right++;
        }
        return ans;
    }
};

4. 最大连续1的个数 III(中等)

把问题转化成:求0的个数不超过k个的最长子数组。

用zero记录窗口中0的个数。进窗口的同时要维护zero,然后判断zero是否> k,如果已经> k了,要出窗口并维护zero,直到<= k。这时就找到了0的个数不超过k个的子数组,更新结果。

class Solution {
public:
    int longestOnes(vector& nums, int k) {
        int n = nums.size();
        int left = 0;
        int right = 0;
        int zero = 0; // 窗口中0的个数
        int ans = 0;
        while (right < n)
        {
            // 进窗口+维护zero
            if (nums[right] == 0)
                zero++;
            // 判断zero是否>k
            while (zero > k && left <= right)
            {
                // 出窗口+维护zero
                if (nums[left] == 0)
                {
                    zero--;
                }
                left++;
            }
            // 更新结果
            ans = max(ans, right - left + 1);
            // 更新右边界
            right++;
        }
        return ans;
    }
};

5. 水果成篮(中等)

把问题转化成:求元素的种类只有2种的最长子数组。

用哈希表记录窗口中元素的种类和次数,key——元素的种类,value——每个元素出现的次数,哈希表的大小表示有多少种元素在窗口中。

进窗口后判断哈希表的大小是否> 2,如果已经> 2了,要出窗口,记得要把value为0的键值对删除,直到哈希表的大小== 2。这时就找到了元素的种类只有2种的子数组,更新结果。

class Solution {
public:
    int totalFruit(vector& fruits) {
        int n = fruits.size();
        int left = 0;
        int right = 0;
        unordered_map hash; // key——元素的种类,value——每个元素出现的次数
        int ans = 0;
        while (right < n)
        {
            // 进窗口
            hash[fruits[right]]++;
            // 判断哈希表的大小是否>2
            while (hash.size() > 2 && left <= right)
            {
                // 出窗口
                hash[fruits[left]]--;
                if (hash[fruits[left]] == 0)
                {
                    hash.erase(fruits[left]);
                }
                left++;
            }
            // 更新结果
            ans = max(ans, right - left + 1);
            // 更新右边界
            right++;
        }
        return ans;
    }
};

前缀和:

1. 寻找数组的中心下标(简单)

【LeetCode】数组精选17题——双指针、滑动窗口、前缀和_第5张图片

如果sum - nums[i] == total - sum,i就是中心下标。

class Solution {
public:
    int pivotIndex(vector& nums) {
        int n = nums.size();
        int total = 0; // 所有元素的和
        for (auto& i : nums)
        {
            total += i;
        }
        int sum = 0; // 表示左子数组的和+nums[i]
        for (int i = 0; i < n; i++)
        {
            sum += nums[i];
            if (sum - nums[i] == total - sum)
                return i;
        }
        return -1;
    }
};

2. 和为 K 的子数组(中等)

【LeetCode】数组精选17题——双指针、滑动窗口、前缀和_第6张图片

每次累加新的前缀和,都往前找找有没有以前的前缀和 == 新的前缀和 - k。

用哈希表记录前缀和出现的次数,默认添加一个键值对(0, 1)。用sum表示当前位置的前缀和。

例如,nums = [3,2,1],k = 3,

将nums[0]添加进sum,sum = 3,sum - k = 0,恰好哈希表中存放了值为0的前缀和,出现的次数为1,将出现的次数添加进ans中,ans = 1。然后把当前位置的前缀和在哈希表中对应的值++,此时哈希表中有2个键值对:(0, 1), (3, 1)。

将nums[1]添加进sum,sum = 5,sum - k = 2,哈希表中没有存放值为2的前缀和,没找到满足条件的子数组,对ans不做处理。然后把当前位置的前缀和在哈希表中对应的值++,此时哈希表中有3个键值对:(0, 1), (3, 1), (5, 1)。

将nums[2]添加进sum,sum = 6,sum - k = 3,恰好哈希表中存放了值为3的前缀和,出现的次数为1,将出现的次数添加进ans中,ans = 2。然后把当前位置的前缀和在哈希表中对应的值++,此时哈希表中有4个键值对:(0, 1), (3, 1), (5, 1), (6, 1)。

循环结束,输出ans。

class Solution {
public:
    int subarraySum(vector& nums, int k) {      
        int n = nums.size();
        unordered_map hash; // key——前缀和 value——出现的次数
        hash[0] = 1; // 添加(0, 1)
        int sum = 0; // 当前位置的前缀和
        int ans = 0;
        for (auto& i : nums)
        {
            sum += i;
            if (hash.count(sum - k))
            {
                ans += hash[sum - k];
            }
            hash[sum]++;
        }
        return ans;
    }
};

3. 连续数组(中等)

把数组所有0替换成-1,题目转变为:找到和为0的最长连续子数组。

【LeetCode】数组精选17题——双指针、滑动窗口、前缀和_第7张图片

每次累加新的前缀和,都往前找找有没有以前的前缀和 == 新的前缀和。

用哈希表记录前缀和对应的最小下标(因为求最大子数组),默认添加一个键值对(0, -1)。用sum表示当前位置的前缀和。

例如,nums = [0,1,0],

将nums[0]添加进sum,如果是0,添加-1,如果是1,添加1,sum = -1,哈希表中没有存放值为-1的前缀和,没找到满足条件的子数组,对ans不做处理。然后把当前位置的前缀和及下标添加进哈希表中,此时哈希表中有2个键值对:(0, -1), (-1, 0)。

将nums[1]添加进sum,如果是0,添加-1,如果是1,添加1,sum = 0,恰好哈希表中存放了值为0的前缀和,下标为-1,所以子数组长度为1 - (-1) = 2,将它和ans比较,取最大的,ans = 2。

将nums[2]添加进sum,如果是0,添加-1,如果是1,添加1,sum = -1,恰好哈希表中存放了值为-1的前缀和,下标为0,所以子数组长度为2 - 0 = 2,将它和ans比较,取最大的,ans = 2。

循环结束,输出ans。

class Solution {
public:
    int findMaxLength(vector& nums) {
        int n = nums.size();
        unordered_map hash; // key——前缀和 value——最小下标
        hash[0] = -1; // 添加(0, -1)
        int sum = 0; // 当前位置的前缀和
        int ans = 0;
        for (int i = 0; i < n; i++)
        {
            sum += nums[i] == 0 ? -1 : 1;
            if (hash.count(sum))
            {
                ans = max(ans, i - hash[sum]);
            }
            else
            {
                hash[sum] = i;
            }
        }
        return ans;
    }
};

4. 二维区域和检索 - 矩阵不可变(中等)

【LeetCode】数组精选17题——双指针、滑动窗口、前缀和_第8张图片

创建一个和matrix一样大的二维前缀和数组sums,sums[i][j]记录matrix从(0,0)到(i,j)的子矩阵数字和。

(r1,c1)->(r2,c2)子矩阵数字和 =

sums[r2][c2] - sums[r2][c1 - 1] - sums[r1 - 1][c2] + sums[r1 - 1][c1 - 1]

如果r1或c1为0,r1 - 1或c1 - 1会越界。

为了防止越界,在sums的上面和左面分别加一行0和一列0,这样的话,sums[i+1][j+1]记录matrix从(0,0)到(i,j)的子矩阵数字和。

所以,(r1,c1)->(r2,c2)子矩阵数字和 =

sums[r2 + 1][c2 + 1] - sums[r2 + 1][c1] - sums[r1][c2 + 1] + sums[r1][c1]

class NumMatrix {
public:
    public:
    NumMatrix(vector>& matrix) {
        int m = matrix.size();
        int n = matrix[0].size();
        sums.resize(m + 1, vector(n + 1, 0)); // 上面和左面分别加一行0和一列0,防止越界
        // 处理前缀和数组
        for (int i = 0; i < m; i++)
        {
            int rowSum = 0;
            for (int j = 0; j < n; j++)
            {
                rowSum += matrix[i][j];
                sums[i + 1][j + 1] = sums[i][j + 1] + rowSum;
            }
        }
    }
    
    int sumRegion(int row1, int col1, int row2, int col2) {
        return sums[row2 + 1][col2 + 1] - sums[row2 + 1][col1] - sums[row1][col2 + 1] + sums[row1][col1];
    }

private:
    vector> sums; // sums[i+1][j+1]记录matrix从(0,0)到(i,j)的子矩阵数字和
};

5. 矩阵区域和(中等)

和上一题“二维矩阵区域和检索 - 矩阵不可变”类似。

注意矩阵左上角和右下角坐标的确定:

  • 左上角:r1 = max(0, i - k)          c1 = max(0, j - k)
  • 右下角:r2 = min(m - 1, i + k)    c2 = min(n - 1, j + k)
class Solution {
public:
    vector> matrixBlockSum(vector>& mat, int k) {
        int m = mat.size();
        int n = mat[0].size();

        // 处理前缀和矩阵
        vector> sums(m + 1, vector(n + 1, 0)); // sums[i+1][j+1]记录matrix从(0,0)到(i,j)的子矩阵数字和
        for (int i = 0; i < m; i++)
        {
            int rowSum = 0;
            for (int j = 0; j < n; j++)
            {
                rowSum += mat[i][j];
                sums[i + 1][j + 1] = sums[i][j + 1] + rowSum;
            }
        }

        // 处理ans矩阵
        vector> ans(m, vector(n));
        for (int i = 0; i < m; i++)
        {
            for (int j = 0; j < n; j++)
            {
                int r1 = max(0, i - k);
                int c1 = max(0, j - k);
                int r2 = min(m - 1, i + k);
                int c2 = min(n - 1, j + k);
                ans[i][j] = sums[r2 + 1][c2 + 1] - sums[r2 + 1][c1] - sums[r1][c2 + 1] + sums[r1][c1];
            }
        }
        return ans;
    }
};

你可能感兴趣的:(LeetCode精选算法题,数据结构,算法,leetcode)