指尖的协奏曲:双指针在数组中的默契配合

双指针是一种在算法和数据结构中常用的技巧,尤其适用于数组、链表等线性数据结构。双指针通常有两个指针在数据结构中进行遍历,借助这两个指针来实现高效的算法。

1. 常见的双指针

1. 1快慢指针

快慢指针是两个指针以不同的速度在数据结构中移动的技巧。通常情况下,一个指针每次移动一步(慢指针),另一个指针每次移动两步(快指针)。常见的应用有:

- 判断链表是否有环:如果链表中有环,快指针最终会追上慢指针。
- 找到链表的中点:当快指针到达末尾时,慢指针正好在中点位置。

ListNode* detectCycle(ListNode* head) 
{
    ListNode *slow = head, *fast = head;
    while (fast && fast->next) 
    {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) 
        {  // 有环
            slow = head;
            while (slow != fast) 
            {
                slow = slow->next;
                fast = fast->next;
            }
            return slow;  // 返回环的入口
        }
    }
    return nullptr;
}

1.2 左右指针(对撞指针)

左右指针一般用于在数组上进行处理。一个指针从左边开始,另一个指针从右边开始,两者向中间靠拢,直到满足某种条件。常见的应用包括:

- 数组的元素对求和问题:如两数之和。
- 判断回文串:左右指针逐一比较元素,直到碰撞。

//判断回文串
bool isPalindrome(const string& s) {
    int left = 0, right = s.size() - 1;
    while (left < right) {
        if (s[left] != s[right]) return false;
        left++;
        right--;
    }
    return true;
}

1.3 滑动窗口

滑动窗口是一种特殊的双指针技巧,用于处理连续子序列的问题。它通常应用在找到数组或字符串中的某个子集,使得子集满足某种要求(如长度、和等)。

- 常用于字符串或数组中的子串、子数组问题,如最大长度、最小和等。
- 通过窗口的左右边界调整窗口大小,从而实现高效搜索。

//数组大于target的最小区间
int minSubArrayLen(int target, vector<int>& nums) {
    int left = 0, sum = 0, result = INT_MAX;
    for (int right = 0; right < nums.size(); ++right) {
        sum += nums[right];
        while (sum >= target) {
            result = min(result, right - left + 1);
            sum -= nums[left++];
        }
    }
    return result == INT_MAX ? 0 : result;
}

双指针方法的优势在于其高效性,特别是在涉及到子数组、子串问题时,可以避免使用嵌套循环,使时间复杂度降低到线性级别。

    接下来我们结合各种运用双指针的的题来熟练掌握双指针的用法。我们接下来主要是使用双指针的思想来解题,其中的指针并不是真正的指针,通常是使用数组下标来充当指针。

2. 移动零

2.1 题目介绍

2.2 思路分析

(1)定义两个指针cur 和dest,cur遍历数组,dest指向已经处理区间的非零元素的最后一个位置,这样我们可以得到三个区间

(2)每当遇到不为零的数时,cur与dest所在位置的数进行交换

2.3 代码实现 

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
       for(int cur = 0, dest = -1; cur < nums.size(); cur++)
       {
        if(nums[cur])
        {
            swap(nums[cur], nums[++dest]);
        }
       }
    }
};

. - 力扣(LeetCode)

3. 复写零

3.1 题目介绍

3.2 思路分析

如果题目没有要求只能对数组就地修改,我们可以通过创建另一个数组,然后遍历原数组,很容易实现代码。如果是就地修改,解题思路是:

(1)先找到最后一个“复写”的值:定义指针cur和dest,如果nums[cur]是零,dest移动两步,非零则移动一步。直到dest指向数组的最后一个元素或后面一个位置,此时cur指向最后一个复写的值。

(2)如果此时dest刚好指向数组的最后一个元素,可以直接从后往前完成复写操作

(3)如果dest指向数组最后一个元素后面一个位置,说明,dest移动了两步,cur此时指向的值是0,这时就要特殊处理一下,将数组最后一个元素的值置为0,dest向前走两步。(这种情况下复写的两个0一个是数组最后一个元素的,另一个是最后元素后面一个位置的,与数组倒数第二个元素无关)

3.3 代码实现 

class Solution {
public:
    void duplicateZeros(vector<int>& arr) {
        int cur = 0, dest = -1, n = arr.size();
        while (cur < n)
        {
            if(arr[cur])  dest++;
            else dest += 2;
            if (dest >= n - 1) break;
            cur++;
        }
        if (dest == n) 
        {
            arr[n - 1] = 0;
            dest -= 2;
            cur--;
        }

        while (cur >= 0)
        {
            if (arr[cur])
            arr[dest--] = arr[cur--];
            else
            {
            arr[dest--] = arr[cur--];
            arr[dest--] = 0;
            }
        }
    }
};

. - 力扣(LeetCode)

4. 快乐数

4.1 题目介绍

4.2 思路分析

       由于这个数最终都会循环,所以我们可以使用快慢指针来解题,等到快指针追上慢指针是判断该数是否为1即可。

(1)定义一个函数用来返回n的替换后的数

(2)定义快慢指针,慢指针每次走一步,快指针每次走两步,当快慢指针相同时,判断此时的值是否为1

4.3 代码实现

class Solution {
public:
    int Square(int n)
    {
        int sum = 0;
        while(n)
        {
            int t = n % 10;
            sum += t * t;
            n /= 10;
        }
        return sum;
    }
    bool isHappy(int n) {
        int slow = n, fast = Square(n);
        while (slow != fast)
        {
            slow = Square(slow);
            fast = Square(Square(fast));
        }
        return slow == 1;
    }
};

. - 力扣(LeetCode)

5. 盛水最多的容器

5.1 题目介绍

5.2 思路分析

为了方便叙述,我们假设「左边边界」小于「右边边界」。 如果此时我们固定⼀个边界,改变另⼀个边界,水的容积会有如下变化形式:
(1)容器的宽度⼀定变小。
(2)由于左边界较小,决定了水的高度。如果改变左边界,新的水面高度度不确定,但是⼀定不会超 过右边的柱子高度,因此容器的容积可能会增大。
(3)如果改变右边界,无论论右边界移动到哪里,新的水面的高度⼀定不会超过左边界,也就是不会 超过现在的水面高度,但是由于容器的宽度减小,因此容器的容积⼀定会变小的。
由此可见,左边界和其余边界的组合情况都可以舍去。所以我们可以 left++ 跳过这个边界,继
续去判断下⼀个左右边界。

所以我们定义左右指针从首尾开始遍历,计算此时区间可以盛纳的水,比较存储盛水最多的容器

每次让两个指针较小的那一个移动。

5.3 代码实现

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

. - 力扣(LeetCode)

6. 有效三角形个数

6.1 题目介绍

6.2 思路分析

可以组成三角形三条边的条件是任意两边之和大于另一条边,因此我们让两条较小的边之和大于另一条边即可。

(1)首先将数组从小到大排序

(2)固定最大的一条边,定义左右指针left和right为另两条边,left从0开始向右遍历,right从小于最大边的位置开始想左遍历

(3)如果两边之和大于另一条边,那么,(left, right)之间的边与right之和也一定大于另一条边。

(4)如果两边之和小于最大边,则说明边太小,left向右移动 

6.3 代码实现

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

. - 力扣(LeetCode)

7. 和为s的两个数字

7.1 题目介绍

7.2 思路分析

定义左右指针遍历数组,商品价格是单调递增的,因此左右指针之和小于target时,左指针移动,大于target,右指针移动。

7.3 代码实现

class Solution {
public:
    vector<int> twoSum(vector<int>& price, int target) {
        int left = 0, right = price.size() - 1;
        vector<int> ret(2);
        while (left < right)
        {
            int sum = price[left] + price[right];
            if(sum > target) right--;
             else if (sum < target) left++;
            else{
                ret[0] = price[left];
                ret[1] = price[right];
                break;
            }
        }
        return ret;
    }
};

. - 力扣(LeetCode)

8. 三数之和

8.1 题目介绍

8.2 思路分析

这个题的思路与有效三角形的个数有相似之处。

(1)首先将数组进行排序

(2)固定一个i为数组的最后一个元素,随着程序的进行向前移动,定义左右指针指针遍历【0,i - 1】之间的数。

(3)当三数之和大于零时,right--来减小数,小于零则left++。

(4)因为答案中不能包含重复三元组,所以每当找到一组符合的数之后,left和right要跳过和符合组相同的数,并且将符合组存下来。

(5)当完成left<right的循环时,i 也要跳过重复数。

8.3 代码实现

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

        }
        return ret;
    }
};

. - 力扣(LeetCode)

9. 四数之和

9.1 题目介绍

9.2 思路分析

该题在上题的基础上在多加一层循环,将四数之和为零改为三数之和为另一个数的负数即可。

9.3 代码实现

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) 
    {
        sort(nums.begin(), nums.end());
        vector<vector<int>> ret;
        for (int i = nums.size() - 1; i >= 3; i--)
        {
            int j = i - 1;
            while (j >= 2)
            {
                int left = 0, right = j - 1;
                while (left < right)
                {   
                    long long newtarget = (long long)target - nums[i] - nums[j];
                    int sum = nums[left] + nums[right];
                    if (sum > newtarget) right--;
                    else if (sum < newtarget) left++;
                    else
                    {
                        ret.push_back({nums[left], nums[right], nums[j], nums[i]});
                        while (left < right && nums[left] == nums[left + 1]) left++;
                        while (left < right && nums[right] == nums[right - 1]) right--;
                        left++, right--;
                    }
                }
                while (j >= 2 && nums[j] == nums[j - 1]) j--;
                j--;
            }
            while (i >= 3 && nums[i] == nums[i - 1]) i--;
        }
        return ret;
    }
};

. - 力扣(LeetCode)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值