前文
在链表、数组、队列、栈等线性数据结构里,经常会用到各种各样的解法,比如针对有序的二分法、针对遍历的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
总结
双指针在解题时还是非常常用的,而且简单易懂,本篇记录到此为止~