Python数据结构与算法篇(三)-- 快慢指针与碰撞指针

        数组和链表代表着计算机最基本的两种存储形式:顺序存储和链式存储,所以他俩可以算是最基本的数据结构。数组是一种基础数据结构,可以用来处理常见的排序和二分搜索问题,典型的处理技巧包括双指针、滑动窗口等,数组是数据结构中的基本模块之一。因为字符串是由字符数组形成的,所以二者是相似的。

        双指针⼜分为中间向两端扩散的双指针、两端向中间收缩的双指针、快慢指针。

        双指针是一种思想,一种技巧或一种方法,并不是什么特别具体的算法,在二分查找等算法中经常用到这个技巧。具体就是用两个变量动态存储两个或多个结点,来方便我们进行一些操作。通常用在线性的数据结构中,比如链表和数组,有时候也会用在图算法中。

        在我们遇到像数组,链表这类数据结构的算法题目的时候,应该要想得到双指针的套路来解决问题。特别是链表类的题目,经常需要用到两个或多个指针配合来记忆链表上的节点,完成某些操作。链表这种数据结构也是树形结构和图的原型,所以有时候在关于图和树形结构的算法题目中也会用到双指针。

1 双指针

        双指针顾名思义,就是同时使用两个指针,在序列、链表结构上指向的是位置,在树、图结构中指向的是节点,通过或同向移动,或相向移动来维护、统计信息。在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向(快慢指针)或者相反方向(碰撞指针或者叫左右指针)的指针进行扫描,从而达到相应的目的。

        换言之,双指针法充分使用了数组有序这一特征,从而在某些情况下能够简化一些运算。

1.1 快慢指针

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

        利用快慢指针可以用来解决某些算法问题,比如:

  • 计算链表的中点:快慢指针从头节点出发,每轮迭代中,快指针向前移动两个节点,慢指针向前移动一个节点,最终当快指针到达终点的时候,慢指针刚好在中间的节点。
  • 判断链表是否有环:如果链表中存在环,则在链表上不断前进的指针会一直在环里绕圈子,且不能知道链表是否有环。使用快慢指针,当链表中存在环时,两个指针最终会在环中相遇。
  • 判断链表中环的起点:当我们判断出链表中存在环,并且知道了两个指针相遇的节点,我们可以让其中任一个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。
  • 求链表中环的长度:只要相遇后一个不动,另一个前进直到相遇算一下走了多少步就好了
  • 求链表倒数第 k 个元素:先让其中一个指针向前走k步,接着两个指针以同样的速度一起向前进,直到前面的指针走到尽头了,则后面的指针即为倒数第 k 个元素。(严格来说应该叫先后指针而非快慢指针)

        快慢指针在链表的详细使用,可以阅读下一篇文章——Python数据结构与算法篇(六)-- 链表的应用

1.2 碰撞指针

        对撞指针(或者称作左右指针)是指在数组中,将指向最左侧的索引定义为左指针(left),最右侧的定义为右指针(right),然后从两头向中间进行数组遍历。一般都是排好序的数组或链表,否则无序的话这两个指针的位置也没有什么意义。特别注意两个指针的循环条件在循环体中的变化,小心右指针跑到左指针左边去了。常用来解决的问题有:

  • 二分查找问题
  • n 数之和问题:比如两数之和问题,先对数组排序然后左右指针找到满足条件的两个数。如果是三数问题就转化为一个数和另外两个数的两数问题。以此类推。

2 常见题型

2.1 快慢指针

  • 掌握数组删除元素的直接覆盖操作
  • 双指针法

题库列表:

  • 26. 删除有序数组中的重复项 (快慢指针)

  • 27. 移除元素 (快慢指针)

  • 80. 删除有序数组中的重复项 II (快慢指针)

  • 283. 移动零(快慢指针)

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

        题目描述:给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        slow, fast = 0, 0
        while fast < len(nums):
            if nums[fast] != nums[slow]:
                slow += 1                   # 每个元素只出现一次,先 slow+1 再覆盖
                nums[slow] = nums[fast]
            fast += 1
        
        return slow + 1
Python数据结构与算法篇(三)-- 快慢指针与碰撞指针_第1张图片

27. 移除元素

        题目描述:给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。不要使用额外的数组空间,你必须仅使用 O ( 1 ) O(1) O(1) 额外空间并 原地 修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

1. 快慢指针

class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        # 快慢指针
        slow, fast = 0, 0
        while fast < len(nums):
            if nums[fast] != val:               # 删除指定元素,直接覆盖掉
                nums[slow] = nums[fast]
                slow += 1
            fast += 1
        return slow

        这里和有序数组去重的解法有一个细节差异,我们这里是先给 nums[slow] 赋值然后再给 slow++,这样可以保证 nums[0…slow-1] 是不包含值为 val 的元素的,最后的结果数组长度就是 slow。

2. 单指针

class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        '''拷贝覆盖'''
        ans = 0
        for num in nums:
            if num!= val:
                nums[ans] = num
                ans += 1
        return ans
Python数据结构与算法篇(三)-- 快慢指针与碰撞指针_第2张图片

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

        题目描述:给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O ( 1 ) O(1) O(1) 额外空间的条件下完成。

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        slow, fast = 1, 2       # 使得出现次数超过两次的元素只出现两次
        while fast < len(nums):
            if nums[fast] != nums[slow-1]:
                slow += 1
                nums[slow] = nums[fast]
            fast += 1
        return  slow + 1
Python数据结构与算法篇(三)-- 快慢指针与碰撞指针_第3张图片

通用解法:

        为了让解法更具有一般性,我们将原问题的 「最多保留 1 位」修改为「最多保留 k 位

        对于此类问题,我们应该进行如下考虑:

        由于是保留 k 个相同数字,对于前 k 个数字,我们可以直接保留。

        对于后面的任意数字,能够保留的前提是:与当前写入的位置前面的第 k 个元素进行比较,不相同则保留。

        此时,初始化时指针 slow 指向数组的起始位置(nums[k-1]),指针 fast 指向指针 slow 的后一个位置(nums[k])。随着指针 fast 不断向后移动,将指针 fast 指向的元素与指 slow 指向的元素进行比较:

  • 如果nums[fast] ≠ nums[slow-k+1],那么nums[slow + 1] = nums[fast];
  • 如果nums[fast] = nums[slow],那么指针q继续向后查找;

283. 移动零
        题目描述:给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。请注意 ,必须在不复制数组的情况下原地对数组进行操作。

1. 快慢指针
写法一:

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        slow, fast = 0, 0
        while fast < len(nums):
            if nums[fast] != 0:                 # 把前面为0的项覆盖掉
                nums[slow] = nums[fast]
                slow += 1
            fast += 1
        nums[slow:] = [0] * (len(nums)-slow)    # 把后面填充为 0
        return nums
Python数据结构与算法篇(三)-- 快慢指针与碰撞指针_第4张图片

写法二:

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        slow, fast = 0, 0
        while fast < len(nums):
            if nums[fast]:
                if nums[slow] == 0:             # 快指针不为零,慢指针为零,进行交换
                    nums[slow], nums[fast] = nums[fast], nums[slow]
                slow += 1
            fast += 1
        return nums
Python数据结构与算法篇(三)-- 快慢指针与碰撞指针_第5张图片

2.2 左右指针

题库列表:

  • 88. 合并两个有序数组:如何将数组所有元素整体后移,防止数组覆盖?

  • 167. 两数之和 II - 输入有序数组(有序数列的首尾双指针)

  • 125. 验证回文串

  • 344. 反转字符串

  • 151. 反转字符串中的单词

  • 345. 反转字符串中的元音字母

  • 11. 盛最多水的容器:经典题目

  • 42. 接雨水:经典题目

  • 75. 颜色分类(左右指针,三色旗)

  • 844. 比较含退格的字符串(左右指针)

88. 合并两个有序数组

        题目描述:给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。

1. 左右指针

class Solution:
    def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
        """
        nums1[m:] = nums2               # 直接合并后排序
        nums1.sort()           
        """
        p0, p1, p2 = m-1, n-1, m+n-1
        while p0 >= 0 or p1 >= 0:
            if p0 == -1:                # num1 已经循环结束
                nums1[p2] = nums2[p1]
                p1 -= 1
            elif p1 == -1:              # # num2 已经循环结束
                nums1[p2] = nums1[p0]
                p0 -= 1
            elif nums1[p0] > nums2[p1]:
                nums1[p2] = nums1[p0]
                p0 -= 1
            else:
                nums1[p2] = nums2[p1]
                p1 -= 1
            p2 -= 1
class Solution:
    def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
        p0, p1, p2 = m-1, n-1, m+n-1
        while p0 >= 0 and p1 >= 0:
            if nums1[p0] > nums2[p1]:   # num1 更大
                nums1[p2] = nums1[p0]
                p0 -= 1
            else:                      
                nums1[p2] = nums2[p1]
                p1 -= 1
            p2 -= 1
        # 最后拼接没有遍历完的数组,由于直接在 nums1 上操作,只需要在nums2没遍历完,拼接起来就可以了
        if p1 >= 0:
            nums1[:p2+1] = nums2 [:p1+1]  

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

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

class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        left, right = 0, len(numbers)-1
        while left < right:
            two_sum = numbers[left] + numbers[right]
            if two_sum == target:
                return [left+1, right+1]    # 题目要求下标从1开始
            elif two_sum > target:          # 两数之和大于目标值,右边的值太大了,right--
                right -= 1
            else:                           # 两数之和小于目标值,左边值太小,left++
                left += 1
Python数据结构与算法篇(三)-- 快慢指针与碰撞指针_第6张图片

125. 验证回文串

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

import re
class Solution:
    def isPalindrome(self, s: str) -> bool:
        '''
        # 思路一:正则表达式
        if not s:
            return True
        s = s.lower()
        pattern = re.compile(r'[^a-z0-9]')   # 正则表达式,把数字和字母都剔除掉
        new_str = pattern.sub('', s)
        return new_str == new_str[::-1]
        
        # 字符串预处理
        new_str = ''.join(ch.lower() for ch in s if ch.isalnum())
        return new_str == new_str[::-1]
        '''
        # 左右指针
        s = s.lower()
        left, right = 0, len(s) - 1
        while left < right:
            # 两个循环找到左侧和右侧为字母或者数字的位置
            while left < len(s) - 1 and not s[left].isalnum():
                left += 1
            while right > 0 and not s[right].isalnum():
                right -= 1
            if left >= right:                   # 判断移动过后的left,right是否满足left在左,right在右的相对位置
                break
            else:
                if s[left] != s[right]:         # 如果左右指针所指不同,则肯定不构成回文
                    return False
                else:                           # 左右指针各前进一步
                    left += 1
                    right -= 1
        return True

        这里使用了正则表达式移除所有非字母数字字符,然后判断新的字符串是否是回文,也可以使用双指针,直接一次遍历,遇到字母数字字符就进行判断。

151. 反转字符串中的单词

        题目描述:给你一个字符串 s,请你反转字符串中 单词 的顺序。单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。

1. 双指针

class Solution:
    def reverseWords(self, s: str) -> str:
        s = s.strip() # 删除首尾空格
        i = j = len(s) - 1
        res = []
        while i >= 0:
            while i >= 0 and s[i] != ' ': i -= 1    # 搜索首个空格
            res.append(s[i + 1: j + 1])             # 添加单词
            while s[i] == ' ': i -= 1               # 跳过单词间空格
            j = i                                   # j 指向下个单词的尾字符
        return ' '.join(res)                        # 拼接并返回
Python数据结构与算法篇(三)-- 快慢指针与碰撞指针_第7张图片

2. 分割 + 倒序

class Solution:
    def reverseWords(self, s: str) -> str: 
        return ' '.join(s.strip().split()[::-1])
Python数据结构与算法篇(三)-- 快慢指针与碰撞指针_第8张图片

344. 反转字符串

        题目描述:编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O ( 1 ) O(1) O(1) 的额外空间解决这一问题。

class Solution:
    def reverseString(self, s: List[str]) -> None:
        """
        return s.reverse()
        s[:] = s[::-1]
        """
        # 左右指针法
        left, right = 0, len(s)-1
        while left <= right:
            s[left], s[right] = s[right], s[left]
            left += 1
            right -= 1
        return s
Python数据结构与算法篇(三)-- 快慢指针与碰撞指针_第9张图片

345. 反转字符串中的元音字母

        题目描述:给你一个字符串 s ,仅反转字符串中的所有元音字母,并返回结果字符串。元音字母包括 ‘a’、‘e’、‘i’、‘o’、‘u’,且可能以大小写两种形式出现不止一次。

class Solution:
    def reverseVowels(self, s: str) -> str:
        str_set = set("aeiouAEIOU")
        head, tail = 0, len(s) - 1
        str_list = list(s)
        while head < tail:
            if str_list[head] in str_set and str_list[tail] in str_set:
                str_list[head], str_list[tail] = str_list[tail], str_list[head]
                head += 1
                tail -= 1
            elif str_list[head] in str_set and str_list[tail] not in str_set:
                tail -= 1
            elif str_list[head] not in str_set and str_list[tail] in str_set:
                head += 1
            else:
                head += 1
                tail -= 1
        return ''.join(str_list)

11. 盛最多水的容器

        题目描述:给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。返回容器可以储存的最大水量。

class Solution:
    def maxArea(self, height: List[int]) -> int:
        # 双指针,移动小的那一边
        head, tail = 0, len(height)-1
        res = 0
        while head < tail:
            if height[head] < height[tail]:
                res = max(res, height[head]*(tail-head))
                head += 1
            else:
                res = max(res, height[tail]*(tail-head))
                tail -= 1
        return res
Python数据结构与算法篇(三)-- 快慢指针与碰撞指针_第10张图片

42. 接雨水

        题目描述:

Python数据结构与算法篇(三)-- 快慢指针与碰撞指针_第11张图片
class Solution:
    def trap(self, height: List[int]) -> int:
        ans = 0
        left, right = 0, len(height) - 1
        leftMax = rightMax = 0

        while left < right:
            leftMax = max(leftMax, height[left])
            rightMax = max(rightMax, height[right])
            if height[left] < height[right]:
                ans += leftMax - height[left]
                left += 1
            else:
                ans += rightMax - height[right]
                right -= 1
        
        return ans
Python数据结构与算法篇(三)-- 快慢指针与碰撞指针_第12张图片

75. 颜色分类

        题目描述:给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

1. 单指针

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        i = 0
        length = len(nums)
        for j in range(length):
            if nums[j] == 0:
                nums[i], nums[j] = nums[j], nums[i]
                i += 1
        for k in range(i, length):
            if nums[k] == 1:
                nums[k], nums[i] = nums[i], nums[k]
                i += 1
Python数据结构与算法篇(三)-- 快慢指针与碰撞指针_第13张图片

2. 左右指针

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        # 定义三个变量,p0 表示数组最左边0的区域,p1是数组最右边2的区域
        i, p0, p1 = 0, 0 , len(nums)-1
        while i <= p1:
            # 如果当前指向的是 0,就把这个元素交换到数组左边
			# 也就是跟 p0 指针交换,之后cur,p0 就往前一动一位
            if nums[i] == 0:
                nums[i], nums[p0] = nums[p0], nums[i]
                p0 += 1
                i += 1
            # 如果当前指向的是2,就把这个元素交换到数组右边
			# 也就是跟p2指针交换,注意此时cur指针就不用移动了
			# 因为右边的一片区域都是2,只要把元素交换过去就可以了,cur不用移动
            elif nums[i] == 2:
                nums[i], nums[p1] = nums[p1], nums[i]
                p1 -= 1
            # 如果是1的话,就不用交换	
            else:
                i += 1
Python数据结构与算法篇(三)-- 快慢指针与碰撞指针_第14张图片

844. 比较含退格的字符串

        题目描述:给定 s 和 t 两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true 。# 代表退格字符。如果对空文本输入退格字符,文本继续为空。

1. 双指针

class Solution:
    def backspaceCompare(self, s: str, t: str) -> bool:
        s_skip_num = 0                              # 记录s的#数量
        t_skip_num = 0                              # 记录t的#数量
        i = len(s) - 1
        j = len(t) - 1
        while True: 
            while (i >= 0):                         # 从后向前,消除S的#
                if (s[i] == '#'): 
                    s_skip_num += 1
                else:
                    if (s_skip_num > 0): 
                        s_skip_num -= 1
                    else:
                        break;
                i -= 1

            while (j >= 0):                         # 从后向前,消除T的#
                if (t[j] == '#'):
                    t_skip_num += 1
                else:
                    if (t_skip_num > 0):
                        t_skip_num -= 1
                    else:
                        break
                j -= 1
            
            # 后半部分
            # 消除完了,接下来比较s[i] != t[j]
            if (i < 0 or j < 0):
                break                              # s 或者 t 遍历到头了
            if (s[i] != t[j]):
                return False
            i -= 1
            j -= 1
        # 说明S和T同时遍历完毕
        if (i == -1 and j == -1):
            return True
        return False

2. 栈模拟法

class Solution:
    def backspaceCompare(self, s: str, t: str) -> bool:
        return self.back_strip(s) == self.back_strip(t)

    def back_strip(self, s):
        s_list = []
        for item in s:
            if item == '#':
                if s_list:
                    s_list.pop()
            else:
                s_list.append(item)
        return ''.join(s_list)

3. 移除元素

class Solution:
    def backspaceCompare(self, s: str, t: str) -> bool:
        list_s, list_t = list(s), list(t)
        idx_s, idx_t = 0, 0
        for i in range(len(list_s)):
            if list_s[i] != '#':
                list_s[idx_s] = list_s[i]
                idx_s += 1
            else:
                idx_s -= 1 if idx_s > 0 else 0
        for i in range(len(list_t)):
            if list_t[i] != '#':
                list_t[idx_t] = list_t[i]
                idx_t += 1
            else:
                idx_t -= 1 if idx_t > 0 else 0
        return list_s[:idx_s] == list_t[:idx_t]

977. 有序数组的平方

        题目描述:给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

class Solution:
    def sortedSquares(self, nums: List[int]) -> List[int]:
        """先平方后排序,每次利用已排序的先验信息,最大值必定在两端出现"""
        length = len(nums)
        left, right = 0, length-1
        result = []
        while left <= right:
            if nums[left] ** 2 < nums[right] ** 2:
                result.append(nums[right] ** 2)
                right -= 1
            else:
                result.append(nums[left] ** 2)
                left += 1
        return result[::-1]

2.3 其他

题库列表:

  • 5. 最长回文子串

  • 15. 三数之和

  • 16. 最接近的三数之和

  • 18. 四数之和

  • 56. 合并区间:数组类操作

5. 最长回文子串

        题目描述:给你一个字符串 s,找到 s 中最长的回文子串。如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。

        思路分析:找回⽂串的难点在于,回⽂串的的⻓度可能是奇数也可能是偶数,解决该问题的核⼼是从中⼼向两端扩散的双指针技巧。如果回⽂串的⻓度为奇数,则它有⼀个中⼼字符;如果回⽂串的⻓度为偶数,则可以认为它有两个中⼼字符。

class Solution:
    """找到以i和i+1为中心的回文字符串"""
    def longestPalindrome(self, s: str) -> str:
        res = ""
        for i in range(len(s)):
            s1 = self.palindrome(s, i, i)
            s2 = self.palindrome(s, i, i+1)
            res = s1 if len(s1) > len(res) else res
            res = s2 if len(s2) > len(res) else res
        return res

    def palindrome(self, s, l, r):
        while l >= 0 and r < len(s) and s[l] == s[r]:
            l -= 1
            r += 1
        return s[l+1:r]

15. 三数之和
        题目描述:给你一个整数数组 nums ,判断是否存在三元组 [ n u m s [ i ] , n u m s [ j ] , n u m s [ k ] ] [nums[i], nums[j], nums[k]] [nums[i],nums[j],nums[k]] 满足 i ! = j 、 i ! = k i != j、i != k i!=ji!=k j ! = k j != k j!=k ,同时还满足 n u m s [ i ] + n u m s [ j ] + n u m s [ k ] = = 0 nums[i] + nums[j] + nums[k] == 0 nums[i]+nums[j]+nums[k]==0。请你返回所有和为 0 且不重复的三元组。注意:答案中不可以包含重复的三元组。

from collections import defaultdict
class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums_sorted = sorted(nums)
        result = []
        for i in range(len(nums_sorted)):
            if nums_sorted[i] > 0:                              # 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
                return result
            '''
            # 错误去重a方法,将会漏掉-1,-1,2 这种情况
            if (nums[i] == nums[i + 1]) {
                continue;
            }
            '''
            if i>0 and nums_sorted[i] == nums_sorted[i-1]:      # 去重
                continue
            left = i+1
            right = len(nums_sorted)-1
            while left < right:
                '''
                # 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0, 0, 0 这种三元组
                while left < right and nums_sorted[left] == nums_sorted[left+1]:    left += 1
                while left < right and nums_sorted[right] == nums_sorted[right-1]:  right -= 1
                '''
                if nums_sorted[i]+nums_sorted[left]+nums_sorted[right] == 0:
                    result.append([nums_sorted[i], nums_sorted[left], nums_sorted[right]])
                    # 在要增加 left,减小 right,但是不能重复,比如: [-2, -1, -1, -1, 3, 3, 3], 
                    # i = 0, left = 1, right = 6, [-2, -1, 3] 的答案加入后,需要排除重复的 -1 和 3
                    while left < right and nums_sorted[left] == nums_sorted[left+1]:
                        left += 1
                    while left < right and nums_sorted[right] == nums_sorted[right-1]:
                        right -= 1
                    # 找到答案时双指针同时收缩
                    left += 1
                    right -= 1
                elif nums_sorted[i]+nums_sorted[left]+nums_sorted[right] > 0:
                    right -= 1
                else:
                    left += 1

        return result
Python数据结构与算法篇(三)-- 快慢指针与碰撞指针_第15张图片

16. 最接近的三数之和
        题目描述:给你一个长度为 n 的整数数组 nums 和 一个目标值 target。请你从 nums 中选出三个整数,使它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在恰好一个解。

class Solution:
    def threeSumClosest(self, nums: List[int], target: int) -> int:
        nums_sorted = sorted(nums)
        diff = float('inf')         # 初始化,因为找最小值,因此把初始值设置成实数的最大值
        length = len(nums)          # 排序是前提
        for i in range(length):
            if i > 0 and nums_sorted[i] == nums_sorted[i-1]:    # 常见的剪枝操作
                continue
            left = i+1                                          # 双指针:指针对撞
            right = length-1
            while left < right:
                temp = nums_sorted[left] + nums_sorted[right] + nums_sorted[i]
                if abs(temp-target) < diff:     
                    diff = abs(temp-target)
                    ans = temp
                # 不管是变小还是变大,尝试的作用是让 temp 与 target 更接近,即 temp 与 target 的绝对值之差越来越小
                if temp > target:        # 如果大了,尝试右边界收缩一格,让 temp 变小
                    right -= 1
                elif temp < target:     # 如果小了,尝试左边界收缩一格,让 target 变大
                    left += 1
                else:                   # 如果已经等于 target 的话, 肯定是最接近的,根据题目要求,返回这三个数的和
                    return target
        return ans
Python数据结构与算法篇(三)-- 快慢指针与碰撞指针_第16张图片

18. 四数之和

        题目描述:给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

  • 0 <= a, b, c, d < n
  • a、b、c 和 d 互不相同
  • nums[a] + nums[b] + nums[c] + nums[d] == target

        你可以按 任意顺序 返回答案 。

class Solution:
    def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
        nums_sorted = sorted(nums)
        result = []
        length = len(nums)
        for i in range(length): 
            # 剪枝处理
            if nums_sorted[i] > target and nums_sorted[i] >= 0:
                break
            # 对 nums_sorted[i] 去重
            if i > 0 and nums_sorted[i] == nums_sorted[i-1]:
                continue
            for j in range(i+1, length):        # 比原来多一层循环
                # 二级剪枝处理
                if nums_sorted[i] + nums_sorted[j] > target and nums_sorted[i] + nums_sorted[j] >= 0:
                    break
                # 对 nums_sorted[j] 去重
                if j > i+1 and nums_sorted[j] == nums_sorted[j-1]:
                    continue
                left = j + 1
                right = length - 1
                while left < right:
                    if nums_sorted[i] + nums_sorted[j] + nums_sorted[left] + nums_sorted[right] == target:
                        result.append([nums_sorted[i], nums_sorted[j], nums_sorted[left], nums_sorted[right]])
                        while left < right and nums_sorted[left] == nums_sorted[left+1]:
                            left += 1
                        while left < right and nums_sorted[right] == nums_sorted[right-1]:
                            right -= 1
                        left += 1
                        right -= 1
                    elif nums_sorted[i] + nums_sorted[j] + nums_sorted[left] + nums_sorted[right] > target:
                        right -= 1
                    else:
                        left += 1
        return result

56. 合并区间

        题目描述:以数组 intervals 表示若干个区间的集合,其中单个区间为 i n t e r v a l s [ i ] = [ s t a r t i , e n d i ] intervals[i] = [starti, endi] intervals[i]=[starti,endi]。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        result= []
        intervals.sort(key=lambda x:x[0])
        for interval in intervals:
            # 如果列表为空,或者当前区间与上一区间不重合,直接添加
            if not result or result[-1][1] < interval[0]:
                result.append(interval)
            else:
                # 否则的话,我们就可以与上一区间进行合并
                result[-1][1] = max(result[-1][1], interval[1])
        return result

左右指针与快慢指针暂时告一段落,但还有很多自己不满意的地方,后面在学习中持续补充,谢谢大家的鼓励和支持!


参考

  • 双指针套路总结:https://zhuanlan.zhihu.com/p/95747836
  • 数组+常见题型与解题策略:https://blog.csdn.net/qq_42647903/article/details/120594856
  • 算法与数据结构(一):滑动窗口法总结:https://blog.csdn.net/Dby_freedom/article/details/89066140

你可能感兴趣的:(Algorithms,and,Data,Structures,LC,and,PAT,数组,双指针,快慢指针,碰撞指针,连续区间)