leetcode刷题笔记——双指针

leetcode刷题笔记——双指针

目前完成的贪心相关的leetcode算法题序号:
中等:142
困难:76

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/reconstruct-itinerary
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

文章目录

    • leetcode刷题笔记——双指针
  • 算法理解
  • 一、142题:环形链表II
    • 1.题干
    • 2.思路
    • 3.代码
    • 4. 问题点思考
  • 二、最小覆盖子串
    • 1.题干
    • 2.解题思路
    • 3.代码
    • 4. 注意点
  • 三、680题:验证回文字符串II
    • 1. 题干
    • 2. 思路
    • 3. 代码
    • 四、
  • 1. 题干
    • 2. 解题思路
    • 3. 代码


算法理解

双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。

若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。

若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。


提示:以下是贪心算法相关的刷题笔记

一、142题:环形链表II

1.题干

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。

说明:不允许修改给定的链表。

2.思路

本题可以用哈希表进行求解,对链表进行遍历,并用哈希表存储每个节点,直到下一次重复寻址到哈希表中的某个节点,即为环的起始节点,但是这种方法的空间复杂度为O(n)。

本题还有一种空间复杂度为O(1)的解法:双指针——快慢指针,具体思路如下:

  • 设置两个指针从链表的头节点开始向后遍历,一个节点步长为1,另一个节点步长为2,进行循环遍历;
  • 遍历完链表,没有与慢指针相遇,则链表没有环路;
  • 如果快慢指针相遇,则将快指针重新指向链表头节点;
  • 快慢指针分别由当前位置,以1为步长遍历链表,直到两者相遇,相遇的节点即为链表环路的起始节点;
    leetcode刷题笔记——双指针_第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. 思路

思路明显,用双指针从字符串的两端向中间遍历:
尝试伪代码:

左指针,右指针  =  0len(s)
while 左指针 < 右指针:
	if:左右指针索引的字符相同:
		左指针++
		右指针++
	else:
		删除左指针对应的字符,右指针不变
		or
		删除右指针对应的字符,左指针不变

		if 两者之一能够构成回文字符串:
			return True
		elsereturn 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. 代码


你可能感兴趣的:(数据结构与算法,指针,算法,leetcode,数据结构)