Python数据结构和算法(四):10分钟掌握双指针在线性数据结构里的使用!

前文

  在链表、数组、队列、栈等线性数据结构里,经常会用到各种各样的解法,比如针对有序的二分法、针对遍历的dfs(深度优先遍历)、bfs(广度优先遍历)等,而其中利用双指针来遍历找值的方法也是非常的万金油,碰到线性结构的题时可以考虑考虑,这里介绍leetcode的题型来让你更快更轻松的了解并掌握该方法的核心!

双指针的定义

  那什么是双指针呢?简单的讲,就是用两指针同时遍历一个线性结构,然后根据不同的条件来让两指针的遍历过程不同。而根据不同的题型,比较常见的有如下三种:

  • 快慢指针
  • 读写指针
  • 前后指针
快慢指针

  比如Python数据结构和算法(三):链表介绍以及经典题型训练(leetcode真题)!里介绍的几道题型:

  • 求链表的中间节点(876. Middle of the Linked List)easy
  • 链表中环的检测(141. Linked List Cycle )easy
  • 删除链表倒数第n个节点(19. Remove Nth Node From End of List)medium

  上面的两道就是利用到了快慢两指针,快指针一次遍历两个数,慢指针一次遍历一个数,这样当快指针到达终点时,慢指针自然就到达链表的中间节点了;而如果有环,快指针则一直到不了终点,直到和慢指针相遇。可以看下如下代码回顾下(详细可以点击链接观看):

class Solution:
#寻找中间节点
    def middleNode(self,head):
        """
        定义快慢指针,快的跑两步,慢的跑一步,当快的跑到终点,慢到达的点即是答案
        :param head:
        :return:
        """
        slow = fast = head
        while fast and fast.next: #注意边界是fast和fast.next都存在
            slow = slow.next
            fast = fast.next.next
        return slow

  而如果要删除倒数第n个点,那也先要找到,此时用快慢指针也是非常轻松,让快指针先跑n个节点,然后快慢指针一起跑,等快指针到目的地后,慢指针所在的位置即是第n个了,代码如下:

class Solution#删除链表倒数第n个点
    def findNthFromEnd(self,head,n):
        """
        定义快慢指针,先让快指针跑n个点,然后当快指针到达终点,此时慢指针所在的位置即是倒数第n个节点
        此时就是链表经典的删除操作:node.next = node.next.next
        :param head:
        :param n:
        :return:
        """
        slow = fast = head
        for _ in range(n):
            fast = fast.next
        while fast.next:#注意边界是fast.next
            slow = slow.next
            fast = fast.next
        slow.next = slow.next.next #要删除slow.next,即将其指向slow,next.next
        return head

  快慢指针适用于链表,因为链表不具备数组这种下标快速定位的功能,所以在链表的时候用到双指针,要想到快慢指针。

读写指针

  读写指针常用于数组的使用,相对于前后指针来说用的较少,通常通过定义read、write两个指针,前者不断的往后遍历,当满足题目条件的时候将read、write所在指针的数交换下继续往下遍历即可。这里举两道数组题:

  • 原地移除数组中为0的到后面(283. Move Zeroes )easy
  • 有序数组删除重复值并返回新的长度(26. Remove Duplicates from Sorted Array.py )easy

  两道都是简单题型,前者是给定数组,将其中为0的移到后面,然后原地返回数组,因为题目强调原地,所以不通过del+insert的方式来删除和插入为0的值,那就需要通过读写指针执行类似冒泡排序的交换操作,题目例子:

example:
Input: [0,1,0,3,12]
Output: [1,3,12,0,0]

  解法:让read指针往后遍历,当碰到非0就将值赋值给write指针,直到read遍历到终点后,将write后的全部定义为0即可。

class MySolution:
    """
    Runtime: 44 ms, faster than 100.00% of Python3 online submissions for Move Zeroes.
    """
    def moveZeroes(self, nums):
        """
        :type nums: List[int]
        :rtype: void Do not return anything, modify nums in-place instead.
        """
        read = write = 0
        n = len(nums)
        while read < len(nums):
            if nums[read] != 0:
                nums[write] = nums[read]
                write += 1
            read += 1
        for i,v in enumerate(nums[write:],write):
            nums[i] = 0
        return nums

  第二道:有序数组删除重复值并返回新的长度,也是强调了需要在数组内原地删除,例子如下:

Example 2:

Given nums = [0,0,1,1,1,2,2,3,3,4],

Your function should return length = 5, with the first five elements 
of nums being modified to 0, 1, 2, 3, and 4 respectively.

It doesn't matter what values are set beyond the returned length.

  解法:与上题类似,定义read、write指针,read往后遍历,条件是当遍历值和前一个值不同的时候,就将这个值赋值给write,直到遍历到终点。要注意的是,因为第一个值肯定非重复值,所以read=write=1,具体如下:


class Solution:
    def removeDuplicates(self, nums): #原地修改,不能新加空间
        if not nums:return 0
        read = write = 1
        while read < len(nums):
            if nums[read] != nums[read-1]:
                nums[write] = nums[read]
                write += 1
            read += 1
        return write

  可以看到代码套路基本都一致,当满足某个条件时就交换write、read指针即可。

前后指针(左右指针)

  要说数组中应用最广的,应该就属前后指针了,我习惯定义变量为左右指针:left、right,即left指针从左往右遍历,right指针从右往左遍历,然后逐一比对二者遍历的值是否满足题目要求。多说无益,直接看题:

  • 求有序数组中两数之和等于给定值(167. Two Sum II - Input array is sorted)easy
  • 求数组中三数之和等于0的所有可能数组(15. 3Sum)medium
  • 求坐标系里的最大面积(11. Container With Most Water)medium

  除了以上三个还有很多medium的题,比如3Sum Closest、4Sum等,就不一一介绍了,先从第一题从数组中找出两数之和等于给定值开始,例子如下:

Example:

Input: numbers = [2,7,11,15], target = 9
Output: [1,2]
Explanation: The sum of 2 and 7 is 9. Therefore index1 = 1, index2 = 2.

  解法:因为要同时求两个值,所以无法用二分法,而又是有序,所以这题非常适合左右指针,同时从左和右两个方向向中遍历,如果大了就right-1,小了就left+1,直到left=right为止,非常简单:

class Solution:
    def Twosum2(self,numbers, target):
        left = 0
        right = len(numbers) - 1
        while left < right:
            res = numbers[left] + numbers[right]
            if res == target:
                return [left + 1, right + 1]  #答案返回值并不是索引,而是认为的计数
            elif res < target:
                left += 1
            else:
                right -= 1

  第二题:求数组中三数之和等于0的所有可能数组,相当于是上个的扩展版本,不过难点是有很多重复值,而且非有序,并且要返回多个满足0的数组,例子如下:

Given array nums = [-1, 0, 1, 2, -1, -4],

A solution set is:
[
  [-1, 0, 1],
  [-1, -1, 2]
]

  看到这种需要返回多个的,最合适的方式是用dfs(深度优先遍历)来逐一遍历,当然这样时间复杂度会较高,不过代码写起来是真的舒服,关于dfs和dp(动态规划)留到下篇分享。回到题目,要用到双指针,我们先将其排序,然后套用上一题的解法,也就是先for循环遍历,假定遍历数为num,然后将target值定义为-num(因为num-num=0嘛),此时又回到了求有序数组中两个值为-num的题目,即回到上题!不过要注意的,因为有很多重复值,所以可以利用while来加速重复值的遍历,代码如下:

class Solution:
    """
    Runtime: 700 ms, faster than 93.20% of Python3 online submissions for 3Sum.
    Memory Usage: 15.9 MB, less than 100.00% of Python3 online submissions for 3Sum.
    """
    def threeSum(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        res = []
        nums.sort()
        for i in range(len(nums)):
            if i>0 and nums[i] == nums[i-1]:
                continue
            target = -nums[i]
            l, r = i+1, len(nums)-1
            while l < r:
                ans = nums[l] + nums[r]
                if ans == target:
                    res.append([nums[i], nums[l], nums[r]])
                    while l<r and nums[l] == nums[l+1]:
                        l = l + 1
                    l += 1
                    while l<r and nums[r] == nums[r-1]:
                        r -= 1
                    r -= 1
                elif ans > target:
                    r -= 1
                else:
                    l += 1
        return res

  第三道:求坐标系里的最大面积。
在这里插入图片描述  这道题目如上,要明白如何求面积:(right-left)*min(nums[right],nums[left]),明白了这个后就知道,我们维护一个当前最大值now_max,然后通过left、right指针不断地往里缩来得到最终的最大值,解法如下:


class Solution1:
    """
    Runtime: 60 ms, faster than 79.17% of Python3 online submissions for Container With Most Water.
    """
    def maxArea(self, height):
        """
        :type height: List[int]
        :rtype: int
        """
        start,end,now_max = 0,len(height)-1,0
        while start < end:
            now_max = max(now_max, (end-start)*min(height[start], height[end]))
            if height[end] < height[start]:
                end -= 1
            else:
                start += 1
        return now_max

总结

  双指针在解题时还是非常常用的,而且简单易懂,本篇记录到此为止~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值