代码随想录算法训练营第二十七天 | 双指针法-栈与队列,两个章节的复习总结

双指针法-栈与队列

  • 双指针法章节
    • 移除元素
    • 删除有序数组中的重复项
    • 移动零
    • 比较含退格的字符串
    • 有序数组的平方
    • 反转字符串
    • 反转字符串II
    • 替换空格
    • 反转字符串中的单词
    • 纯手撕版,需要注意的地方还蛮多的
    • 反转链表
    • 删除链表的倒数第 N 个结点
      • 这道题之前做的时候没有注意,边界处理是需要着重考虑的,就是删除节点是链表尾结点的情况。
      • 为什么会有上述问题?要从本质上去分析!我们要删除倒数第N个节点,那么slow指针最后就应该停在倒数第N+1个节点,假设我们最后fast指针指向None(这个可由while循环的判断条件来控制),那么在结果处,fast和slow指针之间,相隔N个节点,注意是“相隔”,所以在一开始移动时,要让fast移动N+1步,才能达到目标。而移动N+1步可能造成链表越界,所以要加入虚拟头。
      • 不加入虚拟头,会面临:当N等于链表长度时,按照我的错误做法,fast会走到尾结点后的None,那么fast.next就会报错,目前我没想到好的解决方案,同样,当链表只有一个节点时,不加入虚拟头,slow.next=slow.next.next也会报错。
      • 本题告诉我,虚拟头节点多么重要,但是我还没有理解到:必须加入虚拟头的原因,不加的话,有很多边界情况难以处理,但这似乎不是必须加入的理由?
      • 单独处理边界情况:N等于链表长度(换句话说就是:删除头结点)
    • 链表相交
    • 环形链表 II
      • 我上一次写这道题的代码
      • 这一次学习了卡哥的写法后的代码
      • 可以看出,选择 while 循环的判断条件也非常有讲究
    • 三数之和
    • 四数之和
    • 用栈实现队列
    • 用队列实现栈
    • 有效的括号
    • 删除字符串中的所有相邻重复项
    • 滑动窗口最大值
      • 自己写的代码,直接就AC了,证明单调队列这里,我还是有点印象的。
      • 卡哥的代码,可以做对比
      • 我觉得最主要的区别就在于:一些极端错误情况的处理上,在实际工程中可能出现类似问题,但是在力扣的示例中,肯定不会有这种情况的。
    • 前K个高频元素
      • 不会,是涉及大顶堆,小顶堆的内容。

双指针法章节

移除元素

class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        n = len(nums)
        slow = 0
        fast = 0
        while fast < n :
            if nums[fast]!=val :
                nums[slow] = nums[fast]
                slow += 1
                fast += 1
            else :
                fast += 1
        return slow

删除有序数组中的重复项

注意本题的判断条件,是 slow-1 , 所以需要先判定 slow > 0

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        n = len(nums)
        slow = 0
        fast = 0
        while fast < n :
            # 注意这个判断条件,是 slow-1 , 所以需要先判定 slow > 0
            if slow > 0 and nums[fast] == nums[slow-1] :
                fast += 1
            else :
                nums[slow] = nums[fast]
                fast += 1
                slow += 1
        return slow

移动零

注意本题,要最后进行一步赋值0的操作

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        n = len(nums)
        slow = 0
        fast = 0
        while fast < n :
            if nums[fast] != 0 :
                nums[slow] = nums[fast]
                slow += 1
                fast += 1
            else :
                fast += 1
        while slow < n :
            nums[slow] = 0
            slow += 1

比较含退格的字符串

class Solution:
    def backspaceCompare(self, s: str, t: str) -> bool:
        slow = 0
        fast = 0
        s = list(s)
        t = list(t)
        ns = len(s)
        nt = len(t)
        # 先做s
        while fast < ns :
            if s[fast] != '#':
                s[slow] = s[fast]
                slow += 1
                fast += 1
            else :
                if slow > 0 :
                    slow -= 1
                fast += 1
        slen = slow
        slow = 0
        fast = 0
        # 再做t
        while fast < nt :
            if t[fast] != '#':
                t[slow] = t[fast]
                slow += 1
                fast += 1
            else :
                if slow > 0 :
                    slow -= 1
                fast += 1
        tlen = slow
        if slen != tlen :
            return False
        else :
            for i in range(slen):
                if s[i]!=t[i]:
                    return False
            return True

有序数组的平方

class Solution:
    def sortedSquares(self, nums: List[int]) -> List[int]:
        n = len(nums)
        res = [0]*n
        start = 0
        end = n-1
        # 倒序给结果数组赋值,这样才能保证是非递减顺序
        index = n-1
        while start <= end :
            if nums[start]+nums[end] > 0 :
                res[index] = nums[end]**2
                end -= 1
                index -= 1
            elif nums[start]+nums[end] < 0 :
                res[index] = nums[start]**2
                index -= 1
                start += 1
            else :
                res[index] = nums[start]**2
                index -= 1
                start += 1
        return res

反转字符串

class Solution:
    def reverseString(self, s: List[str]) -> None:
        """
        Do not return anything, modify s in-place instead.
        """
        n = len(s)
        slow = 0
        fast = n-1
        while slow < fast :
            s[slow],s[fast] = s[fast],s[slow]
            slow += 1
            fast -= 1

反转字符串II

class Solution:
    def reverseStr(self, s: str, k: int) -> str:
        n = len(s)
        for i in range(0,n,2*k):
            temp = s[i:i+k]
            s = s[:i] + temp[::-1] + s[i+k:]
        return s

替换空格

class Solution:
    def replaceSpace(self, s: str) -> str:

        s = list(s)
        n = len(s)
        count = 0
        for i in s :
            if i == ' ':
                count += 1
        extend = [0]*count*2
        s = s + extend
        newn = n + count*2
        slow = n-1
        fast = newn-1
        while slow > -1 :
            if s[slow]!=' ' :
                s[fast] = s[slow]
                slow -= 1
                fast -= 1
            else :
                s[fast-2:fast+1] = '%20'
                fast -= 3
                slow -= 1
        return ''.join(s)

反转字符串中的单词

使用 split() 函数版

class Solution:

    def reverseWords(self, s: str) -> str:
        # 甚至可以不用这个strip
        # 直接split也会把首尾的空格去掉
        s = s.strip()
        s = s[::-1]
        s = s.split()
        n = len(s)
        for i in range(n) :
            temp = s[i]
            s[i] = temp[::-1]
        return ' '.join(s)

纯手撕版,需要注意的地方还蛮多的

class Solution:

    def reverseWords(self, s: str) -> str:
        s = list(s)
        n = len(s)
        slow = 0
        fast = 0
        while fast < n :
            if slow == 0 and s[fast]==' ':
                fast += 1
            elif slow > 0 and s[fast]==' ' and s[slow-1] != ' ':
                s[slow] = s[fast]
                slow += 1
                fast += 1
            elif slow > 0 and s[fast]==' ' and s[slow-1] == ' ':
                fast += 1
            # 上面对 fast 做了加一操作,下面这里的 if 是和上面独立的
            # 所以在进行一切操作之前,一定要加上最外层while循环的判断条件 fast < n
            if fast < n and s[fast] != ' ':
                s[slow] = s[fast]
                slow += 1
                fast += 1
        # 最后一个字符是否是空格,是的话,就去掉
        # 我上面的逻辑就会导致,最后一位可能是空格
        if s[slow-1] == ' ':
            s = s[:slow-1]
            length = slow-1
        else :
            s = s[:slow]
            lenght = slow

        s = s[::-1]  
        left = 0
        right = 0
        while right < slow :
            if s[right] != ' ':
                right += 1
            else :
                temp = s[left:right]
                s[left:right] = temp[::-1]
                right += 1
                left = right
            # 这里也要注意,要单独处理 right 走到最后一个位置的情况,由于前面已经去掉了空格
            # 所以要单独处理
            if right == slow-1 :
                temp = s[left:]
                s[left:] = temp[::-1]
                right += 1
                left = right

        return ''.join(s)

反转链表

class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        pre = None
        cur = head
        while cur :
            temp = cur.next
            cur.next = pre
            pre = cur
            cur = temp
        return pre

删除链表的倒数第 N 个结点

这道题之前做的时候没有注意,边界处理是需要着重考虑的,就是删除节点是链表尾结点的情况。

边界错误情况:

class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        slow = head
        fast = head       
        while n > 0:
            fast = fast.next
            n -= 1

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

歪打正着+虚拟头节点:

class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        virtual = ListNode(0,head)
        slow = virtual
        fast = virtual
        while n > 0 :
            fast = fast.next
            n -= 1
        while fast.next :
            fast = fast.next
            slow = slow.next
        slow.next = slow.next.next

        return virtual.next

卡哥的代码

class Solution:
    def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
        # 创建一个虚拟节点,并将其下一个指针设置为链表的头部
        dummy_head = ListNode(0, head)
        
        # 创建两个指针,慢指针和快指针,并将它们初始化为虚拟节点
        slow = fast = dummy_head
        
        # 快指针比慢指针快 n+1 步
        for i in range(n+1):
            fast = fast.next
        
        # 移动两个指针,直到快速指针到达链表的末尾
        while fast:
            slow = slow.next
            fast = fast.next
        
        # 通过更新第 (n-1) 个节点的 next 指针删除第 n 个节点
        slow.next = slow.next.next
        
        return dummy_head.next

为什么会有上述问题?要从本质上去分析!我们要删除倒数第N个节点,那么slow指针最后就应该停在倒数第N+1个节点,假设我们最后fast指针指向None(这个可由while循环的判断条件来控制),那么在结果处,fast和slow指针之间,相隔N个节点,注意是“相隔”,所以在一开始移动时,要让fast移动N+1步,才能达到目标。而移动N+1步可能造成链表越界,所以要加入虚拟头。

不加入虚拟头,会面临:当N等于链表长度时,按照我的错误做法,fast会走到尾结点后的None,那么fast.next就会报错,目前我没想到好的解决方案,同样,当链表只有一个节点时,不加入虚拟头,slow.next=slow.next.next也会报错。

本题告诉我,虚拟头节点多么重要,但是我还没有理解到:必须加入虚拟头的原因,不加的话,有很多边界情况难以处理,但这似乎不是必须加入的理由?

单独处理边界情况:N等于链表长度(换句话说就是:删除头结点)

class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        slow = head
        fast = head       
        
        while n > 0 :
            fast = fast.next
            n -= 1
        # 单独处理,当链表长度等于N的情况,即:删除头结点
        if fast == None :
            head = head.next
        else :
            while fast.next :
                fast = fast.next
                slow = slow.next
            slow.next = slow.next.next
        return head

链表相交

class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
        curA = headA
        curB = headB
        countA = 0
        countB = 0
        while curA :
            curA = curA.next
            countA += 1
        while curB :
            curB = curB.next
            countB += 1
        # 在下面这个判断之后,只需要考虑 countA >= countB 的情况了
        if countA < countB :
            headA,headB = headB,headA
            countA,countB = countB,countA

        diff = countA - countB
        
        while diff > 0 :
            headA = headA.next
            diff -= 1
        while headA :
            if headA == headB :
                return headA
            headA = headA.next
            headB = headB.next
        return None

环形链表 II

这里列出一个代码对比:

我上一次写这道题的代码

class Solution:
    def detectCycle(self, head: ListNode) -> ListNode:
        slow = head
        fast = head
        
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
            
            # If there is a cycle, the slow and fast pointers will eventually meet
            if slow == fast:
                # Move one of the pointers back to the start of the list
                slow = head
                while slow != fast:
                    slow = slow.next
                    fast = fast.next
                return slow
        # If there is no cycle, return None
        return None

这一次学习了卡哥的写法后的代码

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

可以看出,选择 while 循环的判断条件也非常有讲究

三数之和

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        n = len(nums)
        res = []
        for i in range(0,n-2):
            if nums[i] > 0 :
                break
            if i > 0 and nums[i] == nums[i-1] :
                continue
            left = i+1
            right = n-1
            while left < right :
                if nums[i] + nums[left] + nums[right] > 0 :
                    right -= 1
                elif nums[i] + nums[left] + nums[right] < 0 :
                    left += 1
                else :
                    path = [nums[i] , nums[left] , nums[right]]   
                    res.append(path)
                    while left < right and nums[right] == nums[right-1]:
                        right -= 1
                    while left < right and nums[left] == nums[left+1]:
                        left += 1
                    left += 1
                    right -= 1
        return res

四数之和

class Solution:
    def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
        nums = sorted(nums)
        n = len(nums)
        res = []
        for i in range(0,n-3):
            if i > 0 and nums[i]==nums[i-1]:
                continue
            # 剪枝细节,一定注意,要考虑target是负数的情况
            if nums[i] > 0 and nums[i] > target:
                # 这里因为是最外层循环,所以break还是return 无所谓
                break
            
            for j in range(i+1,n-2):
                if j > i+1 and nums[j]==nums[j-1]:
                    continue
                # 剪枝细节,一定注意,要考虑target是负数的情况,同时要留意到:
                # 多个负数相加是更小的负数,所以(nums[i]+nums[j]) > 0是必须的
                # 比如 target=-8,解是[-1,-2,-2,-3],前两个的加和是大于target的!
                if (nums[i]+nums[j]) > 0 and (nums[i]+nums[j]) > target:
                    # 注意这里,不能是return ,而是break
                    break
                
                left = j+1
                right = n-1
                temp = target - (nums[i]+nums[j])
                while left < right :
                    if nums[left]+nums[right] > temp :
                        right -= 1
                    elif nums[left]+nums[right] < temp :
                        left += 1
                    else :
                        path = [nums[i],nums[j],nums[left],nums[right]]
                        res.append(path)
                        while left < right and nums[left]==nums[left+1]:
                            left += 1
                        while left < right and nums[right]==nums[right-1]:
                            right -= 1
                        left += 1
                        right -= 1
        return res

用栈实现队列

class MyQueue:

    def __init__(self):
        self.pushin = []
        self.pushout = []

    def push(self, x: int) -> None:
        self.pushin.append(x)
       

    def pop(self) -> int:
        if self.pushout != [] :
            return self.pushout.pop()
        else :
            self.pushout = self.pushin[::-1]
            self.pushin = []
            return self.pushout.pop()
        

    def peek(self) -> int:
    # 我觉得这里是唯一的难点,搞清楚代码复用的逻辑
    # pop出来后,在对pushout,append进去就好了
    # 这样下次再pop,还是这个值,是正确的!
    # 因为pushout已经可以看做是一个队列了
        num = self.pop()
        self.pushout.append(num)
        return num
        
       
    def empty(self) -> bool:
        if self.pushin == [] and self.pushout == []:
            return True
        else :
            return False

用队列实现栈

两个队列实现:

from collections import deque
class MyStack:

    def __init__(self):
        self.dq1 = deque()
        self.dq2 = deque()
        self.number = 0



    def push(self, x: int) -> None:
        self.dq1.append(x)
        self.number += 1


    def pop(self) -> int:
        if self.empty():
            return None
        for i in range(0,self.number-1):
            self.dq2.append(self.dq1.popleft())
        num = self.dq1.popleft()
        self.dq1 , self.dq2 = self.dq2 , self.dq1
        self.number -= 1
        return num
        
    

    def top(self) -> int:
        return self.dq1[-1]


    def empty(self) -> bool:
        if len(self.dq1) == 0:
            return True
        else :
            return False

一个队列实现:

from collections import deque
class MyStack:

    def __init__(self):
        self.dq1 = deque()
        self.number = 0



    def push(self, x: int) -> None:
        self.dq1.append(x)
        self.number += 1


    def pop(self) -> int:
        if self.empty():
            return None
        for i in range(0,self.number-1):
            self.dq1.append(self.dq1.popleft())
        num = self.dq1.popleft()  
        self.number -= 1
        return num
        
    

    def top(self) -> int:
        return self.dq1[-1]


    def empty(self) -> bool:
        if len(self.dq1) == 0:
            return True
        else :
            return False

有效的括号

class Solution:
    def isValid(self, s: str) -> bool:
    
        stack = []
        n = len(s)
        i = 0
        while i < n :
            if s[i]=='(' :
               stack.append(')')
            elif s[i]=='[' :
                stack.append(']')
            elif s[i]=='{' :
                stack.append('}')
            else :
                if stack == [] or stack.pop() != s[i]:
                    return False
            i += 1
        if len(stack)!=0 :
            return False
        else :
            return True

删除字符串中的所有相邻重复项

class Solution:
    def removeDuplicates(self, s: str) -> str:
        stack = []
        n = len(s)
        i = 0
        while i < n :
            if stack == [] :
                stack.append(s[i])
            else :
                if s[i] == stack[-1]:
                    stack.pop()
                else :
                    stack.append(s[i])
            i += 1
        
        return ''.join(stack)

滑动窗口最大值

自己写的代码,直接就AC了,证明单调队列这里,我还是有点印象的。

from collections import deque
class DQ:
    def __init__(self):
        self.dq = deque()
    def push(self,val):
        if len(self.dq) == 0 :
            self.dq.append(val)
        else :
            while len(self.dq) != 0 and val > self.dq[-1] :
                self.dq.pop()
            self.dq.append(val)
    def top(self):
        return self.dq[0]
    def popleft(self):
        self.dq.popleft()
                
class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        res = []
        dq = DQ()
        n = len(nums)
        for i in range(k):
            dq.push(nums[i])
        res.append(dq.top())
        for i in range(k,n):
            if nums[i-k] == dq.top() :
                dq.popleft()
            dq.push(nums[i])
            res.append(dq.top())
        return res

卡哥的代码,可以做对比

from collections import deque


class MyQueue: #单调队列(从大到小
    def __init__(self):
        self.queue = deque() #这里需要使用deque实现单调队列,直接使用list会超时
    
    #每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
    #同时pop之前判断队列当前是否为空。
    def pop(self, value):
        if self.queue and value == self.queue[0]:
            self.queue.popleft()#list.pop()时间复杂度为O(n),这里需要使用collections.deque()
            
    #如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
    #这样就保持了队列里的数值是单调从大到小的了。
    def push(self, value):
        while self.queue and value > self.queue[-1]:
            self.queue.pop()
        self.queue.append(value)
        
    #查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
    def front(self):
        return self.queue[0]
    
class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        que = MyQueue()
        result = []
        for i in range(k): #先将前k的元素放进队列
            que.push(nums[i])
        result.append(que.front()) #result 记录前k的元素的最大值
        for i in range(k, len(nums)):
            que.pop(nums[i - k]) #滑动窗口移除最前面元素
            que.push(nums[i]) #滑动窗口前加入最后面的元素
            result.append(que.front()) #记录对应的最大值
        return result

我觉得最主要的区别就在于:一些极端错误情况的处理上,在实际工程中可能出现类似问题,但是在力扣的示例中,肯定不会有这种情况的。

前K个高频元素

不会,是涉及大顶堆,小顶堆的内容。

卡哥的代码:

#时间复杂度:O(nlogk)
#空间复杂度:O(n)
import heapq
class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        #要统计元素出现频率
        map_ = {} #nums[i]:对应出现的次数
        for i in range(len(nums)):
            map_[nums[i]] = map_.get(nums[i], 0) + 1
        
        #对频率排序
        #定义一个小顶堆,大小为k
        pri_que = [] #小顶堆
        
        #用固定大小为k的小顶堆,扫描所有频率的数值
        for key, freq in map_.items():
            heapq.heappush(pri_que, (freq, key))
            if len(pri_que) > k: #如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
                heapq.heappop(pri_que)
        
        #找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
        result = [0] * k
        for i in range(k-1, -1, -1):
            result[i] = heapq.heappop(pri_que)[1]
        return result

前K个高频元素–大顶堆小顶堆

你可能感兴趣的:(算法)