Leetcode双指针合集

双指针

双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。

  1. 若两个指针指向同一数组,遍历方向相同且不会相交,也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索
  2. 若两个指针指向同一数组,但遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。

指针与常量:

int x;
int *p1 = &x;//指针可以被修改,值也可以被修改
const int *p2 = &x;//指针可以被修改,值不可以被修改(const int)
int *const p3 = &x;//指针不可以被修改(*const),值可以被修改
const int *const p4 = &x;//指针不可以被修改,值也不可以被修改

指针函数与函数指针:

// addition是指针函数,一个返回类型是指针的函数
int* addition(int a, int b) {
	int* sum = new int(a + b);
	return sum;
}
int subtraction(int a, int b) {
	return a - b;
}
int operation(int x, int y, int (*func)(int, int)) {
	return (*func)(x,y);
}
// minus是函数指针,指向函数的指针
int (*minus)(int, int) = subtraction;
int* m = addition(1, 2);
int n = operation(3, *m, minus);

Two Sum

167.两数之和 II - 输入有序数组

给你一个下标从 1 开始的整数数组numbers,该数组已按非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数target的两个数。如果设这两个数分别是numbers[index1]numbers[index2],则1 <= index1 < index2 <= numbers.length

以长度为2的整数数组[index1, index2]的形式返回这两个整数的下标index1index2

你可以假设每个输入 只对应唯一的答案 ,而且你不可以重复使用相同的元素。

你所设计的解决方案必须只使用常量级的额外空间。

示例 1:
输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。

示例 2:
输入:numbers = [2,3,4], target = 6
输出:[1,3]
解释:2 与 4 之和等于目标数 6 。因此 index1 = 1, index2 = 3 。返回 [1, 3] 。

示例 3:
输入:numbers = [-1,0], target = -1
输出:[1,2]
解释:-1 与 0 之和等于目标数 -1 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。

思路:
采用相反方向的双指针来寻找这两个数字:

  1. 指针l指向最小的元素,即数组最左边,向右遍历;
  2. 指针r指向最大的元素,即数组最右边,向左遍历;
  3. 判断达到解的条件:如果两指针指向元素之和等于给定值,即为我们要求的结果;如果小于,左边的指针需要右移一位;如果大于,右边的指针需要左移一位。

代码:

class Solution {
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
        int l = 0;
        int r = numbers.size() - 1;
        while(l < r)
        {
            if (numbers[l] + numbers[r] == target) break;
            else if (numbers[l] + numbers[r] < target) l++;
            else r--;//注意这里需要r指针左移,所以是--,不是++
        }
        return vector<int>{l+1,r+1};
    }
};

归并两个有序数组

88.合并两个有序数组

给你两个按非递减顺序排列的整数数组nums1nums2,另有两个整数mn ,分别表示nums1nums2中的元素数目。

请你合并nums2nums1中,使合并后的数组同样按非递减顺序排列。

注意:最终,合并后数组不应由函数返回,而是存储在数组nums1中。为了应对这种情况nums1的初始长度为m + n,其中前m 个元素表示应合并的元素,后n个元素为0,应忽略。nums2的长度为n

示例 1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。

示例 2:
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1] 和 [] 。
合并结果是 [1] 。

示例 3:
输入:nums1 = [0], m = 0, nums2 = [1], n = 1
输出:[1]
解释:需要合并的数组是 [] 和 [1] 。
合并结果是 [1] 。
注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。

思路1:双指针法

分别用两个指针指向两个数组,每次取出较小的数字放到新开辟的数组中。(需要额外开辟空间)

思路2:逆向双指针法

nums1的后半部分是空的,可以直接覆盖而不会影响结果。因此可以指针设置为从后向前遍历,每次取两者之中的较大者放进nums1的最后面。需要注意的是,如果指向数组1的指针小于0,还需要将数组2的数字一一赋值给数组1,但是如果指向数组2的指针小于0,我们不需要再管数组1剩余的数字,它们已经按照顺序排布好了。

代码1:

class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        int p1 = 0;
        int p2 = 0;
        int sorted[m + n];
        int cur;
        while(p1 < m || p2 < n)
        {
            if(p1 == m)
            {
                cur = nums2[p2++];
            }
            else if (p2 == n)
            {
                cur = nums1[p1++];
            }
            else if(nums1[p1] < nums2[p2])
            {
                cur = nums1[p1++];
            }
            else
            {
                cur = nums2[p2++];
            }
            sorted[p1 + p2 - 1] = cur;
        }
        for(int i = 0; i < m + n; i++)
        {
            nums1[i] = sorted[i];
        }
    }
};

代码2:

class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        int p1 = m - 1;
        int p2 = n - 1;
        int len = m + n - 1;
        while(p1 >= 0 || p2 >= 0)
        {
            if (p1 == -1) nums1[len--] = nums2[p2--];
            else if (p2 == -1) break;//可直接break
            else if (nums1[p1] > nums2[p2]) nums1[len--] = nums1[p1--];
            else nums1[len--] = nums2[p2--];
        }

    }
};

对于使用++i还是i++i++++i 都是将i1,但是i++ 返回值为i,而++i返回值为i+1。如果只是希望增加i的值,而不需要返回值,则推荐使用++i,其运行速度会略快一些。

快慢指针

142.环形链表Ⅱ

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

图解:
在这里插入图片描述
以下图中的环形链表为例,各指针指向的元素依次是:
环形链表示例

fastslow备注
33
02
20
-4-4快慢指针第一次相遇
3-4快指针回到原点,慢指针仍指向-4
22快慢指针第二次相遇,相遇的节点即为环路开始点
  1. 如果存在环路,为什么快慢指针一定会相遇?
    参考该篇文章:快慢指针一定会相遇的精髓在于当“快指针出现在慢指针后面”之后,每一次“快指针往前走两步、慢指针往前走一步”,相当于快指针和慢指针之间的相对距离减少1步。
    假如说,当快指针刚刚绕到慢指针后面时,快指针离慢指针有n步。那么,对于接下来的每一次“快指针往前走两步、慢指针往前走一步”,快指针和慢指针之间的距离由n步变成n-1步、由n-1步变成n-2步、……、由3步变成2步、由2步变成1步、由1步变成0步。

  2. 为什么第二次相遇的节点是环路的开始点?
    参考力扣题解,个人觉得讲得清晰易懂。

代码:

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode *slow = head, *fast = head;
        //判断是否存在环路
        do{
            if(!fast || !fast->next) return nullptr; //fast只要不指向NULL即证明有环
            fast = fast->next->next;
            slow = slow->next;
        }while(fast != slow);
        fast = head;
        while(fast != slow){
            slow = slow->next;
            fast = fast->next;
        }
        return fast;       
    }
};

滑动窗口

76.最小覆盖子串

给你一个字符串s 、一个字符串t 。返回s中涵盖t所有字符的最小子串。如果s中不存在涵盖t所有字符的子串,则返回空字符串 ""

注意:

对于t中重复字符,我们寻找的子字符串中该字符数量必须不少于t中该字符数量。
如果s中存在这样的子串,我们保证它是唯一的答案。

示例 1:
输入:s = “ADOBECODEBANC”, t = “ABC”
输出:“BANC”

示例 2:
输入:s = “a”, t = “a”
输出:“a”

示例 3:
输入: s = “a”, t = “aa”
输出: “”
解释: t 中两个字符 ‘a’ 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。

以下为滑动窗口解法思路模板,参考B站视频

  1. 滑动窗口解法思路(寻找最长):
    ——核心:左右双指针(L、R)在起始点,R向右逐位滑动循环
    ——每次滑动过程中
    如果:窗内元素满足条件,R向右扩大窗口,并更新最优结果
    如果:窗内元素不满足条件,L向右缩小窗口
    ——R到达结尾
    代码:
初始化left,right,result,bestResult
while(右指针没有指到结尾){
	窗口扩大,加入right对应元素,更新当前result
	while(result不满足要求){
		窗口缩小,移除left对应元素,left右移
	}
	更新最优结果bestResult
	right++}
返回bestResult;
  1. 滑动窗口解法思路(寻找最短):
    ——核心:左右双指针(L、R)在起始点,R向右逐位滑动循环
    ——每次滑动过程中
    如果:窗内元素满足条件,L向左缩小窗口,并更新最优结果
    如果:窗内元素不满足条件,R向右扩大窗口
    ——R到达结尾
    代码:
while(右指针没有指到结尾){
	窗口扩大,加入right对应元素,更新当前result
	while(result满足要求){
		更新最优结果bestResult
		窗口缩小,移除left对应元素,left右移
	}
	right++}
返回bestResult;

目前还无法完全理解,先暂时跳过这道题,后续弄懂再更新。

代码:

class Solution {
public:
    string minWindow(string s, string t) {
        //把t中的字符全部放到map中
        unordered_map<char,int> m;
        for(char c : t) m[c]++;
        
        int count = t.size();
        int left = 0;//窗口的左边界
        int right = 0;//窗口的右边界
        //满足条件的窗口开始位置
        int strStart = -1;
        //满足条件的窗口的长度
        int len = INT_MAX;

        while(right < s.size()){
            if(m[s[right++]]-- > 0) count--;//如果右指针扫描的字符存在于map中,就减1,记录之后右指针要往右移
            //检查窗口是否把t中字符全部覆盖了,如果覆盖了,要移动窗口的左边界
            //找到最小的能全部覆盖的窗口
            while(count == 0){
                //如果现在窗口比之前保存的还要小,就更新窗口的长度
                //以及窗口的起始位置
                if(right - left < len){
                    len = right - left;
                    strStart = left;
                }
                //移除窗口最左边的元素,也就是缩小窗口,左指针往右移(这块不是很懂!!)
                if(m[s[left++]]++ == 0) count++;
            }
        }
        if(strStart == -1) return "";
        else return s.substr(strStart,len);
    }
};

练习

633.平方数之和

给定一个非负整数c,你要判断是否存在两个整数ab,使得a^2+b^2 =c

示例 1:
输入:c = 5
输出:true
解释:1 * 1 + 2 * 2 = 5

示例 2:
输入:c = 3
输出:false

提示:

0 <= c <= 2^31 - 1

代码:

class Solution {
public:
    bool judgeSquareSum(int c) {//0是整数 没说两个整数不能一样
        long left = 0;
        long right = (int)sqrt(c);
        long sum;
        while(left <= right){
            sum = pow(left,2) + pow(right,2);
            if(sum < c) left++;
            else if(sum > c) right--;//注意是--
            else return true;
        }
        return false;
    }
};

680.验证回文串 II

给你一个字符串s最多可以从中删除一个字符。

请你判断s是否能成为回文字符串:如果能,返回true;否则,返回false

示例 1:
输入:s = “aba”
输出:true

示例 2:
输入:s = “abca”
输出:true
解释:你可以删除字符 ‘c’ 。

示例 3:
输入:s = “abc”
输出:false

思路:

我的思路是采用相反方向的双指针遍历字符串:

  1. 指针l初始化指向字符串首,向右遍历;
  2. 指针r初始化指向字符串尾,向左遍历;
  3. 判断能否成为回文串的条件:如果两指针指向元素相等,则左指针右移一位,右指针左移一位;如果两指针指向元素不相等,cnt计数加1,左指针右移或右指针左移。最后判断cnt是否小于等于1备注:这里不能单单只让右指针左移或者左指针右移,因为有可能出现如deeee或者eeeed的情况,单独的判断会出错。

最终运行的结果还可以,代码应该还有简化的空间,但不管黑猫白猫能抓住老鼠的都是好猫,通过即可:
在这里插入图片描述

代码:

class Solution {
public:
    bool validPalindrome(string s) {
        int n = s.length();
        int l1 = 0, l2 = 0;
        int r1 = n-1, r2 = n-1;
        int cnt1 = 0, cnt2 = 0;
        while(l1 <= r1){
            if (s[l1] == s[r1])
            {
                l1++;
                r1--;
            }
            else{
                cnt1++;
                r1--;
            }
        }
        while(l2 <= r2){
            if (s[l2] == s[r2])
            {
                l2++;
                r2--;
            }
            else{
                cnt2++;
                l2++;
            }
        }
        return cnt1 <= 1 || cnt2 <= 1;
    }
};

524.通过删除字母匹配到字典里最长单词

给你一个字符串s和一个字符串数组dictionary,找出并返回dictionary中最长的字符串,该字符串可以通过删除s中的某些字符得到。

如果答案不止一个,返回长度最长且字母序最小的字符串。如果答案不存在,则返回空字符串。

示例 1:
输入:s = “abpcplea”, dictionary = [“ale”,“apple”,“monkey”,“plea”]
输出:“apple”

示例 2:
输入:s = “abpcplea”, dictionary = [“a”,“b”,“c”]
输出:“a”

思路:双指针

指针i指向字符串s,指针j指向dictionary中每一个字符串d

  1. d[j] == s[i]时,i++j++
  2. d[j] != s[i]时,只有i++

最后判断指针j是否指向字典中某字符串的末尾,如果是,则判断字符串的长度和存放结果的字符串res的长度,若前者长度更长,则存放进res,若相等,则判断两字符串首字母序哪个更小,更小的存放进res

代码:

class Solution {
public:
    string findLongestWord(string s, vector<string>& dictionary) {
        string res;
        for (string d : dictionary){
            int j = 0;
            for (int i = 0;i < s.size() && j < d.size();i++){
                if (d[j] == s[i]){
                    j++;
                }
            }
            if (j == d.size()){
                if (d.size() > res.size()){
                    res = d;
                }
                else if (d.size() == res.size()){
                    res = d < res ? d : res;
                }
            }
        }
        return res;
    }
};

340. 至多包含 K 个不同字符的最长子串(会员题目-未做)

更新时间:2022/10/03

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值