vlambda博客
学习文章列表

常用算法-双指针系列

双指针问题

本篇博客主要讲的内容都和双指针系列有关,主要介绍双指针,快慢指针,滑动窗口等技法在代码中的应用。关于滑动窗口,后面会再出一篇博客对其进行详解,本篇主要是入门

什么是双指针?

首先我们看一个问题,如何返回链表的倒数第K个节点?

可能你立马会想到,我直接反转链表,然后顺序遍历第K个节点不就行了?没错,但是来看双指针怎么做的

用双指针怎么做?准确说应该是快慢指针,两个指针一开始都指向头结点,

首先,快指针先从链表的head开始往后遍历K个节点,此时慢指针不动,仍然指向head

然后,慢指针和快指针以相同速度同时往后遍历链表,当快指针到达末尾时,慢指针所指的位置就是对应的倒数第K个节点。

(咱们换个场景,好比一个人想测一条路的第倒数39米,那么就先给这个人一把39米长的大刀,刀尖向前,此人手持刀柄,从起点开始跑,当刀尖到了路的尽头,那么刀柄就是第倒数39米,在这一题中,刀尖就是快指针,刀柄就是慢指针)

瞬间理解了双指针对吧?(其实算法就是生活中的小妙招的合集),当然双指针问题有很多版本,什么快慢指针,合并指针,滑动窗口等等。下面介绍一些和双指针相关的题目(题目都来自leetcode,可以根据题号搜索):

下面是leetcode上关于双指针系列的一些题目,大部分是初中级,有基础的朋友可以直接点击链接或者去leetcode搜索对应题号开始做题。喜欢看我内容的朋友可以继续看后面内容,后面主要是个人理解和题解。

简单篇

167 两数相加

88 合并两个有序数组

680. 验证回文字符串Ⅱ

141. 环形链表

142. 环形链表 II

680. 验证回文字符串 Ⅱ

中级篇

633. 平方数之和

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

3. 无重复字符的最长子串

困难篇

76. 最小覆盖子串

接下来对上面习题讲解

NO.167 两数相加 [简单]

题目如下

给定一个已按照升序排列的整数数组numbers,请你从数组中找出两个数满足相加之和等于目标数target。

示例 1:

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

示例 2:
输入:numbers = [2,3,4], target = 6
输出:[1,3]

示例 3:
输入:numbers = [-1,0], target = -1
输出:[1,2]

题解(有很多种解法,这里主要介绍双指针的解法)

咱们这么理解,牛郎从鹊桥(数组)的这边出发,织女从那边出发,他们每走一步就算一下他们两路标的和,如果这个和比目标值大,那就织女走一步,如果比目标值小,那么牛郎走一步,直到遇到正好和目标值一样大的位置,如果牛郎和织女都碰面了还没遇到目标值,那就说明无解了。

同理对于排序好的数组,

那么我们可以使用两个指针,一个从左往右跑,一个从右往左跑,两个指针的值相加,得到一个和 X

X < target ?左指针右移 右指针不动

x > target ? 左指针不动,右指针左移

x == target? 返回结果

代码如下

class Solution(object):
    def twoSum(self, numbers, target):
        """
        :numbers: 排好序的数组
        :target: 目标值
        :return: 对应下标的集合
        """

        left, right = 0, len(numbers) - 1
        while left <= right:
            s = numbers[left] + numbers[right]
            if s == target:
                return [left + 1, right + 1]
            elif s < target:
                left += 1
            else:
                right -= 1

NO.88 合并两个有序数组 [简单]

题目

给你两个有序整数数组nums1 和 nums2,请你将 nums2 合并到nums1中,使 nums1 成为一个有序数组。

初始化nums1 和 nums2 的元素数量分别为m 和 n 。你可以假设nums1 的空间大小等于 m + n,这样它就有足够的空间保存来自 nums2 的元素。

示例 1:

输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]

示例 2:

输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]

题解

这个题就很简单了啊,哥哥弟弟从兜里掏钱,每个人的钱排好序了,先掏小的,掏出来两个人比较大小,然后按顺序扔到桶里面,最后掏完了,把这个桶给哥哥不就行了?

两个指针分别遍历两个数组,然后取值比较大小,按顺序存到临时数组中,最后临时数组赋值给nums1就行了。

代码也很简单

class Solution:
    def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
        last_list = list()
        p1, p2 = 00
        while p1 < m or p2 < n:
            if p1 == m:
                last_list.append(nums2[p2])
                p2 += 1
            elif p2 == n:
                last_list.append(nums1[p1])
                p1 += 1
            elif nums1[p1] < nums2[p2]:
                last_list.append(nums1[p1])
                p1 += 1
            else:
                last_list.append(nums2[p2])
                p2 += 1
        nums1[:] = last_list

NO.141 判断链表有没有环 [中等] -- 快慢指针


题解

这道题其实有很多解法,因为主讲双指针,那我就只介绍双指针解法。

快慢指针解法:

通俗的讲,假设这个链表是一个环形链表(特殊情况哈),就好比如如何用两辆车判断地球是圆的?

两辆车沿着同一个方向跑,一辆车一天跑一千公里,一个辆车一天跑20公里,如果跑的快的车追上了跑得慢的车,那么是不是就可以判断地球是圆的了?(夸父逐日)

代码:

class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None

class Solution:
    def hasCycle(self, head: ListNode) -> bool:
        if not head or not head.next:
            return False
        slow ,fast= head,head.next
        while slow != fast:
            if not fast or not fast.next:
                return False
            slow = slow.next
            fast = fast.next.next
        return True

NO.142 环列表之返回环的节点

给定一个链表,返回链表开始入环的第一个节点。如果链表无环,则返回null。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。

说明:不允许修改给定的链表。

进阶:你是否可以使用 O(1) 空间解决此题?

常用算法-双指针系列

题解 (同样使用双指针咯)

这个核心思路还是用快慢指针,和上一题很像,但是需要返回入环的第一个节点,这个就需要用点心思了

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def detectCycle(selfhead: ListNode) -> ListNode:
        fast, slow = head, head
        while True:
            if not (fast and fast.next): return
            fast, slow = fast.next.next, slow.next
            if fast == slow: break
        fast = head
        while fast != slow:
            fast, slow = fast.next, slow.next
        return fast

NO.3 无重复字符的最长子串

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

题解

这也是快慢指针的一种解法,江湖人称滑动窗口,具体操作如下:

代码

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        # 如果字符串为空字符串 返回最长子串为0
        if s == "":
            return 0

        # 用set来记录滑动窗口类的所有字符
        char_set = set()
        n = len(s)

        # 设置滑动窗口的左右边界
        left,right = 0,0
        max_length = 0
        for right in range(n):
            # 当有新字符加入,和窗口里字符有重复 左窗口右移一位 直到不存在
            while s[right] in char_set:
                char_set.remove(s[left])
                left += 1
            # 将新字符加入滑动窗口
            char_set.add(s[right])
            # 更新最大子串的长度
            max_length = max(max_length,len(char_set))
        return max_length

NO.633 平方数之和[中等]

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

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

题解

这玩意不就是牛郎织女双指针嘛?

只是右指针的边界我们可以缩小到 C开平方根的数取整

比如给定 C = 34,那么右指针为C开方 = 5, 然后就是一起走了

代码

import math
class Solution:
    def judgeSquareSum(self, c: int) -> bool:
        i,j = 1,int(math.sqrt(c))  # i从1开始算,因为0*0 = 0 无意义
        if j* j == c:
            return True
        while i <= j:
            temp = i*i + j*j
            if temp == c:
                return True
            if temp > c:
                j = j -1
            else:
                i = i + 1
        return False

NO.680 验证回文字符串 Ⅱ [简单]

给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串(左右对称的字符串)。

示例 1:
输入: "aba"
输出: True (因为他本身就是回文字符串)

示例 2:
输入: "abca"
输出: True
解释: 你可以删除c字符。

题解

这道题其实应该算考查贪心算法的,但是用双指针也能做。

同样的,两个指针,一个从左往右,一个从右往前,每次移动后判断内圈是不是回文数(通过反转字符串比较实现)这个不好讲解,直接看代码吧。

代码

class Solution:
    def validPalindrome(self, s: str) -> bool:
        leftright = 0len(s) - 1
        while left < right:
            if s[left] != s[right]:
                return s[left + 1:right + 1] == s[left + 1:right + 1][::-1or s[left:right] == s[left:right][::-1]
            left += 1
            right -= 1
        return True

NO.76 最小覆盖子串 [困难] -- 滑动窗口

这道题比较难,看不懂的先略过,咱们优先搞定<困难等级的题目

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

注意:如果 s 中存在这样的子串,我们保证它是唯一的答案。

题解

假设我们子串只有一个字符的情况

这个很好理解

if len(t== 1 :
    return t if t in s else ""

代码

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        if len(t)==1return t if t in s else ""
        m,n = len(s),len(t)
        # count代表需要元素的总个数,dic代表每个元素分别还需要的个数
        count,dic = n,{}
        for c in t:dic[c] = dic.get(c,0)+1
        num,res = m+1,""
        i,j = 0,0

        while j<m:
            if s[j] in dic:
                dic[s[j]] -= 1
                if dic[s[j]]>=0:
                    count -=1
            while count==0:
                if j-i+1<num:
                    num = j-i+1
                    res = s[i:j+1]
                if s[i] in dic:
                    dic[s[i]]+=1
                    if dic[s[i]]>0:
                        count +=1
                i+=1
            j+=1
        return res

总结

  • 双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。

  • 若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。

  • 若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。