查看: 8253| 回复: 4
跳转到指定楼层
上一主题 下一主题
收起左侧

[Leetcode] 双指针完全攻略

 
全局:

注册一亩三分地论坛,查看更多干货!

您需要 登录 才可以下载或查看附件。没有帐号?注册账号

x
本帖最后由 SimonXDong 于 2020-1-8 08:16 编辑

双指针 two pointer 问题非常的非常的常见。一般来说,two pointer 的问题可以分为两类。

异构指针
一般是针对 array 类型的题目。array 当中有两类元素,他们 之间 或者 各自 有某种顺序。我们维护两个 pointer,每个 pointer 负责一种一类,这样就能在保证顺序的情况下,进行某种操作。

关键词:

array 类型
两类元素
有一定的有序性

可能直接这么概括比较抽象,我们来看几道 LeetCode 的高频题目。

283. Move Zeroes




这道题的题意就是,如果你只能用交换操作,如何才能把0都移动到后面,并保持非零元素相对顺序不变。很明显,这道题完全符合上面总结的三种关键词。

所以这道题的思路就是,用两个 pointer ,一个 pointer 指向 当前最靠前的 0,另外遍历非零元素即可。但需要保证指向0的pointer一定要在另外一个pointer之前才可以。

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        i, j = 0, 0   # i 是指向 0 的 pointer,j 是指向非零的 pointer

        while i < len(nums) and j<len(nums):
            while nums!=0:
                i += 1
                if i >= len(nums):
                    return nums
                j=i

            while nums[j]==0:
                j += 1
                if j >= len(nums):
                    return nums
            nums, nums[j] = nums[j], nums
        return nums

当然这一题还有一种比较讨巧的写法:
第一次遇到非零元素,将非零元素与数组 nums[0] 互换,第二次遇到非零元素,将非零元素与 nums[1] 互换,第三次遇到非零元素,将非零元素与 nums[2],以此类推,直到遍历完数组

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        i = j = 0
        for i in range(len(nums)):
            if nums != 0:
                nums[j] , nums= nums , nums[j]
                j += 1


这种写法为什么会work呢?因为这么做可以保证,在进行第 n 次替换的时候,之前的非零元素已经全部完成了替换,并且全部汇集到了前 n - 1 个位置,所以本次替换一定是没有问题的。



这个其实就是后面讲的 Cyclic Sort 。



88. Merge Sorted Array




这个题很明显也符合上面三个关键词。但是当你去使用 two pointer 的时候,in-place 的操作会导致一些数被覆盖掉。要解决这个覆盖的问题,cost也比较大,就是要频繁的移动 nums1。

这个时候,一个非常常用的编程思维是:

换个方向,别有洞天。
对于这道题,如果从后向前的使用 two pointer,就没有了覆盖的问题。因为后面的值本来就是没有用的,随便覆盖。




剩下的操作就跟 merge two sorted list 没什么差别了。

class Solution(object):
    def merge(self, nums1, m, nums2, n):
        """
        :type nums1: List[int]
        :type m: int
        :type nums2: List[int]
        :type n: int
        :rtype: void Do not return anything, modify nums1 in-place instead.
        """
        # two get pointers for nums1 and nums2
        p1 = m - 1
        p2 = n - 1
        # set pointer for nums1
        p = m + n - 1

        # while there are still elements to compare
        while p1 >= 0 and p2 >= 0:
            if nums1[p1] < nums2[p2]:
                nums1[p] = nums2[p2]
                p2 -= 1
            else:
                nums1[p] =  nums1[p1]
                p1 -= 1
            p -= 1

        # add missing elements from nums2
        nums1[:p2 + 1] = nums2[:p2 + 1]

FaceBook面试真题:Dot Product of Sparse Vectors

Suppose we have very large sparse vectors (most of the elements in vector are zeros)Find a data structure to store themCompute the Dot Product.
Follow-up:
What if one of the vectors is very small?
很容易想到,sparse vector可以用 dict (hashmap)来存。 对于稀疏矩阵,我们需要找到对应位置的数来乘,这个地方可以用到 two pointer 的来找。因为两个 sparse vector 的 (position, value) 可以是有序的。

a = [(1,2),(2,3),(100,5)]
b = [(0,5),(1,1),(100,6)]

i = 0; j = 0  # 两个 pointer
result = 0
while i < len(a) and j < len(b):
    if a[0] == b[j][0]:
        result += a[1] * b[j][1]
        i += 1
        j += 1
    elif a[0] < b[j][0]:
        i += 1
    else:
        j += 1
print(result)


二分法


二分查找也是一种非常有用的编程思想,他的实现方式也是通过不断变化左右指针得到的。

def binarySearch(nums, target):
    left, right = 0, len(nums) - 1
    while left <= right:
        mid = (right + left)/2
        if nums[mid] == target:
            return mid
        elif nums[mid] < target:
            left = mid + 1
        elif nums[mid] > target:
            right = mid - 1
    return -1
LeetCode 42. Trapping Rain Water
在解决这道题是,其实有两个难点。首先第一个难点就是,如何对这个问题建模。我们在建模的时候,不一定要顺着直观的感觉去走。比如本题中,如果直观的去建模的话,那就是要先找到有多少个“坑”,然后分别算每个“坑”能装多少水。但是当你去找“坑”的时候,你就会发现,“坑”的情况是很复杂的,而这种复杂恰恰就是程序难写的根源。

对于这一题,有一个非常简单的方法可以解决。我们现在不去找有多少坑,而是看每一个 bin 能对最终装水贡献多少。




比如上面图中的第一个bin,贡献是0,第二个贡献也是0,而第三个贡献是1.第四个贡献是0,第五个贡献是1,第六个贡献是2...

实际上:

位置 i 能容下雨水量:

min(left_max, right_max) - height

所以,问题就变成了:如何找所有位置的左右两边的柱子的最大值?

很明显,我们可以用三种方法来解决:

动态规划。
我们可以分别算每个位置的左边最大值和右边最大值。

class Solution:
    def trap(self, height: List[int]) -> int:
        if not height: return 0
        n = len(height)
        max_left = [0] * n
        max_right = [0] * n
        max_left[0] = height[0]
        max_right[-1] = height[-1]
        # 找位置i左边最大值
        for i in range(1, n):
            max_left = max(height, max_left[i-1])
        # 找位置i右边最大值
        for i in range(n-2, -1, -1):
            max_right = max(height, max_right[i+1])
        #print(max_left)
        #print(max_right)
        # 求结果
        res = 0
        for i in range(n):
            res += min(max_left, max_right) - height
        return res
2. 栈

这道题,直观上讲,如果想找能盛水的柱子,肯定要有回退操作的。因为只有确定了后面有更高的柱子,才能确定某个柱子能装水。凡是有这种需要回退的题目,栈都是一个非常好的选择。

如果要用栈的,基本思路是,如果待入栈元素比栈顶小,直接入栈。反之,栈顶元素出栈,进行计算,然后再入栈。这样的一个栈,很明显就是一个单调栈。

class Solution:
    def trap(self, height: List[int]) -> int:
        if not height: return 0
        n = len(height)
        stack = []
        res = 0
        for i in range(n):
            #print(stack)
            while stack and height[stack[-1]] < height:
                tmp = stack.pop()
                if not stack: break
                res += (min(height, height[stack[-1]]) - height[tmp]) * (i-stack[-1] - 1)
            stack.append(i)
        return res
3. 双指针

这一题用双指针来写还是有些难度的,思路会比较绕,这里详细的说一说。用双指针解,和上面解法相比的好处在于空间复杂度是


,也就是常说的边走边算,不用额外存储一些东西。

首先我们定义两个指针,left 和 right,前者从最左边开始,后者从最右边开始。然后我们维护两个最大值,l_max是height[0..left]中最高柱子的高度,r_max是height[right..end]的最高柱子的高度。

上面提到过,每个柱子能装的水的量是由

min(left_max, right_max) - height

决定的。

但是仔细分析会发现,我们要得到这个值


并不一定非得把

left_max
和
right_max

都算出来。例如,如果我们知道l_max < r_max,至于这个 r_max 是不是右边最大的,无所谓。现有的信息足够我们算出

min(left_max, right_max)

一定是l_max。

class Solution:
    def trap(self, height: List[int]) -> int:
        if not height: return 0
        left = 0
        right = len(height) - 1
        res = 0
        # 记录左右边最大值
        left_max = height[left]
        right_max = height[right]
        while left < right:
            if left_max < right_max:
                res += left_max - height[left]
                left += 1
                left_max = max(left_max, height[left])
            else:
                res += right_max - height[right]
                right -= 1
                right_max = max(right_max, height[right])
        return res
这道题还是比较难的,下面总结一下这道题目。

对暴力解法的思考是很重要的,暴力解法是优化解法的基础。通过对暴力解法的思考、分析,可以帮助我们得到一些优化解法的细节和思路。这道题难就难在,暴力枚举的方法都不是很好看出来。 所以对于这种问题,我们不要想整体,而应该去想局部。对于这道题,我们不要去想每个坑的情况,而应该去想每个柱子的情况。

快慢指针
快慢指针一般用于链表。为什么链表的问题,很多都要用到快慢指针呢?因为链表的idexing是很弱的,每次只能看到自己的 next(非常“短视”)。通过快慢指针,我们可以“之后”或者“之前”的一些元素,实际上是增加了“视野”。

下面看几道题目。

判断链表是否有环
经典解法就是用两个指针,一个每次前进两步,一个每次前进一步。如果不含有环,跑得快的那个指针最终会遇到 null,说明链表不含环;如果含有环,快指针最终会超慢指针一圈(只要有环,总是能追的上),和慢指针相遇,说明链表含有环。

def hasCyle(head):
    fast, slow = head, head

    while (fast != None) and (fast.next != None):
        fast = fast.next.next
        slow = slow.next
        if fast == slow:
            return True

    return False
已知链表有环,找到环的起点
当快慢指针相遇时,让其中任一个指针重新指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。这里就不展开分析了。

类似的思路,还可以解决“寻找链表中点”的问题。还是让快指针每次走两步,慢指针走一步,当快指针走到尽头,慢指针就正好在中点位置上。还有“寻找链表的倒数第 k 个元素,让快指针先走 k 步,然后快慢指针开始同速前进。这样当快指针走到链表末尾 null 时,慢指针所在的位置就是倒数第 k 个链表节点。



最后吐槽一下地里的编辑器不能用markdown。。。。


更多图片 小图 大图
组图打开中,请稍候......

评分

参与人数 22大米 +121 收起 理由
Falldawn + 2 很有用的信息!
bryanjhy + 3 给你点个赞!
gougou9903 + 2 很有用的信息!
wsc_go + 1 给你点个赞!
神奇海螺呢 + 2 给你点个赞!

查看全部评分


上一篇:DA SQL 电面题
下一篇:大家一般刷多久开始面试

本帖被以下淘专辑推荐:

无效楼层,该帖已经被删除
🔗
admin 2020-1-15 02:33:37 | 只看该作者
全局:
本文被选为全站置顶文章之一。
作者获得大米奖励。谢谢你的分享。


地里发帖框的确不支持markdown,但可以选择 "<>" ,然后选择代码类型,会让代码在外观展示上更好看。
回复

使用道具 举报

🔗
righteous 2020-1-15 02:55:44 | 只看该作者
本楼:
全局:
谢谢分享
回复

使用道具 举报

🔗
wsc_go 2020-1-18 23:19:31 | 只看该作者
全局:
本帖最后由 wsc_go 于 2020-1-18 09:24 编辑

感谢lz分享

第一道题目,解法一看不太明白,特别是这行

nums, nums[j] = nums[j], nums

应该意思是元素交换,但其中nums是List[int], nums[j] 是int

还有
nums != 0

应该是将List[int]和0做比较,不明白是什么意思

-------
是个python 小白,希望有人帮忙解释一下,谢谢哈


回复

使用道具 举报

🔗
opxiao 2022-8-12 13:20:14 | 只看该作者
全局:
谢谢分享~~~
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册账号
隐私提醒:
  • ☑ 禁止发布广告,拉群,贴个人联系方式:找人请去🔗同学同事飞友,拉群请去🔗拉群结伴,广告请去🔗跳蚤市场,和 🔗租房广告|找室友
  • ☑ 论坛内容在发帖 30 分钟内可以编辑,过后则不能删帖。为防止被骚扰甚至人肉,不要公开留微信等联系方式,如有需求请以论坛私信方式发送。
  • ☑ 干货版块可免费使用 🔗超级匿名:面经(美国面经、中国面经、数科面经、PM面经),抖包袱(美国、中国)和录取汇报、定位选校版
  • ☑ 查阅全站 🔗各种匿名方法

本版积分规则

>
快速回复 返回顶部 返回列表