常见双指针问题汇总

lxf2023-12-15 06:10:01

双指针

在LeetCode中,双指针问题通常是指使用两个指针在一个序列中向相反方向移动,以协同完成某种操作或达成某种条件的问题。

在解决双指针问题时,一般要满足以下条件:

  1. 序列是有序的。

  2. 需要在序列中寻找或比较元素。

  3. 操作需要在 O(n) 的时间复杂度内完成。

双指针问题的解法一般有两种:

  1. 两个指针从两端开始向中间移动。

  2. 两个指针从同一端开始向相反方向移动。

在双指针问题中,关键是要确定指针的移动方式和指针移动时的判断条件,这样才能正确地完成操作或达成条件。另外,需要注意指针的边界问题,避免出现指针越界的情况。

125. 验证回文串 - 力扣(Leetcode)

如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。

字母和数字都属于字母数字字符。

给你一个字符串 s,如果它是 回文串 ,返回 true **;否则,返回 **false **。

 

示例 1:

输入: s = "A man, a plan, a canal: Panama"

输出: true

解释: "amanaplanacanalpanama" 是回文串。

示例 2:

输入: s = "race a car"

输出: false

解释: "raceacar" 不是回文串。

示例 3:

输入: s = " "

输出: true

解释: 在移除非字母数字字符之后,s 是一个空字符串 "" 。

由于空字符串正着反着读都一样,所以是回文串。

 

提示:

  • 1 <= s.length <= 2 * 105
  • s 仅由可打印的 ASCII 字符组成
class Solution:
    def isPalindrome(self, s: str) -> bool:
        s = s.lower()
        l, r = 0, len(s) - 1
        while l < r:
            while l < r and not s[l].isalnum():
                l += 1
            while l < r and not s[r].isalnum():
                r -= 1
            if s[l] == s[r]:
                l += 1
                r -= 1
                continue
            else:
                return False
        return True

这段代码的主要逻辑如下:

  1. 将字符串转换为小写,并去掉其中的非字母数字字符。

  2. 定义两个指针 l 和 r,分别指向字符串的左右两端。

  3. 在 l < r 的条件下,循环执行以下操作:

    • 如果 s[l] 和 s[r] 不同,则返回 False。
    • 否则,将 l 和 r 分别向中间移动一位。

需要注意的是,为了确保在字符串中只比较字母和数字字符,这段代码使用了 isalnum() 方法判断字符是否为字母数字字符。同时,在判断字符是否相等时,这段代码使用了 continue 关键字来继续执行下一轮循环,以避免多余的判断。

因为这道题被分在双指针里,所以这么写,其实用python实现的话没必要这么麻烦的, 代码如下:

class Solution:
    def isPalindrome(self, s: str) -> bool:
        s = s.lower()
        tmp = ''
        for c in s:
            if c.isalnum():
                tmp += c
        return tmp == tmp[::-1]

392. 判断子序列 - 力扣(Leetcode)

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace""abcde"的一个子序列,而"aec"不是)。

进阶

如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?

致谢:

特别感谢 ****@pbrother 添加此问题并且创建所有测试用例。

 

示例 1:

输入: s = "abc", t = "ahbgdc"

输出: true

示例 2:

输入: s = "axc", t = "ahbgdc"

输出: false

 

提示:

  • 0 <= s.length <= 100
  • 0 <= t.length <= 10^4
  • 两个字符串都只由小写字符组成。
class Solution:
    def isSubsequence(self, s: str, t: str) -> bool:
        ps = 0
        pt = 0
        while ps < len(s) and pt < len(t):
            if s[ps] == t[pt]:
                ps += 1
                pt += 1
                continue
            pt += 1
        return True if ps == len(s) else False

这段代码的主要逻辑如下:

  1. 定义两个指针 ps 和 pt,分别指向字符串 s 和字符串 t 的开头。

  2. 在 ps < len(s) 和 pt < len(t) 的条件下,循环执行以下操作:

    • 如果 s[ps] == t[pt],则将 ps 和 pt 分别加 1。
    • 否则,将 pt 加 1,继续比较下一个字符。
  3. 如果在比较过程中,指针 s 已经到达了字符串 s 的末尾,则说明 s 是 t 的子序列,返回 True;否则,返回 False。

需要注意的是,这段代码使用了一个三元表达式来判断指针 s 是否到达了字符串 s 的末尾,如果是,则返回 True,否则返回 False。

11. 盛最多水的容器 - 力扣(Leetcode)

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

说明: 你不能倾斜容器。

 

示例 1:

常见双指针问题汇总

输入: [1,8,6,2,5,4,8,3,7]

输出: 49

解释: 图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

示例 2:

输入: height = [1,1]

输出: 1

 

提示:

  • n == height.length
  • 2 <= n <= 105
  • 0 <= height[i] <= 104
class Solution:
    def maxArea(self, height: List[int]) -> int:
        l,r = 0,len(height)-1
        res = 0
        while l < r:
            s = min(height[l],height[r])*(r-l)
            res = max(s,res)
            if height[l] < height[r]:
                l += 1
            else:
                r -= 1
        return res

这是一个双指针问题,用左右指针分别指向数组的首尾,计算这两个指针之间的面积,然后将指向数字较小的那个指针向内移动一位,再计算新的面积,并更新最大值,直到左右指针相遇,返回最大值。由于指针每次移动都会缩小宽度,因此一定会得到最大的面积。

具体实现如下:用 lr 分别表示指向数组首尾的左右指针,用 res 存储最大的面积值。在每次循环中,计算当前左右指针之间的面积,取最大值并更新 res。然后将指向数字较小的那个指针向内移动一位,再次计算新的面积。循环直到左右指针相遇,返回最大值。

代码实现中,可以使用 min 函数来获取左右指针对应的高度中的最小值,同时用 max 函数来更新最大面积。 时间复杂度为O(n),空间复杂度为O(1)。这个算法的思路非常简单,关键是如何证明正确性。 其正确性可以由下面两个方面来证明:

  1. 如果移动高度较高的指针,矩形的高度不会再次增加,而宽度一定会减少,所以面积一定会减小。因此,需要移动较矮的指针,才有可能使面积变大。

  2. 假设初始时左指针指向的线段较短,右指针指向的线段较长,设左指针位置为i,右指针位置为ji < j,假设向内移动左指针,新位置为i',那么右指针仍然指向位置j或更靠左的位置,因为移动左指针不会改变右指针的位置。设新的矩形面积为S',那么有S' = min(height[i'], height[j]) * (j - i'),因为i' > i,所以j - i' < j - i,又因为height[i'] <= height[i],所以min(height[i'], height[j]) <= min(height[i], height[j]),因此S' <= S,也就是说,移动左指针不会使矩形的面积变大,而向内移动右指针同理。因此,应该移动高度较低的那个指针,才有可能使矩形的面积变大。

167. 两数之和 II - 输入有序数组 - 力扣(Leetcode)

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

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

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

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

 

示例 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] 。

 

提示:

  • 2 <= numbers.length <= 3 * 104
  • -1000 <= numbers[i] <= 1000
  • numbers 按 非递减顺序 排列
  • -1000 <= target <= 1000
  • 仅存在一个有效答案
class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        f,e = 0,len(numbers)-1
        while f < e:
            if target == numbers[f] + numbers[e]:
                return [f+1,e+1]
            elif target > numbers[f] + numbers[e]:
                f += 1
            elif target < numbers[f] + numbers[e]:
                e -= 1
        return [-1 , -1]

由于数组已按非递减顺序排列,因此我们可以使用双指针来解决这个问题。

定义两个指针f和e,分别指向数组的首尾位置。然后不断地向中间靠拢,直到找到两个数使得它们的和等于目标数target,或者指针相遇。

具体来说,我们可以每次比较numbers[f]和numbers[e]的和与target的大小,如果和小于target,则f向右移动一位,否则e向左移动一位。如果和等于target,则返回两个数的下标。

最后,如果没有找到满足条件的两个数,则返回[-1, -1]。

时间复杂度为O(n),其中n是数组的长度。因为每个指针最多遍历一次数组。空间复杂度为O(1),因为只使用了常量级的额外空间。

15. 三数之和 - 力扣(Leetcode)

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

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

 

 

示例 1:

输入: nums = [-1,0,1,2,-1,-4]

输出: [[-1,-1,2],[-1,0,1]]

解释:

nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。

nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。

nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。

不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。

注意,输出的顺序和三元组的顺序并不重要。

示例 2:

输入: nums = [0,1,1]

输出: []

解释: 唯一可能的三元组和不为 0 。

示例 3:

输入: nums = [0,0,0]

输出: [[0,0,0]]

解释: 唯一可能的三元组和为 0 。

 

提示:

  • 3 <= nums.length <= 3000
  • -105 <= nums[i] <= 105
nums.sort()
if nums[0] > 0: return []
l = len(nums)
res = []
for i in range(l):
    if i > 0 and nums[i] == nums[i - 1]:
        continue
    j = i + 1
    k = l - 1
    while j < k:
        if nums[i] + nums[j] + nums[k] < 0:
            j += 1
        elif nums[i] + nums[j] + nums[k] > 0:
            k -= 1
        else:
            res.append([nums[i], nums[j], nums[k]])
            while j < k and nums[j] == nums[j + 1]:
                j += 1
            while j < k and nums[k] == nums[k - 1]:
                k -= 1
            j += 1
            k -= 1
return res

具体实现步骤:

  1. 将数组 nums 升序排序

  2. 遍历数组 nums,假设当前遍历到的数为 nums[i]

    • nums[i] 大于 0,则跳出循环,因为三个大于 0 的数之和必定大于 0

    • nums[i] 等于上一个遍历到的数 nums[i-1],则跳过当前遍历,避免重复求解

    • i+1 开始,使用双指针 jk 分别指向当前数的下一个数和数组的最后一个数

      • 如果 nums[i] + nums[j] + nums[k] 大于 0,则说明需要将 nums[k] 变小,k -= 1
      • 如果 nums[i] + nums[j] + nums[k] 小于 0,则说明需要将 nums[j] 变大,j += 1
      • 如果 nums[i] + nums[j] + nums[k] 等于 0,则将当前组合 [nums[i], nums[j], nums[k]] 加入到结果列表中,并跳过重复的数,即将 jk 分别向后和向前移动一位,j += 1k -= 1
  3. 返回结果列表 res

总的时间复杂度为 O(n2)O(n^2),其中 nn 为数组的长度,因为代码使用了排序和双指针的方法,而排序的时间复杂度为 O(nlogn)O(n\log n),双指针的时间复杂度为 O(n)O(n)。由于 nlognn\log nnn 更大,因此总的时间复杂度为 O(n2)O(n^2)


本文正在参加「」

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!