数据结构与算法——双指针

引言

双指针是一种算法技巧,通常用于在数组或链表等数据结构上进行高效的遍历和查找。双指针技术利用两个指针(或索引)来遍历数据结构,这两个指针可以在数据结构上移动,以协助解决特定的问题。双指针技术的目的是通过减少不必要的遍历,降低算法的时间复杂度。

双指针的常见类型

  1. 快慢指针
    • 快指针(fast)和慢指针(slow)以不同的速度遍历链表或数组。
    • 常用于检测链表中的环、找到链表的中间节点、判断链表是否回文等。
  2. 滑动窗口
    • 通常用于解决数组中的子数组、子字符串等问题。
    • 窗口的大小可以固定,也可以变化,通过移动窗口的左右边界来遍历数组。
    • 常见的应用包括寻找最长不含重复字符的子字符串、最小覆盖子串等。
  3. 对撞指针(或称为双指针相向而行):
    • 从数组的两端开始,两个指针分别向中间移动。
    • 当两个指针相遇时,遍历结束。
    • 常用于解决有序数组中的某些问题,如寻找两个数的和等于给定值、反转链表等。

双指针的优势

  • 减少时间复杂度:通过减少不必要的遍历,双指针技术通常可以将时间复杂度降低到O(n)或更低,其中n是数据结构的大小。
  • 空间复杂度低:双指针技术通常只需要常数级别的额外空间(用于存储指针或索引),因此空间复杂度较低。

经典例题

1. 移动零

题目描述
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序不变。你必须在原地(不使用额外数组)修改输入数组并在使用 O(1) 额外空间的条件下完成。

解题思路

  • 使用双指针方法,一个指针(left)指向当前非零元素应该放置的位置,另一个指针(right)用于遍历数组。
  • 遍历数组时,如果right指针指向的元素不是0,则将其与left指针指向的元素交换,并将两个指针都向前移动一位。
  • 如果right指针指向的元素是0,则只将right指针向前移动一位。
  • 遍历结束后,所有非零元素都移动到了数组的前面,而数组末尾的部分则填充了剩余的0。
#include <vector>  
  
void moveZeroes(std::vector<int>& nums) {  
    int left = 0;  
    for (int right = 0; right < nums.size(); ++right) {  
        if (nums[right] != 0) {  
            nums[left++] = nums[right];  
        }  
    }  
    while (left < nums.size()) {  
        nums[left++] = 0;  
    }  
}

2. 盛最多水的容器

题目描述
给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,使得垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

说明:你不能倾斜容器,且 n 的值至少为 2。

解题思路

  • 使用双指针方法,一个指针(left)指向数组的起始位置,另一个指针(right)指向数组的末尾位置。
  • 初始化最大容量为0,然后进入一个循环,直到left指针不小于right指针。
  • 在每次循环中,计算当前左右指针所构成的容器的容量(即min(height[left], height[right]) * (right - left)),并将其与最大容量进行比较和更新。
  • 然后,根据左右指针所指向的高度,移动高度较小的那个指针(因为移动高度较大的指针不会增加容器的容量,反而可能减小)。
  • 循环结束后,最大容量即为所求。
#include <vector>  
#include <algorithm>  
  
int maxArea(std::vector<int>& height) {  
    int left = 0, right = height.size() - 1;  
    int maxArea = 0;  
    while (left < right) {  
        int currentArea = std::min(height[left], height[right]) * (right - left);  
        maxArea = std::max(maxArea, currentArea);  
        if (height[left] < height[right]) {  
            ++left;  
        } else {  
            --right;  
        }  
    }  
    return maxArea;  
}

3. 三数之和

题目描述
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 abc ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

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

解题思路

  • 首先对数组进行排序,以便后续处理。
  • 使用三个指针,ileft 和 right,其中 i 从数组的起始位置开始遍历,left 初始化为 i + 1right 初始化为数组的末尾位置。
  • 对于每个 i,固定它,然后使用 left 和 right 指针在 i 之后的子数组中寻找和为 -nums[i] 的两个数。
  • 如果找到了这样的两个数,则将它们作为一个三元组添加到结果中,并移动 left 和 right 指针以跳过重复的元素,避免添加重复的三元组。
  • 如果和大于 -nums[i],则移动 right 指针以减小和;如果和小于 -nums[i],则移动 left 指针以增加和。
  • 当 left 指针超过 right 指针时,说明在当前的 i 后面已经找不到满足条件的三元组了,因此将 i 指针向后移动一位,并重置 left 和 right 指针。
  • 重复上述过程,直到遍历完整个数组。
#include <vector>  
#include <algorithm>  
  
std::vector<std::vector<int>> threeSum(std::vector<int>& nums) {  
    std::vector<std::vector<int>> result;  
    std::sort(nums.begin(), nums.end());  
  
    for (int i = 0; i < nums.size() - 2; ++i) {  
        if (i > 0 && nums[i] == nums[i - 1]) continue; // Skip duplicates  
  
        int left = i + 1, right = nums.size() - 1;  
        while (left < right) {  
            int sum = nums[i] + nums[left] + nums[right];  
            if (sum == 0) {  
                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;  
            } else if (sum < 0) {  
                ++left;  
            } else {  
                --right;  
            }  
        }  
    }  
    return result;  
}

4. 接雨水

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

说明

  • 宽度为 n 的非负整数数组 height 代表柱子的高度。
  • 由于宽度的限制,雨水不能流向左侧或右侧。
  • 水只能沿着柱子的高度向上流动。

解题思路(使用双指针和单调栈的简化版,但非传统双指针解法):

  • 这个问题通常使用单调栈来解决,但这里我提供一个简化的双指针思路(注意这不是最典型的双指针解法,但可以作为另一种思考方式)。
  • 实际上,更直观的双指针解法可能需要结合栈或者其他数据结构来准确计算每个位置能接的雨水量。但这里我们可以采用一种“从两边向中间逼近”的思路,结合之前遇到的最大高度来估算。
  • 然而,为了简化说明,这里我提供一个基于“桶接水”思想的简化思路(注意这不是最优解,但有助于理解):
    • 遍历数组两次,第一次从左到右,记录每个位置左侧的最大高度;第二次从右到左,记录每个位置右侧的最大高度。
    • 遍历数组,对于每个位置,计算其左右两侧最大高度的较小值(减去当前位置的高度,如果当前位置高度更高则不计入),这就是该位置能接的雨水量。
    • 将所有位置的雨水量相加,得到总雨水量。
#include <vector>  
#include <stack>  
  
int trap(std::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;  
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值