【Leetcode Hot100】错题复习(Python版)

一、栈

1. 字符串解码

给定一个经过编码的字符串,返回它解码后的字符串。

编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。

你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。

此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。

解题思路

嵌套括号,需要由内而外生成与拼接字符串 -> 栈的先入后出

 代码

class Solution:
    def decodeString(self, s: str) -> str:
        stack,res,multi=[],'',0
        for c in s:
            if c=='[':
                stack.append([multi,res])
                res,multi='',0
            elif c==']':
                curmulti,lastres=stack.pop()
                res=lastres+curmulti*res
            elif '0'<=c<='9':
                multi=multi*10+int(c)
            else:
                res+=c
        return res

** multi=multi*10+int(c) 的含义是防止数字十位、百位的出现,例如:

"100":第一个字符是’1‘ : 0 * 10 + 1 = 1; 第二个字符是’0‘ : 1 * 10 + 0 = 10; 第三个字符是’0‘ : 10 * 10 + 0 = 100

二、排序

1. 快速排序

排序过程

a. 取一个元素p,使得元素p归位

b. 列表p被分为2部分,左边比p小,右边比p大

c. 递归完成排序

算法模板

def patition(li,left,right):
    tmp=li[left]
    while left < right:
        while left < right and li[right]>=tmp: #右边比p大
            right-=1
        li[left]=li[right]
        while left < right and li[left]<=tmp: #左边比p小
            left+=1
        li[right]=li[left]
    li[left]=tmp
    return left
def quick_sort(li,left,right):
    if left < right:
        mid=patition(li,left,right)
        quick_sort(li,left,mid-1) #递归左区间
        quick_sort(li,mid+1,right) #递归右区间
    return li

(1)数组中第k个最大的元素

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

解题思路:普通方法sort的时间复杂度为O(nlogn),因此需要使用快速排序,如果元素p恰好在第k个位置,则立即返回

代码:

import random
class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        def quick_sort(nums,k):
            #随机选择基数
            pivot=random.choice(nums)
            big,small,equal=[],[],[]
            #将大于、小于、等于pivot的元素分至big,small,equal中
            for num in nums:
                if num>pivot: #大于
                    big.append(num)
                elif num<pivot: #小于
                    small.append(num)
                else:
                    equal.append(num)
            if k<=len(big): #第k大元素在big内
                return quick_sort(big,k)
            if len(nums)-len(small)<k:
                #第k大元素在small中
                return quick_sort(small,k-len(nums)+len(small))
            return pivot
        
        return quick_sort(nums,k)

2. 堆排序

小根堆:根节点的值比它的孩子都小

大根堆:根节点的值比它的孩子都大

实现:heapq(小顶堆)

heapq.heappush(堆,值)  将值加入堆,自动维护小根堆

heapq.heappop(堆,值) 弹出堆顶的值,并自动维护小根堆

heap[0]:取出堆顶元素

实现大顶堆的方法: 小顶堆的插入和弹出操作均将元素 取反 即可。

(1)数据流的中位数

中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。

  • 例如 arr = [2,3,4] 的中位数是 3 。
  • 例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。

实现 MedianFinder 类:

  • MedianFinder() 初始化 MedianFinder 对象。

  • void addNum(int num) 将数据流中的整数 num 添加到数据结构中。

  • double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。

解题思路:用大根堆B保存较小的一半,用小根堆A保存较大的一半

函数 addNum(num) 

当 m=n(即 N 为 偶数):需向 A 添加一个元素。实现方法:将新元素 num 插入至 B ,再将 B 堆顶元素插入至 A 。
当 m=n(即 N 为 奇数):需向 B 添加一个元素。实现方法:将新元素 num 插入至 A ,再将 A 堆顶元素插入至 B 。

函数findMedian()

当 m=n( N 为 偶数):则中位数为 ( A 的堆顶元素 + B 的堆顶元素 )/2。

当 m=n( N 为 奇数):则中位数为 A 的堆顶元素。

代码:

class MedianFinder:

    def __init__(self):
        self.A=[] #小顶堆,保存较大的一半
        self.B=[] #大顶堆,保存较小的一半

    def addNum(self, num: int) -> None:
        if len(self.A) != len(self.B):
            heappush(self.A,num)
            heappush(self.B,-heappop(self.A))
        else:
            heappush(self.B,-num)
            heappush(self.A,-heappop(self.B))

    def findMedian(self) -> float:
        return self.A[0] if len(self.A) !=len(self.B) else (self.A[0]-self.B[0])/2.0
        
# Your MedianFinder object will be instantiated and called as such:
# obj = MedianFinder()
# obj.addNum(num)
# param_2 = obj.findMedian()

3. 冒泡排序

对于长度为n的数组,进行n-1趟(最后一趟剩余一个元素,不用交换),每趟进行n-i-1次移动(i表示趟数),移动需要判断前一个元素是否比后一个元素大,如果是,则交换前后元素

如果一趟下来,没有交换过,则认为数组已经有序

def bubble_sort(lst):
    for i in range(len(lst)-1): #第i趟
        exchange=False
        for j in range(len(lst)-i-1): #指针移动位置
            if lst[j]>lst[j+1]:
                lst[j],lst[j+1]=lst[j+1],lst[j] #前后交换
                exchange=True
        if not exchange:
            return

三、贪心算法

1. 跳跃游戏

给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。

 解题思路

查看当前左边加上跳跃长度是否覆盖到最后一个下标,如果覆盖则True,否则False

代码

class Solution:
    def canJump(self, nums: List[int]) -> bool:
        if len(nums)==1 : return True
        cover=0
        for i in range(len(nums)):
            if i<=cover:
                cover=max(i+nums[i],cover)
                if cover >=len(nums)-1:
                    return True
        return False

2. 跳跃游戏Ⅱ

给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]

每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

  • 0 <= j <= nums[i] 
  • i + j < n

返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]

解题思路

看当前覆盖距离是否到达倒数第二个点,如果是,步数还要加1

代码

class Solution:
    def jump(self, nums: List[int]) -> int:
        cur_distance=0
        res=0
        next_distance=0
        for i in range(len(nums)-1):
            next_distance=max(i+nums[i],next_distance)
            if i==cur_distance:
                res+=1
                cur_distance=next_distance
        return res

3. 划分字母区间

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。

返回一个表示每个字符串片段的长度的列表。

解题思路

记录每个字符最后出现的位置,当遍历到这个位置的时候可以切割

代码

class Solution:
    def partitionLabels(self, s: str) -> List[int]:
        dicts={} #记录最远的位置
        for i,w in enumerate(s):
            dicts[w]=i
        res=[]
        start,end=0,0
        for i,ch in enumerate(s):
            end=max(end,dicts[ch])
            if i==end:
                res.append(end-start+1)
                start=i+1
        return res

四、动态规划

1. 分割等和子集

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

解题思路

这是一个01背包问题

背包容量:分割的两个子集和,数组和的一半

物品:数字

物品的重量和价值:数字大小

动规五部曲

(1)dp数组的含义:dp[i]表示容量为i的背包,所背物品的最大价值为dp[i]

(2)递推公式:求能装的最大价值

装入物品i:dp[i-nums[i]]+nums[i];不装入物品i,背包价值和容量不变:dp[i]

dp[i] = max(dp[i], dp[i-nums[i]]+nums[i])

(3)初始化:题目说数组元素不超过100,数组大小不超过200,因此总和不大于20000,则等和子集最大为10000

dp=[0]*10001

dp[0]一定为0

(4)遍历顺序:一维01背包遍历需要先物品后背包,背包倒序

(5)举例推导

代码

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        #01背包
        #dp[i]表示第i个位置分割的子集和为dp[i]
        #背包:和的一半
        #物品:数字;种类和价值:数字大小

        #如果数组有奇数个,则无法平分
        if sum(nums)%2==1:
            return False
        target=sum(nums)//2 #背包容量

        dp=[0]*10001
        #遍历顺序:先物品后背包,背包倒序
        for n in nums:
            for i in range(target,n-1,-1):
                dp[i]=max(dp[i],dp[i-n]+n)
        if dp[target]==target:
            return True
        else:
            return False

2. 最长有效括号

给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号

子串的长度。

解题思路

有效括号必须以’)‘结尾

当遇到')'时,括号成对出现,需要个数+2

如果出现嵌套括号,'(())' 则需要加上嵌套括号的个数,dp[i-1]+2

判断前面是否还存在括号,'()(())'   dp[i-dp[i-1]-2]

动规五部曲

(1)dp[i]的含义:以i为结尾的子串包含的最长有小括号数为dp[i]

(2)递推公式:dp[i]= dp[i-1] + dp[i-dp[i-1]-2] +2

条件:当前字符为')',s[i-dp[i-1]-1]为'(',并且i-dp[i-1]-1>=0(为什么不是-2,因为-2则是-1,dp[-1]表示最后一个元素)

(3)初始化:dp初始化为0,dp[0]=0

(4)遍历顺序:依次遍历字符串s

代码

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        if s=='': return 0
        #dp[i]表示以i结尾的子串的最长有效括号为dp[i]
        dp=[0]*len(s)

        for i,ch in enumerate(s):
            if ch==')' and i-dp[i-1]-1>=0 and s[i-dp[i-1]-1]=='(':
                dp[i]=dp[i-1]+dp[i-dp[i-1]-2]+2
        return max(dp)

3. 最长回文子串

给你一个字符串 s,找到 s 中最长的回文子串。

动规五部曲

(1)dp[i][j]的含义:dp[i][j]表示子串[i,```,j]是否为回文子串

(2)递推公式:

当s[i]==s[j]时,当前子串是否为回文取决于内部是否为回文子串,即dp[i][j]=dp[i+1][j-1]

当[i+1,j-1]只有一个元素时或者没有元素,则一定回文,因此需要判断j-i<3

(3)初始化:初始化为False,对角元素即i=j时为True

(4)遍历顺序:由于i<=j,因此先遍历j(1~len(s)),再遍历i(0~j)

需要一个记录最大回文子串和起始下标的变量,如果dp[i][j]为True,则判断j-i+1>length

代码

class Solution:
    def longestPalindrome(self, s: str) -> str:
        #dp[i][j]:表示子串s[i,```,j]的是否为回文子串

        #初始化
        dp=[[False]*len(s) for _ in range(len(s))]
        for i in range(len(s)): #对角线元素一定为回文子串
            dp[i][i]=True
        length=1 #用于更新最长回文子串的长度
        begin=0 #用于记录回文子串的起始位置

        for j in range(1,len(s)):
            for i in range(j):
                if s[i]!=s[j]:
                    dp[i][j]=False
                else:
                    if j-i<3:
                        dp[i][j]=True
                    else:
                        dp[i][j]=dp[i+1][j-1]
                
                if dp[i][j] and j-i+1 > length:
                    length=j-i+1
                    begin=i
        return s[begin:begin+length]

五、技巧

1. 只出现一次的数字

给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。

解题思路

使用字典使用了O(n)空间,题目要求只使用常量空间,因此使用位运算方法

异或运算的结果就是只出现一次的元素

代码

class Solution:
    def singleNumber(self, nums: List[int]) -> int:

        #位运算,O(1)空间
        #异或运算的结果就是只出现一次的数字
        return reduce(lambda x,y:x^y,nums)

**reduce接受一个函数和一个可迭代对象,函数会被应用到可迭代对象的每个元素上,然后将结果累积起来。函数接受x和y两个参数,x和y进行异或运算,应用到nums的每个元素并累积起来。

2. 下一个排列

整数数组的一个 排列  就是将其所有成员以序列或线性顺序排列。

  • 例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3][1,3,2][3,1,2][2,3,1] 。

整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

  • 例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。
  • 类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。
  • 而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。

给你一个整数数组 nums ,找出 nums 的下一个排列。

必须 原地 修改,只允许使用额外常数空间。

解题思路

(1)从后向前,找第一个前大于后的元素

(2)从后向前找第一个大于替换元素的最小元素

(3)交换

(4)交换的后部分升序排列

代码

class Solution:
    def nextPermutation(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        i=len(nums)-1
        while i>0 and nums[i-1]>=nums[i]: #找到第一个前面大于后面的元素
            i-=1
        if i!=0:
            j=len(nums)-1
            while nums[j]<=nums[i-1]: #找到第一个替换元素的最小元素
                j-=1
            #交换
            nums[i-1],nums[j]=nums[j],nums[i-1]
        
        #排序
        nums[i:]=sorted(nums[i:])

3. 寻找重复元素

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。

假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。

你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。

解题思路

方法一:二分搜索

统计 nums[i] <= 当前元素的个数cnt,如果cnt大于当前当前元素说明有重复,找到第一个大于的值;否则说明没有重复

方法二:快慢指针法

先让慢指针一次走一步,快指针一次走两步;如果相遇,说明存在重复元素;再让慢指针回到起始点,快指针在相遇点,同时一次一步,相遇的点就是重复的元素(环形链表题)

代码

class Solution:
    def findDuplicate(self, nums: List[int]) -> int:
        #方法一:二分搜索
        #思路:统计nums[i]<=当前元素的个数cnt,如果cnt大于当前当前元素说明有重复,找到第一个大于的值;否则说明没有重复
        l,r=1,len(nums)-1
        ans=-1
        while l<=r:
            mid=(l+r)//2
            cnt=0
            for i in range(len(nums)):
                cnt+= nums[i] <= mid
            if cnt <= mid:
                l=mid+1
            else:
                r=mid-1
                ans=mid
        return ans

        #方法二:快慢指针法
        #思路:先让慢指针一次走一步,快指针一次走两步;如果相遇,说明存在重复元素;再让慢指针回到起始点,快指针在相遇点,同时一次一步,相遇的点就是重复的元素
        slow,fast=0,0
        while True:
            fast=nums[fast]
            fast=nums[fast]
            slow=nums[slow]
            if fast ==slow:
                break
        slow=0
        while slow != fast:
            slow=nums[slow]
            fast=nums[fast]
        return slow

六、链表

1. 两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

解题思路

逆序存储-逆序创建新节点,这个新节点的值由l1的值、l2的值和进位累加得到,存储的是累加值的余数,同时需要更新进位数(累加值的除数),然后更新当前节点、l1节点和l2节点

代码

class Solution:
    def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
        cur=dummy=ListNode() #创建虚拟节点
        carry=0 #进位
        while l1 or l2 or carry:
            s=carry+(l1.val if l1 else 0)+(l2.val if l2 else 0) #累加
            cur.next=ListNode(s%10) #余数作为新节点
            carry=s//10 #更新进位
            cur=cur.next
            if l1: l1=l1.next
            if l2: l2=l2.next
        return dummy.next

2. 删除链表倒数第n个节点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

解题思路

快慢指针法:倒数第n个节点,快慢指针相差n+1(因为慢指针需要指向倒数第n个节点的前一个节点才能删除);先让快指针走n+1步,再让块、慢指针同时移动,直至快指针指向链表末尾,此时慢指针指向倒数第n+1个节点

虚拟节点:如果倒数第n个节点是头节点,则涉及到头节点的处理,因此使用虚拟节点

代码

class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        #双指针法:先让快指针走n+1步,再让快慢指针同时移动,直至快指针到达链表末尾
        #虚拟节点,防止倒数第n个节点为头节点
        dummy=ListNode(next=head)
        fast,slow=dummy,dummy
        for _ in range(n+1):
            fast=fast.next
        
        while fast:
            fast=fast.next
            slow=slow.next
        slow.next=slow.next.next
        return dummy.next

3. 两两交换链表的节点

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)

解题思路

虚拟节点:设计头节点的处理,使用虚拟节点dummy

注意交换顺序:dummy->2->1->3

临时保存重要节点:1、3

更新下一个节点:cur=cur.next.next

代码

class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if not head: return 
        #虚拟节点
        cur=dummy=ListNode(next=head)
        while cur.next and cur.next.next:
            tmp1=cur.next
            tmp2=cur.next.next.next
            cur.next=cur.next.next
            cur.next.next=tmp1
            tmp1.next=tmp2
            cur=cur.next.next
        return dummy.next

4. K 个一组翻转链表

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

解题思路

代码 

class Solution:
    # 翻转一个子链表,并且返回新的头与尾
    def reverse(self, head: ListNode, tail: ListNode):
        prev = tail.next
        p = head
        while prev != tail:
            nex = p.next
            p.next = prev
            prev = p
            p = nex
        return tail, head

    def reverseKGroup(self, head: ListNode, k: int) -> ListNode:
        hair = ListNode(0)
        hair.next = head
        pre = hair

        while head:
            tail = pre
            # 查看剩余部分长度是否大于等于 k
            for i in range(k):
                tail = tail.next
                if not tail:
                    return hair.next
            nex = tail.next
            head, tail = self.reverse(head, tail)
            # 把子链表重新接回原链表
            pre.next = head
            tail.next = nex
            pre = tail
            head = tail.next
        
        return hair.next

5. 随机链表的复制

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 

例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。

返回复制链表的头节点。

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:

  • val:一个表示 Node.val 的整数。
  • random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为  null 。

你的代码  接受原链表的头节点 head 作为传入参数。

解题思路

使用字典,键源节点,值新节点

遍历每个节点,创建新的引用next和random

代码

class Solution:
    def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
        if not head : return 
        #初始化字典 {源节点:新节点}
        dicts={}
        cur=head
        #复制链表,遍历建立新节点,添加键值对
        while cur:
            node=Node(cur.val)
            dicts[cur]=node
            cur=cur.next
        #构建新链表的引用指向next和random
        cur=head
        while cur:
            dicts[cur].next=dicts.get(cur.next)
            dicts[cur].random=dicts.get(cur.random)
            cur=cur.next
        return dicts[head]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值