算法刷题总结 (七) 双指针

news2024/9/29 23:38:02

算法总结7 双指针

  • 一、双指针的概念
    • 1.1、什么是双指针?
    • 1.2、常见类型
      • 1.2.1、快慢指针
      • 1.2.2、左右端点指针
      • 1.2.3、区间指针 - 滑动窗口
      • 汇总
  • 二、经典例题
    • 2.1、快慢指针
      • (1)、链表判环
        • 141. 环形链表
        • 142. 环形链表 II
        • 287. 寻找重复数
        • 876. 链表的中间结点
      • (2)、读写指针
        • 26. 删除有序数组中的重复项 - 仅保留一次
        • 80. 删除有序数组中的重复项 II - 保留两次重复
        • 递推:删除且保留k次重复
        • 202. 快乐数
    • 2.2、左右端点指针
      • (1)、二分法
        • 33. 搜索旋转排序数组
        • 875. 爱吃香蕉的珂珂
      • (2)、有序数组暴力枚举 - N数和问题
        • 1. 两数之和
        • 15. 三数之和
        • 18. 四数之和
        • 递推:N数之和
        • 881. 救生艇
      • (3)、其他暴力枚举
        • 75. 颜色分类 - 类似于荷兰国旗问题
        • 977. 有序数组的平方
    • 2.3、区间指针 - 滑动窗口
      • (1)、定长滑动窗口
        • 1456. 定长子串中元音的最大数目
        • 剑指 Offer 22. 链表中倒数第k个节点
      • (2)、变长滑动窗口
        • 713. 乘积小于 K 的子数组
  • 参考

在这里插入图片描述

一、双指针的概念


1.1、什么是双指针?

顾名思议,双指针就是两个指针,但是该指针不同于 C,C++中的指针地址,而是一种记录两个索引的算法思想。

实际上,在很多简单题目中,我们经常使用单指针,比如我们通过索引来遍历某数组:


# 可以这样
for i in range(n):
	print(nums[i])
# 当然也可以这样
i = 0
while i<n:
	print(nums[i])
	i+=1
# 这样写为了引申出双指针,因为双指针一般用while来遍历

在这里插入图片描述

那么双指针实际上就是有两个这样的指针,最为经典的就是二分法中的左右双指针。

left, right = 0, len(nums)-1

while left<right:
	if 一定条件:
		return 合适的值,一般是 l 和 r 的中点
	elif 一定条件:
		l+=1
	else:
		r-=1
# 因为 l == r,因此返回 l 和 r 都是一样的
return l

在这里插入图片描述

其实双指针是一个很宽泛的概念,就好像数组,链表一样,其类型会有很多很多, 比如二分法经常用到左右端点双指针滑动窗口会用到快慢指针固定间距指针。 因此双指针其实是一种综合性很强的类型,类似于数组,栈等。 但是我们这里所讲述的双指针,往往指的是某几种类型的双指针,而不是“只要有两个指针就是双指针了”。

有了这样一个算法框架,或者算法思维,有很大的好处。它能帮助你理清思路,当你碰到新的问题,在脑海里进行搜索的时候,双指针这个词就会在你脑海里闪过,闪过的同时你可以根据双指针的所有套路和这道题进行穷举匹配,这个思考解题过程本来就像是算法。



在这里插入图片描述

1.2、常见类型

指针一般情况下将分为三种类类型,分别是:

类型特点
快慢指针两个指针步长不同,一般情况下,快的走两步,慢的走一步
左右端点指针两个指针分别指向头尾,并往中间移动,步长不确定,一般为1
区间指针一般为滑动窗口,两个指针及其间距视作整体,窗口有定长有变长,每次操作窗口整体向右滑动

不管是哪一种双指针,只考虑双指针部分的话 ,由于最多还是会遍历整个数组一次,因此时间复杂度取决于步长,如果步长是 1,2 这种常数的话,那么时间复杂度就是 O(N),如果步长是和数据规模有关(比如二分法),其时间复杂度就是 O(logN)。并且由于不管规模多大,我们都只需要最多两个指针,因此空间复杂度是 O(1)。下面我们就来看看双指针的常见套路有哪些。


1.2.1、快慢指针

本方法需要我们对「Floyd 判圈算法」(又称龟兔赛跑算法)有所了解。

假想「乌龟」和「兔子」在链表上移动,「兔子」跑得快,「乌龟」跑得慢。当「乌龟」和「兔子」从链表上的同一个节点开始移动时,如果该链表中没有环,那么「兔子」将一直处于「乌龟」的前方;如果该链表中有环,那么「兔子」会先于「乌龟」进入环,并且一直在环内移动。等到「乌龟」进入环时,由于「兔子」的速度快,它一定会在某个时刻与乌龟相遇,即套了「乌龟」若干圈。

我们可以根据上述思路来解决本题。具体地,我们定义两个指针,一快一慢。慢指针每次只移动一步,而快指针每次移动两步。初始时,慢指针在位置 head,而快指针在位置 head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。

具体的的示意图如下,同时也可以参考相似思路的,且比较简单的例题 141. 环形链表。

1.开始,乌龟slow在起始点,兔子fast在起点的下一个点。
在这里插入图片描述
2.乌龟走得慢每次走一步,兔子走得快,每次走两步。
在这里插入图片描述
继续走,兔子先进入环。
在这里插入图片描述

继续走,兔子一圈环快走完了,而乌龟刚进入环
在这里插入图片描述
最后乌龟走第一圈的时候,兔子第二圈刚好遇上。
在这里插入图片描述
注意:
当然具体第几圈遇上是不确定的,根据步长与环的大小相关,但是乌龟与兔子在圈中循环跑时,只要步长不一致,他们之间的最近距离会不断减少,总会相遇。

但是一般情况下会设置slow走一步,fast走两步,这个设定会产生很多有规律的数学推导,比如:142. 环形链表 II 中的快慢指针做法。

细节:

为什么我们要规定初始时慢指针在位置 head,快指针在位置 head.next,而不是两个指针都在位置 head(即与「乌龟」和「兔子」中的叙述相同)?

观察下面的代码,我们使用的是 while 循环,循环条件先于循环体。由于循环条件一定是判断快慢指针是否重合,如果我们将两个指针初始都置于 head,那么 while 循环就不会执行。因此,我们可以假想一个在 head 之前的虚拟节点,慢指针从虚拟节点移动一步到达 head,快指针从虚拟节点移动两步到达 head.next,这样我们就可以使用 while 循环了。

当然,我们也可以使用 do-while 循环或者其他方法。此时,我们就可以把快慢指针的初始值都置为 head。(所以,从这里可以得知,快慢指针初始化的值,可以相同也可以不同,具体取决于后面的判断条件)

复杂度分析:

时间复杂度: O ( N ) O(N) O(N),其中 N N N 是链表中的节点数。空间复杂度: O ( 1 ) O(1) O(1)
当链表中不存在环时,快指针将先于慢指针到达链表尾部,链表中每个节点至多被访问两次;当链表中存在环时,每一轮移动后,快慢指针的距离将减小一。而初始距离为环的长度,因此至多移动 N N N 轮。我们只使用了两个指针的额外空间。

题目类型:

问题例题
1判断链表是否有环;寻找入环节点141. 环形链表 | 142. 环形链表 II | 287. 寻找重复数
2读写指针。将快指针的内容记录到慢指针的位置,典型的题目是原地删除(前置移动)重复元素。26. 删除有序数组中的重复项 | 80. 删除有序数组中的重复项 II | 202. 快乐数

伪代码模板:

# 1.fast与slow初始化不同

fast, slow = head, head.next
# 有环则一定相遇 退出循环后,后面return True
while fast!=slow :
	if not fast or not fast.next:
		return False
	slow=slow.next
	fast=fast.next.next
return True
	
# 2.fast与slow初始化相同
# fast = slow = head
fast = head
slow = head
while fast and fast.next:
  slow=slow.next
  fast=fast.next.next
  # 有环则一定相遇 return True
  if slow == fast:
  	return True
return False

1.2.2、左右端点指针

问题例题
1二分查找33. 搜索旋转排序数组 | 875. 爱吃香蕉的珂珂
2有序数组暴力枚举。区别于上面的二分查找,这种算法指针移动是连续的,而不是跳跃性的1. 两数之和 | 15. 三数之和 | 18. 四数之和 | 881. 救生艇
3其他暴力枚举。比如:双边比较从大到小枚举,双边按条件枚举,无需排序或者已经有序(当然2和3其实可以归为一类)977. 有序数组的平方 | 75. 颜色分类(Dutch National Flag Problem)

伪代码模板:

l = 0
r = n - 1
while l < r:
  if 找到了:
    return 找到的值
  if 一定条件1:
    l += 1
  else if  一定条件2:
    r -= 1
return 没找到

1.2.3、区间指针 - 滑动窗口

区间指针例题
1定长滑动窗口1456. 定长子串中元音的最大数目 | 剑指 Offer 22. 链表中倒数第k个节点
2变长滑动窗口713. 乘积小于 K 的子数组

伪代码模板:

l = 0
r = k
while 没有遍历完:
  自定义逻辑
  l += 1
  r += 1
return 合适的值

汇总

快慢指针左右端点指针区间指针-滑动窗口
判断链表是否有环;寻找入环节点二分查找定长滑动窗口
读写指针。将快指针的内容记录到慢指针的位置,典型的题目是原地删除(前置移动)重复元素。有序数组暴力枚举。区别于上面的二分查找,这种算法指针移动是连续的,而不是跳跃性的变长滑动窗口
其他暴力枚举。比如:双边比较从大到小枚举,双边按条件枚举,无需排序或者已经有序(当然2和3其实可以归为一类)


二、经典例题

2.1、快慢指针

问题例题
1判断链表是否有环;寻找入环节点141. 环形链表 | 142. 环形链表 II | 287. 寻找重复数
2读写指针。将快指针的内容记录到慢指针的位置,典型的题目是原地删除(前置移动)重复元素26. 删除有序数组中的重复项 | 80. 删除有序数组中的重复项 II | 202. 快乐数

(1)、链表判环

141. 环形链表

141. 环形链表

解法1:哈希表
最容易想到的方法是遍历所有节点,每次遍历到一个节点时,判断该节点此前是否被访问过。

具体地,我们可以使用哈希表来存储所有已经访问过的节点。每次我们到达一个节点,如果该节点已经存在于哈希表中,则说明该链表是环形链表,否则就将该节点加入哈希表中。重复这一过程,直到我们遍历完整个链表即可。

注意:Python中的哈希表为字典和集合。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def hasCycle(self, head: Optional[ListNode]) -> bool:
        seen = set()
        # 如果有环,虽然while死循环,但一定能在while中return True
        while head:
            if head in seen:
                return True
            seen.add(head)
            head = head.next
        # 没有环则head的最后一个next会None而退出循环
        return False

解法2:快慢指针

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def hasCycle(self, head: Optional[ListNode]) -> bool:
        slow = head
        fast = head

        while fast and fast.next:
            slow=slow.next
            fast=fast.next.next
            if fast==slow:
                return True
        return False

142. 环形链表 II

142. 环形链表 II

解法1:哈希表
思路同上

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
        seen = set()
        # 不允许修改表,用一个临时的指针来操作
        cur = head
        while cur:
            if cur in seen:
                return cur
            seen.add(cur)
            cur=cur.next
        return None

解法2:快慢指针
找数学规律:当快慢指针在环中相遇,链表的起点到入环点=快慢指针相遇点到入环点的距离。
所以相遇之后,定义新的游标在链表起点,此时该游标和慢指针一起以相同步长走,相遇即到了入环点。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
        slow = fast = head
        
        while fast and fast.next:
            fast = fast.next.next
            slow = slow.next

            if fast==slow:
                cur = head
                while cur!=slow:
                    cur=cur.next
                    slow=slow.next
                return cur
        return None

287. 寻找重复数

287. 寻找重复数
这题比较巧妙的一点是将nums的每个值当做下一个点的坐标,从而进行连接起来。我们来看看这个例子:
1 4 6 6 6 2 3
值为6时会指向索引6值为3的点,再以3为索引,又指向索引为3值为6的索引。

这道题同上一题 环形链表 II 的解法一致,重复元素即表示入环点

class Solution:
    def findDuplicate(self, nums: List[int]) -> int:
        fast = slow = nums[0]

        # 至少存在一个重复的数,说明不会死循环,一定存在slow==fast的情况
        # 不同判断是否有环,因为一定有
        while True:
            slow = nums[slow]
            fast = nums[nums[fast]]
            # 同环形链表的解法
            # 1. 先记录第一次相遇
            if slow == fast:
                # 记录一个起点与slow一同移动直到相遇,即为入环点
                cur = nums[0]
                while cur!=slow:
                    cur = nums[cur]
                    slow = nums[slow]
                return cur
        return None

当然也有哈希表解法,同上,但时间复杂度高。


876. 链表的中间结点

慢指针走一步,快指针走两步,当快指针走到结尾,慢指针会走到链表中间。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]:
        slow = fast = head

        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
        return slow

(2)、读写指针

26. 删除有序数组中的重复项 - 仅保留一次

26. 删除有序数组中的重复项
快指针用来判断重复,是否与前一个一样;慢指针用来存储非重复的值。

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        slow = fast = 1

        while fast<len(nums):
            if nums[fast]!=nums[slow-1]:
                nums[slow]=nums[fast]
                slow+=1
            fast+=1
        return slow

80. 删除有序数组中的重复项 II - 保留两次重复

80. 删除有序数组中的重复项 II
这里保留重复的两次,题目解法同上。数组的前两个数必然可以被保留,因此,两个指针从2开始。

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        slow = fast = 2

        while fast<len(nums):
            if nums[fast]!=nums[slow-2]:
                nums[slow] = nums[fast]
                slow+=1
            fast+=1
        return slow

递推:删除且保留k次重复

从前面两题我们可以总结出,如过要保留重复的k次:

class Solution:
    def removeDuplicates(self, nums: List[int], k: int) -> int:
    	# 从第k个开始
        slow = fast = k

        while fast<len(nums):
            if nums[fast]!=nums[slow-k]:
                nums[slow] = nums[fast]
                slow+=1
            fast+=1
        return slow

202. 快乐数

202. 快乐数
通过反复调用 getNext(n) 得到的链是一个隐式的链表。隐式意味着我们没有实际的链表节点和指针,但数据仍然形成链表结构。起始数字是链表的头 “节点”,链中的所有其他数字都是节点。next 指针是通过调用 getNext(n) 函数获得。

意识到我们实际有个链表,那么这个问题就可以转换为检测一个链表是否有环。因此我们在这里可以使用弗洛伊德循环查找算法。这个算法是两个奔跑选手,一个跑的快,一个跑得慢。在龟兔赛跑的寓言中,跑的慢的称为 “乌龟”,跑得快的称为 “兔子”。

不管乌龟和兔子在循环中从哪里开始,它们最终都会相遇。这是因为兔子每走一步就向乌龟靠近一个节点(在它们的移动方向上)。
在这里插入图片描述

class Solution:
    def isHappy(self, n: int) -> bool:
        def get_next(number):
            total_sum = 0
            while number>0:
                number, digit = divmod(number, 10)
                total_sum+=digit**2
            return total_sum
        
        slow = fast = n
        # fast!=1判断是否是快乐数
        # fast!=slow 说明有环,进行打破死循环
        # 快乐数的判断快于环的判断,所以会在打破循环前判断是否是快乐数
        while fast!=1:
            slow = get_next(slow)
            fast = get_next(get_next(fast))
            if fast==slow:
                break
        return fast==1


2.2、左右端点指针

问题例题
1二分查找33. 搜索旋转排序数组 | 875. 爱吃香蕉的珂珂
2有序数组暴力枚举。区别于上面的二分查找,这种算法指针移动是连续的,而不是跳跃性的1. 两数之和 | 15. 三数之和 | 18. 四数之和 | 881. 救生艇
3其他暴力枚举。比如:双边比较从大到小枚举,双边按条件枚举,无需排序或者已经有序(当然2和3其实可以归为一类)977. 有序数组的平方 | 75. 颜色分类(Dutch National Flag Problem)

在这里插入图片描述

(1)、二分法

33. 搜索旋转排序数组

33. 搜索旋转排序数组

但是这道题中,数组本身不是有序的,进行旋转后只保证了数组的局部是有序的,这还能进行二分查找吗?答案是可以的。

可以发现的是,我们将数组从中间分开成左右两部分的时候,一定有一部分的数组是有序的。拿示例来看,我们从 6 这个位置分开以后数组变成了 [4, 5, 6] 和 [7, 0, 1, 2] 两个部分,其中左边 [4, 5, 6] 这个部分的数组是有序的,其他也是如此。

这启示我们可以在常规二分查找的时候查看当前 mid 为分割位置分割出来的两个部分 [l, mid] 和 [mid + 1, r] 哪个部分是有序的,并根据有序的那个部分确定我们该如何改变二分查找的上下界,因为我们能够根据有序的那部分判断出 target 在不在这个部分:

如果 [l, mid - 1] 是有序数组,且 target 的大小满足 [nums[l],nums[mid]),则我们应该将搜索范围缩小至 [l, mid - 1],否则在 [mid + 1, r] 中寻找。
如果 [mid, r] 是有序数组,且 target 的大小满足 (nums[mid+1],nums[r]],则我们应该将搜索范围缩小至 [mid + 1, r],否则在 [l, mid - 1] 中寻找。

在这里插入图片描述

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        l, r = 0, len(nums)-1
        while l<=r:
            mid = (l+r)//2
            if nums[mid]==target:
                return mid
            else:
                # 以有序序列为分界线,进行两次二分,需要区分有顺序的两个部分
                # [4,5,6,7,0,1,2]
                # 说明在左半区 [4,5,6,7]
                if nums[0]<=nums[mid]:
                    # 在该半区之中再去二分,以mid为中点
                    # 在左半边[4]
                    if nums[0]<=target<nums[mid]:
                        r = mid - 1
                    # 在右半边[5,6,7]
                    else:
                        l = mid + 1
                # [5,6,7,0,1,2,4]
                # 否则在右半区 [0,1,2,4]
                else:
                    # [1,2,4]
                    if nums[mid]<target<=nums[len(nums)-1]:
                        l = mid+1
                    # [0]
                    else:
                        r = mid-1
        return -1

875. 爱吃香蕉的珂珂

875. 爱吃香蕉的珂珂
这一题要注意一点,当sum_time==h时,不能直接return mid,因为比如:math.ceil(10/5)到math.ceil(10/9)这个5-9与10相除向上取整结果都为2,但是珂珂喜欢慢慢吃,也就是吃的尽量少一点,所以要取最小值5,所以,我们在sum_time==h时,试探性的将right=mid-1,而不是直接return mid。

最后退出循环后,l>=r,之所以会大,因为sum_time==h后的right=mid-1不成功,下一次循环l=mid+1而加回来,无法在变小了,所以最后返回return l 而不是 r。

import math
class Solution:
    def minEatingSpeed(self, piles: List[int], h: int) -> int:
        l, r = 1, max(piles)

        while l<=r:
            mid = (l+r)//2
            sum_time = sum([math.ceil(i/mid) for i in piles])
            if sum_time>h:
                l = mid+1
            elif sum_time<h:
                r = mid-1
            else:
                # 对于 [1,1,1,999999999] 和 10
                # 值在 142857143 和 二分得出的156250000中间结果都为10
                # 但是珂珂喜欢慢慢吃,也就是说数值得最小到刚好满足10h吃完
                # 所以当sum_time==h时,咱们还是要减少mid的值试试
                # 即便减少不成功,下一次sum_time>h时,l = mid+1也会加回来
                # 所以最后while结束后,应该返回left
                r = mid-1
        return l

(2)、有序数组暴力枚举 - N数和问题

已知一组数组和一个目标值target,求不重复的在数组中取N个数的和为target的组合。

一般做这样的题的思路是用双指针,分别指向数组的左右两端,并且,数组需要排好序,从小到大。

因为排序后,左右指针才能有规律的移动,比如:当left+right的值大于target,说明他们两个太大了,需要减小,那么只能通过right左移来减小总体值(为什么不让left左移呢?因为不能重复取,之前取过了,就要移向新的值);当left+right的值小于target,那么只能通过left右移来增加总体值。

当然,当left+right的值等于target,即为结果。


在这里插入图片描述

1. 两数之和

1. 两数之和
这题因为需要记录索引,所以将排序前,将值与原始索引绑定起来。

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        num_ind = []
        # 值与坐标进行绑定
        for ind, val in enumerate(nums):
            num_ind.append([val, ind])
        num_ind.sort(key=lambda x:x[0])
        # 开始双指针
        left, right = 0, len(nums)-1
        while left<right:
        	# 三个条件,>target, <target, =target
            if num_ind[left][0]+num_ind[right][0]>target:
                right-=1
            elif num_ind[left][0]+num_ind[right][0]<target:
                left+=1
            else:
                return [num_ind[left][1], num_ind[right][1]]
        return []

在这里插入图片描述

15. 三数之和

15. 三数之和

咱们利用上一题的函数两数之和,每次遍历第一个数,该第一个数的后面的数求两数之和,与第一个数相加为target则保存为结果。

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        res = []
        nums.sort()
        # 类似于两数之和
        def twoSum(start, target):
            res = []
            left, right = start, len(nums)-1
            while left<right:
                #(注意不是left>0)因为起始点不是0而是start
                if left>start and nums[left]==nums[left-1]:
                    left+=1
                    continue
                if right<len(nums)-1 and nums[right]==nums[right+1]:
                    right-=1
                    continue
                if nums[left]+nums[right]>target:
                    right-=1
                elif nums[left]+nums[right]<target:
                    left+=1
                else:
                    res.append([nums[left],nums[right]])
                    right-=1
                    left+=1
            return res 
        # 这里减去2,也就是至少保证剩下两个数在-1和-2。当然也可以不减
        for start in range(len(nums)-2):
            # 重复的需要去掉 [-1, -1, 0, 1] 这里前面两个-1都会取到后面的[0,1]
            if start>0 and nums[start]==nums[start-1]:
                continue
            # 这里除了left和right(去掉right,一定为len(nums)无需传入)
            # 第三个参数传入负的值,因为三数和为零
            # 在传入个起始坐标,
            twolist = twoSum(start+1, -nums[start])

            for twol in twolist:
                if sum(twol+[nums[start]])==0:
                    res.append(twol+[nums[start]])
        return res

18. 四数之和

18. 四数之和
这题同样利用上前面两数之和与三数之和,层层嵌套,最内层还是两数之和。外面两层的三数之和与四数之和分别与两数之和相加,为target则return或者保存为最终的结果。

class Solution:
    def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
        res = []
        nums.sort()
        # 相同的两数之和
        def twoSum(start, target):
            res = []
            left, right = start, len(nums)-1
            while left<right:
                if left>start and nums[left]==nums[left-1]:
                    left+=1
                    continue
                if right<len(nums)-1 and nums[right]==nums[right+1]:
                    right-=1
                    continue
                if nums[left]+nums[right]>target:
                    right-=1
                elif nums[left]+nums[right]<target:
                    left+=1
                else:
                    res.append([nums[left],nums[right]])
                    left+=1
                    right-=1
            return res
        # 相同的三数之和
        def threeSum(start, target):
            res = []
            for sec in range(start, len(nums)):
                if sec>start and nums[sec]==nums[sec-1]:
                    continue
                twolist = twoSum(sec+1, target-nums[sec])

                for twol in twolist:
                    if sum(twol+[nums[sec]])==target:
                        res.append(twol+[nums[sec]])
            return res
        # 可以减3,也可以不减
        for start in range(len(nums)-3):
            if start>0 and nums[start]==nums[start-1]:
                continue
            
            threelist = threeSum(start+1, target-nums[start])

            for threel in threelist:
                if sum(threel+[nums[start]])==target:
                    res.append(threel+[nums[start]])
        return res




递推:N数之和

我们可以发现,除了最内层的两数之和这个函数,其他函数可以层层嵌套,写成递归形式,于是我们整理如下:

def nSum(nums, start, target, k):
    res = []
    # 大于两数之和的层层嵌套
    if k>2:
        for i in range(start, len(nums)):
            if i>start and nums[i]==nums[i-1]:
                continue
            nlist = nSum(nums, i+1, target-nums[i],k-1)
            for nl in nlist:
                if sum(nl+[nums[i]])==target:
                    res.append(nl+[nums[i]])
    # 两数之和
    else:
        left, right = start, len(nums)-1
       
        while left<right:
            
            if left>start and nums[left]==nums[left-1]:
                left+=1
                continue
            if right<len(nums)-1 and nums[right]==nums[right+1]:
                right-=1
                continue
            if nums[left]+nums[right]>target:
                right-=1
            elif nums[left]+nums[right]<target:
                left+=1
            else:
                res.append([nums[left],nums[right]])
                right-=1
                left+=1
    return res

# 四数之和
nums = [1,0,-1,0,-2,2] 
k = 4
target = 0
start = 0
# [[1, 2, -1, -2], [0, 2, 0, -2], [0, 1, 0, -1]]
# 三数之和
nums = [-1,0,1,2,-1,-4]
k = 3
nums.sort()
target = 0
start = 0
k = 3
# 函数入口
nSum(nums, start, target, k)

在这里插入图片描述

881. 救生艇

881. 救生艇

class Solution:
    def numRescueBoats(self, people: List[int], limit: int) -> int:
        people.sort()
        light,heavy = 0, len(people)-1
        count = 0
        while light<=heavy:
            if people[light]+people[heavy]<=limit:
                light+=1
                heavy-=1
            else:
                heavy-=1
            count+=1
        return count

(3)、其他暴力枚举

在这里插入图片描述

75. 颜色分类 - 类似于荷兰国旗问题

75. 颜色分类
两个左右指针分别用来存储0和2,遍历nums,找到0则与左指针交换,找到2则与右指针交换,注意相同值的交换[2,1,2],所以需要判断交换后nums[i]是否还为原值,除此之外,需要防止越界,内部要加上判断条件 i<=p2。

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        n = len(nums)
        p0, p2, i = 0, n-1, 0
        while i<=p2:
            # 防止 [2,1,2],if改为while
            # 防止 [2,2,2],p2一直-1小于0越界,加上while i<=p2
            while i<=p2 and nums[i]==2:
                nums[i], nums[p2] = nums[p2], nums[i]
                p2-=1
            if nums[i]==0:
                nums[i], nums[p0] = nums[p0], nums[i]
                p0+=1
            i+=1
        return nums

977. 有序数组的平方

977. 有序数组的平方
两端的平方为最大值,每次将最大值放入一个新生成的list的从右到左放置。

class Solution:
    def sortedSquares(self, nums: List[int]) -> List[int]:
        n = len(nums)
        ans = [0]*n
        left,right,pos = 0, n-1,n-1

        while left<=right:
            if nums[left]**2>nums[right]**2:
                ans[pos]=nums[left]**2
                left+=1
            else:
                ans[pos]=nums[right]**2
                right-=1
            pos-=1
        return ans


2.3、区间指针 - 滑动窗口

固定间距指针例题
1定长滑动窗口1456. 定长子串中元音的最大数目 | 剑指 Offer 22. 链表中倒数第k个节点
2变长滑动窗口713. 乘积小于 K 的子数组

(1)、定长滑动窗口

1456. 定长子串中元音的最大数目

1456. 定长子串中元音的最大数目

先求出从起点开始定长窗口,每次移动,去掉首部,加上尾部。

class Solution:
    def maxVowels(self, s: str, k: int) -> int:
        def isVowel(ch):
            return int(ch in 'aeiou')
        count = 0
        for i in range(k):
            if isVowel(s[i]):
                count+=1
        ans = count
        
        for i in range(k, len(s)):
            count = count-isVowel(s[i-k])+isVowel(s[i])
            ans = max(ans, count)
        return ans

剑指 Offer 22. 链表中倒数第k个节点

剑指 Offer 22. 链表中倒数第k个节点

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def getKthFromEnd(self, head: ListNode, k: int) -> ListNode:
        left = right = head
        while k:
            right=right.next
            k-=1
        while right:
            left=left.next
            right=right.next
        return left

(2)、变长滑动窗口

713. 乘积小于 K 的子数组

713. 乘积小于 K 的子数组
本题采用的是双指针滑动窗口,大循环是右指针的移动,内部小循环是左指针的移动。

class Solution:
    def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
        left = right =0
        # 记录组合个数
        ans = 0
        # 记录乘积
        mul = 1

        while right<len(nums):
            mul*=nums[right]
            # 防止left一直+而越界,需要left<=right
            while mul>=k and left<=right:
                mul/=nums[left]
                left+=1
            #每次右指针位移到一个新位置,应该加上 x 种数组组合:
            #  nums[right]
            #  nums[right-1], nums[right]
            #  nums[right-2], nums[right-1], nums[right]
            #  nums[left], ......, nums[right-2], nums[right-1], nums[right]
            ans+=right-left+1
            right+=1

        return ans

参考

官方解题 环形链表
快慢指针
官方解题 搜索旋转排序数组
713.官方思路秒懂○注释详细○双指针滑窗 【附通用滑窗模板】

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/429373.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

人工智能学习07--pytorch15(前接pytorch10)--目标检测:FPN结构详解

FPN&#xff1a;用于目标检测的特征金字塔网络 backbone:骨干网络&#xff0c;例如cnn的一系列。&#xff08;特征提取&#xff09; (a)特征图像金字塔 检测不同尺寸目标。 首先将图片缩放到不同尺度&#xff0c;针对每个尺度图片都一次通过算法进行预测。 但是这样一来&#…

第二章:HTML CSS 网页开发基础(一)

一、HTML HTML&#xff1a;是一种网页制作标注性语言。HTML通过浏览器的翻译将页面呈现给用户。 1、1 HTML文档结构 HTML文档由4个主要标记组成&#xff0c;<html>、<head>、<title>、<body> <html>标记 该标记是所有html文件的开头&a…

[STM32F103C8T6]DMA

DMA(Direct Memory Access&#xff0c;直接存储器访问) 提供在外设与内存、存储器和存储器、外设 与外设之间的高速数据传输使用。它允许不同速度的硬件装置来沟通&#xff0c;而不需要依赖于 CPU&#xff0c;在这个时间中&#xff0c;CPU对于内存的工作来说就无法使用。 我自己…

实验设备管理系统【GUI/Swing+MySQL】(Java课设)

系统类型 Swing窗口类型Mysql数据库存储数据 使用范围 适合作为Java课设&#xff01;&#xff01;&#xff01; 部署环境 jdk1.8Mysql8.0Idea或eclipsejdbc 运行效果 本系统源码地址&#xff1a;https://download.csdn.net/download/qq_50954361/87682549 更多系统资源库…

构建新摩尔定律下的算力分发网络

摩尔定律最早由英特尔创始人之一戈登摩尔在1965年提出&#xff0c;他认为集成电路上可以容纳的晶体管数目在大约每经过18到24个月便会增加一倍。20年后的今天&#xff0c;面对日新月异的社会和突飞猛进的数字化需求&#xff0c;摩尔定律也随着社会的进步而被赋予了新的定义。Li…

第八章 法律关系

目录 第一节 法律关系的概念 一、法律关系的定义与特征 二、法律关系的种类 &#xff08;一&#xff09;纵向&#xff08;隶属&#xff09;的法律关系和横向&#xff08;平权&#xff09;的法律关系 &#xff08;二&#xff09;单向&#xff08;单务&#xff09;法律关系、双…

基于ITIL搭建公司IT治理服务框架

ITIL帮助组织标准化其IT服务和流程。ITIL文档包括各种政策、程序和指南&#xff0c;帮助IT团队向客户提供优质的服务。 根据ITIL框架的五个阶段&#xff0c;ITIL文档被广泛分类为不同的类别。 这些阶段包括服务战略、服务设计、服务过渡、服务操作和持续服务改进。每个类别都有…

android内存泄漏检测,Android内存泄露检测之LeakCanary的使用

为了能够简单迅速的发现内存泄漏&#xff0c;Square公司基于MAT开源了LeakCanary。使用LeakCanary&#xff0c;在内存泄漏后&#xff0c;通过分析引用链可以分析内存泄漏的原因&#xff0c;LeakCanary用于检测Activity、Fragment的内存泄漏。 下面通过一些实际案例来进行分析。…

如何替换spring boot中spring框架的版本

背景 我开源的一个项目中&#xff0c;有朋友提到存在Spring Framework 身份认证绕过漏洞(CVE-2023-20860)。 解决方案是升级spring 框架版本&#xff1a; Spring Framework 5.3.X 系列用户建议升级Spring Framework到5.3.26及以上安全版本修复该漏洞Spring Framework 6.0.X 系…

HashMap 学习笔记

HashMap&#xff1a; 为什么经常深入考察&#xff1f; 基于java写的代码会访问很多东西&#xff0c;比如数据库&#xff0c;缓存&#xff0c;消息中间件。 HashMap数据结构 底层是数组 原本已经有很多个位置了 原本是取模但是优化成了性能更高的hash&(n-1) 让哈希值…

【性能测试】5年测试老鸟,总结性能测试基础到指标,进阶性能测试专项......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 性能测试是为了评估…

java 接口,接口的特性,接口实现多态,面向接口编程

package cn.zy.cellphone; /**接口是一种引用数据类型。使用interface声明接口&#xff0c;形式* 形式:public interface 接口名称{}* 接口不能拥有构造方法&#xff0c;不能用于创建对象*接口可以多继承。一个接口可以继承多个其他接口*列如&#xff1a;public interface Broa…

任何人都可以学习车载测试吗?车载测试有什么门槛?

1、车载测试是什么&#xff1f; 车载测试分很多种&#xff0c;有软件测试、硬件测试、性能测试、功能测试等等&#xff0c;每一项测试的内容都不一样&#xff0c;我们所说的车载测试主要指的是汽车软件的功能测试&#xff0c;也就是针对汽车实现的某一个功能&#xff0c;而进行…

windows平台多版本nodejs共存工具nvm-windows 中文文档

2023年4月14日11:11:49 官网地址&#xff1a;https://github.com/coreybutler/nvm-windows 下载地址&#xff1a;https://github.com/coreybutler/nvm-windows/releases nodejs版本列表 https://nodejs.org/en/download/releases PS C:\Users\pc\Desktop> nvm -h Running ve…

关于测试,我发现了哪些新大陆

关于测试 平常也只是听说过一些关于测试的术语&#xff0c;但并没有使用过测试工具。偶然看到编程老师在课堂上使用龙测为我们展示了一波测试的相关操作&#xff0c;我发现了一些新大陆&#xff0c;这篇文章就记录一下有关测试的相关操作。 开启测试之旅 进入官网了解到 龙测…

java学习之接口二

目录 一、接口vs继承 一、继承 二、接口 二、接口多态特性 一、多态参数 二、多态数组 三、多态传递 三、接口练习 一、接口vs继承 一、继承 package com.hspedu.interface_;public class ExtendsVsInterface {public static void main(String[] args) {LittleMonkey…

5G物理层信道pdcch说明(留档)

网络七层协议OSI是一个开放性的通信系统互连参考模型。 它是国际标准组织制定的一个指导信息互联、互通和写作的网络规范。 开放&#xff1a;是指只要遵循OSI标准&#xff0c;位于世界的任何地方的任何系统之间都可以进行通讯&#xff1b;开放系统&#xff1a;是指遵循互联网协…

【结构型模式】装饰者模式

文章目录优秀借鉴1、概述2、结构3、实现方式3.1、案例引入3.2、实现步骤3.3、案例实现4、装饰者模式优缺点5、结构型模式对比5.1、装饰者模式和代理模式5.2、装饰者模式和适配器模式6、应用场景优秀借鉴 装饰模式 — Graphic Design Patterns设计模式 | 装饰者模式及典型应用黑…

基于图卷积神经网络GCN的二部图链路预测方法实现思路和完整代码【可用于疾病-靶点、miRNA-疾病等相关预测】

本文解决的问题 基本假设&#xff1a;二部图只有邻接矩阵&#xff0c;没有节点特征&#xff0c;并且进行链路预测&#xff0c;有部分链路未知。 如果你有初始节点特征&#xff0c;直接换掉即可 实现思路 这段代码主要是用于构建一个基于图卷积神经网络&#xff08;GCN&#…

内网域渗透总结(红日靶场一)

一、靶场环境搭建 红日靶场&#xff1a;http://vulnstack.qiyuanxuetang.net/vuln/detail/2/靶机通用密码&#xff1a;hongrisec2019win7win2003win2008 这个搭建靶场的过程是我经过实际操作&#xff08;踩坑&#xff09;总结的最简单省事的方式。 首先创建一个仅主机模式的…