详解双指针算法

一、双指针算法

​ 双指针算法是指利用两个指针遍历数组(链表),左右指针相向前进同向前进,在遍历过程中根据某种限制条件进行筛选,通常可以把时间复杂度降低至O(n)。

二、双指针题目

2.1 leetcode80-删除排序数组中的重复项II

题目描述

给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素最多出现两次,返回移除后数组的新长度。

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

示例 1:

给定 nums = [1,1,1,2,2,3],

函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3 。

你不需要考虑数组中超出新长度后面的元素。
示例 2:

给定 nums = [0,0,1,1,1,1,2,3,3],

函数应返回新长度 length = 7, 并且原数组的前五个元素被修改为 0, 0, 1, 1, 2, 3, 3 。

你不需要考虑数组中超出新长度后面的元素。

思路分析

​ 设置左右指针,且双指针同向前进,左指针的作用是维护只有两个相同数字的区域,右指针的作用是遍历数组, 将满足条件的数字交换给左指针。

​ 右指针在遍历时需要设置计数器以及记录当前的数字,并及时更新当前数字的个数。

​ 当个数大于等于3时,跳过此数字,右指针继续前进。

​ 当个数小于3时,将数字交换给左指针,同时左指针向前移动一格,右指针继续前进。

​ 时间复杂度:O(n) //只遍历一遍数组

​ 空间复杂度:O(1) //原地修改数组

具体代码

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        '''
            双指针
            思路分析:
                设置左右指针,且指针同向前进,左指针记录到3个相同值时,留在原地,
            右指针继续前进,当遇到不同值时,右指针将值交换给左指针,然后左指针向
            前移动一格。
                左指针的作用是维护只有两个相同数字的区域,右指针的作用是遍历数组,
            将满足条件的数字交换给左指针。
                右指针在遍历时需要设置计数器,计算当前数字的个数。
                当个数大于等于3时,跳过此数字。
                当个数小于3时,将数字交换给左指针。
        '''
        if len(nums) == 0:
            return 0
        left, right = 0, 0
        count = 0
        cur = nums[0]
        while right < len(nums):
            
            if cur == nums[right]:
                count += 1
                if count <= 2 :
                    nums[left] = nums[right]
                    left += 1
            else:
                cur = nums[right]
                count = 1
                nums[left] = nums[right]
                left += 1

            right += 1
        
        return left

2.2 leetcode40-接雨水

题目描述

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

img

上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 图片来自leetCode

思路分析

解法一 按行取

​ 按照行取水,每行有水的地方左右都有柱子,即水坑的列高都小于两边柱子的列高。
​ 按行进行遍历,行数由最高的行确定。遍历height数组
​ 当遇到高于等于当前行号的柱子时,表示左墙壁存在,开始取水。
​ 当遇到小于当前行号的柱子时,表示坑存在,进行蓄水。
​ 当又遇到大于等于当前行号的柱子时,表示右墙壁(下一个坑的左墙壁)存在,累加坑中的蓄水量,并重置蓄水量为0。

​ 只有当左右墙壁均存在时,才会蓄水,进行蓄水量的累加计算。

​ 开始时sum = 0

​ 若height[i] >= col,则说明左墙壁存在,开始取水,start = True

​ 当start为true时有两种情况.

​ 若height[i] <= col,tmp = tmp + 1.

​ 若height[i] > col, sum = sum + tmp

​ 时间复杂度:O(m*n)

​ 空间复杂度:O(1)

解法二 按列取

​ 按照列取水,某一列可以取水,那么这一列两边必然也存在高于或等于当前列高的左右墙壁。若此列可以取水,由于木桶效应,取水量自然取决于左右两边最高墙壁中较矮的那一面墙壁。

​ 判断某一列是否积水,简单说会出现三种情况:

​ 当前柱子高度小于等于较矮的柱子,此时此列可以蓄水,蓄水值 = min(leftMaxHeight, rightMaxHeight) - height[i]

​ 当前柱子高度大于较矮的柱子,此时列不可以蓄水。

​ 时间复杂度: O(m*n)

​ 空间复杂度:O(1)

解法三 动态规划

​ 我们注意到在上一种解法中,求每列的左右最高墙壁时都要重新遍历数组,考虑利用dp数组记录左右两边的最高墙壁高度。在取水之前先找出每列的左右两边的最高墙壁,要用时直接查表即可。

​ 动态转移方程如下:

​ dpLeftMaxHeight[i] = max( dpLeftMaxHeight[i-1], height[i])

​ dpRightMaxHeight[j] = max(dpRightMaxHeight[j+1], height[j])

​ 时间复杂度:O(n)

​ 空间复杂度:O(n)

解法四 双指针

​ 在上一种解法中,我们使用了两个dp数组来记录每列的左右两边的最高墙壁高度。因为取水情况取决于左右两边最高墙壁的较小值,考虑在遍历过程中使用左右双指针。当左指针所在列较高时,从右边开始取水。当右指针所在列较高时,从左边开始取水。

具体代码

class Solution:
    def trap(self, height: List[int]) -> int:
        left, right = 0 , len(height)-1
        maxLeft, maxRight = 0, 0 
        sum = 0
        while left < right:
            if height[left] < height[right]:
                maxLeft = max(maxLeft, height[left])
                tmp = maxLeft - height[left]
                sum += tmp
                left = left + 1
            else:
                maxRight = max(maxRight, height[right])
                tmp = maxRight - height[right]
                sum += tmp
                right = right -1
        return sum

2.3 最小覆盖子串

题目描述

​ 给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字母的最小子串。

输入: S = “ADOBECODEBANC”, T = “ABC”
输出: “BANC”
说明:

如果 S 中不存这样的子串,则返回空字符串 “”。
如果 S 中存在这样的子串,我们保证它是唯一的答案。

思路分析

​ 滑动窗口是指维护一个window,判断窗口中是否包含need字符串中的所有字母。
​ 滑动窗口为[left, right],首先向前移动right指针,扩大滑动窗口,直到窗口中包含所有字母。此时找到一个可行解。然后向前移动left指针,缩小滑动窗口范围,同时更新最小字符串的数量。
​ 直到window中不再包含所有need中的字母,重新移动right指针,重复之前的步骤,
​ 直到right指针大于字符串s的长度。

​ 关键是如何判断窗口中包含need字符串中的所有字母。使用字典记录每个字符串中每个字母的出现次数。

​ window = { A: 1, B: 2, C: 3}

​ need = { B: 1, C: 2}

具体代码

class Solution:
    def containAll(self, window: dict, need: dict) -> bool:
        res = True
        for key,val in need.items():
            count = window.get(key)
            if count == None or count < val:
                res = False
                break
        return res

    def minWindow(self, s: str, t: str) -> str:
        left, right = 0, 0
        window, need = {}, {}
        #统计need中的字母个数
        for c in t:
            if need.get(c) == None:
                need[c] = 1
            else:
                need[c] = need[c] + 1
        
        minCount = len(s) + 1
        res = [left, right]
        while right < len(s):
            if window.get(s[right]) == None:
                window[s[right]] = 1
            else:
                window[s[right]] = window[s[right]] + 1
            #扩大窗口找到可行解
            while self.containAll(window, need):
                count = right - left  + 1
                if count < minCount:
                    minCount = count
                    res = [left, right]
                #向右移动左指针,缩小窗口找到最优解
                window[s[left]] = window[s[left]] - 1
                left = left + 1

            right = right + 1
        if minCount == (len(s)+1):
            return ""
        
        return s[res[0] : res[1]+1]
动左指针,缩小窗口找到最优解
                window[s[left]] = window[s[left]] - 1
                left = left + 1

            right = right + 1
        if minCount == (len(s)+1):
            return ""
        
        return s[res[0] : res[1]+1]

你可能感兴趣的:(leetcode)