leetcode刷题笔记——双指针
目前完成的贪心相关的leetcode算法题序号:
中等:142
困难:76
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/reconstruct-itinerary
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
文章目录
算法理解
双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。
若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。
若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。
提示:以下是贪心算法相关的刷题笔记
一、142题:环形链表II
1.题干
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
说明:不允许修改给定的链表。
2.思路
本题可以用哈希表进行求解,对链表进行遍历,并用哈希表存储每个节点,直到下一次重复寻址到哈希表中的某个节点,即为环的起始节点,但是这种方法的空间复杂度为O(n)。
本题还有一种空间复杂度为O(1)的解法:双指针——快慢指针,具体思路如下:
- 设置两个指针从链表的头节点开始向后遍历,一个节点步长为1,另一个节点步长为2,进行循环遍历;
- 遍历完链表,没有与慢指针相遇,则链表没有环路;
- 如果快慢指针相遇,则将快指针重新指向链表头节点;
- 快慢指针分别由当前位置,以1为步长遍历链表,直到两者相遇,相遇的节点即为链表环路的起始节点;
数学推导:
假设慢指针以步长1遍历了x个节点,则快指针步长2就遍历了2x个节点,当快慢指针第一次相遇,就有:
快指针遍历完链表中环路外的节点后,有绕了环路n圈之后与慢指针相遇;
可得:a+n(b+c)+b = a+(n+1)b+nca+n(b+c)+b = a+(n+1)b+nc
慢指针走过的路程为:a+b(快指针会在慢指针走完第一圈环路之前就追上慢指针)
则有:a+(n+1)b+nc=2(a+b) ⟹ a=c+(n−1)(b+c)
即为如果分别从头节点和快慢指针相遇点开始,以步长1遍历链表,则他们必会相遇在环路的开始节点处。
3.代码
class Solution:
def detectCycle(self, head: ListNode) -> ListNode:
#初始化快慢指针为链表的头节点
p1, p2 = head, head
#设定快慢指针的遍历速度
while p2 and p2.next:
p1 = p1.next
p2 = p2.next.next
#如果遇到快慢指针相遇的情况
if p1 == p2:
#就将其中之一重新初始化为头节点
p2 = head
#分别从头节点和相遇节点,以1为步长进行遍历,两者比相遇在链表环路的起始节点
while p1 != p2:
p1 = p1.next
p2 = p2.next
return p1
#如果没有相遇,则说明没有环路
return None
4. 问题点思考
关于为什么快指针会在慢指针走完环路第一圈就追上的思考:
慢指针的速度差是1(节点),即每次迭代,快慢指针之间的节点数-1,这是重点
当慢指针进入环路初始节点的时候,快指针必然已经在环路中,这时就等效为快指针落后于慢指针一个距离(A-B),这个距离小于环路A的总长度,所以快指针追上慢指针(速度差补完这段路程差)的时间必然小于慢指针走完整个环路的时间。
二、最小覆盖子串
1.题干
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
注意:如果 s 中存在这样的子串,我们保证它是唯一的答案。
字符串t中可能出现重复的字符。
2.解题思路
此题思路分为以下注意步骤:
1)将字符串t中的各个字符及其出现的频词记录在哈希表中;
2)在字符串s上用左右双指针从0索引处向右遍历:
- 开始由右指针向右滑动,每次遇到t中的字符时,维护哈希表,判断该字符是否在哈希表中,并判断当前窗口是否缺该字符;
- 直到窗口中包含了t字符串中的所有字符之后,将左指针向右滑动,每一次对左侧滑出窗口的字符进行判断,该字符是否在t字符串中,且该字符移除后,窗口中是否缺少该字符;
- 左指针每移动一次,判断当前窗口是否包含了字符串t中的所有字符,如果都包含了,且窗口长度小于前面记录的窗口长度,则更新窗口长度和位置;
3)重复2)的遍历过程,直至遍历完s字符串,即可得到最小覆盖字串;
3.代码
class Solution:
def minWindow(self, s: str, t: str) -> str:
#新建哈希表用于存储字符串t中,各个字符及其出现的频数信息
hashmap = collections.defaultdict(int)
for ch in t:
hashmap[ch] += 1
#定义左右指针,均从0索引开始
left, right = 0, 0
#初始化最小窗口的右边界为s字符串的右边界
min_right = len(s) - 1
#初始化最小长度为inf,能够同时作为s字符串中是否存在覆盖t字符串的一个标记
min_len = float('inf')
#counts变量作为判断当前窗口中还缺少几个t字符串中的字符,其计数规则需要着重注意,初始化为字符串t的长度
counts = len(t)
#左右指针的遍历终止条件为:1)左指针不超过右指针,2)右指针不超过字符串的有边界(此处由于右指针会多加1,所以有“=”)
while right >= left and right <= len(s):
if counts == 0:
#如果当前窗口包含了t字符串中的所有字符
if right - left < min_len:
#且当前窗口长度小于前面记录的最小窗口长度,则更新最小窗口长度和窗口右边界
min_len = right - left
min_right = right
if s[left] in hashmap:
#此时要右移窗口的左边界,来减小窗口长度,如果左边界字符在t字符串中,则维护哈希表,该字符的需求+1(需求可能为负,表示当前窗口中该字符有多余)
hashmap[s[left]] += 1
if hashmap[s[left]] > 0:
#如果左指针右移之后,窗口中缺少了该字符(该字符的需求大于0),则counts要+1,表示该窗口未能包含所有的t字符串中字符
counts += 1
#左指针右移之后,+1,无论前面的判断结果如何,左指针必须+1
left += 1
if counts > 0:
#如果当前窗口还缺少t字符串中的字符,则右移右指针,扩大窗口
if right < len(s) and s[right] in hashmap:
#如果右指针新指向的字符为字符串t中的字符,则判断当前窗口是否缺该字符,如果缺,则counts(缺少的字符数量)-1(注意:这里要考虑边界问题,要多一个right范围的判断)
if hashmap[s[right]] > 0:
counts -= 1
#无论缺不缺,都要将哈希表中该字符的值-1,更新对这个字符的需求情况,为负表示多余,后续如果做窗口放出一个该字符,也不会使得窗口重新产生对该字符的需求
hashmap[s[right]] -= 1
#无论如何,右指针右移,需要+1
right += 1
#需要判断s字符串中是否出现过能够覆盖字符串t的字串,如果没有出现过,则返回“”
return "" if min_len > len(s) else s[min_right-min_len:min_right]
4. 注意点
开始做此题时,没有采用counts记录窗口对字符的需求数量,而是多次的判断哈希表中各个字符的需求之和是否为0,结果也能够作对,但是时间复杂度较高。
以后对于这种类似计数的需要,还是用单变量计数会比较好,只是在这个变量值的维护上,需要多动动脑筋
三、680题:验证回文字符串II
1. 题干
给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。
2. 思路
思路明显,用双指针从字符串的两端向中间遍历:
尝试伪代码:
左指针,右指针 = 0,len(s)
while 左指针 < 右指针:
if:左右指针索引的字符相同:
左指针++
右指针++
else:
删除左指针对应的字符,右指针不变
or
删除右指针对应的字符,左指针不变
if 两者之一能够构成回文字符串:
return True
else:
return False
3. 代码
递归实现
class Solution:
def validPalindrome(self, s: str) -> bool:
def search(s, left, right, counts):
#为了进行递归,函数中需要传入进行回文判断的边界,以及允许出错的剩余次数
while left < right:
#循环条件是左指针小于右指针,当左指针>=右指针,说明遍历结束
if s[left] != s[right]:
#如果左右指针对应索引的字符一同,且允许错误的次数为0,则返回False
#否则,将此时的左右指针中的一个元素删除,并将允许错误次数-1,进入递归过程
if counts == 0:
return False
return search(s, left+1, right, counts-1) or search(s, left, right-1, counts-1)
#如果左右指针对应索引的字符相同,则左右指针继续向中间索引
left += 1
right -= 1
return True
return search(s, 0, len(s)-1, 1)
思路很简单,重点是用递归的思想进行实现会简洁很多,要熟悉递归的思维
四、
1. 题干
2. 解题思路
3. 代码