双指针

"learning……"

Posted by Ryan on January 5, 2025

算法描述

双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。
若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。
若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。

双指针常有以下应用:

  1. 滑动窗口
  2. 数组归并
  3. 快慢指针
  4. 高精度计算

两数之和题

leetcode.167.两数之和:在一个增序的整数数组里找到两个数,使它们的和为给定值。因为数组已经排好序,我们可以采用方向相反的双指针来寻找这两个数字,一个初始指向最 小的元素,即数组最左边,向右遍历;一个初始指向最大的元素,即数组最右边,向左遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
        int l=0,r=numbers.size()-1,sum;
        while(l<r)
        {
            sum=numbers[l]+numbers[r];
            if(sum==target)break;
            else if(sum>target) --r;
            else ++l;
        }
        return {l+1,r+1};
    }
};

归并两个有序数组EZ

leetcode.88:输入是两个数组和它们分别的长度 m 和 n。其中第一个数组的长度被延长至 m + n,多出的n 位被 0 填补。题目要求把第二个数组归并到第一个数组上,不需要开辟额外空间.

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
class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        int p=m+n-1,i=m-1,j=n-1;
        while(i>=0&&j>=0)
        {
            if(nums1[i]>nums2[j])
            {
                nums1[p]=nums1[i];
                i--;
                p--;
            }
            else
            {
                nums1[p]=nums2[j];
                j--;
                p--;
            }
        }
        while(i>=0)
        {
            nums1[p]=nums1[i];
            i--;
            p--;
        }
        while(j>=0)
        {
            nums1[p]=nums2[j];
            j--;
            p--;
        }
    }
};

快慢指针

leetcode.142快慢指针:给定一个链表,如果有环路,找出环路的开始点。对于链表找环路的问题,有一个通用的解法——快慢指针(Floyd 判圈法)。给定两个指针,分别命名为 slow 和 fast,起始位置在链表的开头。每次 fast 前进两步,slow 前进一步。如果 fast可以走到尽头,那么说明没有环路;如果 fast 可以无限走下去,那么说明一定有环路,且一定存在一个时刻 slow 和 fast 相遇。当 slow 和 fast 第一次相遇时,我们将 fast 重新移动到链表开头,并让 slow 和 fast 每次都前进一步。当 slow 和 fast 第二次相遇时,相遇的节点即为环路的开始点。

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
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        if(head==NULL) return NULL;
        ListNode *slow=head,*fast=head;
        
        do
        {
            if(!fast||!fast->next) return NULL;
            fast=fast->next->next;
            slow=slow->next;
        }while(fast!=slow);
        fast=head;
        while(fast!=slow)
        {
            fast=fast->next;
            slow=slow->next;
        }
        return fast;
    }
};

滑动窗口(很难)

leetcode.76.最小覆盖字串:给定两个字符串 S 和 T,求 S 中包含 T 所有字符的最短连续子字符串的长度,同时要求时间复杂度不得超过 O(n)。

  1. 统计目标频数,做成hash映射target,用于查询。
  2. 创建窗口映射,用于实时记录。
  3. 窗口不断向右扩展,直到到达源字符串末尾
    1. 每次扩展首先将新char记录进window,right指针右移。
    2. 做一个判断:去target找新char是否需要关注,若是且频数相等,说明窗口内已经包含target所需的一个字符,那么valid++(valid是target内字符达到计数的个数)。
    3. 当valid计数满足target要求(valid==target.size()),不断尝试缩减窗口
      1. 检查当前窗口是否比之前记录的短(minnum),如果短则更新minnum,更新最短窗口开始的位置start=left
      2. 保存left指向的char,left++缩短窗口,检查缩短是否造成valid减小
  4. 返回结果
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
class Solution {
public:
    string minWindow(string s, string t) {
        //创建目标(目标集合中的字母顺序不重要,重要的是字母及其频数)
        unordered_map<char,int> target;
        for(char c:t)
        {
            target[c]++;
        }
        //创建窗口集合用来统计窗口内字母频数
        unordered_map<char,int> window;
        //窗口
        int left=0,right=0,valid=0,start=0,minnum=INT_MAX;
        while(right<s.size())
        {
            char newchar=s[right];
            window[newchar]++;
            right++;
            if(target.find(newchar)!=target.end()&&window[newchar]==target[newchar])
            {
                valid++;
            }
            while(valid==target.size())
            {
                if(minnum>right-left)
                {
                    minnum=right-left;
                    start=left;
                }
                //尝试缩短
                char c=s[left];
                left++;
                if(target.find(c)!=target.end())
                {
                    if(target[c]==window[c])
                    {
                        valid--;
                    }
                    window[c]--;
                }
            }
        }
        if(minnum==INT_MAX) return "";
        return s.substr(start,minnum);

    }
};

练习

633.整数能否拆成平方和。
680.验证回文串II
524.归并两个有序数组的变形题。

340.至多包含 K 个不同字符的最长子串:题目要求的是找到一个字符串中,至多包含 K 个不同字符的最长子串的长度。这是一个典型的滑动窗口问题,使用双指针技术来实现。

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
#include <iostream>
#include <vector>
#include <unordered_map>
#include <algorithm>

using namespace std;

class Solution {
public:
    int lengthOfLongestSubstringKDistinct(string s, int k) {
        unordered_map<char, int> map; // 用于存储字符及其频次
        int left = 0;  // 滑动窗口的左指针
        int maxLength = 0;  // 最大子串长度

        for (int right = 0; right < s.length(); ++right) {
            map[s[right]]++;  // 增加右指针指向字符的频次

            // 如果不同字符数超过k,收缩窗口
            while (map.size() > k) {
                map[s[left]]--;  // 减少左指针指向字符的频次
                if (map[s[left]] == 0) {
                    map.erase(s[left]);  // 移除字符,保持哈希表内字符数不超过k
                }
                left++;  // 移动左指针
            }

            // 更新最大子串长度
            maxLength = max(maxLength, right - left + 1);
        }

        return maxLength;
    }
};

int main() {
    Solution solution;
    string s = "eceba";
    int k = 2;

    int result = solution.lengthOfLongestSubstringKDistinct(s, k);
    cout << "Length of longest substring with at most " << k << " distinct characters: " << result << endl;
    
    return 0;
}