LEETCODE PATTERNS & Neetcode 刷题记录(Leetcode题解-Python语言)

LEETCODE PATTERNS 官网在这个链接,Neetcode 官网在这个链接

If input array is sorted then 遇到有序数组用二分或双指针

  • Binary search
  • Two pointers

If asked for all permutations/subsets then 求排列或子集用回溯

  • Backtracking

If given a tree then 遇到树就用深度优先搜索或广度优先搜索

  • DFS
  • BFS

If given a graph then 遇到图也用深度优先搜索或广度优先搜索

  • DFS
  • BFS

If given a linked list then 遇到链表用双指针

  • Two pointers

If recursion is banned then 无法递归就用栈

  • Stack

If must solve in-place then 要原地返回结果就交换数据或用一个指针存储多个数据

  • Swap corresponding values
  • Store one or more different values in the same pointer

If asked for maximum/minimum subarray/subset/options then 求最大或最小子数组、子集就用动态规划

  • Dynamic programming

If asked for top/least K items then 求 top/least K 就用堆

  • Heap

If asked for common strings then

  • Map
  • Trie

Else

  • Map/Set for O(1) time & O(n) space
  • Sort input for O(nlogn) time and O(1) space

目录


一、Array & Hashing 数组与哈希

217. 存在重复元素

题目页面

解法一:蛮力解法,从头开始遍历数组中的元素,对于每个元素,都将其与后面的所有元素进行比较,若有重复则返回 True,可知此解法时间复杂度为 O ( n 2 ) O({n^2}) O(n2),空间复杂度为 O ( 1 ) O(1) O(1)

解法二:排序后遍历,将数组排序之后,重复的元素一定是相邻的,所以很容易比较出来,但排序需要时间开销,所以此解法时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间复杂度为 O ( 1 ) O(1) O(1)

解法三:哈希表,利用哈希表插入与查找都只需要常数时间的特性,把出现过的元素记录在哈希表中,若后面遍历元素时发现其已经在哈希表里了,就返回 True,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def containsDuplicate(self, nums: List[int]) -> bool:
        record = set()
        for num in nums:
            if num in record:
                return True
            else:
                record.add(num)
        return False

448. 找到所有数组中消失的数字

题目页面

解法一:遇到这种数组中的数字范围与数组下标可以一一对应的,思路就是把数字与下标对应起来。在此题中,可以先遍历数组的数字,以数字的绝对值(正数)加一,作为下标,将该下标的数字设为负数,相当于是记录出现过的数字。第二次遍历则寻找正数,即下标的数字是没出现过的,作为答案。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def findDisappearedNumbers(self, nums: List[int]) -> List[int]:
        for num in nums:
            index = abs(num) - 1
            nums[index] = -abs(nums[index])
        return [i+1 for i, num in enumerate(nums) if num > 0]

解法二:另一种方法,还是把数组中出现过的数字作为下标,将该下标对应的值加上 n,则没有被加上 n 的值对应的下标就是没出现过,也就是没出现过的数字。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def findDisappearedNumbers(self, nums: List[int]) -> List[int]:
        n = len(nums)
        for num in nums:
            index = (num - 1) % n
            nums[index] += n
        return [i+1 for i, num in enumerate(nums) if num <= n]

442. 数组中重复的数据

题目页面

解法:与上一题解法二一样的思路,出现过的数字作为下标,将下标对应的数字值加 n,则重复的数一定被加了 2 次的 n,所以寻找数值大于 2n 的即可。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def findDuplicates(self, nums: List[int]) -> List[int]:
        n = len(nums)
        for num in nums:
            index = (num - 1) % n
            nums[index] += n
        return [i+1 for i, num in enumerate(nums) if num > 2 * n]

242. 有效的字母异位词

题目页面

解法一:哈希表,分别把两个字符串中的字符以及字符的出现次数记录在两个哈希表中,然后遍历哈希表看看两个哈希表的键、值是否完全一样,注意 Python 中索引不存在的键会报错,要用 get 函数,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def isAnagram(self, s: str, t: str) -> bool:
        countS = {}
        countT = {}
        if len(s) != len(t):
            return False
        for i in range(len(s)):
            countS[s[i]] = 1 + countS.get(s[i], 0)
            countT[t[i]] = 1 + countT.get(t[i], 0)
        for ch in countS:
            if countS[ch] != countT.get(ch, 0):
                return False
        return True

利用 collections 里面的 Counter 可以一行解决:

class Solution:
    def isAnagram(self, s: str, t: str) -> bool:
        return Counter(s) == Counter(t)

解法二:排序法,对两个字符串进行排序,判断它们是否相等即可,优化了空间但损失了时间,时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def isAnagram(self, s: str, t: str) -> bool:
        return sorted(s) == sorted(t)

1. 两数之和

题目页面

解法一:蛮力解法,从头开始遍历数组中的元素,对于每个元素,都将其与后面的所有元素进行相加,判断它们的和是否等于 target,时间复杂度为 O ( n 2 ) O({n^2}) O(n2),空间复杂度为 O ( 1 ) O(1) O(1)

解法二:哈希表,把出现过的元素以及对应下标记录在哈希表中,若后面遍历元素 num 时发现 target - num 已经在哈希表里了,就返回 True,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        hashmap = dict()
        for i, num in enumerate(nums):
            if target - num in hashmap:
                return [hashmap[target - num], i]
            else:
                hashmap[num] = i

49. 字母异位词分组

题目页面

解法一:排序,对每一个字符串都进行排序,可知互为异位词的字符串排序后的结果是一样的,所以可以把该结果作为每一组异位词的标识(key),注意列表不能作为键,所以要转换为字符串或者元组。假设有 m 个字符串,每个字符串的平均长度为 n,排序耗时是 n log ⁡ n n\log n nlogn,总的时间复杂度为 O ( m n log ⁡ n ) O(mn\log n) O(mnlogn),空间复杂度为 O ( m n ) O(mn) O(mn)

class Solution:
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        ans = collections.defaultdict(list)
        for word in strs:
            ans[str(sorted(word))].append(word)
        return list(ans.values())

解法二:解法一中是将异位词排序后的结果作为标识,这里我们还能将异位词中每个字母出现的次数作为标识(注意将列表转换为字符串或元组),时间复杂度为 O ( m n ) O(mn) O(mn),空间复杂度为 O ( m n ) O(mn) O(mn)

class Solution:
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        ans = collections.defaultdict(list)
        for word in strs:
            count = [0] * 26
            for ch in word:
                count[ord(ch) - ord('a')] += 1
            ans[tuple(count)].append(word)
        return list(ans.values())

347. 前 K 个高频元素

题目页面

解法一:首先对每个元素进行频率统计,耗时 O ( n ) O(n) O(n),然后对频率进行排序,再输出最大的 K 个,总时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn),但这不符合题目要求,我们的解法必须优于 O ( n log ⁡ n ) O(n\log n) O(nlogn)

解法二:首先对每个元素进行频率统计,然后将频率加入到最大堆中,这样进行 K 次弹出,即可得到最大的 K 个频率,时间复杂度为 O ( k log ⁡ n ) O(k\log n) O(klogn)

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        count = collections.Counter(nums)
        heap = []
        for num, cnt in count.items():
            heapq.heappush(heap, (-cnt, num))
        ans = []
        for _ in range(k):
            ans.append(heapq.heappop(heap)[1])
        return ans

解法三:首先对每个元素进行频率统计,然后借鉴桶排序的思想,可知元素的出现次数最大为数组长度,所以可以构建一个频次数组,下标代表出现次数,而值是一个列表,其中记录了出现次数为该下标的所有元素。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        count = collections.Counter(nums)
        freq = [[] for _ in range(len(nums) + 1)]
        for num, cnt in count.items():
            freq[cnt].append(num)
        ans = []
        for i in range(len(nums), 0, -1):
            for num in freq[i]:
                ans.append(num)
                if len(ans) == k:
                    return ans

238. 除自身以外数组的乘积

题目页面

解法一:最直观的想法,就是求数组的总乘积,然后用总乘积除以每一个位置的结果,就是该位置除自身以外数组的乘积,但是题目禁止用除法,所以不考虑这个解法。

解法二:前缀积 + 后缀积,在每一个位置上,它除自身以外数组的乘积,实际上就是其左边所有元素乘积(前缀积)与其右边所有元素乘积(后缀积)的乘积。所以可以用两个数组分别记录前缀积和后缀积,然后输出数组的第 i 个位置的答案,就是前缀积数组第 i - 1 个位置与后缀积数组第 i + 1 个位置的元素乘积。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def productExceptSelf(self, nums: List[int]) -> List[int]:
        ans = [1] * len(nums)

        prefix = [1] * len(nums)
        prefix[0] = nums[0]
        for i in range(1, len(nums)):
            prefix[i] = prefix[i-1] * nums[i]
        
        postfix = [1] * len(nums)
        postfix[-1] = nums[-1]
        for i in range(len(nums) - 2, -1, -1):
            postfix[i] = postfix[i+1] * nums[i]
        
        ans[0] = postfix[1]
        ans[-1] = prefix[-2]
        for i in range(1, len(nums)-1):
            ans[i] = prefix[i-1] * postfix[i+1] 
        
        return ans

解法三:对解法二进行优化,由于题目说明输出数组不计入空间复杂度,所以可以将前缀积记录在输出数组中,然后在计算后缀积的同时,将其与输出数组对应位置的值相乘,即可得到答案。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def productExceptSelf(self, nums: List[int]) -> List[int]:
        ans = [1] * len(nums)

        prefix = 1
        for i in range(len(nums)):
            ans[i] = prefix
            prefix *= nums[i]
        
        postfix = 1
        for i in range(len(nums) - 1, -1, -1):
            ans[i] *= postfix
            postfix *= nums[i]
        
        return ans

36. 有效的数独

题目页面

解法:每一行、每一列、以及每一个 3*3 宫都用一个哈希表记录出现过的数字,遍历二维数组,如果不是数字则跳过,如果是数字且出现过,就是无效的数独,否则就将其加入到所在行、列、宫的哈希表中。

class Solution:
    def isValidSudoku(self, board: List[List[str]]) -> bool:
        cols = collections.defaultdict(set)
        rows = collections.defaultdict(set)
        squares = collections.defaultdict(set)

        for r in range(len(board)):
            for c in range(len(board[0])):
                if board[r][c] == '.':
                    continue
                if (board[r][c] in rows[r] or 
                    board[r][c] in cols[c] or
                    board[r][c] in squares[(r // 3, c // 3)]):
                    return False
                rows[r].add(board[r][c])
                cols[c].add(board[r][c])
                squares[(r // 3, c // 3)].add(board[r][c])

        return True

271. 字符串的编码与解码

题目页面

解法:当我们有一个字符串列表,要将其编码为字符串且能够解码,首先想到是利用一些分隔符例如逗号、分号、竖线等等,但题目说字符串中可能为任意字符,所以无法使用分隔符。也有另一种方法,就是用一个列表存储每个字符串的长度,但题目说不能存储状态。所以我们要想到将长度直接放在字符串的前面,但字符串也可能是数字,所以得在长度后面加个分割符,此处用 ‘#’。在解码的时候,先提取数字,直到遇到 ‘#’,就将后面的字符串加入列表。

class Codec:
    def encode(self, strs: List[str]) -> str:
        """Encodes a list of strings to a single string.
        """
        ans = ''
        for s in strs:
            ans += str(len(s)) + '#' + s
        return ans

    def decode(self, s: str) -> List[str]:
        """Decodes a single string to a list of strings.
        """
        ans = []
        i = 0
        while i < len(s):
            j = i
            while s[j] != '#':
                j += 1
            length = int(s[i:j])
            ans.append(s[j + 1 : j + 1 + length])
            i = j + 1 + length
        return ans

128. 最长连续序列

题目页面

解法:一种直观的想法是对数组进行排序,然后再找出最长连续序列,但不符合题目要求的时间复杂度。考虑每一个连续序列,实际上都有一个开始的数,那如何识别一个数是起始数呢?方法就是看这个数的减一是否在数组中,如果不在则说明它是起始数。为了加速判断数是否在数组中,我们可以借助集合。如果遇到了起始数,则继续判断它的加一是否在集合中,直到不在为止,记录下长度。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)
在这里插入图片描述

class Solution:
    def longestConsecutive(self, nums: List[int]) -> int:
        NumSet = set(nums)
        ans = 0
        for num in nums:
            if (num - 1) not in NumSet:
                length = 1
                while (num + length) in NumSet:
                    length += 1
                ans = max(ans, length)
        return ans

二、Two Pointers 双指针

977. 有序数组的平方

题目页面

解法一:最简单的方法就是将数组 nums 中的数平方后直接排序。时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)

解法二:利用数组 nums 是有序的特点,设置左右两个指针,每次将绝对值较大(平方也会较大)的那个数的平方加入 ans,然后移动指针,最后逆序返回 ans 即可。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def sortedSquares(self, nums: List[int]) -> List[int]:
        left = 0
        right = len(nums) - 1
        ans = []
        while left <= right:
            if abs(nums[left]) > abs(nums[right]):
                ans.append(nums[left] ** 2)
                left += 1
            else:
                ans.append(nums[right] ** 2)
                right -= 1
        return ans[::-1]

125. 验证回文串

题目页面

解法一:由于题目只要求考虑字母与数字,所以要忽略所有的其他符号,遍历字符串,isalnum() 为真才加入到 newStr 中,最后判断 newStr 与自身的反转是否相同即可。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def isPalindrome(self, s: str) -> bool:
        newStr = ""

        for ch in s:
            if ch.isalnum():
                newStr += ch.lower()
        
        return newStr == newStr[::-1]

解法二:创建一个经过处理的新字符串消耗了空间,用双指针就可以优化空间复杂度。左右指针分别从字符串的左边与右边开始遍历,遇到字母或者数字才进行比较,如果不同就返回 False,直到两个指针相遇,返回 True。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def isPalindrome(self, s: str) -> bool:
        left = 0
        right = len(s) - 1

        while left < right:
            while left < right and not s[left].isalnum():
                left += 1
            while left < right and not s[right].isalnum():
                right -= 1
            if s[left].lower() != s[right].lower():
                return False
            left += 1
            right -= 1
        
        return True

844. 比较含退格的字符串

题目页面

解法:比较两个字符串,首先会想到用双指针,分别比较两个字符串中的字符,但是字符串中存在退格符,如果从前往后遍历的话,是不知道后面有没有退格符的,所以应该从后往前遍历,每当遇到退格符就说明指针要多移动一次,知道两个指针都移动到开头为止。时间复杂度为 O ( n + m ) O(n + m) O(n+m),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def backspaceCompare(self, s: str, t: str) -> bool:
        i, j = len(s) - 1, len(t) - 1
        skipS = skipT = 0
        while i >= 0 or j >= 0:
            while i >= 0:
                if s[i] == '#':
                    skipS += 1
                    i -= 1
                elif skipS > 0:
                    skipS -= 1
                    i -= 1
                else:
                    break
            while j >= 0:
                if t[j] == '#':
                    skipT += 1
                    j -= 1
                elif skipT > 0:
                    skipT -= 1
                    j -= 1
                else:
                    break
            if i >= 0 and j >= 0:
                if s[i] != t[j]:
                    return False
            elif i >= 0 or j >= 0:
                return False
            i -= 1
            j -= 1
        return True

167. 两数之和 II - 输入有序数组

题目页面

解法一:最弱的方法,是遍历每个数字,然后将其与后面的所有数字都进行相加,即枚举出所有的两数之和,这样的时间复杂度为 O ( n 2 ) O({n^2}) O(n2);如果改用哈希表,就跟 两数之和 一样,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

解法二:注意这题的输入是有序数组,遇到有序就应该想到双指针。解法二就是用双指针分别从两边出发,指针指向的元素之和大了左指针就右移,小了右指针就左移,直到遇到符合目标的和或者两指针相交。注意下标从 1 开始。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        left = 0
        right = len(numbers) - 1
        while left < right:
            sum = numbers[left] + numbers[right]
            if sum < target:
                left += 1
            elif sum > target:
                right -= 1
            else:
                return [left+1, right+1]

15. 三数之和

题目页面

解法:由于题目说明不能出现重复的三元组,所以可以通过排序进行去重,遍历元素下标为 i,如果 i - 1 的元素与 i 是一样的,就跳过 i,否则将其作为三数之一,然后剩下的就是两数之和问题了。可以用哈希表解决,但是消耗了 O ( n ) O(n) O(n) 的空间,使用双指针则可以在 O ( 1 ) O(1) O(1) 空间复杂度下解决问题。同样地,在双指针移动的时候,也应该去重,如果移动的下个位置是重复位置则跳过。总的时间复杂度为 O ( n 2 ) O({n^2}) O(n2),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        ans = []
        for i in range(len(nums)):
            if i > 0 and nums[i-1] == nums[i]:
                continue
            left = i + 1
            right = len(nums) - 1
            while left < right:
                sum = nums[i] + nums[left] + nums[right]
                if sum < 0:
                    left += 1
                elif sum > 0:
                    right -= 1
                else:
                    ans.append([nums[i], nums[left], nums[right]])
                    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 ans

16. 最接近的三数之和

题目页面

解法:与上一题基本一样,只是需要加上判断当前三数之和是否更接近 target。时间复杂度为 O ( n 2 ) O({n^2}) O(n2),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def threeSumClosest(self, nums: List[int], target: int) -> int:
        nums.sort()
        ans = 10**7
        for i in range(len(nums)):
            if i > 0 and nums[i-1] == nums[i]:
                continue
            left = i + 1
            right = len(nums) - 1
            while left < right:
                sum = nums[i] + nums[left] + nums[right]
                if abs(sum - target) < abs(ans - target):
                    ans = sum
                if sum == target:
                    return target
                elif sum < target:
                    while left < right and nums[left+1] == nums[left]:
                        left += 1
                    left += 1
                else:
                    while left < right and nums[right-1] == nums[right]:
                        right -= 1
                    right -= 1
        return ans

713. 乘积小于 K 的子数组

题目页面

解法:双指针 + 滑动窗口,思路就是利用滑动窗口记录子数组的乘积,每次右指针都右移一位,以扩大窗口,检查当乘积大于等于 k 时就左指针右移,以缩小窗口。重点是为什么 ans 是增加 right - left + 1,这是因为,例如已有子数组 [1, 2],此时新进来一个 3 变成 [1, 2, 3],则多了的子数组为 [3] [1, 2] [1, 3] [1, 2, 3],都是包含 3 的,数量就是 right - left + 1。时间复杂度为 O ( n ) O({n}) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
        left = 0
        proc = 1
        ans = 0
        for right in range(len(nums)):
            proc *= nums[right]
            while proc >= k and left <= right:
                proc /= nums[left]
                left += 1
            ans += right - left + 1
        return ans

75. 颜色分类

题目页面

解法:要在原地对颜色按照红色、白色、蓝色顺序排列,使用三个指针 red、white、blue。red 和 white 初始在最左边,blue 初始在最右边。每次考虑 white 指针,如果其遇到了白色,自然就是 white指针右移一位;如果其遇到了红色,则与 red 指针的元素进行交换,然后 red 指针右移一位,相当于 red 指针逐渐从左到右开拓红色区域;如果其遇到了蓝色,则与 blue 指针的元素进行交换,然后 blue 指针左移一位,相当于 blue 指针逐渐从右到左开拓蓝色区域。重点在于,因为 white 指针和 red 指针一样都是从左开始的,所以 white 与 red 交换后不可能得到蓝色元素,交换后 white 和 red 指针都可以右移;但 white 与 blue 交换就可能得到红色元素,所以交换后只有 blue 指针可以左移,而 white 指针不能右移。时间复杂度为 O ( n ) O({n}) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        red, white, blue = 0, 0, len(nums)-1
        
        while white <= blue:
            if nums[white] == 0:
                nums[red], nums[white] = nums[white], nums[red]
                white += 1
                red += 1
            elif nums[white] == 1:
                white += 1
            else:
                nums[white], nums[blue] = nums[blue], nums[white]
                blue -= 1

11. 盛最多水的容器

题目页面

解法:利用双指针,初始表示最大的长度,此时的高度取决于两条线中较矮的那条,计算出面积。然后长度减少 1,由于高度取决于较矮的线,如果移动了较高线的指针,后面的面积肯定比当前小(高度不变或变小,长度变小),所以移动较矮线的指针,直到双指针相遇为止。

class Solution:
    def maxArea(self, height: List[int]) -> int:
        left, right = 0, len(height) - 1
        ans = 0
        while left < right:
            area = (right - left) * min(height[left], height[right])
            ans = max(ans, area)
            if height[left] < height[right]:
                left += 1
            else:
                right -= 1
        return ans

42. 接雨水

题目页面

解法一:核心思路还是木桶效应,考虑每一个高度的位置 i,它所在的位置能接多少水,除了和自身高度 height[i] 有关,还和它左右两边的高度有关,更准确地说,是左边最大高度 maxLeft 和右边最大高度 maxRight 中较小的那个,即 min(maxLeft, maxRight),如图所示
在这里插入图片描述
在这里插入图片描述
第一张图中,高度 height[i] 为 2,这意味着如果它想接到雨水,左右两边的最大高度都要至少为 3,产生了高度差为 1 才能接到 1 的水。而实际的 maxLeft 为 1,maxRight 为 3,所以 min(maxLeft, maxRight) = 1,高度差为负数,所以接不到水。

第二张图中,高度 height[i] 为 1,位置 i 的 maxLeft 为 2,maxRight 为 3,所以 min(maxLeft, maxRight) = 2,min(maxLeft, maxRight) - height[i] 即高度差为 1,接到 1 的水。

因此,我们可以用两个数组分别记录每个位置的 maxLeft 和 maxRight,然后在每个位置上求 min(maxLeft, maxRight) - height[i],最后再求和即可。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)
在这里插入图片描述
解法二:利用双指针,我们可以优化空间复杂度。方法是用左右两个指针,先指向最左和最右位置,同时把两个位置的值 height[left] 和 height[right] 作为初始的 maxLeft 和 maxRight。然后,移动 maxLeft 和 maxRight 中值较小的那个指针,如下图所示,移动了 left 指针。
在这里插入图片描述
我们也知道边界是不可能接到水的,移动一次之后 left 指针到达了非边界,此时的 maxLeft 显然是 0,但 maxRight 实际上是 3 而不是 1 呀,但是没有关系,因为要取的是 min(maxLeft, maxRight),而我们已经知道 maxLeft 小于 maxRight(所以才移动了 left 指针),所以实际上 min(maxLeft, maxRight) 就是 maxLeft。这样我们就可以求接水值 maxLeft - height[left],再通过比较 maxLeft 与 height[left] 更新 maxLeft,最后再移动 maxLeft 和 maxRight 两者中较小的指针,重复上面的步骤。在代码写法上,可以将更新最大高度放在求接水值的前面,这样就不用判断接水值是否为负数了。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def trap(self, height: List[int]) -> int:
        if not height:
            return 0
        
        left, right = 0, len(height) - 1
        maxLeft, maxRight = height[left], height[right]
        ans = 0
        while left < right:
            if maxLeft < maxRight:
                left += 1  # 移动指针
                maxLeft = max(maxLeft, height[left]) # 更新最大高度
                ans += maxLeft - height[left]  # 求接水值
            else:
                right -= 1
                maxRight = max(maxRight, height[right])
                ans += maxRight - height[right]
        return ans

三、Sliding Window 滑动窗口

121. 买卖股票的最佳时机

题目页面

解法一:双指针法,左指针表示买入,右指针表示卖出,初始时左指针在最左边,右指针在其右边。当左指针的价格大于等于右指针价格时,是无法进行交易的,此时左指针应该移动到右指针的位置(而不是简单地左指针右移一位,因为可能在交易多次之后出现了更低的买入价,见第二张图),右指针还是在其右边;当左指针价格小于右指针价格时可以交易,计算出赚到的利润,并更新最大的利润,然后右指针右移。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)
在这里插入图片描述
在这里插入图片描述

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if len(prices) == 1:
            return 0
        left, right = 0, 1
        ans = 0
        while right < len(prices):
            if prices[left] >= prices[right]:
                left = right
                right = left + 1
            else:
                ans = max(ans, prices[right] - prices[left])
                right += 1
        return ans

3. 无重复字符的最长子串

题目页面

解法:用滑动窗口记录下无重复字符的子串,右指针一直右移,直到遇到一个字符是已经存在于滑动窗口中的,此时就开始将左指针右移,直到这个字符从滑动窗口中消失为止。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        if not s:
            return 0
        record = set()
        left, right = 0, 0
        ans = 0
        while right < len(s):
            while s[right] in record:
                record.remove(s[left])
                left += 1
            record.add(s[right])
            ans = max(ans, right - left + 1)
            right += 1
        return ans

424. 替换后的最长重复字符

题目页面

解法:用滑动窗口 count 记录下子字符串中的各字母以及出现频率,滑动窗口的两边为左右指针,则滑动窗口的长度为 right - left + 1。可知,所包含的相同字母一定是滑动窗口中出现频率最高的字母 max(count.values()),其余被替换的字母,数量最多为 k,即 (right - left + 1) - max(count.values()) <= k,如果出现了大于 k,则需要移动左指针,直到被替换字母数量不大于 k 为止。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def characterReplacement(self, s: str, k: int) -> int:
        count = {}
        left, right = 0, 0
        ans = 0
        while right < len(s):
            count[s[right]] = 1 + count.get(s[right], 0)
            while (right - left + 1) - max(count.values()) > k:
                count[s[left]] -= 1
                left += 1
            ans = max(ans, right - left + 1)
            right += 1
        return ans

有一种优化方法,是用一个变量 maxF 记录字母出现的最大频率,取代每次寻找最大频率 max(count.values()) 的操作。当出现了新的更大的最大频率时,自然是更新它,但当最大频率减少时,不需要改变它的值。这是因为
只有当 (right - left + 1) - maxF <= k,且长度与最大频率都变大了,答案 ans 才会真正被更新;如果长度变小,自然不可能为答案,如果只有长度变大,maxF 变小,也是不可能满足小于等于 k 的条件的。

class Solution:
    def characterReplacement(self, s: str, k: int) -> int:
        count = {}
        left, right = 0, 0
        ans = 0
        maxF = 0
        while right < len(s):
            count[s[right]] = 1 + count.get(s[right], 0)
            maxF = max(maxF, count[s[right]])
            while (right - left + 1) - maxF > k:
                count[s[left]] -= 1
                left += 1
            ans = max(ans, right - left + 1)
            right += 1
        return ans

567. 字符串的排列

题目页面

解法一:最直观的思路,我们可以用一个长度与 s1 相同的滑动窗口,在 s2 中进行遍历,并判断每个滑动窗口里面的字符是否与 s1 完全一样,时间复杂度为 O ( m ∗ n ) O(m*n) O(mn) 优化的做法是用两个哈希表,分别记录 s1 与滑动窗口中出现的字符及其出现次数,每次判断这两个哈希表是否相等即可,因为字符只可能为小写字母(26个),所以时间复杂度为 O ( 26 ∗ n ) O(26*n) O(26n)

解法二:用一个变量 matches 记录两个字符串中有多少个字符是匹配的,若 matches 等于 26 则返回 True,以代替解法一中的哈希表比较。首先,构建两个长度为 26 的列表,分别表示 s1 和滑动窗口中 26 个字母的出现次数,用 s1 和第一个滑动窗口对 matches 初始化。然后,每次向右移动滑动窗口,更新加入的字母的出现次数,如果该字母匹配了,matches 就加一,而如果该字母从匹配变成了不匹配,matches 就减一(注意不是从不匹配变成不匹配);对于退出的字母同理。时间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def checkInclusion(self, s1: str, s2: str) -> bool:
        if len(s1) > len(s2): 
            return False
        
        count1, count2 = [0] * 26, [0] * 26
        for i in range(len(s1)):
            count1[ord(s1[i]) - ord('a')] += 1
            count2[ord(s2[i]) - ord('a')] += 1
        
        matches = 0
        for i in range(26):
            if count1[i] == count2[i]:
                matches += 1
        
        left = 0
        for right in range(len(s1), len(s2)):
            if matches == 26: return True
            
            index = ord(s2[right]) - ord('a')
            count2[index] += 1
            if count1[index] == count2[index]:
                matches += 1
            elif count1[index] + 1 == count2[index]:
                matches -= 1
            
            index = ord(s2[left]) - ord('a')
            count2[index] -= 1
            if count1[index] == count2[index]:
                matches += 1
            elif count1[index] - 1 == count2[index]:
                matches -= 1
            left += 1
        return matches == 26

四、Stack 栈

20. 有效的括号

题目页面

解法:如果是三个左括号之一,就 push 进入栈;如果是三个右括号之一,就检查栈顶,若栈顶为空就肯定不匹配,不为空则考察弹出的栈顶元素,若不是对应的左括号则返回 False。最后如果还有左括号(栈非空),也返回 False。时间复杂度为 O ( n ) O({n}) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def isValid(self, s: str) -> bool:
        Map = {')':'(', ']':'[', '}':'{'}
        stack = []
        for ch in s:
            if ch not in Map:
                stack.append(ch)
            else:
                if not stack or stack[-1] != Map[ch]:
                    return False
                else:
                    stack.pop()
        return not stack            

155. 最小栈

题目页面

解法:实现栈的常规操作很简单,但要求栈可以随时返回最小值是个难点。思路就是把栈想象成一个筒,是先进后出的,然后在记录每个元素的同时记录它和下一个邻近元素中哪一个更小,则这个记录的就是栈中的最小值。

class MinStack:

    def __init__(self):
        self.stack = []

    def push(self, val: int) -> None:
        if not self.stack:
            self.stack.append((val, val))
        else:
            self.stack.append((val, min(val, self.stack[-1][1])))

    def pop(self) -> None:
        self.stack.pop()

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

    def getMin(self) -> int:
        return self.stack[-1][1]

150. 逆波兰表达式求值

题目页面

解法:分类讨论,字符是数字的话就入栈,是运算符的话就让两个操作数出栈,进行运算后把结果再入栈,注意入栈的一定得是 int 而不是字符串类型,同时整除用的是作除法后取 int 而不是取整。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def evalRPN(self, tokens: List[str]) -> int:
        stack = []
        index = 0
        while index < len(tokens):
            token = tokens[index]
            if token not in '+-*/':
                stack.append(int(token))
            else:
                num1 = stack.pop()
                num2 = stack.pop()
                if token == '+':
                    stack.append(num1 + num2)
                elif token == '-':
                    stack.append(num2 - num1)
                elif token == '*':
                    stack.append(num2 * num1)
                else:
                    stack.append(int(num2 / num1))
            index += 1
        return int(stack[-1])

22. 括号生成

题目页面

解法:这题要进行括号生成,则类似于排列组合,但是必须保证括号是有效的。因此,使用回溯法,记录的状态为左括号的数量和右括号的数量,则当左右括号数量相同且均为 n 时,是可以返回的一个答案。接下来就是添加左括号或右括号,添加左括号的限制就是其当前数量不能大于 n 个,而添加右括号的限制是其当前数量不能大于等于左括号的数量,这样才能保证括号是有效的。
在这里插入图片描述

class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        stack = []
        ans = []

        def backtrack(left, right):
            if left == right == n:
                ans.append(''.join(stack))
                return
            
            if left < n:
                stack.append('(')
                backtrack(left + 1, right)
                stack.pop()
            
            if left > right:
                stack.append(')')
                backtrack(left, right + 1)
                stack.pop()
        
        backtrack(0, 0)
        return ans

739. 每日温度

题目页面

解法:遍历每一天的温度,用一个栈记录每天的温度下标,当遍历到第 i 天时,比较第 i 天是否大于前面某天的温度,如果是则弹出该天(已找到答案),并在 ans 数组中记录下天数(差值),而实际上这个栈是一个单调(递减)的栈。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
        n = len(temperatures)
        stack = []
        ans = [0 for _ in range(n)]

        for i in range(n):
            while stack and temperatures[stack[-1]] < temperatures[i]:
                ans[stack[-1]] = i - stack[-1]
                stack.pop()
            stack.append(i)
        
        return ans

853. 车队

题目页面

解法:直观上,可以把每辆车看作是线性方程组中的一个方程,X轴是时间,Y轴是位置,速度就是直线的斜率,如下图所示。两辆车能组成车队实际上相当于两条直线在 y = target 之下可以相交。
在这里插入图片描述
将每辆车都放在一条轴上考虑,车辆按照起始位置从大到小排序,但如何判断两辆车是否相交呢?方法就是计算它们到终点 target 的时间,如果位置靠前的车到达终点的时间不大于位置靠后的车,则说明它们可以相交,如图所示,绿色车和蓝色车可以组成车队。此时车队整体的速度实际上由绿车决定,因为蓝车虽然比绿车快,但、并入车队后只能跟着绿车同速前进。所以后续的车只需要与车队的第一辆车比较即可知道能否并入车队,如果不能则自己作为一个新车队。时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间复杂度为 O ( n ) O(n) O(n)
在这里插入图片描述

class Solution:
    def carFleet(self, target: int, position: List[int], speed: List[int]) -> int:
        combine = sorted(zip(position, speed), reverse=True)
        stack = []
        for p, s in combine:
            if not stack:
                stack.append((p, s, (target - p) / s))
            else:
                if (target - p) / s <= stack[-1][2]:
                    continue
                else:
                    stack.append((p, s, (target - p) / s))
        return len(stack)

五、Binary Search 二分查找

704. 二分查找

题目页面

解法:两种分法,时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left = 0
        right = len(nums) - 1
        
        while left < right:
            mid = left + (right - left) // 2
            if nums[mid] < target:
                left = mid + 1
            else:
                right = mid
                
        if nums[left] == target:
            return left

        return -1
class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left = 0
        right = len(nums) - 1
        
        while left < right:
            mid = left + (right - left + 1) // 2
            if nums[mid] > target:
                right = mid - 1
            else:
                left = mid
                
        if nums[right] == target:
            return right

        return -1

35. 搜索插入位置

题目页面

解法: 本题与 704 题基本一样,区别只是不要求找到一样的元素,而是要找到第一个大于等于 target 的元素索引,如果 target 比所有元素都大,则插入到列表末尾。时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        if nums[-1] < target:
            return len(nums)
        
        left = 0
        right = len(nums)- 1
        
        while left < right:
            mid = left + (right - left) // 2
            if nums[mid] < target:
                left = mid + 1
            else:
                right = mid
        
        return left

744. 寻找比目标字母大的最小字母

题目页面

解法:题目要求寻找比目标字母大的最小字母,如果目标字母比列表中的所有字母都大,则返回列表的第一个字母。与 35 题的区别是,这题是必须比目标字母大,不可以相等,所以在判断条件那里 mid 所在的区间的字母如果都小于等于 target,就到另一个区间去寻找。时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def nextGreatestLetter(self, letters: List[str], target: str) -> str:
        if target >= letters[-1]:
            return letters[0]
            
        left = 0
        right = len(letters) - 1
        
        while left < right:
            mid = left + (right - left) // 2
            if letters[mid] <= target:
                left = mid + 1
            else:
                right = mid
                
        return letters[left]

162. 寻找峰值(852. 山脉数组的峰顶索引)

题目页面 题目页面

解法:找到大于左右相邻元素的值,若 nums[mid] < nums[mid + 1],则目标区间在右边,剩下两个元素时,mid向下取整等于left,可以取到更大值 left = mid + 1 = right。时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def findPeakElement(self, nums: List[int]) -> int:
        left = 0
        right = len(nums) - 1

        while left < right:
            mid = left + (right - left) // 2
            if nums[mid] < nums[mid + 1]:
                left = mid + 1
            else:
                right = mid
        
        return left

153. 寻找旋转排序数组中的最小值

题目页面

解法:本题没有 target,只需要不断地找无序的区间(同时也是拐点所在的区间)即可,由剩余两个元素时的情况可以知道,退出循环时必然 left 等于右边的元素,即拐点的右边(最小值)。时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def findMin(self, nums: List[int]) -> int:
        left = 0
        right = len(nums)- 1

        while left < right:
            mid = left + (right - left) // 2
            if nums[mid] < nums[right]: # 右边区间有序,拐点一定在左边区间
                right = mid
            else: # 右边区间无序,拐点一定在右边区间
                left = mid + 1
        
        return nums[left]

154. 寻找旋转排序数组中的最小值 II

题目页面

解法:本题是153题的进阶版,与81题类似,就是多了元素可能重复这个条件。由于存在无法判断是否有序的情况,所以要单独讨论,出现这种情况时就缩小范围 right -= 1,其余情况还是正常找拐点所在区间。时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def findMin(self, nums: List[int]) -> int:
        left = 0
        right = len(nums) - 1

        while left < right:
            mid = left + (right - left) // 2
            if nums[mid] < nums[right]: # 右边区间有序,拐点一定在左边区间
                right = mid
            elif nums[mid] > nums[right]: # 右边区间无序,拐点一定在右边区间
                left = mid + 1
            else: # mid与右边界相等,无法判断,只能缩小范围
                right -= 1
        
        return nums[left]

33. 搜索旋转排序数组

题目页面

解法:这题的数组是循环有序,对于 mid 来说,要么是 mid 所在的左边区间(分法一)有序,要么是右边区间有序,所以首先要判断哪个区间有序,再到有序区间进行 target 的寻找(因为 mid 与 target 的比较一定是在有序区间进行的)。

右边区间有序,判断条件是 if nums[mid] < target <= nums[right] ,第一个取小于号是因为 mid 在左边区间,一定小于在右边区间的 target,而第二个取小于等于号是因为 target 可能是最右边的元素。

左边区间有序,判断条件是 if nums[left] <= target <= nums[mid],同理,target 和 mid 都在左边区间,都可能等于最左边的元素。时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left = 0
        right = len(nums) - 1

        while left < right:
            mid = left + (right - left) // 2
            if nums[mid] < nums[right]:
                if nums[mid] < target <= nums[right]:
                    left = mid + 1
                else:
                    right = mid
            else:
                if nums[left] <= target <= nums[mid]:
                    right = mid
                else:
                    left = mid + 1
        
        return left if nums[left] == target else -1

81. 搜索旋转排序数组 II

题目页面

解法:作为33题的进阶版,这道题难在数组中的元素是可能相同的,如果出现 nums[mid] == nums[right] 的情况,无法判断左边区间还是右边区间是有序的。对于这种情况,每次缩减 right -= 1 即右边界左移一位,直到可以判断左右区间哪个有序为止。时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def search(self, nums: List[int], target: int) -> bool:
        left = 0
        right = len(nums) - 1

        while left < right:
            mid = left + (right - left) // 2
            if nums[mid] < nums[right]:  # 右边区间有序
                if nums[mid] < target <= nums[right]:
                    left = mid + 1
                else:
                    right = mid
            elif nums[mid] > nums[right]:  # 右边区间无序
                if nums[left] <= target <= nums[mid]:
                    right = mid
                else:
                    left = mid + 1
            else:  # mid与右边界相等,无法判断,只能缩小范围
                if nums[right] == target:
                    return True
                else:
                    right -= 1
        
        return nums[left] == target

74. 搜索二维矩阵

题目页面

解法:把二维矩阵看成是一个有序的数组,行、列坐标就是商数和余数,对数组使用二分即可。时间复杂度为 O ( log ⁡ m n ) O(\log mn) O(logmn),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
        left = 0
        right = len(matrix) * len(matrix[0]) - 1

        while left < right:
            mid = left + (right - left) // 2
            row, col = mid // len(matrix[0]), mid % len(matrix[0])
            if matrix[row][col] < target:
                left = mid + 1
            else:
                right = mid
        
        row, col = left // len(matrix[0]), left % len(matrix[0])
        return matrix[row][col] == target

240. 搜索二维矩阵 II

题目页面

解法一:遍历所有的行,在每一行使用二分搜索,时间复杂度为 O ( m log ⁡ n ) O(m\log n) O(mlogn),空间复杂度为 O ( 1 ) O(1) O(1)

解法二:从矩阵的右上角开始搜索,若当前元素大于目标,说明一定在左边,列位置 col -= 1,若当前元素小于目标,说明一定在下面,行位置 row += 1,直到找到目标元素或者越界为止。时间复杂度为 O ( m + n ) O(m + n) O(m+n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
        row = 0
        col = len(matrix[0]) - 1

        while row < len(matrix) and col >= 0:
            if matrix[row][col] > target:
                col -= 1
            elif matrix[row][col] < target:
                row += 1
            else:
                return True
        
        return False

658. 找到 K 个最接近的元素

题目页面

解法:数组的区间为 [0, n-1],查找的子区间长度为 k。我们可以通过查找子区间左端点位置,从而确定子区间。查找子区间左端点可以通过二分查找来降低复杂度。因为子区间为 k,所以左端点最多取到 n-k 的位置,即左端点的区间为 [0, n-k]。设定两个指针 left 指向 0,right 指向 n-k。每次取 left 和 right 中间位置 mid,判断 x 与左右边界的差值。x 与左边的差值为 x - arr[mid],x 与右边界的差值为 arr[mid + k] - x。

如果 x 与左边界的差值 > x 与右边界的差值,即 x - arr[mid] > arr[mid + k] - x,说明正确的子区间应该往右移一点,则将左边界 left 右移,使得左端点也随之右移;

如果 x 与左边界的差值 <= x 与右边界的差值, 即 x - arr[mid] <= arr[mid + k] - x,说明正确的子区间应该往左移一点,则将右边界 right 向左移,使得左端点也随之左移。时间复杂度为 O ( log ⁡ n + k ) O(\log n + k) O(logn+k),空间复杂度为 O ( k ) O(k) O(k)

class Solution:
    def findClosestElements(self, arr: List[int], k: int, x: int) -> List[int]:
        left = 0
        right = len(arr) - k

        while left < right:
            mid = left + (right - left) // 2
            if x - arr[mid] > arr[mid + k] - x:
                left = mid + 1
            else:
                right = mid
        
        return arr[left:left + k]

六、Linked List 链表

206. 反转链表

题目页面

解法一:迭代法,使用前一个节点 pre 和当前节点 cur。在节点 cur 时,先把下个指针记到 temp,然后 cur 的 next 指针反过来指向 pre,已反向的 cur 变为 pre,然后 cur 向右移一位,直到链表结束。在写法上,可以发现变量是头尾相连的,以此作为记忆。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def reverseList(self, head: ListNode) -> ListNode:
        pre = None
        cur = head
        while cur:
            temp = cur.next     # 把下个指针记到 temp
            cur.next = pre      # cur 的 next 指针反过来指向 pre
            pre = cur           # 已反向的 cur 变为 pre
            cur = temp          # cur 向右移一位
        return pre	

解法二:递归法,对于链表 [1, 2, 3],可以将反转 [1, 2, 3] 分解为已经反转了 [2, 3] 后反转 [1],再将反转 [2, 3] 分解为已经反转了 [3] 后反转 [2]。基本情况就是反转单个节点或者空节点,例如反转 [3],是无法反转的,直接返回自身。在反转 [2, 3] 时,我们正处于 [2] (head),反转就是将它 next 的 next 即 [3] 的 next 指向自己,然后 [2] 的 next 指向空(作为表尾)。反转 [1, 2, 3] 也同理。时间复杂度为 O ( n ) O({n}) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def reverseList(self, head: ListNode) -> ListNode:
        if not head or not head.next:
            return head
        
        newHead = self.reverseList(head.next)  # 下一个节点,反转后的
        head.next.next = head # 将下一个节点的 next 指向自己
        head.next = None  # 自己的 next 指向空,作为队尾
        return newHead  # 返回反转后的下一个节点

21. 合并两个有序链表

题目页面

解法:合并两个有序的链表,方法就是新建一个节点 head,迭代进行:如果两个链表都非空,就让新链表指向数值小的节点,然后移动下一位,直到其中一个链表为空,最后把另一个链表作为新链表剩下的部分。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
        head = ListNode(0)
        cur = head
        while list1 and list2:
            if list1.val < list2.val:
                cur.next = list1
                list1 = list1.next
            else:
                cur.next = list2
                list2 = list2.next
            cur = cur.next
        cur.next = list1 if list1 else list2
        return head.next

143. 重排链表

题目页面

解法:由于题目要求不能只交换值,且不返回链表,所以不能遍历链表用列表记录值再生成新的链表,只能对链表进行原地操作。观察到重排的方式是左边一个节点接着右边一个节点,问题在于右边节点指针是无法向前的,自然就想到反转链表。先用快慢指针找到中点,然后把链表断开两半并将后半部分链表反转,这样问题就变成了合并两个链表了。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def reorderList(self, head: ListNode) -> None:
        """
        Do not return anything, modify head in-place instead.
        """
        # 快慢指针找到中点(两个则是左边那个)
        fast = head
        slow = head
        while fast.next and fast.next.next:
            fast = fast.next.next
            slow = slow.next
        secondHalf = slow.next
        slow.next = None
        # 反转后半部分
        pre = None
        while secondHalf:
            temp = secondHalf.next
            secondHalf.next = pre
            pre = secondHalf
            secondHalf = temp
        # 从左右两端开始
        left = head
        right = pre
        while right:
            temp1 = left.next
            temp2 = right.next
            left.next = right
            right.next = temp1
            left = temp1
            right = temp2

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

题目页面

解法:删除链表的倒数第 n 个节点,用双指针法,第一个指针先遍历 n 次,然后两个指针一起遍历,这样当第一个指针遍历完之后,第二个指针正好遍历了(链表长度 - n)次,其位置即为要删除的位置。注意由于可能删除头节点,所以要用 dummyHead。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
        dummyHead = ListNode(0, head)
        fast = dummyHead
        for _ in range(n):
            fast = fast.next
        slow = dummyHead
        while fast.next:
            fast = fast.next
            slow = slow.next
        slow.next = slow.next.next
        return dummyHead.next

138. 复制带随机指针的链表

题目页面

解法一:哈希表,这题的难点在于 random 指针,它有可能指向后面未创建的节点,所以必须先遍历一次链表再分配 random 指针,但是直接分配是不可能的,因为无法表示新链表中的节点,直接赋值的话 random 指针也只是指向旧链表中的节点而已。想要表示某个东西的话,用哈希表是很好的选择。我们先遍历一次链表,创建新节点并且让旧新节点一一对应;然后第二次遍历链表,此时就可以通过这个哈希表找到新节点的 next 和 random 指针在新链表中该指向的节点了。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

"""
# Definition for a Node.
class Node:
    def __init__(self, x: int, next: 'Node' = None, random: 'Node' = None):
        self.val = int(x)
        self.next = next
        self.random = random
"""

class Solution:
    def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
        oldToCopy = {None : None}
        # 第一次遍历
        cur = head
        while cur:
            copy = Node(cur.val)
            oldToCopy[cur] = copy
            cur = cur.next
        # 第二次遍历
        cur = head
        while cur:
            copy = oldToCopy[cur]
            copy.next = oldToCopy[cur.next]
            copy.random = oldToCopy[cur.random]
            cur = cur.next
        
        return oldToCopy[head]

2. 两数相加 & 445. 两数相加 II

题目页面1

题目页面2

解法:两个链表对应位置相加,对于第一题,数字是逆序存储的,即链表头的数字是最低位,所以相加时会产生进位。使用两个队列,每次弹出队首元素进行相加,两个链表不等长的话短的那个会加0,余数作为结果链表的新节点,而商数除以10后作为进位(下一位的加数之一),最后如果还有一个进位也要考虑到。对于第二题,数字是顺序存储的,使用两个即可,注意输出也得是正序,所以链表是从尾部开始生成的,cur 初始为 None,每次 cur被新节点指向,然后 cur 再移动到新节点处,直到最后 cur 即为头节点(相当于反转链表)。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
        q1, q2 = [], []
        while l1:
            q1.append(l1.val)
            l1 = l1.next
        while l2:
            q2.append(l2.val)
            l2 = l2.next
        dummyHead = ListNode(0)
        cur = dummyHead
        carry = 0
        while q1 or q2 or carry:
            a = q1.pop(0) if q1 else 0
            b = q2.pop(0) if q2 else 0
            sum = a + b + carry
            carry = sum // 10
            newNode = ListNode(sum % 10)
            cur.next = newNode
            cur = cur.next
        return dummyHead.next
class Solution:
    def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
        s1, s2 = [], []
        while l1:
            s1.append(l1.val)
            l1 = l1.next
        while l2:
            s2.append(l2.val)
            l2 = l2.next
        cur = None
        carry = 0
        while s1 or s2 or carry:
            a = s1.pop() if s1 else 0
            b = s2.pop() if s2 else 0
            sum = a + b + carry
            carry = sum // 10
            newNode = ListNode(sum % 10)
            newNode.next = cur
            cur = newNode
        return cur

141. 环形链表

题目页面

解法一:用哈希表记录下每一个节点,然后看看是否有相同节点,有的话就是存在环形,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

解法二:快慢指针,如果链表是有环的,则当快慢指针都进入环之后,由于快指针比慢指针速度快 1,所以每次移动,它们之间的距离都会被缩小 1,而环最大的长度也不超过链表长度 n,所以它们之间的距离一定能被缩小到 0 的。时间复杂度为 O ( n ) O({n}) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

使用快慢指针时要注意:如果初始化两个指针都是 head,则 while 循环中必须先赋值再判断是否相等,而不能先判断相等(因为初始时就是相等的)。

class Solution:
    def hasCycle(self, head: Optional[ListNode]) -> bool:
        fast = head
        slow = head
        while fast and fast.next:
            fast = fast.next.next
            slow = slow.next
            if fast == slow:
                return True
        return False

876. 链表的中间结点

题目页面

解法一:遍历链表,记录链节点,然后返回中间那个。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def middleNode(self, head: ListNode) -> ListNode:
        all_nodes = []
        while head:
            all_nodes.append(head)
            head = head.next
        return all_nodes[len(all_nodes) // 2]

解法二:更好地是用快慢指针,慢指针每次移到next,快指针每次移到 next 的 next,循环结束返回慢指针即为中间节点。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def middleNode(self, head: ListNode) -> ListNode:
        fast = head
        slow = head
        while fast and fast.next:  # 两个中间结点取后者,就这样写
            fast = fast.next.next
            slow = slow.next
        return slow

如果两个中间结点要取前者,条件就变为 fast.next and fast.next.next,如下所示:

class Solution:
    def middleNode(self, head: ListNode) -> ListNode:
        fast = head
        slow = head
        while fast.next and fast.next.next:  
            fast = fast.next.next
            slow = slow.next
        return slow

287. 寻找重复数

题目页面

解法一:二分法,这题给定一个包含 n + 1 个整数的数组,其数字都在 1 到 n 之间,只有一个数字是重复的。因此,对于某个数字 x 来说,正常来说小于等于 x 的数字应该有 x 个,例如有1、2、3、4 共 4 个数字小于等于 4,如果大于 4了,则说明 1、2、3、4 其中有一个数字重复了,所以右边界左移,反之左边界右移。时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def findDuplicate(self, nums: List[int]) -> int:
        left = 1
        right = len(nums) - 1
        while left < right:
            mid = left + (right - left) // 2 
            cnt = 0 # 记录小于等于mid的元素个数
            for num in nums:
                if num <= mid:
                    cnt += 1
            if cnt > mid:
                right = mid
            else:
                left = mid + 1
        return left

解法二:由于数组取值为 1 至 n,而数组共有 n + 1 个数,所以可以把数组中的数作为 next 指针,将其视为链表,重复数相当于带环链表中的入环点,因为它是被两个节点指向的。所以问题转换成了求入环点的问题,使用快慢指针即可,时间复杂度为 O ( n ) O({n}) O(n),空间复杂度为 O ( 1 ) O(1) O(1)
在这里插入图片描述

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

        temp = 0
        while True:
            slow = nums[slow]
            temp = nums[temp]
            if slow == temp:
                break
        
        return temp

146. LRU 缓存

题目页面

解法:LRU 缓存,因为要求函数 get 和 put 必须以 O ( 1 ) O(1) O(1) 的平均时间复杂度运行,所以基础的数据结构一定是哈希表(常数时间的查询与插入)。但是要实现 LRU(Least Recently Used 最近最少使用) 的功能,每次 put 的时候都要更新(key,value)对的位置,而哈希表是不存在顺序关系的,所以需要借助另一个数据结构,双向链表的帮助。
在这里插入图片描述
如图所示,我们可以让哈希表的 value 指向双向链表中的节点,该节点 Node 类存储了 key、value、prev 指针和 next 指针,且双向链表左右两边分别有一个节点 left 和 right,代表了 LRU 和 MRU。get 的时候,如果访问过某个 key、value,就要把它移动到双向链表的最右端(right 的左边),移动的方法是删除节点并将其插入到 right 的左边。put 的时候,如果 key 已经存在,就先删除它,然后统一让 value 指向 Node,Node 也插入到双向链表的最右端表示最近被用过;如果哈希表已经超出容量,就将 LRU (left 的右边一个节点)删除,哈希表和双向链表的内容都要删除。

class Node:
    def __init__(self, key, val):
        self.key, self.val = key, val
        self.prev = self.next = None
    
class LRUCache:

    def __init__(self, capacity: int):
        self.cap = capacity
        self.cache = {} # 从 key 映射到 Node
        # 初始化双向链表,left 为 LRU,right 为 MRU
        self.left, self.right = Node(0, 0), Node(0, 0)
        self.left.next, self.right.prev = self.right, self.left

    # 删除 node
    def remove(self, node):
        prev, nxt = node.prev, node.next
        prev.next, nxt.prev = nxt, prev
    
    # 在最右边插入 node
    def insert(self, node):
        prev, nxt = self.right.prev, self.right
        prev.next = nxt.prev = node
        node.next, node.prev = nxt, prev
        
    def get(self, key: int) -> int:
        if key in self.cache:
            self.remove(self.cache[key])
            self.insert(self.cache[key])
            return self.cache[key].val
        return -1
        
    def put(self, key: int, value: int) -> None:
        if key in self.cache:
            self.remove(self.cache[key])
        self.cache[key] = Node(key, value)
        self.insert(self.cache[key])
        
        if len(self.cache) > self.cap:
            # 如果已经超过容量了,就找到 LRU 然后将其从双向链表和哈希表中去除
            lru = self.left.next
            self.remove(lru)
            del self.cache[lru.key]

七、Trees 树

7A、BFS 广度优先搜索(层序遍历)

102. 二叉树的层序遍历

题目页面

解法:BFS,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def levelOrder(self, root: TreeNode) -> List[List[int]]:
        if not root:
            return []
        ans = []
        queue = collections.deque()
        queue.append(root)
        while queue:
            cur = []
            for _ in range(len(queue)):
                node = queue.popleft()
                cur.append(node.val)
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            ans.append(cur)
        return ans

107. 二叉树的层序遍历 II

题目页面

解法:BFS,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def levelOrderBottom(self, root: TreeNode) -> List[List[int]]:
        if not root:
            return []
        ans = []
        queue = collections.deque()
        queue.append(root)
        while queue:
            cur = []
            for _ in range(len(queue)):
                node = queue.popleft()
                cur.append(node.val)
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            ans.append(cur)
        return ans[::-1]

103. 二叉树的锯齿形层序遍历

题目页面

解法:BFS,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def zigzagLevelOrder(self, root: TreeNode) -> List[List[int]]:
        if not root:
            return []
        ans = []
        flag = True
        queue = collections.deque()
        queue.append(root)
        while queue:
            cur = []
            for _ in range(len(queue)):
                node = queue.popleft()
                cur.append(node.val)
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            if flag:
                ans.append(cur)
                flag = False
            else:
                ans.append(cur[::-1])
                flag = True
        return ans

199. 二叉树的右视图

题目页面

解法:BFS,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []
        ans = []
        queue = collections.deque()
        queue.append(root)
        while queue:
            for _ in range(len(queue)):
                node = queue.popleft()
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            ans.append(node.val)
        return ans

637. 二叉树的层平均值

题目页面

解法:BFS,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def averageOfLevels(self, root: Optional[TreeNode]) -> List[float]:
        if not root:
            return []
        ans = []
        queue = collections.deque()
        queue.append(root)
        while queue:
            length = len(queue)
            count = 0
            for _ in range(length):
                node = queue.popleft()
                count += node.val
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            ans.append(count / length)
        return ans

111. 二叉树的最小深度

题目页面

解法:BFS,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def minDepth(self, root: TreeNode) -> int:
        if not root:
            return 0
        ans = 1
        queue = collections.deque()
        queue.append(root)
        while queue:
            for _ in range(len(queue)):
                node = queue.popleft()
                if not node.left and not node.right:
                    return ans
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            ans += 1
        return ans

104. 二叉树的最大深度

题目页面

解法一:递归法深度遍历,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( h e i g h t ) O(height) O(height)

class Solution:
    def maxDepth(self, root: Optional[TreeNode]) -> int:
        if not root:
            return 0
        leftDepth = self.maxDepth(root.left)
        rightDepth = self.maxDepth(root.right)
        return max(leftDepth, rightDepth) + 1

解法二:迭代法深度遍历

class Solution:
    def maxDepth(self, root: Optional[TreeNode]) -> int:
        if not root:
            return 0
        stack = [(root, 1)]
        ans = 1
        while stack:
            node, depth = stack.pop()
            ans = max(ans, depth)
            if node:
                if node.left:
                    stack.append((node.left, depth + 1))
                if node.right:
                    stack.append((node.right, depth + 1))
        return ans

解法三:广度遍历,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为取决于队列大小

class Solution:
    def maxDepth(self, root: Optional[TreeNode]) -> int:
        if not root:
            return 0
        queue = collections.deque()
        queue.append(root)
        depth = 0
        while queue:
            for _ in range(len(queue)):
                node = queue.popleft()
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            depth += 1
        return depth

662. 二叉树最大宽度

题目页面

解法:BFS,在队列中使用多一个参数 pos 来记录位置,只需要记住的是,位置为 pos 的节点(从 1 开始)的左子节点位置是 2 * pos - 1,右子节点的位置是 2 * pos。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def widthOfBinaryTree(self, root: Optional[TreeNode]) -> int:
        if not root:
            return 0
        ans = 1
        queue = collections.deque()
        queue.append([root, 1])
        while queue:
            cur = []
            for _ in range(len(queue)):
                node, pos = queue.popleft()
                cur.append(pos)
                if node.left:
                    queue.append([node.left, pos * 2 - 1])
                if node.right:
                    queue.append([node.right, pos * 2])
            ans = max(ans, max(cur) - min(cur) + 1)
        return ans

116. 填充每个节点的下一个右侧节点指针

题目页面

解法:BFS,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def connect(self, root: 'Optional[Node]') -> 'Optional[Node]':
        if not root:
            return root
        queue = collections.deque()
        queue.append(root)
        while queue:
            pre = None
            for _ in range(len(queue)):
                node = queue.popleft()
                if pre:
                    pre.next = node
                pre = node
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
        return root

117. 填充每个节点的下一个右侧节点指针 II

题目页面

解法:BFS,代码与上一题完全一样,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def connect(self, root: 'Node') -> 'Node':
        if not root:
            return root
        queue = collections.deque()
        queue.append(root)
        while queue:
            pre = None
            for _ in range(len(queue)):
                node = queue.popleft()
                if pre:
                    pre.next = node
                pre = node
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
        return root

7B、DFS 深度优先搜索

226. 翻转二叉树

题目页面

解法:在每个节点处交换左右子节点的值,然后对左子树和右子树递归,具体代码看我的这篇文章


100. 相同的树

题目页面

解法:比较两个二叉树是否相同,首先比较其根节点是否相同,若不相同则直接返回 False,相同则比较左右子树是否都相同。递归返回的基本情况有:两个节点是否同时为空?是否一个为空而另一个不为空?他们的值是否相同?最后是递归的判断,他们的左右子树是否都相同?时间复杂度为 O ( min ⁡ ( m , n ) ) {{O(\min(m, n))}} O(min(m,n)),空间复杂度为 O ( min ⁡ ( m , n ) ) {{O(\min(m, n))}} O(min(m,n))

class Solution:
    def isSameTree(self, p: TreeNode, q: TreeNode) -> bool:
        if not p and not q:
            return True
        if not p or not q:
            return False
        if p.val != q.val:
            return False
        return (self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right))

572. 另一棵树的子树

题目页面

解法:首先考虑边界条件,如果目标子树为空则返回 True,如果树为空(目标子树不为空)则返回 False,然后就使用上一题判断两棵树相同的函数,判断当前节点的树与目标子树是否相同,相同则返回 True,不相同就继续遍历左右子树。

class Solution:
    def isSubtree(self, root: TreeNode, subRoot: TreeNode) -> bool:
        if not subRoot: 
            return True
        if not root: 
            return False
        if self.isSameTree(root, subRoot):
            return True
        return (self.isSubtree(root.left, subRoot) or self.isSubtree(root.right, subRoot))
    
    def isSameTree(self, p: TreeNode, q: TreeNode) -> bool:
        if not p and not q:
            return True
        if not p or not q:
            return False
        if p.val != q.val:
            return False
        return (self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right))

617. 合并二叉树

题目页面

解法:两个二叉树同时遍历,在相同位置上,如果同时为空则合并后也为空,如果其中一个为空则合并后为另一个非空的,如果都非空则合并后该节点的值为两者之和,然后左右子树都是合并后的(递归)。时间复杂度为 O ( min ⁡ ( m , n ) ) {{O(\min(m, n))}} O(min(m,n)),空间复杂度为 O ( min ⁡ ( m , n ) ) {{O(\min(m, n))}} O(min(m,n))

class Solution:
    def mergeTrees(self, root1: TreeNode, root2: TreeNode) -> TreeNode:
        if not root1 and not root2:
            return None
        if not root1:
            return root2
        if not root2:
            return root1
        root1.val += root2.val
        root1.left = self.mergeTrees(root1.left, root2.left)
        root1.right = self.mergeTrees(root1.right, root2.right)
        return root1

1448. 统计二叉树中好节点的数目

题目页面

解法:递归返回的基本情况,若节点为空则返回 0,否则就返回节点及其所有子节点的好节点的总数目,用 m 来记录从根节点出发到当前节点的最大节点值,若当前节点值不小于 m 则当前节点也是好节点,加一。

class Solution:
    def goodNodes(self, root: TreeNode) -> int:
        
        def dfs(root, m):
            if not root:
                return 0
            good = 0
            if root.val >= m:
                good = 1
            m = max(m, root.val)
            leftTree = dfs(root.left, m)
            rightTree = dfs(root.right, m)
            return leftTree + rightTree + good
        
        ans = dfs(root, root.val)
        return ans

112. 路径总和

题目页面

解法:递归返回的基本情况,1、节点是否为空,若为空则说明路径已经结束,总和达不到目标,返回 False;2、节点不为空,如果是叶节点,则判断其值与目标是否相同,相同就返回 True,否则为 False;3、如果是普通节点,那就对其左右子树进行递归,但是目标值要减去当前的节点值。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( h e i g h t ) O(height) O(height)

class Solution:
    def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
        if not root:
            return False
        if not root.left and not root.right:
            return root.val == targetSum
        return self.hasPathSum(root.left, targetSum - root.val) or self.hasPathSum(root.right, targetSum - root.val)

113. 路径总和 II

题目页面

解法:这题不是求有没有路径,而是要把路径都找出来,答案放在列表 ans 里。同样还是递归地深度遍历,用一个 path 列表记录路径,如果是叶节点且和数符合条件,就往 ans 里面加入路径 path,否则就遍历左子树和右子树,记得递归的最后要把 path 进行弹出,然后注意往 ans 里面加入的是 path 的浅拷贝,可以写成 path.copy() 或者 path[:] 。时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
        ans = []
        path = []

        def dfs(root: TreeNode, targetSum: int):
            if not root:
                return
            path.append(root.val)
            if not root.left and not root.right and targetSum == root.val:
                ans.append(path.copy())
            dfs(root.left, targetSum - root.val)
            dfs(root.right, targetSum - root.val)
            path.pop()
        
        dfs(root, targetSum)
        return ans

437. 路径总和 III

题目页面

解法一:这题规定路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的。所以从根节点开始,对于每个节点,都要寻找以它为起点的所有路径,记录总和等于 targetSum 的路径数,即函数 rootSum;然后对其左右子节点递归,即 self.pathSum(root.left, targetSum)self.pathSum(root.right, targetSum) 。时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def pathSum(self, root: TreeNode, targetSum: int) -> int:
        def rootSum(root, targetSum):
            if root is None:
                return 0
            ans = 0
            # 因为不需要在叶子节点结束,所以不是 return 1
            if root.val == targetSum:
                ans += 1
            ans += rootSum(root.left, targetSum - root.val)
            ans += rootSum(root.right, targetSum - root.val)
            return ans
        
        if root is None:
            return 0
        ans = rootSum(root, targetSum)
        ans += self.pathSum(root.left, targetSum)
        ans += self.pathSum(root.right, targetSum)
        return ans

543. 二叉树的直径

题目页面

解法:首先得知道,二叉树的直径不一定是以根节点为顶点的,如下图所示
在这里插入图片描述
因此,我们需要遍历所有的节点,使用递归法。当我们遍历到一个节点时,如果它为空则返回 0;否则,假设我们已知其左子树和右子树的高度,也可知以它作为顶点的直径 = 左右子树高度之和加一,比较它与答案的大小判断是否更新答案,最后返回该节点作为子树的高度,也就是左右子树较大的高度然后加一。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( h e i g h t ) O(height) O(height)

class Solution:
    def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
        self.ans = 1

        def dfs(root):
            if not root:
                return 0
            leftDepth = dfs(root.left)  # 左子树高度
            rightDepth = dfs(root.right)  # 右子树高度
            self.ans = max(self.ans, leftDepth + rightDepth + 1)  # 判断其直径是否大于当前最大值
            return max(leftDepth, rightDepth) + 1  # 返回 root 作为子树的高度 
        
        dfs(root)
        return self.ans - 1

110. 平衡二叉树

题目页面

解法:与求二叉树直径一样的思路,遍历每个节点,假设已知其左子树和右子树的高度,如果两者之差的绝对值大于 1 则不是平衡二叉树。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( h e i g h t ) O(height) O(height)

class Solution:
    def isBalanced(self, root: TreeNode) -> bool:
        self.ans = True

        def dfs(root) -> int:
            if not root:
                return 0
            leftHeight = dfs(root.left)
            rightHeight = dfs(root.right)
            if abs(leftHeight - rightHeight) > 1:
                self.ans = False
            return max(leftHeight, rightHeight) + 1
            
        dfs(root)
        return self.ans

98. 验证二叉搜索树

题目页面

解法一:根据二叉搜索树的性质,其中序遍历一定是递增的,如果不是则非二叉搜索树。注意判断 pre 的条件一定得是 if pre != None 而不是 if pre,因为有个用例就是根节点值为 0 的。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def isValidBST(self, root: Optional[TreeNode]) -> bool:
        if not root:
            return True
        stack = []
        stack.append(root)
        pre = None
        while stack:
            node = stack.pop()
            if node:
                if node.right:
                    stack.append(node.right)
                stack.append(node)
                stack.append(None)
                if node.left:
                    stack.append(node.left)
            else:
                node = stack.pop()
                if pre != None and pre >= node.val:
                        return False
                pre = node.val
        return True

解法二:从二叉搜索树中每个节点自身受到的限制来看,实际上就是限制了它们的范围。假设有如下图的二叉树,其根节点就相当于取值范围为正负无穷,左子节点 3 的取值范围为负无穷到 5,右子节点的取值范围为 5 到正无穷,其余同理。因此值为 4 的节点不符合要求,因为它的取值范围为 5 到 7。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

在这里插入图片描述

在这里插入图片描述

class Solution:
    def isValidBST(self, root: Optional[TreeNode]) -> bool:

        def valid(node, left, right):
            if not node:
                return True
            if node.val >= right or node.val <= left:
                return False
            return valid(node.left, left, node.val) and valid(node.right, node.val, right)
        
        return valid(root, float('-inf'), float('inf'))

235. 二叉搜索树的最近公共祖先

题目页面

解法:对于两个节点 p 和 q,在二叉搜索树中,它们的公共祖先的值一定在 p 和 q 之间,根节点一定是公共祖先,所以从根节点开始,通过判断当前节点值与 p、q 节点值的大小关系,即可找到最近公共祖先。时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        cur = root

        while cur:
            if p.val > cur.val and q.val > cur.val:
                cur = cur.right
            elif p.val < cur.val and q.val < cur.val:
                cur = cur.left
            else:
                return cur

236. 二叉树的最近公共祖先

题目页面

解法:从根节点开始寻找最近公共祖先,每当找到一个节点,就要将它返回给父节点,则第一个左右子节点都非空的节点即为最近公共祖先。返回的情况就三种,空节点、p 或 q,然后遍历左右子节点,左右子节点也是三种情况:同时非空,说明当前节点就是最近公共祖先,返回当前节点;只有一个非空,就返回那个非空的(p 或 q);两个都为空,返回的就是空。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

在这里插入图片描述

class Solution:
    def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        def dfs(root: 'TreeNode', p: 'TreeNode', q: 'TreeNode'):
            # 如果当前节点为空,则说明 p、q 不在 node 的子树中,不可能为公共祖先,直接返回 None
            if not root:
                return None
            
            # 如果当前节点 root 等于 p 或者 q,那么 root 就是 p、q 的最近公共祖先,直接返回 root,不需要遍历子树了
            if root == p or root == q:
                return root
            
            # 递归遍历左子树、右子树,并判断左右子树结果
            node_left = dfs(root.left, p, q)
            node_right = dfs(root.right, p, q)
            # 如果左右子树都不为空,则说明 p、q 在当前根节点的两侧,当前根节点就是他们的最近公共祖先
            if node_left and node_right:
                return root

            return node_left if node_left else node_right
        
        ans = dfs(root, p, q)
        return ans

654. 最大二叉树

题目页面

解法:以数组中的最大值构建二叉树的题目,使用递归,基本情况就是数组为空则返回,否则就以数组中的最大值构建二叉树,其左右子树都利用递归得到。时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def constructMaximumBinaryTree(self, nums: List[int]) -> TreeNode:
        if not nums:
            return None
        root_index = nums.index(max(nums))
        leftTree = self.constructMaximumBinaryTree(nums[:root_index])
        rightTree = self.constructMaximumBinaryTree(nums[root_index + 1:])
        root = TreeNode(val=max(nums), left=leftTree, right=rightTree)
        return root

105. 从前序与中序遍历序列构造二叉树

题目页面

解法:从前序找到根节点,然后找到其在中序的位置 root_index,则该位置的左边 [:root_index] 为左子树,右边 [root_index+1:] 为右子树。因为已知左子树的长度为 root_index,则前序中的左子树为 [1:root_index+1],右子树为 [root_index+1:]。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
        if not preorder:
            return None
        root_val = preorder[0]
        root_index = inorder.index(root_val)
        leftTree = self.buildTree(preorder[1:root_index+1], inorder[:root_index])
        rightTree = self.buildTree(preorder[root_index+1:], inorder[root_index+1:])
        root = TreeNode(val=root_val, left=leftTree, right=rightTree)
        return root

优化的写法是,递归的时候传入指针而不是传入整个列表,所以列表不变而指针变,左子树的长度是两个指针之差:

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
        def myBuildTree(preorder_left: int, preorder_right: int, inorder_left: int, inorder_right: int):
            if preorder_left > preorder_right:  # 判断空树
                return None

            root = TreeNode(preorder[preorder_left])  # 根节点
            inorder_root = inorder.index(root.val)  # 根节点下标
            size_left_subtree = inorder_root - inorder_left  # 左子树长度
            root.left = myBuildTree(preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1)  # 左子树
            root.right = myBuildTree(preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right)  # 右子树
            return root
        
        n = len(preorder)
        return myBuildTree(0, n - 1, 0, n - 1)

更加优化的写法,是用一个字典(哈希映射)记录下中序列表中数值与下标位置的关系,方便快速找到根的位置:

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
        def myBuildTree(preorder_left: int, preorder_right: int, inorder_left: int, inorder_right: int):
            if preorder_left > preorder_right:  # 判断空树
                return None

            root = TreeNode(preorder[preorder_left])  # 根节点
            inorder_root = index[root.val]  # 根节点下标
            size_left_subtree = inorder_root - inorder_left  # 左子树长度
            root.left = myBuildTree(preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1)  # 左子树
            root.right = myBuildTree(preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right)  # 右子树
            return root
        
        n = len(preorder)
        # 构造哈希映射,帮助我们快速定位根节点
        index = {element: i for i, element in enumerate(inorder)}
        return myBuildTree(0, n - 1, 0, n - 1)

297. 二叉树的序列化与反序列化

题目页面

解法:序列化的思路就是前序遍历,将各个节点值之间通过 ‘,’ 连接起来;反序列化时,首先根据 ‘,’ 进行分隔,得到的列表就是各个节点的值(字符串),然后进行深度遍历 dfs,对每个节点进行树节点构建,左右子树则是递归。

class Codec:

    def serialize(self, root):
        """Encodes a tree to a single string.
        
        :type root: TreeNode
        :rtype: str
        """
        if not root:
            return ""
        leftTree = str(self.serialize(root.left))
        rightTree = str(self.serialize(root.right))
        return str(root.val) + ',' + leftTree + ',' + rightTree

    def deserialize(self, data):
        """Decodes your encoded data to tree.
        
        :type data: str
        :rtype: TreeNode
        """
        if not data:
            return None
        
        dataList = data.split(',')

        def dfs(root):
            node = dataList.pop(0)
            if node == '':
                return
            root = TreeNode(int(node))
            root.left = dfs(dataList)
            root.right = dfs(dataList)
            return root
        
        return dfs(dataList)

八、Tries 前缀树

208. 实现 Trie (前缀树)

题目页面

解法:前缀树是一种树形数据结构,能进行高效的前缀查找。在本题中,要存储的是由小写字母组成的字符串,insert 方法就是将字符串中的每一个字母,都作为一个节点,且字符串的字母从左到右对应树的从上到下。同时,要标记一个单词的结尾,即节点是否为单词结尾。这样 search 方法就是要判断找到的节点是否为单词结尾,而 startsWith 就只需要找到节点即可。可以看到三个方法在代码上是十分相似的。
在这里插入图片描述

class TrieNode:
    def __init__(self):
        self.children = {}
        self.endOfWord = False  # 节点字母是否为一个单词的结尾
        
class Trie:

    def __init__(self):
        self.root = TrieNode()

    def insert(self, word: str) -> None:
        cur = self.root
        for ch in word:
            if ch not in cur.children:
                cur.children[ch] = TrieNode()
            cur = cur.children[ch]
        cur.endOfWord = True

    def search(self, word: str) -> bool:
        cur = self.root
        for ch in word:
            if ch not in cur.children:
                return False
            cur = cur.children[ch]
        return cur.endOfWord

    def startsWith(self, prefix: str) -> bool:
        cur = self.root
        for ch in prefix:
            if ch not in cur.children:
                return False
            cur = cur.children[ch]
        return True

211. 添加与搜索单词 - 数据结构设计

题目页面

解法:与上一题类似,借助前缀树对单词进行存储,唯一的区别在于单词中可能含有通配符 ‘.’。当字母为通配符时,应该遍历所有的子节点路径,即深度优先遍历,利用递归函数实现,函数需要传入的参数是从单词的哪个字母(下标)开始考虑,同时在前缀树中匹配到了哪个节点;当字母不为通配符时,就与上一题一样进行迭代即可。
在这里插入图片描述

class TrieNode:
    def __init__(self):
        self.children = {}
        self.endOfWord = False
        
class WordDictionary:

    def __init__(self):
        self.root = TrieNode()

    def addWord(self, word: str) -> None:
        cur = self.root

        for ch in word:
            if ch not in cur.children:
                cur.children[ch] = TrieNode()
            cur = cur.children[ch]
        cur.endOfWord = True

    def search(self, word: str) -> bool:
        def dfs(j, root):
            cur = root

            for i in range(j, len(word)):
                ch = word[i]
                if ch == '.':
                    for child in cur.children.values():
                        if dfs(i + 1, child):
                            return True
                    return False
                else:
                    if ch not in cur.children:
                        return False
                    cur = cur.children[ch]
            return cur.endOfWord
        
        return dfs(0, self.root)

九、Heap / Priority Queue 堆 / 优先队列

703. 数据流中的第 K 大元素

题目页面

解法:使用一个长度为 K 的最小堆,每当总元素多于 K 个时,都会 heappop 弹走最小的元素,剩下的 K 个元素实际上是最大的 K 个,而这 K 个最大的元素中最小的即为第 K 大元素。时间复杂度为 O ( n log ⁡ k ) O(n\log k) O(nlogk),空间复杂度为 O ( k ) O(k) O(k)

class KthLargest:

    def __init__(self, k: int, nums: List[int]):
        self.k = k
        self.heap = nums
        heapq.heapify(self.heap)

    def add(self, val: int) -> int:
        heapq.heappush(self.heap, val)
        while len(self.heap) > self.k:
            heapq.heappop(self.heap)
        return self.heap[0]

1046. 最后一块石头的重量

题目页面

解法:使用最大堆(每个数字都为负的最小堆),这样保证了每次弹出的两个石头都是最大的,如果它们重量相等则粉碎(不加入新的石头),如果不相等则加入相减后的新石头。最后如果剩下一块石头就返回它的重量,否则就返回 0。时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def lastStoneWeight(self, stones: List[int]) -> int:
        heap = [-i for i in stones]
        heapq.heapify(heap)

        while len(heap) > 1:
            stone1 = heapq.heappop(heap) * -1
            stone2 = heapq.heappop(heap) * -1
            if stone1 != stone2:
                heapq.heappush(heap, stone2 - stone1)

        return abs(heap[0]) if heap else 0

973. 最接近原点的 K 个点

题目页面

解法一:将每个点到原点的距离(不用开方)从小到大排序后,取出前 k个即可。时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)

解法二:将每个点到原点的距离与点的坐标一同放入一个长度为 n 的最小堆中,然后弹出 K 次即可。时间复杂度为 O ( k log ⁡ n ) O(k\log n) O(klogn),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def kClosest(self, points: List[List[int]], k: int) -> List[List[int]]:
        heap = []
        for p in range(len(points)):
            heapq.heappush(heap, (points[p][0] ** 2 + points[p][1] ** 2, points[p]))
        ans = []
        for i in range(k):
            ans.append(heapq.heappop(heap)[1])
        return ans

215. 数组中的第K个最大元素

题目页面

解法一:排序后直接取第 K 个最大的元素,这是最基本的思路,时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn)。改进的方法是使用堆,可以对数组中的元素取负构建最大堆,然后弹出 K 次得到答案;也可以直接将数组变为最小堆,弹出元素使得最小堆大小为 K,则这 K 个最大的元素中最小的那个即为答案。时间复杂度为 O ( n + k log ⁡ n ) O(n + k\log n) O(n+klogn),空间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)

class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        heapq.heapify(nums)
        while len(nums) > k:
           heapq.heappop(nums)
        return nums[0]

解法二:快速选择算法,核心思想就是从数组随机选一个数,将数组里大于这个数的放一边,小于这个数的放另一边,这个数夹在它们中间,那这个数实际上已经被排好序了,同时也知道了它是第几大或第几小的元素了。在这题中,我们每次随机选择一个 pivot(此处为最右边的值),然后遍历区间所有的其他元素,比较元素与 pivot 的大小,同时用一个 p 指针计算有多少个元素是小于等于 pivot 的,遍历完成后就会使得小于等于 pivot 的元素在左边区间,然后是 pivot,而大于 pivot 的元素在右边区间。假设 k 是目标元素在已排序数组中的下标,如果 k 恰好等于 p,则答案就是 nums[p],因为已知数组中有多少个元素大于或小于它了。如果 k 不等于 p,则到左边区间或者右边区间继续寻找。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)
在这里插入图片描述

class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        k = len(nums) - k # 目标元素在已排序数组中的下标

        def quickSelect(l, r):
            pivot = nums[r] # pivot 设为最右的元素
            p = l # 计算有多少个元素小于等于 pivot
            for i in range(l, r):
                if nums[i] <= pivot:
                    nums[i], nums[p] = nums[p], nums[i]
                    p += 1
            nums[r], nums[p] = nums[p], nums[r]

            if p > k:
                return quickSelect(l, p - 1)
            elif p < k:
                return quickSelect(p + 1, r)
            else:
                return nums[p]
        
        return quickSelect(0, len(nums) - 1)

621. 任务调度器

题目页面

解法:首先,我们要看出最优的策略是每次执行剩余次数最多的任务,因此,任务是什么字母实际上不重要,我们要统计每个任务的剩余次数,并利用最大堆每次弹出最大的。其次,执行任务存在冷却时间,每当执行完一个任务,我们可以记录下次能执行该任务的时间,假设执行的时间为 time,冷却时间为 n,则下次能执行的时间就是 time + n 了,又因为 time 一定是递增的,所以可以用队列来记录任务的剩余次数和下次可执行时间。
在这里插入图片描述
如图所示,初始共有三种任务 A、B、C,剩余次数为 3、2、2,假设初始时间为 time = 0;我们先把任务的剩余次数都加入到最大堆中,然后开始弹出次数最大的(此处即为 -3),对其执行完一次后次数减一(变为 -2),把新的剩余次数和这个任务下次可执行时间(0 + n)加入到队列中。之后时间 time 加一为 1,看看队列中有无可执行的任务,没有就继续弹出,队列中再加入一个元素。时间 time 加一为 2,此时由于队列中存在冷却完毕可执行的任务,所以可以将其重新加入最大堆中。因为统计了每种任务的剩余次数,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)

class Solution:
    def leastInterval(self, tasks: List[str], n: int) -> int:
        count = collections.Counter(tasks)  # 统计每种任务的剩余次数
        maxHeap = [-cnt for cnt in count.values()]  # 将任务的剩余次数加入最大堆中
        heapq.heapify(maxHeap)
        queue = collections.deque()  # 记录任务的剩余次数与下次可执行时间
        time = 0  # 时间轴

        while maxHeap or queue:
            time += 1
            if maxHeap:
                cnt = 1 + heapq.heappop(maxHeap)  # 剩余次数减一,因为最大堆用的负数所以加一
                if cnt:  # 如果任务还有剩余次数,就在队列中记录下次可执行时间
                    queue.append([cnt, time + n])
            if queue and queue[0][1] == time:  # 如果队列最前的任务已经到达可执行时间,就弹出它加入最大堆
                heapq.heappush(maxHeap, queue.popleft()[0])
        
        return time

355. 设计推特

题目页面

解法:首先,我们考虑如果实现关注与取关的操作,由于每个 follower 都可能关注多个 followee,所以需要一个哈希表,使得每个 follower 映射到一个包含多个 followee 的列表,此时关注操作就是 append list( O ( 1 ) O(1) O(1) 时间复杂度),但要实现取关操作,如果是在列表的话就是 O ( n ) O(n) O(n) 时间复杂度了。想到添加与删除均为 O ( 1 ) O(1) O(1) 的数据结构,那就是集合 set 了,所以用 set 保存 followee 。
然后,每一个 user 都会发推,于是需要另一个哈希表,将 user 映射到包含其发过的推文的列表。由于检索推文需要根据时间排序,所以列表中保存的应该是一个个 pair(推文时间,推文 id)。显然越新的推文时间越大,所以使用最大堆来记录推文 id 与时间,这里我们将时间反向更新,记作 count,每次都减一;并且要考虑 user 关注的所有人与它自身的推文列表,假设共有 k 个列表,则用 k 个指针 index 指向每个列表的最尾端(这个人的最新推文),然后将这 k 个最新推文都放入最大堆,每次弹出最新的,然后更新该推文所在的列表指针 index,直到列表为空或者推文已经够 10 条为止。

class Twitter:

    def __init__(self):
        self.count = 0
        self.followMap = collections.defaultdict(set)
        self.tweetMap = collections.defaultdict(list)

    def postTweet(self, userId: int, tweetId: int) -> None:
        self.tweetMap[userId].append([self.count, tweetId])  # 最大堆使用负数
        self.count -= 1

    def getNewsFeed(self, userId: int) -> List[int]:
        ans = []
        maxHeap = []
        self.followMap[userId].add(userId)  # 用户关注的人包括用户自身

        for followeeId in self.followMap[userId]:
            if followeeId in self.tweetMap:  # 如果被关注人发过推文
                index = len(self.tweetMap[followeeId]) - 1  # 这个人发的最新推文,也是列表中的最后元素
                count, tweetId = self.tweetMap[followeeId][index]  # 取出最新推文的时间与id
                maxHeap.append([count, tweetId, followeeId, index - 1])  # index - 1 是列表指向的下一条推文
        heapq.heapify(maxHeap)
        while maxHeap and len(ans) < 10:
            count, tweetId, followeeId, index = heapq.heappop(maxHeap)
            ans.append(tweetId)
            if index >= 0:  # 这个被关注人的推文列表里还有推文
                count, tweetId = self.tweetMap[followeeId][index]  # 取出推文的时间与id
                heapq.heappush(maxHeap, [count, tweetId, followeeId, index - 1])  # index - 1 是列表指向的下一条推文
        return ans    

    def follow(self, followerId: int, followeeId: int) -> None:
        self.followMap[followerId].add(followeeId)

    def unfollow(self, followerId: int, followeeId: int) -> None:
        if followeeId in self.followMap[followerId]:
            self.followMap[followerId].remove(followeeId)

十、Backtracking 回溯

关于回溯,看这篇博客即可,模板和思路是一样的,重点是找准什么时候加入答案、什么时候返回、以及什么时候可以跳过循环。

加入答案的路径列表记得用浅拷贝。

78. 子集

题目页面

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        ans = []
        path = []

        def backtracking(index):
            ans.append(path[:])
            if index == len(nums):
                return
            
            for i in range(index, len(nums)):
                path.append(nums[i])
                backtracking(i + 1)
                path.pop()
        
        backtracking(0)
        return ans

90. 子集 II

题目页面

class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        ans = []
        path = []

        def backtracking(index):
            ans.append(path[:])
            if index == len(nums):
                return
            
            for i in range(index, len(nums)):
                if i > index and nums[i-1] == nums[i]:
                    continue
                path.append(nums[i])
                backtracking(i + 1)
                path.pop()
        
        nums.sort()
        backtracking(0)
        return ans

77. 组合

题目页面

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        nums = [i + 1 for i in range(n)]
        self.k = k
        ans = []
        path = []

        def backtracking(index):
            if len(path) == self.k:
                ans.append(path[:])
                return

            for i in range(index, len(nums)):
                path.append(nums[i])
                backtracking(i + 1)
                path.pop()
        
        backtracking(0)
        return ans

46. 全排列

题目页面

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        ans = []
        path = []

        def backtracking(index):
            if len(path) == len(nums):
                ans.append(path[:])
                return
            
            for i in range(len(nums)):
                if nums[i] not in path:
                    path.append(nums[i])
                    backtracking(i + 1)
                    path.pop()
        
        backtracking(0)
        return ans

47. 全排列 II

题目页面

class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        ans = []
        path = []
        visited = [False for _ in range(len(nums))]

        def backtracking(index):
            if len(path) == len(nums):
                ans.append(path[:])
                return
            
            for i in range(len(nums)):
                if i > 0 and nums[i] == nums[i-1] and visited[i-1] == True:
                    continue
                
                if not visited[i]:
                    visited[i] = True
                    path.append(nums[i])
                    backtracking(i + 1)
                    path.pop()
                    visited[i] = False
        
        nums.sort()
        backtracking(0)
        return ans

39. 组合总和

题目页面

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        ans = []
        path = []
        self.target = target

        def backtracking(index):
            if sum(path) > self.target:
                return     
            elif sum(path) == self.target:
                ans.append(path[:])
                return        
            
            for i in range(index, len(candidates)):
                if candidates[i] + sum(path) > self.target:
                    break
                path.append(candidates[i])
                backtracking(i)
                path.pop()
        
        candidates.sort()
        backtracking(0)
        return ans

40. 组合总和 II

题目页面

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        ans = []
        path = []
        self.target = target

        def backtracking(index):
            if sum(path) > self.target:
                return
            elif sum(path) == self.target:
                ans.append(path[:])
                return

            for i in range(index, len(candidates)):
                if candidates[i] + sum(path) > self.target:
                    break
                if i > index and candidates[i] == candidates[i-1]:
                    continue
                path.append(candidates[i])
                backtracking(i + 1)
                path.pop()
        
        candidates.sort()
        backtracking(0)
        return ans

17. 电话号码的字母组合

题目页面

class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        if not digits:
            return []

        phone_dict = {
            "2": "abc",
            "3": "def",
            "4": "ghi",
            "5": "jkl",
            "6": "mno",
            "7": "pqrs",
            "8": "tuv",
            "9": "wxyz"
        }

        ans = []
        path = []

        def backtracking(index):
            if index == len(digits):
                ans.append("".join(path))
            else:
                digit = digits[index]
                for letter in phone_dict[digit]:
                    path.append(letter)
                    backtracking(index + 1)
                    path.pop()

        backtracking(0)
        return ans

79. 单词搜索

题目页面

class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        path = set()

        def backtracking(row, col, index):
            if index == len(word):
                return True
            if (row < 0 or col < 0 or row >= len(board) or col >= len(board[0]) or
                word[index] != board[row][col] or (row, col) in path):
                return False
            
            path.add((row, col))
            ans = (backtracking(row + 1, col, index + 1) or
                  backtracking(row, col + 1, index + 1) or
                  backtracking(row - 1, col, index + 1) or
                  backtracking(row, col - 1, index + 1))
            path.remove((row, col))
            return ans
        
        for row in range(len(board)):
            for col in range(len(board[0])):
                if backtracking(row, col, 0):
                    return True
            
        return False

22. 括号生成

题目页面

解法:这题要进行括号生成,则类似于排列组合,但是必须保证括号是有效的。因此,使用回溯法,记录的状态为左括号的数量和右括号的数量,则当左右括号数量相同且均为 n 时,是可以返回的一个答案。接下来就是添加左括号或右括号,添加左括号的限制就是其当前数量不能大于 n 个,而添加右括号的限制是其当前数量不能大于等于左括号的数量,这样才能保证括号是有效的。

在这里插入图片描述

class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        ans = []
        path = []

        def backtracking(left, right):
            if len(path) > 2 * n or right > left:
                return
            if left == right == n:
                ans.append("".join(path))
                return
            
            path.append('(')
            backtracking(left + 1, right)
            path.pop()

            path.append(')')
            backtracking(left, right + 1)
            path.pop()
        
        backtracking(0, 0)
        return ans

131. 分割回文串

题目页面

解法:假设有字符串 aab,当处于位置 0 时,index = 0,可能的分割方案为 a、aa、aab(用 [index : i] 切片来控制),判断方案是否满足回文,若满足则位置移动到 i + 1,后面同理。

在这里插入图片描述

class Solution:
    def partition(self, s: str) -> List[List[str]]:
        ans = []
        path = []

        def backtracking(index):
            if index >= len(s):
                ans.append(path[:])
                return
            
            for i in range(index, len(s)):
                if self.isPali(s, index, i):
                    path.append(s[index:i + 1])
                    backtracking(i + 1)
                    path.pop()
        
        backtracking(0)
        return ans
    
    def isPali(self, s, l, r):
        while l < r:
            if s[l] != s[r]:
                return False
            l += 1
            r -= 1
        return True

十三、1-D Dynamic Programming 一维动态规划

70. 爬楼梯

题目页面

class Solution:
    def climbStairs(self, n: int) -> int:
        if n == 1 or n == 2:
            return n
        dp = [0] * (n+1)
        dp[1] = 1
        dp[2] = 2
        for i in range(3, n+1):
            dp[i] = dp[i-1] + dp[i-2]
        return dp[n]

746. 使用最小花费爬楼梯

题目页面

class Solution:
    def minCostClimbingStairs(self, cost: List[int]) -> int:
        n = len(cost)
        dp = [0] * (n+1)
        dp[0] = cost[0]
        dp[1] = cost[1]
        for i in range(2, n):
            dp[i] = cost[i] + min(dp[i-1], dp[i-2])
        dp[n] = min(dp[n-1], dp[n-2])
        return dp[n]

303. 区域和检索 - 数组不可变

题目页面

解法:计算区间和的巧妙思路,就是用一个列表记录到目前为止加进来元素的累积和,这样的话,区间 left 到 right 的和也就是到 right 的累积和减去到 left 的累积和。

class NumArray:

    def __init__(self, nums: List[int]):
        self.dp = [0]
        for num in nums:
            self.dp.append(self.dp[-1] + num)

    def sumRange(self, left: int, right: int) -> int:
        return self.dp[right + 1] - self.dp[left]

十五、贪心

53. 最大子数组和

题目页面

解法:用 total 记录当前子数组的和,ans 记录最大的 total,每当子数组和小于 0 时,就让 total 变回 0 重新开始求和,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        ans = nums[0]
        total = 0
        for num in nums:
            total += num
            ans = max(ans, total)
            if total < 0:
                total = 0
        return ans

55. 跳跃游戏

题目页面

解法:用 rightMost 记录r当前位置能跳到的最远位置,则如果可以一直跳下去,rightMost 就会一直增大,最后返回 True,如果有地方卡住了,rightMost 就会停止增大(小于 i),时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

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

45. 跳跃游戏 II

题目页面

解法:rightMost 就是当前位置能跳到的最远位置,而 end 就记录了上次跳跃可达范围的右边界(下次的最右起跳点)(第一个是 0,第二个是 i + nums[i],等等),每当 i 等于 end 时,就是发生了一次跳跃,ans 加一。同时把目前能跳到的最远位置 end 变成了下次起跳位置的右边界 rightMost。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def jump(self, nums: List[int]) -> int:
        rightMost = 0  # 目前能跳到的最远位置
        ans = 0  # 跳跃次数
        end = 0  # 上次跳跃可达范围右边界(下次的最右起跳点)
        for i in range(len(nums) - 1):
            if rightMost >= i:
                rightMost = max(rightMost, i + nums[i])
                if i == end:  # 到达上次跳跃能到达的右边界了
                    end = rightMost  # 目前能跳到的最远位置变成了下次起跳位置的右边界
                    ans += 1  # 进入下一次跳跃
        return ans

十六、Intervals 区间

252. 会议室

题目页面

解法:处理区间的好方法就是作图,在数轴上面查看区间有利于理解题目。这题的关键是先根据区间的开头从小到大排序,当开头是有序的时候,如果前一个区间的结尾大于后一个区间的开头,即两个区间有重叠部分,则这两个会议一定不能同时参加,返回 False。时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间复杂度为 O ( 1 ) O(1) O(1)

在这里插入图片描述

class Solution:
    def canAttendMeetings(self, intervals: List[List[int]]) -> bool:
        if len(intervals) == 0:
            return True
        intervals.sort(key=lambda i: i[0])
        lastEnd = intervals[0][1]
        for i in range(1, len(intervals)):
            if intervals[i][0] < lastEnd:
                return False
            lastEnd = intervals[i][1]
        return True

56. 合并区间

题目页面

解法:这题的思路与上一题类似,都是先排序,然后从小到大比较一个区间的结尾与下一个区间的开头。这里我们会先 append 最左边的区间到 ans 中,然后遍历后面区间的开头,与 lastEnd = ans[-1][1] 即上一个区间的结尾进行比较,如果当前区间是可以合并的,就更新 ans[-1][1],这样区间的结尾就会变大,否则就 append 一个新的区间,重复上述操作。时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)

在这里插入图片描述

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        if not intervals:
            return None
        intervals.sort(key=lambda i:i[0])
        ans = [intervals[0]]
        for i in range(1, len(intervals)):
            lastEnd = ans[-1][1]  # 前面区间的结尾
            start, end = intervals[i]  # 当前区间的开头与结尾
            if start <= lastEnd:
                ans[-1][1] = max(lastEnd, end)
            else:
                ans.append([start, end])
        return ans

986. 区间列表的交集

题目页面

解法:两个区间 a 和 b 出现交集的各种情况如下,

在这里插入图片描述

总结出来的规律就是如果两个区间的头尾出现交叉,则必有交集

在这里插入图片描述

交集区间的开头和结尾,分别就是两个区间开头的 max 和结尾的 min

在这里插入图片描述

这样,就能求出两个区间 a 和 b 的交集区间了,然后结尾比较靠前的区间就不再考虑了(肯定没有交集了),考虑其所在列表的下一个区间。时间复杂度为 O ( m + n ) O(m + n) O(m+n),空间复杂度为 O ( m + n ) O(m + n) O(m+n)

class Solution:
    def intervalIntersection(self, firstList: List[List[int]], secondList: List[List[int]]) -> List[List[int]]:
        i = 0
        j = 0
        ans = []
        while i < len(firstList) and j < len(secondList):
            a_start, a_end = firstList[i]
            b_start, b_end = secondList[j]
            if a_start <= b_end and b_start <= a_end:  # 出现交叉
                ans.append([max(a_start, b_start), min(a_end, b_end)])
            
            if a_end <= b_end:
                i += 1
            else:
                j += 1
                
        return ans

57. 插入区间

题目页面

解法:题目已经帮我们排序了,所以不用排序。这题的重点是插入区间的三种情况(作图更方便理解):1、插入区间的结尾在区间 i 的开头前面,所以可以直接在答案中加入插入区间,再加上区间 i 和它右边剩余的区间,然后返回;2、插入区间的开头在区间 i 的结尾后面,说明区间 i 不需要被合并,可以加入答案;3、插入区间与区间 i 发生了交叉,需要合并,合并后的区间跟上一题一样。除了第一种情况加入了插入区间后返回,其余情况在返回 ans 前都要记得加入插入区间。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]:
        ans = []
        for i in range(len(intervals)):
            if newInterval[1] < intervals[i][0]:
                ans.append(newInterval)
                return ans + intervals[i:]
            elif newInterval[0] > intervals[i][1]:
                ans.append(intervals[i])
            else:
                newInterval = [min(newInterval[0], intervals[i][0]), max(newInterval[1], intervals[i][1])]
        
        ans.append(newInterval)
        return ans

435. 无重叠区间

题目页面

解法:这题与合并区间类似,首先还是对区间进行排序,如下图

在这里插入图片描述

设 lastEnd 为 2,然后遍历区间,遇到 [1, 3],由于该区间的开头 1 小于 lastEnd,所以它们是重叠的,对于重叠的区间,应该删除哪一个?贪心的做法就是删除结尾较长的那个,然后 ans 加一。

在这里插入图片描述

然后遍历到区间 [2, 3],它的开头 2 不小于 lastEnd,所以不重叠,更新 lastEnd 为它的结尾 3

在这里插入图片描述

时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)

class Solution:
    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        if not intervals:
            return None
        intervals.sort(key=lambda i:i[0])
        lastEnd = intervals[0][1]
        ans = 0
        for i in range(1, len(intervals)):
            start, end = intervals[i]
            if lastEnd > start:
                lastEnd = min(lastEnd, end)
                ans += 1
            else:
                lastEnd = end
        return ans

253. 会议室 II

题目页面

解法:将每个区间的开头和结尾分开,作为 start 列表和 end 列表,然后排序。使用双指针 p1 和 p2,分别指向 start 和 end,每当遇到 start[p1] 小于 end[p2],说明有个会议要开,但之前最早结束的会议都还没结束,所以只能用新的会议室, count 加一;如果 start[p1] 大于等于 end[p2],则说明有个会议结束了,让出会议室,count 减一。最后的 ans 就是 count 取到的最大值。时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间复杂度为 O ( n ) O(n) O(n)

在这里插入图片描述

class Solution:
    def minMeetingRooms(self, intervals: List[List[int]]) -> int:
        start = sorted([i[0] for i in intervals])
        end = sorted([i[1] for i in intervals])
        count = ans = 0
        p1 = p2 = 0
        while p1 < len(start):
            if start[p1] < end[p2]:
                count += 1
                p1 += 1
            else:
                count -= 1
                p2 += 1
            ans = max(ans, count)
        return ans

十七、Math & Geometry 数学与几何

66. 加一

题目页面

解法:简单题,当某位出现 9 的时候,将该位置零,然后给前一位加一,如果前一位也是 9,以此类推,直到如果第一位也是 9,那就变为 0,返回时判断第一位是否为 0,如果是则在前面加个 1,唯一要注意的就是数字 0,遇到它直接返回 1 即可。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def plusOne(self, digits: List[int]) -> List[int]:
        if digits == [0]:
            return [1]
            
        i = len(digits) - 1
        while i >= 0:
            if digits[i] != 9:
                digits[i] += 1
                break
            else:
                digits[i] = 0
                i -= 1
        
        return [1] + digits if digits[0] == 0 else digits

50. Pow(x, n)

题目页面

解法:最直观的想法是将 x 自乘 n 次,但是这只是线性时间复杂度的解法。更好的想法,是利用分治,要计算 x n {x^n} xn,实际上只需要计算得到 x n 2 {x^{{n \over 2}}} x2n,让其自乘即可,这样一直将指数减半,直到 n 等于 0 为止。时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn),空间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)

在这里插入图片描述

class Solution:
    def myPow(self, x: float, n: int) -> float:
        
        def helper(x, n):
            if x == 0:
                return 0
            if n == 0:
                return 1
            ans = helper(x * x, n // 2)
            return ans if n % 2 == 0 else ans * x

        ans = helper(x, abs(n))
        return ans if n >= 0 else 1 / ans

73. 矩阵置零

题目页面

解法一:使用行、列两个数组,用来记录某行某列是否需要置零,时间复杂度为 O ( m n ) O(mn) O(mn),空间复杂度为 O ( m + n ) O(m + n) O(m+n)

在这里插入图片描述

class Solution:
    def setZeroes(self, matrix: List[List[int]]) -> None:
        """
        Do not return anything, modify matrix in-place instead.
        """
        rows = [1] * len(matrix)
        columns = [1] * len(matrix[0])
        for r in range(len(matrix)):
            for c in range(len(matrix[0])):
                if matrix[r][c] == 0:
                    rows[r] = 0
                    columns[c] = 0
        
        for r in range(len(rows)):
            if rows[r] == 0:
                matrix[r] = [0] * len(matrix[0])
        
        for c in range(len(columns)):
            if columns[c] == 0:
                for r in range(len(matrix)):
                    matrix[r][c] = 0

解法二:对于第 r 行第 c 列的元素 matrix[r][c],如果它等于 0,则将其所在行、列的第一个元素置零,作为这整行和整列都要置零的标记。如果本身就是第一行的元素,出现 0 了,就用一个变量 rowZero 记录第一行需要置零即可。时间复杂度为 O ( m n ) O(mn) O(mn),空间复杂度为 O ( 1 ) O(1) O(1)

在这里插入图片描述

如图所示,从第一行开始遍历,因为第一行第二列的元素为 0,所以左上角单独紫色方格中写入 0,也就是 rowZero 为 True,代表第一行需要置零;遍历完第一行后,遍历第二行,因为第二行第二列的元素等于 0,所以把第一行第二列的元素和第二行第一列的元素置零,后面同理。

class Solution:
    def setZeroes(self, matrix: List[List[int]]) -> None:
        """
        Do not return anything, modify matrix in-place instead.
        """
        ROWS = len(matrix)
        COLS = len(matrix[0])
        rowZero = False

        for r in range(ROWS):
            for c in range(COLS):
                if matrix[r][c] == 0:
                    matrix[0][c] = 0
                    if r > 0:
                        matrix[r][0] = 0
                    else:
                        rowZero = True

        for r in range(1, ROWS):
            for c in range(1, COLS):
                if matrix[0][c] == 0 or matrix[r][0] == 0:
                    matrix[r][c] = 0

        if matrix[0][0] == 0:
            for r in range(ROWS):
                matrix[r][0] = 0

        if rowZero:
            for c in range(COLS):
                matrix[0][c] = 0

54. 螺旋矩阵

题目页面

解法:用 left、right、top、bottom 分别表示矩阵的左、右、上、下边界,然后先遍历矩阵的最上面一层,将上边界指针 top 下移一位,再遍历矩阵的最右边一层,将右边界指针 right 左移一位,以此类推。注意反向遍历之前也要判断指针是否相遇,否则如下图中的用例最后会多出一个 6,是因为最后 right = 2,left = 1,top = 1,bottom = 1。时间复杂度为 O ( m n ) O(mn) O(mn),空间复杂度为 O ( 1 ) O(1) O(1)

在这里插入图片描述

class Solution:
    def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
        left, right = 0, len(matrix[0])
        top, bottom = 0, len(matrix)
        ans = []

        while left < right and top < bottom:
            for i in range(left, right):
                ans.append(matrix[top][i])
            top += 1

            for i in range(top, bottom):
                ans.append(matrix[i][right - 1])
            right -= 1

            if not (left < right and top < bottom):
                break
            
            for i in range(right - 1, left - 1, -1):
                ans.append(matrix[bottom - 1][i])
            bottom -= 1

            for i in range(bottom - 1, top - 1, -1):
                ans.append(matrix[i][left])
            left += 1
        
        return ans

48. 旋转图像

题目页面

解法:与上一题类似的思路,用上下左右四个边界指针,将值转移到旋转 90° 后的位置上。为了方便,可以先将左上的值存储起来,然后将左下的值转移到左上,右下到左下,右上到右下,存储起来的左上到右上。每次循环完成一个圆圈的旋转,循环次数是 right - left(距离),循环变量 i 用来控制是要旋转哪个相对位置(蓝色、绿色、红色)。时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( 1 ) O(1) O(1)

在这里插入图片描述

在这里插入图片描述

class Solution:
    def rotate(self, matrix: List[List[int]]) -> None:
        """
        Do not return anything, modify matrix in-place instead.
        """
        left, right = 0, len(matrix[0]) - 1

        while left < right:
            for i in range(right - left):
                top, bottom = left, right
                topleft = matrix[top][left + i]
                matrix[top][left + i] = matrix[bottom - i][left]
                matrix[bottom - i][left] = matrix[bottom][right - i]
                matrix[bottom][right - i] = matrix[top + i][right]
                matrix[top + i][right] = topleft
            left += 1
            right -= 1

202. 快乐数

题目页面

解法一:模拟,根据题意描述,如果数字 n 在求各位平方和的过程中,出现了重复且不等于 1,那就是陷入了循环,返回 False。例如下图中 n = 2,它就会在 37 出现循环。时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn),空间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)

在这里插入图片描述

class Solution:
    def isHappy(self, n: int) -> bool:
        visit = set()
        while n not in visit:
            visit.add(n)
            n = self.square_sum(n)
            if n == 1:
                return True
        return False
    
    def square_sum(self, n):
    # 求数字各位的平方和
        output = 0
        while n:
            n, digit = divmod(n, 10)
            output += digit ** 2
        return output

解法二:这题实际上类似于判断链表是否有环,所以可以用快慢指针的方法,这样就不需要用哈希表记录出现过的数字了。时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def isHappy(self, n: int) -> bool:
        slow = n
        fast = self.square_sum(n)
        while fast != 1 and slow != fast:
            slow = self.square_sum(slow)
            fast = self.square_sum(self.square_sum(fast))
        return fast == 1
    
    def square_sum(self, n):
        output = 0
        while n:
            n, digit = divmod(n, 10)
            output += digit ** 2
        return output

43. 字符串相乘

题目页面

解法:使用数学上竖式乘法的方法,从个位开始,每两个数进行相乘,假设是第 i 位和第 j 位相乘,则乘积累加到第 i + j 位上,进位放到第 i + j + 1 位,第 i + j 位保留余数。记得要做两次反转,并且去除前导 0。时间复杂度为 O ( m n ) O(mn) O(mn),空间复杂度为 O ( m + n ) O(m + n) O(m+n)

在这里插入图片描述

class Solution:
    def multiply(self, num1: str, num2: str) -> str:
        # 如果有一个数为 0,乘积为 0
        if num1 == '0' or num2 == '0':
            return '0'
        # 乘积的位数不大于 m + n
        ans = [0] * (len(num1) + len(num2))
        # 反转,将个位作为索引 0 开始遍历
        num1, num2 = num1[::-1], num2[::-1]

        for i in range(len(num1)):
            for j in range(len(num2)):
                digit = int(num1[i]) * int(num2[j])
                ans[i + j] += digit
                ans[i + j + 1] += ans[i + j] // 10
                ans[i + j] = ans[i + j] % 10
        
        ans = ans[::-1]
        temp = 0
        # 反转后,要去除前导 0
        while temp < len(ans) and ans[temp] == 0:
            temp += 1
        ans = map(str, ans[temp:])
        return "".join(ans)

2013. 检测正方形

题目页面

解法:对于每一个点,想检测是否有三个点能与它组成正方形,最直观的思想是进行三次 for 循环,分别检查三个点是否满足条件,但这样的时间复杂度为 O ( n 3 ) O(n^3) O(n3)。由于是检测正方形,实际上我们只需要先检测是否存在对角线上的点,若不存在或者遇到点自己就跳过,若存在则计算对角线点与剩下两个点的数目的乘积并累加到答案,最后输出答案即可。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class DetectSquares:

    def __init__(self):
        self.points = collections.defaultdict(int)  # 初始化哈希表(字典)

    def add(self, point: List[int]) -> None:
        self.points[tuple(point)] += 1  # 更新该点出现的次数,注意键必须是不可变变量,所以不能直接用point(列表是可变变量)

    def count(self, point: List[int]) -> int:
        ans = 0  # 返回的方案数量
        x, y = point  # 取出该点的横纵坐标
        for p, t in self.points.items():  # 遍历寻找其对角线上的点,p为点坐标,t为出现的次数
            if p[0] == x or p[1] == y:  # 与该点共线不能作为对角线点,跳过
                continue
            if abs(p[0] - x) != abs(p[1] - y):  # 长宽不等,不能围成正方形,跳过
                continue
            p0, p1 = (p[0], y), (x, p[1])  # 很容易计算出可以围成正方形的另外两个点的坐标
            ans += t * self.points.get(p0, 0) * self.points.get(p1, 0)  # 方案数量为这三个点出现的次数相乘
        return ans

十八、Bit Manipulation 位运算

136. 只出现一次的数字

题目页面

解法一:由于数组中只有一个数字时只出现一次,其余数字都出现两次,所以可以用一个集合,当数字第一次出现时加入集合,第二次出现时退出集合,那最后集合剩下的数字即为答案。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

解法二:位运算,一个数的二进制的每一位只可能为 1 或者 0,考虑所有出现两次的数字,它们每一位上的 0 都可以忽略(因为 1 或 0 与 0 做异或都得到自己),而剩下的 1 一定是偶数个(因为出现两次),偶数个 1 做异或一定得到 0,所以所有出现两次的数字做异或会得到 0,而只出现一次的数字与 0 做异或得到其本身,即为答案。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        ans = 0
        for num in nums:
            ans ^= num
        return ans

191. 位1的个数

题目页面

解法一:统计位 1 的个数,每次判断最低位是否为 1 ,然后右移即可。判断最低位是否为 1 有两个方法:与 1 进行与运算;对 2 进行取余,相当于判断是奇数还是偶数。右移也有两个方法:位运算右移;除以 2 。时间复杂度为 O ( 1 ) O(1) O(1),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def hammingWeight(self, n: int) -> int:
        ans = 0
        while n:
            ans = ans + (n & 1)
            n = n >> 1
        return ans
class Solution:
    def hammingWeight(self, n: int) -> int:
        ans = 0
        while n:
            ans += n % 2
            n //= 2
        return ans

解法二:由于中间位可能存在大量的 0,会造成不必要的运算,有没有方法可以直接找到值为 1 的位呢?方法是有的,就是每次让 n 与 n - 1 进行与运算,每次运算都会消去最右边的 1,直到 n 变为 0,运算的次数即为答案。时间复杂度为 O ( 1 ) O(1) O(1),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def hammingWeight(self, n: int) -> int:
        ans = 0
        while n:
            n = n & (n - 1)
            ans += 1
        return ans

338. 比特位计数

题目页面

解法一:逐个数字进行位 1 计数,由上一题可知,每个数字判断最低位是否为 1,然后右移 1 位(相当于除以 2),又因为每个数字都不大于 n,时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn),共有 n 个数字,所以时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def countBits(self, n: int) -> List[int]:
        ans = []
        for i in range(n+1):
            temp = 0
            while i:
                temp = temp + (i & 1)
                i = i >> 1
            ans.append(temp)
        return ans

解法二:动态规划,通过列举 0 到 8 的二进制位为 1 的数量,可以发现规律:2、3 的最低位重复了 0、1 的模式,而第二位固定为 1;4、5、6、7 的最低两位重复了 0、1、2、3 的模式,而第三位固定为 1。时间复杂度为 O ( n ) O( n) O(n),空间复杂度为 O ( n ) O(n) O(n)
在这里插入图片描述

class Solution:
    def countBits(self, n: int) -> List[int]:
        dp = [0] * (n + 1)
        offset = 1
        for i in range(1, n + 1):
            if offset * 2 == i:
                offset = i
            dp[i] = 1 + dp[i - offset]
        return dp

190. 颠倒二进制位

题目页面

解法一:最直接的思路,还是从二进制的最低位开始,判断其是否为 1,如果是则答案的最高位(对称)也为 1,时间复杂度为 O ( 1 ) O(1) O(1),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def reverseBits(self, n: int) -> int:
        ans = 0
        for i in range(32):
            ans <<= 1
            ans = ans + (n & 1)
            n >>= 1
        return ans

另外一种实现代码如下,把 ans 的左移一位放到了赋值中,然后加法用或运算代替:

class Solution:
    def reverseBits(self, n):
        ans = 0
        for i in range(32):
            ans = (ans << 1) | (n & 1)
            n >>= 1
        return ans

解法二:分治,把数字分成两半然后互换,对每一半再进行相同的操作,如图所示
在这里插入图片描述

class Solution:
    # @param n, an integer
    # @return an integer
    def reverseBits(self, n):
        n = (n >> 16) | (n << 16);
        n = ((n & 0xff00ff00) >> 8) | ((n & 0x00ff00ff) << 8);
        n = ((n & 0xf0f0f0f0) >> 4) | ((n & 0x0f0f0f0f) << 4);
        n = ((n & 0xcccccccc) >> 2) | ((n & 0x33333333) << 2);
        n = ((n & 0xaaaaaaaa) >> 1) | ((n & 0x55555555) << 1);
        return n;

268. 丢失的数字

题目页面

这题可以用哈希表记录数字,但是空间复杂度为 O ( n ) O(n) O(n),不符合题目要求。

解法一:位运算,正常的数组与缺失的数组做异或,相同的数会异或为0,剩下缺失的数。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def missingNumber(self, nums: List[int]) -> int:
        ans = len(nums)
        for i in range(len(nums)):
            ans = ans ^ i ^ nums[i]
        return ans

解法二:对数组求和,正常的数组和减去缺失的数组和,差值就是缺失的数。时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

class Solution:
    def missingNumber(self, nums: List[int]) -> int:
        return sum([i for i in range(len(nums)+1)]) - sum(nums)

371. 两整数之和

题目页面

解法:由于不能用加法和减法,所以只能想到用位运算。观察到二进制加法中,0 + 0 得 0,1 + 0 或 0 + 1 得 1,1 + 1 得 0 进 1,实际上相当于异或运算,除了两者均为 1 时会产生进位。所以先进行异或运算,然后用与运算判断是否进位,是的话就左移一位,且这里我们不是逐位运算,而是对整个数字进行运算。Python 中由于整数是无限长的,所以需要特别处理。时间复杂度为 O ( 1 ) O(1) O(1),空间复杂度为 O ( 1 ) O(1) O(1)

在这里插入图片描述

MASK1 = 0x100000000  # 2^32
MASK2 = 0x80000000  # 2^31
MASK3 = 0x7FFFFFFF  # 2^31-1

class Solution:
    def getSum(self, a: int, b: int) -> int:
        a %= MASK1
        b %= MASK1
        while b != 0:
            carry = ((a & b) << 1) % MASK1
            a = (a ^ b) % MASK1
            b = carry
        if a & MASK2:  # 负数
            return ~((a ^ MASK2) ^ MASK3)
        else:  # 正数
            return a

7. 整数反转

题目页面

解法:思路就是每次用 x 对 10 取余得到最低位,然后加到 ans 上,x 再对 10 取整以去掉最低位,直到 x 等于 0 为止。关键是要判断反转后的数是否越界,以上界为例,方法就是看反转后的数除最低位外的各位的值,是否大于上界相同位的值,若大于就肯定是越界,又或者是除最低位外都相同,但最低位比上界的最低位大,那也是越界。还有要注意的是 Python 中负数的取整和取余,参考这篇博客。时间复杂度为 O ( 1 ) O(1) O(1),空间复杂度为 O ( 1 ) O(1) O(1)在这里插入图片描述

class Solution:
    def reverse(self, x: int) -> int:
        MAX = 2 ** 31 - 1
        MIN = -(2 ** 31)
        
        ans = 0
        while x:
            digit = x % 10 if x > 0 else -((-x) % 10)
            x = int(x / 10)
            if (ans > MAX // 10) or (ans == MAX // 10 and digit >= MAX % 10):
                return 0
            if (ans < int(MIN / 10)) or (ans == int(MIN / 10) and (-digit) >= (-MIN) % 10):
                return 0
            ans = ans * 10 + digit
        return ans
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值