利用指针找最大值_双指针完全攻略

本文介绍了双指针在解决数组和链表问题中的应用,包括如何利用双指针在异构数组中移动零元素、合并有序数组、处理稀疏向量,以及使用双指针解决二分查找问题,如找雨水陷阱。同时,文章还探讨了快慢指针在判断链表环、寻找环起点和链表中点等问题中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Two-Pointer

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

异构指针

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

关键词:

  1. array 类型
  2. 两类元素
  3. 有一定的有序性

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

283. Move Zeroes

b944d37ca04f78b639ba21651251dd26.png

这道题的题意就是,如果你只能用交换操作,如何才能把 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[i]!=0:
                i += 1
                if i >= len(nums):
                    return nums
                j=i

            while nums[j]==0:
                j += 1
                if j >= len(nums):
                    return nums
            nums[i], nums[j] = nums[j], nums[i]
        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[i] != 0:
                nums[j] , nums[i]= nums[i] , nums[j]
                j += 1

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

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

88. Merge Sorted Array

14fb82cf63c0dbdc526f81db6250f413.png

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

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

换个方向,别有洞天。

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

92546be1b7ab6af7022eeefa561492c8.png

剩下的操作就跟 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[i][0] == b[j][0]:
        result += a[i][1] * b[j][1]
        i += 1
        j += 1
    elif a[i][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 能对最终装水贡献多少。

46c50ecfd8c2b09969713b80fec09f38.png

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

实际上:

位置 i 能容下雨水量:

2e1727aa868cd114eeb151da5c78776d.png

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

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

1. 动态规划

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

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[i] = max(height[i], max_left[i-1])
        # 找位置i右边最大值
        for i in range(n-2, -1, -1):
            max_right[i] = max(height[i], max_right[i+1])
        #print(max_left)
        #print(max_right)
        # 求结果
        res = 0
        for i in range(n):
            res += min(max_left[i], max_right[i]) - height[i]
        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[i]:
                tmp = stack.pop()
                if not stack: break
                res += (min(height[i], height[stack[-1]]) - height[tmp]) * (i-stack[-1] - 1)
            stack.append(i)
        return res

3. 双指针

这一题用双指针来写还是有些难度的,思路会比较绕,这里详细的说一说。用双指针解,和上面解法相比的好处在于空间复杂度是 O(1),也就是常说的边走边算,不用额外存储一些东西。

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

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

2e1727aa868cd114eeb151da5c78776d.png

决定的。

但是仔细分析会发现,我们要得到这个值并不一定非得把

6538ae7ec7217d57599202918c070a62.png

b5172193d91811d2d0e7e63ee8c4c346.png

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

9579df2a11987c6e4a383462f4a4318f.png

一定是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 个链表节点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值