zoukankan      html  css  js  c++  java
  • 双指针法

    双指针

    双指针的过程:在遍历数组的过程中,定义两个相同方向或者相反方向的指针进行遍历,根据两个指针所在的数据进行处理,从而达到相应的目的。

    首尾指针(对撞指针)

    对撞指针是指在数组中,将最左侧的索引(0)定义为左指针(left),最右侧的索引(数组长度-1)定义为右指针(right),然后两个指针都向中间移动遍历数组。

    适用场景:连续数组、字符串。

    125. 验证回文串

    https://leetcode-cn.com/problems/valid-palindrome/

    给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。

    说明:本题中,我们将空字符串定义为有效的回文串。

    示例 1:

    输入: "A man, a plan, a canal: Panama"
    输出: true
    解释:"amanaplanacanalpanama" 是回文串

    示例 2:

    输入: "race a car"
    输出: false
    解释:"raceacar" 不是回文串
     

    提示:

    1 <= s.length <= 2 * 105
    字符串 s 由 ASCII 字符组成

    """
    使用双指针。初始时,左右指针分别指向 sgood 的两侧,随后我们不断地将这两个指针相向移动,每次移动一步,并判断这两个指针指向的字符是否相同。当这两个指针相遇时,就说明 sgood 是回文串。
    
    isalnum() # 判断是否由数字或字母组成
    isdigit() # 判断全部是由整数组成
    isalpha() # 判断是否全部由字母组成
    """
    
    class Solution:
        def isPalindrome(self, s: str) -> bool:
            # 去除标点空格等,判断是否由数字或字母组成
            sgood = "".join(ch.lower() for ch in s if ch.isalnum())
    
            left, right = 0,  len(sgood) - 1
            
            while left < right:
                if sgood[left] != sgood[right]:
                    return False
                left, right = left + 1, right - 1
            return True

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

    https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/

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

    函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 1 开始计数 ,所以答案数组应当满足 1 <= answer[0] < answer[1] <= numbers.length 。

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

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

    提示:

    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]:
            left, right = 0, len(numbers) - 1
    
            while left < right:
                if numbers[left] + numbers[right] > target:
                    right -= 1
                elif numbers[left] + numbers[right] < target:
                    left += 1
                else:
                    return [left + 1, right + 1]
            return [-1, -1]

    11. 盛最多水的容器

    https://leetcode-cn.com/problems/container-with-most-water/

    给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 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

    示例 3:

    输入:height = [4,3,2,1,4]
    输出:16

    示例 4:

    输入:height = [1,2,1]
    输出:2
     

    提示:

    n == height.length
    2 <= n <= 105
    0 <= height[i] <= 104

    """
    本题是一道经典的面试题,最优的做法是使用「双指针」。如果读者第一次看到这题,不一定能想出双指针的做法。
    我们先从题目中的示例开始,一步一步地解释双指针算法的过程。稍后再给出算法正确性的证明。
    
    题目中的示例为:
    [1, 8, 6, 2, 5, 4, 8, 3, 7]
    在初始时,左右指针分别指向数组的左右两端,它们可以容纳的水量为 min(1, 7) * 8 = 8  # 8是下标的长度,即宽。
    
    此时我们需要移动一个指针。移动哪一个呢?直觉告诉我们,应该移动对应数字较小的那个指针(即此时的左指针)。
    这是因为,由于容纳的水量是由:
    
    两个指针指向的数字中较小值 * 指针之间的距离 决定的。
    
    如果我们移动数字较大的那个指针,那么前者「两个指针指向的数字中较小值」不会增加,后者「指针之间的距离」会减小,
    那么这个乘积会减小。因此,我们移动数字较大的那个指针是不合理的。因此,我们移动 数字较小的那个指针。
    
    所以,我们将左指针向右移动:
    [1, 8, 6, 2, 5, 4, 8, 3, 7]
        ^                    ^
    此时可以容纳的水量为 min(8,7)*7=49。由于右指针对应的数字较小,我们移动右指针:
    [1, 8, 6, 2, 5, 4, 8, 3, 7]
        ^                 ^
    此时可以容纳的水量为 min(8,3)*6=18。由于右指针对应的数字较小,我们移动右指针:
    [1, 8, 6, 2, 5, 4, 8, 3, 7]
        ^              ^
    此时可以容纳的水量为 min(8,8)*5=40。两指针对应的数字相同,我们可以任意移动一个,例如左指针:
    [1, 8, 6, 2, 5, 4, 8, 3, 7]
           ^           ^
    此时可以容纳的水量为 min(6,8)*4=24。由于左指针对应的数字较小,我们移动左指针
    
    按这个规律一直移动指针,直到两个指针重合。
    
    在我们移动指针的过程中,计算到的最多可以容纳的数量为 49,即为最终的答案。
    """
    
    class Solution:
        def maxArea(self, height: List[int]) -> int:
            max_area = 0
            left, right = 0, len(height) - 1
    
            while left < right:
                if height[left] <= height[right]:
                    area = height[left] * (right - left)
                    max_area = max(max_area, area)
                    left += 1
                else:
                    area = height[right] * (right - left)
                    max_area = max(max_area, area)
                    right -= 1
            return max_area

    快慢指针

    快慢指针:两个指针从同一侧开始遍历数组,将这两个指针分别定义为快指针(fast)和慢指针(slow),
    两个指针以不同的策略移动,直到两个指针的值相等(或其他特殊条件)为止,如 fast 每次增长两个,slow 每次增长一个。

    141. 环形链表

    https://leetcode-cn.com/problems/linked-list-cycle/

    给定一个链表,判断链表中是否有环。

    如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

    如果链表中存在环,则返回 true 。 否则,返回 false 。

    进阶:

    你能用 O(1)(即,常量)内存解决此问题吗?

    示例 1:

     

    输入:head = [3,2,0,-4], pos = 1
    输出:true
    解释:链表中有一个环,其尾部连接到第二个节点。

    示例 2:

     

    输入:head = [1,2], pos = 0
    输出:true
    解释:链表中有一个环,其尾部连接到第一个节点。

    示例 3:

     

    输入:head = [1], pos = -1
    输出:false
    解释:链表中没有环。
     

    提示:

    链表中节点的数目范围是 [0, 104]
    -105 <= Node.val <= 105
    pos 为 -1 或者链表中的一个 有效索引 。

    方法1:遍历

    """
    最容易想到的方法是遍历所有节点,每次遍历到一个节点时,判断该节点此前是否被访问过。
    具体地,我们可以使用哈希表来存储所有已经访问过的节点。每次我们到达一个节点,如果该节点已经存在于哈希表中,则说明该链表是环形链表,否则就将该节点加入哈希表中。重复这一过程,直到我们遍历完整个链表即可。
    """
    
    # Definition for singly-linked list.
    # class ListNode:
    #     def __init__(self, x):
    #         self.val = x
    #         self.next = None
    
    class Solution:
        def hasCycle(self, head: ListNode) -> bool:
            seen = set()
            while head:
                if head in seen:
                    return True
                seen.add(head)
                head = head.next
            return False

    方法2:快慢指针

    """
    本方法需要读者对「Floyd 判圈算法」(又称龟兔赛跑算法)有所了解。
    
    假想「乌龟」和「兔子」在链表上移动,「兔子」跑得快,「乌龟」跑得慢。当「乌龟」和「兔子」从链表上的同一个节点开始移动时,如果该链表中没有环,那么「兔子」将一直处于「乌龟」的前方;如果该链表中有环,那么「兔子」会先于「乌龟」进入环,并且一直在环内移动。等到「乌龟」进入环时,由于「兔子」的速度快,它一定会在某个时刻与乌龟相遇,即套了「乌龟」若干圈。
    
    我们可以根据上述思路来解决本题。具体地,我们定义两个指针,一快一满。慢指针每次只移动一步,而快指针每次移动两步。初始时,慢指针在位置 head,而快指针在位置 head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。
    
    为什么我们要规定初始时慢指针在位置 head,快指针在位置 head.next,而不是两个指针都在位置 head(即与「乌龟」和「兔子」中的叙述相同)?
    
    观察下面的代码,我们使用的是 while 循环,循环条件先于循环体。由于循环条件一定是判断快慢指针是否重合,如果我们将两个指针初始都置于 head,那么 while 循环就不会执行。因此,我们可以假想一个在 head 之前的虚拟节点,慢指针从虚拟节点移动一步到达 head,快指针从虚拟节点移动两步到达 head.next,这样我们就可以使用 while 循环了。
    
    当然,我们也可以使用 do-while 循环。此时,我们就可以把快慢指针的初始值都置为 head。
    """
    # Definition for singly-linked list.
    # 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 = head
            fast = head.next
    
            while slow != fast:
                if not fast or not fast.next:
                    # 无环的时候,快指针能够到达连表末尾
                    return False
                slow = slow.next
                fast = fast.next.next
            return True

    26. 删除有序数组中的重复项

    https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/

    给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。

    不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

    说明:

    为什么返回数值是整数,但输出的答案是数组呢?

    请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

    你可以想象内部操作如下:

    // nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
    int len = removeDuplicates(nums);

    // 在函数里修改输入数组对于调用者是可见的。
    // 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
    for (int i = 0; i < len; i++) {
        print(nums[i]);
    }
     
    示例 1:

    输入:nums = [1,1,2]
    输出:2, nums = [1,2]
    解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
    示例 2:

    输入:nums = [0,0,1,1,1,2,2,3,3,4]
    输出:5, nums = [0,1,2,3,4]
    解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
     

    提示:

    0 <= nums.length <= 3 * 104
    -104 <= nums[i] <= 104
    nums 已按升序排列

    """
    这道题目的要求是:对给定的有序数组 nums 删除重复元素,在删除重复元素之后,每个元素只出现一次,并返回新的长度,上述操作必须通过原地修改数组的方法,使用 O(1) 的空间复杂度完成。
    
    由于给定的数组 nums 是有序的,因此对于任意 i<j,如果 nums[i]=nums[j],则对任意 i≤k≤j,必有 nums[i]=nums[k]=nums[j],即相等的元素在数组中的下标一定是连续的。利用数组有序的特点,可以通过双指针的方法删除重复元素。
    
    如果数组 nums 的长度为 0,则数组不包含任何元素,因此返回 0。
    
    当数组 nums 的长度大于 0 时,数组中至少包含一个元素,在删除重复元素之后也至少剩下一个元素,因此 nums[0] 保持原状即可,从下标 1 开始删除重复元素。
    
    定义两个指针fast 和 slow 分别为快指针和慢指针,快指针表示遍历数组到达的下标位置,慢指针表示下一个不同元素要填入的下标位置,初始时两个指针都指向下标 1。
    
    假设数组 nums 的长度为 n。将快指针 fast 依次遍历从 1 到 n−1 的每个位置,对于每个位置,如果 nums[fast] != nums[fast-1]
    �
    说明 nums[fast] 和之前的元素都不同,因此将 nums[fast] 的值复制到 nums[slow],然后将 slow 的值加 1,即指向下一个位置。
    
    遍历结束之后,从nums[0] 到 nums[slow−1] 的每个元素都不相同且包含原数组中的每个不同的元素,因此新的长度即为 slow,返回 slow 即可。
    
    """
    class Solution:
        def removeDuplicates(self, nums: List[int]) -> int:
            if not nums:
                return 0
            
            n = len(nums)
            fast = slow = 1
    
            while fast < n:
                if nums[fast] != nums[fast - 1]:
                    nums[slow] = nums[fast]
                    slow += 1
                fast += 1
            return slow

    80. 删除有序数组中的重复项 II

    https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array-ii/

    给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 最多出现两次 ,返回删除后数组的新长度。

    不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

    说明:

    为什么返回数值是整数,但输出的答案是数组呢?

    请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

    你可以想象内部操作如下:

    // nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
    int len = removeDuplicates(nums);

    // 在函数里修改输入数组对于调用者是可见的。
    // 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
    for (int i = 0; i < len; i++) {
        print(nums[i]);
    }
     

    示例 1:

    输入:nums = [1,1,1,2,2,3]
    输出:5, nums = [1,1,2,2,3]
    解释:函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3 。 不需要考虑数组中超出新长度后面的元素。

    示例 2:

    输入:nums = [0,0,1,1,1,1,2,3,3]
    输出:7, nums = [0,0,1,1,2,3,3]
    解释:函数应返回新长度 length = 7, 并且原数组的前五个元素被修改为 0, 0, 1, 1, 2, 3, 3 。 不需要考虑数组中超出新长度后面的元素。
     

    提示:

    1 <= nums.length <= 3 * 104
    -104 <= nums[i] <= 104
    nums 已按升序排列

    """
    因为给定数组是有序的,所以相同元素必然连续。我们可以使用双指针解决本题,遍历数组检查每一个元素是否应该被保留,如果应该被保留,就将其移动到指定位置。
    具体地,我们定义两个指针 slow 和 fast 分别为慢指针和快指针,其中慢指针表示处理出的数组的长度,快指针表示已经检查过的数组的长度,即 nums[fast] 表示待检查的第一个元素,nums[slow−1] 为上一个应该被保留的元素所移动到的指定位置。
    
    因为本题要求相同元素最多出现两次而非一次,所以我们需要检查上上个应该被保留的元素 nums[slow−2] 是否和当前待检查元素 nums[fast] 相同。
    
    当且仅当 nums[slow−2]=nums[fast] 时,当前待检查元素 nums[fast] 不应该被保留(因为此时必然有 nums[slow−2]=nums[slow−1]=nums[fast])。最后 slow 即为处理好的数组的长度。
    
    特别地,数组的前两个数必然可以被保留,因此对于长度不超过 2 的数组,我们无需进行任何处理,对于长度超过 2 的数组,我们直接将双指针的初始值设为 2 即可。
    """
    class Solution:
        def removeDuplicates(self, nums: List[int]) -> int:
            n = len(nums)
            if n < 3:
                return n
    
            fast = slow = 2
    
            while fast < n:
                if nums[fast] != nums[slow - 2]:
                    nums[slow] = nums[fast]
                    slow += 1
    
                fast += 1
            return slow
  • 相关阅读:
    VS Code 调试报错
    Nginx反向代理设置
    Nginx 的配置文件
    Nginx 的常用的命令
    CentOS7安装Nginx
    Docker配置
    Centos7 安装MySQL 5.7
    限制Redis使用的最大内存
    C#操作Redis
    Font Awesome 字体图标
  • 原文地址:https://www.cnblogs.com/Zzbj/p/15389890.html
Copyright © 2011-2022 走看看