Leetcode hot100 python解法

不同容器常见操作的时间复杂度表格(平均 O(1)都是介于 O(1)和 O(n)之间):

操作类型列表 (list)元组 (tuple)集合 (set)字典 (dict)字符串 (str)
索引访问O(1)O(1)N/A平均 O(1)O(1)
赋值O(1)不支持N/A平均 O(1)不支持
追加元素平均 O(1)不支持平均 O(1)平均 O(1)不支持
插入元素O(n)不支持N/AN/A不支持
删除元素O(n)不支持平均 O(1)平均 O(1)不支持
弹出最后一个元素O(1)不支持N/AN/A不支持
弹出指定位置元素O(n)不支持N/AN/A不支持
查找元素O(n)O(n)平均 O(1)平均 O(1)O(n*m)
获取长度O(1)O(1)O(1)O(1)O(1)
切片操作O(b-a)O(b-a)N/AN/AO(b-a)
集合操作N/AN/A见下注释N/AN/A
连接字符串N/AN/AN/AN/AO(total_length)

image-20250217201345653

集合操作(set)详细时间复杂度:

  • 交集:O(min(len(set), len(other_set)))
  • 并集:O(len(set) + len(other_set))
  • 差集:O(len(set))
  • 对称差集:O(len(set) + len(other_set))

说明:

  • N/A: 表示该操作不适用于该数据结构。
  • 平均时间复杂度最坏时间复杂度:对于基于哈希的数据结构(如集合和字典),通常情况下操作是常数时间复杂度 O(1),但在极端情况下(如所有元素都发生哈希冲突),时间复杂度可能退化为 O(n)。
  • 切片操作:对于列表和元组,切片操作的时间复杂度取决于切片的长度(即 b-a),而不是整个容器的大小。
  • 查找子串:对于字符串,查找子串的时间复杂度为 O(n*m),其中 n 是主串长度,m 是子串长度。

这个表格提供了更简洁的信息,帮助你快速了解不同数据结构的主要操作时间复杂度。如果你有更多问题或需要进一步的帮助,请随时告诉我!


理清楚 python 传递对象的方式

python 传递对象的方式很神奇,叫做 按对象传递

其实呢就是传递地址,或者说是把一个带地址的 class 封装好了传递过去,也就是说当你将一个对象作为函数参数传递过去时,不像 C/C++ 一样默认复制一份 按值传递python 则是 默认按引用传递

对于一个参数(param)来说

传参方式PythonC/C++
传递fun(param.copy())/(fun(param [:] ))(仅适用列表)fun(param)(默认)
引用 传递fun(param)(默认)fun(& param)

一个简单的例子

path = []

def dfs(depth, path):
    # 当 depth = 6 时退出深搜
    if depth == 6:
        return

    path.append(depth)

    # dfs(depth+1, path)
    # > 这里是左值(有地址的对象), 所以是按引用传递
    # >>> print 结果为 [0,1,2,3,4,5]

    dfs(depth+1, path.copy())
    # > 这里是 copy(), 所以是按值传递
    # >>> print 结果为 [0,1]

    if depth == 1:
        print(path)
        # 如果按值传递会 print [0,1] 如果按引用传递会 print [0,1,2,3,4,5]

dfs(0, [])

函数写为对象(类)的好处

可以起到 函数声明 的作用

一个简单的例子来说明,类的方法 可以对 全局 起作用,而 函数 只能对其 下方 的作用域起作用

类的方法写法

class Solution:
    def preorderTraversal(self, root: TreeNode) -> List [int]:
        result = []
        # 可以将 preorder 函数在下方声明, 上方调用
        self.preorder(root, result)
        return result
    
    def preorder(self, node, result):
        if node:
            result.append(node.val)
            self.preorder(node.left, result)
            self.preorder(node.right, result)

注意:不要写成子方法!

class Solution:
    def preorderTraversal(self, root: TreeNode) -> List [int]:
        result = []
        # 可以将 preorder 函数在下方声明, 上方调用
        self.preorder(root, result)
        return result
        
        # 这样缩进就错了!
        def preorder(self, node, result):
            if node:
                result.append(node.val)
                self.preorder(node.left, result)
                self.preorder(node.right, result)
            

子函数写法

class Solution:
    def preorderTraversal(self, root: TreeNode) -> List [int]:
        result = []
        preorder(root, result)
        return result
    
    def preorder(node, result):
        if node:
            result.append(node.val)
            preorder(node.left, result)
            preorder(node.right, result)
    
    # 如果想要这么写, 则会弹出报错'NameError: name 'preorder' is not defined. '
    # 说明 preorder 不定义为类的方法的话, 定义在下方的函数就只能对更下方的作用域起作用
    NameError: name 'preorder' is not defined. Did you mean: 'byteorder'?
    ^^^^^^^^
    preorder(root, result)
Line 4 in preorderTraversal (Solution.py)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ret = Solution().preorderTraversal(param_1)
Line 38 in _driver (Solution.py)
    _driver()
Line 53 in <module> (Solution.py)

说明 preorder 不定义为 类的方法 的话, 定义在下方的函数 就只能对 更下方的作用域 起作用

Leetcode hot100 题

1. 两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。

你可以按任意顺序返回答案。

一开始我的解法(太慢了)

1866ms 击败 15.41%

class Solution:
    def twoSum(self, nums: List [int], target: int) -> List [int]:
        n = len(nums)
        for i in range(n-1):
            for j in range(i+1, n):
                if nums [i] + nums [j] == target:
                    return [i, j]

        return []

现在用的解法(双指针法)

8ms 击败 44.35%

class Solution:
    def twoSum(self, nums: List [int], target: int) -> List [int]:
        # 构造一个含 index 的数组,同时对其进行排序
        matrix = list(enumerate(nums))
        matrix.sort(key = lambda x: x [1])

        # 构造两个 lambda 函数方便理解
        value = lambda x: matrix [x][1]
        index = lambda x: matrix [x][0]

        # 初始化两个指针
        left = 0
        right = len(nums)-1

        # 循环结束条件为:两指针不相等
        while left != right:
            if value(left) + value(right) > target:
                right-= 1
            elif value(left) + value(right) < target:
                left+= 1
            else:
                return [index(left), index(right)] 
    

哈希解法

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

2. 两数相加

正确解法

class ListNode:
    def __init__(self, val = 0, next = None):
        self.val = val
        self.next = next

class Solution:
    def addTwoNumbers(self, l1, l2):
        dummy = ListNode()  # 创建一个哑节点,方便处理结果链表
        cur = dummy
        carry = 0

        while l1 or l2 or carry:
            # 将 carry 作为总数值
            if l1:
                carry += l1.val
                l1 = l1.next
            if l2:
                carry += l2.val
                l2 = l2.next

            cur.val = ListNode(carry%10)
            cur = cur.next

            carry//= 10            
            
        return dummy.next  # 返回哑节点的下一个节点作为结果链表的头

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

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

提示:

  • 0 <= s.length <= 5 * 104
  • s 由英文字母、数字、符号和空格组成

哈希表(set/dict)维护指针法

15ms 击败 91.72%

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        n = len(s)
        ans = 0
        left = 0
        charSet = set()  # 使用集合来存储当前窗口内的字符
        
        for right in range(n):  # 遍历字符串,right 作为滑动窗口的右边界
            while s [right] in charSet:  # 如果当前字符已经在集合中,移动左边界直到没有重复字符
                charSet.remove(s [left])  # 移除左边界的字符
                left += 1
            charSet.add(s [right])  # 将当前字符加入集合
            ans = max(ans, right - left + 1)  # 更新答案
            
        return ans
    

0x3f 做法

from collections import Counter

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        ans = 0
        # 使用 Counter 来记录当前窗口中每个字符的出现次数
        cnt = Counter()
        left = 0

        # 遍历字符串 s,right 作为右指针,c 为当前字符
        for right, c in enumerate(s):
            cnt [c] += 1
            while cnt [c] > 1:
                # 减少左指针所指向字符的计数,并将左指针向右移动,缩小窗口
                cnt [s[left]] -= 1
                left += 1
            # 当前窗口的长度是(right - left + 1)
            ans = max(ans, right - left + 1)
            
        return ans

维护指针法

23ms 击败 57.96%

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

8. 字符串转换整数 (atoi)

请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数。

函数 myAtoi(string s) 的算法如下:

  1. 空格: 读入字符串并丢弃无用的前导空格(" "
  2. 符号: 检查下一个字符(假设还未到字符末尾)为 '-' 还是 '+'。如果两者都不存在,则假定结果为正。
  3. 转换: 通过跳过前置零来读取该整数,直到遇到非数字字符或到达字符串的结尾。如果没有读取数字,则结果为 0。
  4. 舍入: 如果整数数超过 32 位有符号整数范围 [−231, 231 − 1] ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −231 的整数应该被舍入为 −231 ,大于 231 − 1 的整数应该被舍入为 231 − 1

返回整数作为最终结果。

官方解法(利用状态机求解)

INT_MAX = 2 ** 31 - 1
INT_MIN = -2 ** 31

class Automaton:
    def __init__(self):
        self.state = 'start'
        self.sign = 1
        self.ans = 0
        self.table = {
            'start': ['start', 'signed', 'in_number', 'end'],
            'signed': ['end', 'end', 'in_number', 'end'],
            'in_number': ['end', 'end', 'in_number', 'end'],
            'end': ['end', 'end', 'end', 'end'],
        }
        
    def get_col(self, c):
        if c.isspace():
            return 0
        if c == '+' or c == '-':
            return 1
        if c.isdigit():
            return 2
        return 3

    def get(self, c):
        self.state = self.table [self.state][self.get_col(c)]
        if self.state == 'in_number':
            self.ans = self.ans * 10 + int(c)
            self.ans = min(self.ans, INT_MAX) if self.sign == 1 else min(self.ans, -INT_MIN)
        elif self.state == 'signed':
            self.sign = 1 if c == '+' else -1

class Solution:
    def myAtoi(self, str: str) -> int:
        automaton = Automaton()
        for c in str:
            automaton.get(c)
        return automaton.sign * automaton.ans

我的解法

我的解法是状态机的变体, 但是只能利用在每个状态 顺序已知 的情况下, 不适合 变通 的情况

class Solution:
    def myAtoi(self, s: str) -> int:
        ture_num = 0
        index = 0
        negFlag = False
        while index < len(s):
            while index < len(s) and s [index] == ' ':
                index+= 1
            while index < len(s) and s [index] in "+-":
                if s [index] == '-':
                    negFlag = True
                index+= 1
                break
            while index < len(s) and (s[index] > = '0' and s [index] <= '9'):
                    ture_num*= 10
                    ture_num+= int(s [index])
                    index+= 1
            break
  
        ture_num = -ture_num if negFlag else ture_num
        if ture_num > 2**31-1:
            ture_num = 2**31-1
        elif ture_num < -2**31:
            ture_num = -2**31

        return ture_num
            

9.回文数

快速解法

class Solution:
    def isPalindrome(self, x: int) -> bool:
        return str(x)==str(x)[::-1] #运用了Python的str函数直接转置int数字为字符串,同时利用[::-1]写法直接进行reverse操作
    

我的解法(没有考虑到 python 自带字符串转置函数)

class Solution:
    def isPalindrome(self, x: int) -> bool:
        if x < 0:
            return False

        s = ""
        while x:
            cur_bit = x%10
            s+= str(cur_bit)
            x//= 10
        
        while len(s)> 1:
            if s [0] != s [-1]:
                return False
            s = s [1:-1]

        return True
    
        

10. 正则表达式匹配

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.''*' 的正则表达式匹配。

  • '.' 匹配任意单个字符
  • '*' 匹配零个或多个前面的那一个元素

所谓匹配,是要涵盖 整个 字符串 s 的,而不是部分字符串。

示例 1:

输入:s = "aa", p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。

示例 2:

输入:s = "aa", p = "a*"
输出:true
解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。

提示:

  • 1 <= s.length <= 20
  • 1 <= p.length <= 20
  • s 只包含从 a-z 的小写字母。
  • p 只包含从 a-z 的小写字母,以及字符 .*
  • 保证每次出现字符 * 时,前面都匹配到有效的字符

C++写法

class Solution {
public:
    bool isMatch(string s, string p) {
       // dp 递推
       // 记忆化搜索
       int n = s.length();
       int m = p.length();
       s.insert(s.begin(),'0');
       p.insert(p.begin(),'0');

       vector <vector<bool> > f(n+1, vector <bool>(m+1, false));
    // f [i][j] 表示 s 串前 i 个字符与 p 串前 j 个字符是否匹配
    // 答案为 f [n][m]

    f [0][0] = true; 
    for(int j = 1; j <= m; j++){
        if(p [j] == '*') f [0][j] = f [0][j-1];
        else if (j + 1 > m || p [j+1] != '*') break; // f [0][j] = false 
        else f [0][j] = true;
    }

    for(int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++){
            if(p [j] == '*'){
                f [i][j] = f [i][j-1];
                continue;
            }

            if(j+1 <= m && p [j+1] == '*'){
                // 空串
                // 1 个 p [j]
                // 若干个 p [j]
                f [i][j] = f [i][j-1] || 
                    (f [i-1][j-1] && (p [j] == '.' || s [i] == p [j])) ||
                    (f [i-1][j] && (p [j] == '.' || s [i] == p [j]));

            } else {
                f [i][j] = f [i-1][j-1] && (p [j] == '.' || s [i] == p [j]);
            }
        }

    return f [n][m];
    }
};

Python 写法

class Solution:
    def isMatch(self, s: str, p: str) -> bool:
        n = len(s)
        m = len(p)
        s = '0' + s
        p = '0' + p
        f = [[False for _ in range(m+1)] for _ in range(n+1)]

        f [0][0] = True
        for j in range(1, m+1):
            if p [j] == '*':
                f [0][j] = f [0][j-1]
            elif j+1 > m or p [j+1]!='*':
                break
            else:
                f [0][j] = True

        for i in range(1, n+1):
            for j in range(1, m+1):
                if p [j] == '*':
                    f [i][j] = f [i][j-1]
                    continue
                
                if j+1 <= m and p [j+1] == '*':
                    f [i][j] = f [i][j-1] or\
                              (f [i-1][j-1] and (p [j] == '.' or s [i] == p [j])) or\
                              (f [i-1][j] and (p [j] == '.' or s [i] == p [j]))
                else:
                    f [i][j] = f [i-1][j-1] and (p [j] == '.' or s [i] == p [j])
        
        return f [n][m]

11. 盛最多水的容器

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0)(i, height[i])

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

说明: 你不能倾斜容器。

示例 1:

img

输入:[1,8,6,2,5,4,8,3,7]
输出:49 
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

示例 2:

输入:height = [1,1]
输出:1

该题思路为 任意情况下 给你两条线,该移动谁可能形成更大的容量,同时注意要节省时间性能,实际上可以观察到

面积 = 底部距离 x 最小值(线段长度 1, 线段长度 2) = 底部距离 x 较短线段长度

底部距离的 单调性: 因为指针是从两边向内靠近, 所以底部距离是在不断缩短的, 呈现单调减的特性

较短线段长度的 单调性:

  • 固定较短线段移动较长线段 时: 新的较短线段 <= 老的较短线段,所以也呈现单调减的特性
  • 固定较长线段移动较短线段 时: 新的较短线段不确定大小关系,所以单调关系也不确定

则:

  • 固定较短线段移动较长线段 时: 面积的单调性 = 单调减 x 单调减,所以也呈现单调减的特性
  • 固定较长线段移动较短线段 时: 面积的单调性 = 单调减 x 不确定,所以单调关系也不确定

所以不能 固定较短线段移动较长线段,而是要 固定较长线段移动较短线段 尝试是否有更大值

双指针法

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

        while left < right:
            area = (right-left)*min(height [left], height [right])
            ans = max(area, ans)
            if height [left] < height [right]:
                left+= 1
            elif  height [left] > height [right]:
                right-= 1
            else:
                left+= 1
            
        return ans

15. 三数之和

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

注意: 答案中不可以包含重复的三元组。

示例 1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。

示例 2:

输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。

示例 3:

输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。

提示:

  • 3 <= nums.length <= 3000
  • -105 <= nums[i] <= 105

本题解法

class Solution:
    def threeSum(self, nums: List [int]) -> List [List[int]]:
        # 时间复杂度 O(n^2)
        # 空间复杂度 O(1)
        nums.sort()
        ans = []
        n = len(nums)
        
        for i in range(n-2):
            # i 跳过重复元素 ##################################
            x = nums [i]
            if i > 0 and x == nums [i-1]:
                continue
            # 优化一 #########################################
            if x + nums [i+1] + nums [i+2] > 0:
                break
            # 优化二 
            if x + nums [-2] + nums [-1] < 0:
                continue
            #################################################
            j = i+1
            k = n-1
            while j < k:
                if nums [j] + nums [k] + x > 0:
                    k-= 1
                elif nums [j] + nums [k] + x < 0:
                    j+= 1
                else:
                    ans.append([x, nums[j], nums [k]])
                    # j 跳过重复元素 ##########################
                    while j < k and nums [j] == nums [j + 1]:
                        j += 1
                    # k 跳过重复元素 
                    while j < k and nums [k] == nums [k - 1]:
                        k -= 1
                    ########################################
                    j += 1
                    k -= 1

        return ans

18. 四数之和

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且 不重复 的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

  • 0 <= a, b, c, d < n
  • abcd 互不相同
  • nums[a] + nums[b] + nums[c] + nums[d] == target

你可以按 任意顺序 返回答案 。

示例 1:

输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]

示例 2:

输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]

提示:

  • 1 <= nums.length <= 200
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109

高效做法(剪枝)

class Solution:
    def fourSum(self, nums: List [int], target: int) -> List [List[int]]:
        quadruplets = list()
        if not nums or len(nums) < 4:
            return quadruplets
        
        nums.sort()
        length = len(nums)
        for i in range(length - 3):
            if i > 0 and nums [i] == nums [i - 1]:
                continue
            if nums [i] + nums [i + 1] + nums [i + 2] + nums [i + 3] > target:
                break
            if nums [i] + nums [length - 3] + nums [length - 2] + nums [length - 1] < target:
                continue
            for j in range(i + 1, length - 2):
                if j > i + 1 and nums [j] == nums [j - 1]:
                    continue
                if nums [i] + nums [j] + nums [j + 1] + nums [j + 2] > target:
                    break
                if nums [i] + nums [j] + nums [length - 2] + nums [length - 1] < target:
                    continue
                left, right = j + 1, length - 1
                while left < right:
                    total = nums [i] + nums [j] + nums [left] + nums [right]
                    if total == target:
                        quadruplets.append([nums[i], nums [j], nums [left], nums [right]])
                        while left < right and nums [left] == nums [left + 1]:
                            left += 1
                        left += 1
                        while left < right and nums [right] == nums [right - 1]:
                            right -= 1
                        right -= 1
                    elif total < target:
                        left += 1
                    else:
                        right -= 1
        
        return quadruplets

正确做法

from typing import List

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

我的做法

class Solution:
    def fourSum(self, nums: List [int], target: int) -> List [List[int]]: 
        n = len(nums)
        if n < 4:
            return []
        nums.sort()
        ans = []

        for i1 in range(n-3):

            x = nums [i1]
            if i1 > 0 and nums [i1-1] == x:
                continue
                
            for i2 in range(i1+1, n-2):

                y = nums [i2]
                if i2 > 0 and nums [i2-1] == y:
                    continue

                j = i2+1
                k = n-1
                
                while j < k:
                    if (x+y) + nums [j] + nums [k] > target:
                        k-= 1
                    elif (x+y) + nums [j] + nums [k] < target:
                        j+= 1
                    else:
                        ans.append([x, y, nums[j], nums [k]])

                        # 跳过重复项
                        while j < k and nums [j+1] == nums [j]:
                            j+= 1
                        while j < k and nums [k-1] == nums [k]:
                            k-= 1

                        j+= 1
                        k-= 1

        return ans

28. 找出字符串中第一个匹配项的下标

简单

给你两个字符串 haystackneedle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1

示例 1:

输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。

示例 2:

输入:haystack = "leetcode", needle = "leeto"
输出:-1
解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。

提示:

  • 1 <= haystack.length, needle.length <= 104
  • haystackneedle 仅由小写英文字符组成

朴素解法

class Solution {
public:
    int strStr(string haystack, string needle) {      
        // 方法一: 朴素解法
        int n = haystack.size(), m = needle.size();
        for(int i = 0; i <= n-m; i++){
            int j = i, k = 0;
            while(k < m && haystack [j] == needle [k]){
                j++;
                k++;
            }
            if(k == m) return i;
        }
        
        return -1;
    }
};

KMP 解法

class Solution {
public:
    int strStr(string haystack, string needle) {
        // 方法二: KMP 解法
        int n = haystack.size(), m = needle.size();
        if(m == 0) return 0;
        // 设置哨兵
        haystack.insert(haystack.begin(),' ');
        needle.insert(needle.begin(),' ');
        vector <int> next(m+1);
        // 预处理 next 数组
        for(int i = 2, j = 0; i <= m; i++){
            while(j && needle [i]!= needle [j+1]) j = next [j];
            if(needle [i] == needle [j+1]) j++;
            next [i] = j;
        }
        // 匹配过程
        for(int i = 1, j = 0; i <= n; i++){
            while(j && haystack [i] != needle [j+1]) j = next [j];
            if(haystack [i] == needle [j+1]) j++;
            if(j == m) return i - m;
        }

        return -1;        
    }
};

KMP 优化解法

class Solution {
public:
    int strStr(string haystack, string needle) {
        // 方法三: KMP 优化解法
        int n = haystack.size(), m = needle.size();
        if(m == 0) return 0;

        // 预处理 next 数组
        vector <int> next(m + 1);
        for(int i = 2, j = 0; i <= m; i++) {
            while(j && needle [i - 1] != needle [j]) j = next [j];
            if(needle [i - 1] == needle [j]) j++;
            next [i] = j;
        }

        // 预处理 nextval 数组
        vector <int> nextval(m + 1);
        for(int i = 2; i <= m; i++) {
            if (needle [i] == needle [next[i]]) 
                // 这里必须是 nextval [next[j]]而不是 next [next[j]]
                // 因为 nextval 前面的部分在遍历时已经经过处理了, 会指向 "祖先" 点, 而 next 并不会
                nextval [i] = nextval [next[i]];
            else 
                nextval [i] = next [i];
        }

        // 匹配过程
        for(int i = 0, j = 0; i < n; i++) {
            while(j && haystack [i] != needle [j]) j = nextval [j];
            if(haystack [i] == needle [j]) j++;
            if(j == m) return i - m + 1;
        }

        return -1;  
    }
};

32. 最长有效括号

困难

字符串 动态规划

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

示例 1:

输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"

示例 2:

输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"

示例 3:

输入:s = ""
输出:0

提示:

  • 0 <= s.length <= 3 * 104
  • s[i]'('')'

动态规划


我的做法(匹配信息存储到一个数组中, 然后变为 <最长True子串问题>)

11ms 击败 30.65%

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        # 数组索引括号匹配然后寻找最长连续 True 的部分
        n = len(s)
        if n < 2:
            return 0

        valid = [False for _ in range(n)]
        stack = []

        for i in range(n):
            if s [i] == '(':
                stack.append(i)
            else:
                if not stack:
                    pass
                else:
                    temp_i = stack.pop()
                    valid [temp_i] = True
                    valid [i] = True

        # 对 valid 数组进行处理 : 找到最长连续 True 串
        ans = 0
        # 对开头进行特殊处理(因为开头无法判断上升沿)
        count = int(valid [0])
        count_state = valid [0]
        
        for i in range(1, n):
            cur = valid [i]
            pre = valid [i-1]

            if count_state == True:
                if pre == True and cur == False:
                    # 在遇到下降沿时更新答案
                    ans = max(count, ans)
                    count_state = False
                    count = 0
                else:
                    # 正常计数
                    count+= 1
            else:
                if pre == False and cur == True:
                    # 在遇到上升沿时重新开始计数
                    count_state = True
                    count = 1
            
            # 在最后一次循环的末尾也要更新 ans
            if i == n-1:
                ans = max(count, ans)

        return ans    

42. 接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例 1:

img

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 

示例 2:

输入:height = [4,2,0,3,2,5]
输出:9

提示:

  • n == height.length
  • 1 <= n <= 2 * 104
  • 0 <= height[i] <= 105

前缀和法

该题的一种思路是使用 前缀和后缀和 来充当墙体,因为雨水都是被两边的“高墙”所拦截的,对 height 数组遍历得到前缀和就是每个坐标 左边的墙 高度,后缀和就是每个坐标 右边的墙 高度,当前这个点能乘放的雨水量就是当前坐标,左右两墙的较矮墙 的高度减去当前坐标本身的高度,对所有坐标如是操作,则能得到最终乘放的雨水量。

精简版

35ms 击败 21.08%

class Solution:
    def trap(self, height: List [int]) -> int:
        n = len(height)
        pre = [0 for _ in range(n)]
        suf = [0 for _ in range(n)]
        pre [0] = height [0]
        suf [-1] = height [-1]

        # 前缀和 后缀和
        for i in range(1, n):
            pre [i] = max(height [i], pre [i-1])
            suf [(n-1)-i] = max(height [(n-1)-i], suf [(n-1)-(i-1)])
                                                                                
            
        return sum(min(pre [i], suf [i]) for i in range(n)) - sum(height)
        
        

具体版

39ms 击败 12.19%

class Solution:
    def trap(self, height: List [int]) -> int:
        n = len(height)
        pre_max = [0 for _ in range(n)]
        suf_max = [0 for _ in range(n)]
        pre_max [0] = height [0]
        suf_max [-1] = height [-1]
        area = 0

        # 前缀和 后缀和
        for i in range(1, n):
            pre_max [i] = max(height [i], pre_max [i-1])
            suf_max [(n-1)-i] = max(height [(n-1)-i], suf_max [(n-1)-(i-1)])
                                
        # 遍历计算区域总面积
           for val, pre, suf in zip(height, pre_max, suf_max):
            area += min(pre, suf) - val
                        
        return area
        

双指针法

11ms 击败 88.01%

另一种思路是利用双指针的方法来优化这个问题。具体思路如下:

  1. 使用两个指针 leftright 分别指向数组的两端。
  2. 维护两个变量 max_leftmax_right,分别记录从左到右和从右到左的最大高度。
  3. 每次移动较小的一端指针,并根据当前的最大高度计算可以存水的量。

为什么移动较小的一端?

  1. 确定存水量的关键
    • 对于每一个位置 i,它能存储的水量取决于它左边和右边的最大高度中的较小值(即 min(max_left, max_right))。
    • 如果当前柱子的高度小于这个最小值,则可以在该柱子上方存储水。
  2. 双指针的工作原理
    • 我们维护两个指针 leftright,分别从数组的两端开始向中间靠拢。
    • 同时,我们也维护两个变量 max_leftmax_right,分别记录从左到右和从右到左的最大高度。
  3. 如何决定移动哪个指针
    • max_left <= max_right 时,说明左边的最大高度小于或等于右边的最大高度。这意味着对于 left 指针指向的位置,它的最大存水量由 max_left 决定。
      • 因此,我们可以安全地移动 left 指针,并根据 max_left 来计算当前位置可以存储的水量。
    • 反之,当 max_left > max_right 时,说明右边的最大高度小于左边的最大高度。这意味着对于 right 指针指向的位置,它的最大存水量由 max_right 决定。
      • 因此,我们可以安全地移动 right 指针,并根据 max_right 来计算当前位置可以存储的水量。
class Solution:
    def trap(self, height: List [int]) -> int:
        left = 0
        right = len(height) - 1
        ans = 0
        pre = height [left]
        suf = height [right]

        while left < right:
            if height [left] < height [right]:
                left += 1
                pre = max(height [left], pre)
                ans += pre - height [left]
            else:
                right -= 1
                suf = max(height [right], suf)
                ans += suf - height [right]

        return ans
            

46. 全排列

示例 1:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

输入:nums = [1]
输出:[[1]]

提示:

  • 1 <= nums.length <= 6
  • -10 <= nums[i] <= 10
  • nums 中的所有整数 互不相同

状态树法

这道题目中状态树做法和深搜做法是一模一样的

class Solution:
    def permute(self, nums: List [int]) -> List [List[int]]:
        n = len(nums)
        s = [False] * n  # 用于标记是否已使用过该位置的数
        path = []  # 当前构建的排列
        ans = []  # 存储所有的排列

        def dfs(iterator):
            if iterator == n:  # 完成一次排列
                ans.append(path.copy())  # 将当前排列加入到结果集中
                return
            
            for i in range(n):
                if not s [i]:  # 如果第i个数还没有被使用
                    s [i] = True  # 标记为已使用
                    path.append(nums [i])  # 将该数添加到当前排列
                    
                    dfs(iterator + 1)  # 递归调用以处理下一个位置
                    
                    path.pop()  # 回溯,撤销上一步的选择
                    s [i] = False  # 取消对该数的使用标记

        dfs(0)
        return ans

深度搜索法

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

        def dfs(iterator):
            if iterator == n:
                ans.append(path.copy())                
                return 

            for i in range(n):
                if not used [i]:
                    used [i] = True
                    path.append(nums [i])
                    dfs(iterator+1)
                    path.pop()
                    used [i] = False

        dfs(0)
        return ans

49. 字母异位词分组

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

字母异位词 是由重新排列源单词的所有字母得到的一个新单词。

示例 1:

输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]

示例 2:

输入: strs = [""]
输出: [[""]]

示例 3:

输入: strs = ["a"]
输出: [["a"]]

提示:

  • 1 <= strs.length <= 104
  • 0 <= strs[i].length <= 100
  • strs[i] 仅包含小写字母

总结对比表

  • N 是字符串数组的长度
  • K 是字符串的最大长度
特性/方法方法 1:排序后分组方法 2:质数乘积哈希方法 3:字符计数表
时间复杂度O(N * K log K)O(N * K)O(N * K)
空间复杂度O(N * K)O(N + 26) ≈ O(N)O(N + 26) ≈ O(N)
优点实现简单,易于理解哈希计算快速,避免了排序操作避免了排序操作,且键值对直接反映了字符频率
缺点排序操作增加了时间复杂度质数乘积可能导致数值溢出,特别是对于较长的字符串对于每个字符串都需要一个固定大小的计数表
适用场景字符串较短或数量较少的情况字符串较长但数量适中,且不担心整数溢出字符串长度适中,且希望避免排序操作
稳定性稳定(相同字母异位词始终生成相同的键)不稳定(依赖于质数乘积的结果,可能因整数溢出导致冲突)稳定(相同字母异位词始终生成相同的键)

字典排序法

11ms 击败 92.18%

class Solution:
    def groupAnagrams(self, strs: List [str]) -> List [List[str]]:
        if len(strs)< 2:
            return [strs]

        ans = {}
        for s in strs:
            temp = "".join(sorted(s))
            ans [temp] = ans.get(temp, []) + [s]
 
        return list(ans.values())

哈希质数相乘法

15ms 击败 54.53%

prime_number   = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101]
numalpha_table = {i: j for i, j in zip("abcdefghijklmnopqrstuvwxyz", prime_number)}

class Solution:
    def groupAnagrams(self, strs: List [str]) -> List [List[str]]:
        if len(strs)< 2:
            return [strs]

        def hash_mul(string):
            result = 1
            for char in string:
                result*= numalpha_table [char]
            return result

        ans = {}
        for s in strs:
            temp = hash_mul(s)
            ans [temp] = ans.get(temp, []) + [s]
 
        return list(ans.values())
        

哈希字典计数法

23ms 击败 18.44%

class Solution:
    def groupAnagrams(self, strs: List [str]) -> List [List[str]]:
        if len(strs) < 2:
            return [strs]

        result = {}
        for s in strs:
            count_table = [0]*26
            for c in s:
                count_table [ord(c)-ord('a')] += 1
            
            key = tuple(count_table)
            result [key] = result.get(key, [])+[s]

        return list(result.values())

我的字典计数法(超时)

超出时间限制

111 / 126 个通过的测试用例

class Solution:
    def groupAnagrams(self, strs: List [str]) -> List [List[str]]:
        if len(strs)< 2:
            return [strs]

        ans = {}
        ans_dict = {}
        for i, string in enumerate(strs):
            temp_dict = Counter(string)
            if not ans:
                ans [string] = [string]
                ans_dict [string] = temp_dict

            # 从头遍历所有已知的异位词字典, 如果不在, 将自己作为新的 key 加入
            else:
                Flag_without = True
                for key in ans_dict:
                    if temp_dict == ans_dict [key]:
                        ans [key].append(string)
                        Flag_without = False
                        break            
                # 遍历完如果不在
                if Flag_without:
                    ans [string] = [string]
                    ans_dict [string] = temp_dict

        return list(ans.values())

        

53. 最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。

示例 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

示例 2:

输入:nums = [1]
输出:1

示例 3:

输入:nums = [5,4,-1,7,8]
输出:23

提示:

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104

进阶: 如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的 分治法 求解。

这道题的所有方法总结如下

总结对比表

方法时间复杂度空间复杂度备注
动态规划O(n)O(n) / O(1)可优化至 O(1)
Kadane 算法O(n)O(1)最优选择之一
前缀和方法O(n)O(n)较多额外空间
双循环暴力法O(n^2)O(1)不推荐用于大规模数据
分治法O(n log n)O(log n)适用于特定场景

Kadane 算法(类似于动态规划, 但是需要的空间更小, 一遍下来只维护两个最大值)

63ms 击败 78.78%

# 时间复杂度 O(n)
# 空间复杂度 O(1)
class Solution:
    def maxSubArray(self, nums: List [int]) -> int:
        max_current = max_global = nums [0]
        for num in nums [1:]:
            max_current = max(num, max_current + num)
            if max_current > max_global:
                max_global = max_current
                
        return max_global

动态规划

147ms 击败 8.87%

# 时间复杂度 O(n)
# 空间复杂度 O(n)
class Solution:
    def maxSubArray(self, nums: List [int]) -> int:
        dp = [-float('inf') for _ in range(len(nums))]
        dp [0] = nums [0]

        for i in range(1, len(nums)):
            dp [i] = max(dp [i-1]+nums [i], nums [i])

        return max(dp)
        

0x3f 的前缀和(想到了)

101ms 击败 36.14%

# 时间复杂度 O(n)
# 空间复杂度 O(n)
class Solution:
    def maxSubArray(self, nums: List [int]) -> int:
        ans = -inf
        min_pre_sum = pre_sum = 0
        for x in nums:
            pre_sum += x  # 当前的前缀和
            ans = max(ans, pre_sum - min_pre_sum)  # 减去前缀和的最小值
            min_pre_sum = min(min_pre_sum, pre_sum)  # 维护前缀和的最小值
        return ans

双循环暴力法

超出时间限制

202 / 210 个通过的测试用例

# 时间复杂度 O(n^2)
# 空间复杂度 O(1)
class Solution:
    def maxSubArray(self, nums: List [int]) -> int:
        # 初始化
        n = len(nums)
        ans = -float('inf') #或者 ans = min(nums)

        # 暴力法双重循环(超时 202 / 210 个通过的测试用例)
        for i in range(n):
            pre = 0
            for j in range(i, n):
                pre+= nums [j]
                if pre > ans:
                    ans = pre

        return ans

分治法(太难想了, 不强求)



56. 合并区间

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间

示例 1:

输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

示例 2:

输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。

提示:

  • 1 <= intervals.length <= 104
  • intervals[i].length == 2
  • 0 <= starti <= endi <= 104

执行用时分布

我的做法

3ms 击败 93.57%

class Solution:
    def merge(self, intervals: List [List[int]]) -> List [List[int]]:
        if len(intervals) <= 1:
            return intervals

        start  = lambda x: x [0]
        end    = lambda x: x [1]
        # 首先排序确定开始时间顺序
        intervals.sort(key = lambda x: start(x))
        result = [intervals[0]]
        
        for i in range(1, len(intervals)):
            # 大于等于都要包含, 否则会错漏 [1,4][4,5] 这样的情况
            if end(result [-1]) >= start(intervals [i]):
                temp_result = result [-1]
                result.pop()
                # 结束时间不定, 所以取最大值即可
                temp_end = max(end(intervals [i]), end(temp_result))
                result.append([start(temp_result), temp_end])
            else:
                result.append(intervals [i])

        return result 

简洁做法

3ms

class Solution:
    def merge(self, intervals: List [List[int]]) -> List [List[int]]:
        if len(intervals)< 2:
            return intervals
        result = []
        intervals.sort(key = lambda x: x [0])
        
        for interval in intervals:
            if len(result) == 0 or interval [0] > result [-1][1]:
                result.append(interval)
            else:
                result [-1][1] = max(result [-1][1], interval [1])
                
        return result

57. 插入区间

给你一个 无重叠的 按照区间起始端点排序的区间列表 intervals,其中 intervals[i] = [starti, endi] 表示第 i 个区间的开始和结束,并且 intervals 按照 starti 升序排列。同样给定一个区间 newInterval = [start, end] 表示另一个区间的开始和结束。

intervals 中插入区间 newInterval,使得 intervals 依然按照 starti 升序排列,且区间之间不重叠(如果有必要的话,可以合并区间)。

返回插入之后的 intervals

注意 你不需要原地修改 intervals。你可以创建一个新数组然后返回它。

示例 1:

输入:intervals = [[1,3],[6,9]], newInterval = [2,5]
输出:[[1,5],[6,9]]

示例 2:

输入:intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]
输出:[[1,2],[3,10],[12,16]]
解释:这是因为新的区间 [4,8] 与 [3,5],[6,7],[8,10] 重叠。

提示:

  • 0 <= intervals.length <= 104
  • intervals[i].length == 2
  • 0 <= starti <= endi <= 105
  • intervals 根据 starti升序 排列
  • newInterval.length == 2
  • 0 <= start <= end <= 105

合并区间改编过来的做法

class Solution:
    def insert(self, intervals: List [List[int]], newInterval: List [int]) -> List [List[int]]:
        # 改动一: intervals 列表边界条件
        if len(intervals) == 0:
            return [newInterval]

        start  = lambda x: x [0]
        end    = lambda x: x [1]
        # 首先排序确定开始时间顺序
        # 改动二: 将 newInterval 插入 intervals 开始作为合并整体遍历
        intervals.append(newInterval)                
        intervals.sort(key = lambda x: start(x))
        result = [intervals[0]]
        
        for i in range(1, len(intervals)):
            # 大于等于都要包含, 否则会错漏 [1,4][4,5] 这样的情况
            if end(result [-1]) >= start(intervals [i]):
                temp_result = result [-1]
                result.pop()
                # 结束时间不定, 所以取最大值即可
                temp_end = max(end(intervals [i]), end(temp_result))
                result.append([start(temp_result), temp_end])
            else:
                result.append(intervals [i])

        return result 

70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例 1:

输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

示例 2:

输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶

提示:

  • 1 <= n <= 45

动态规划

class Solution:
    def climbStairs(self, n: int) -> int:
        # 方法二
        if n < 2:
            return 1

        dp = [0 for _ in range(n+1)]
        dp [0] = dp [1] = 1

        for i in range(2, n+1):
            dp [i] = dp [i-1] + dp [i-2]

        return dp [-1]

        # 方法一
        # if n < 2:
        #     return dp [n]

        # dp = [1,1]

        # for i in range(2, n+1):
        #     dp.append(dp [-1] + dp [-2])

        # return dp [-1]

77. 组合

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

示例 1:

输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

示例 2:

输入:n = 1, k = 1
输出:[[1]]

提示:

  • 1 <= n <= 20
  • 1 <= k <= n
class Solution:
    def combine(self, n: int, k: int) -> List [List[int]]:
        if n == 0 or k > n:
            return []
        elif n == k:
            return [list(range(1, n+1))]
        
        # 回溯函数(组合类型)
        # params: 待挑选数组长度
        # params: 每个组合需要的数组长度
        # params: 待挑选的数组开始索引(即迭代器 iterator, 用迭代器普适性更强)
        # params: 挑选过程中的路径 path
        def backtracking(n, k, begin, path):
            if len(path) == k:
                ans.append(path.copy())
                return

            for i in range(begin, n+1):
                path.append(i)
                # 注意传递的是 i+1, 而不是 begin+1
                # 意思就是从当前挑选的数往后挑选, 不会回头看
                # begin 表示的不是当前的数, 而是这一轮 dfs 开始的数
                backtracking(n, k, i+1, path)
                path.pop()

        ans = []
        backtracking(n, k,1, [])

        return ans

78. 子集

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2:

输入:nums = [0]
输出:[[],[0]]

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • nums 中的所有元素 互不相同
class Solution:
    def subsets(self, nums: List [int]) -> List [List[int]]:
        # 每次选择拿不拿, 迭代器一直往前走, 迭代器结束时将 path 加入 ans 并 return
        ans = []
        path = []

        # 回溯函数(子集类型)
        # params: 待挑选的集合
        # params: 迭代器 iterator
        # params: 挑选过程中的路径 path
        def backtracking(nums, iterator, path):
            # 当 iterator 到底时加入 path
            if iterator == len(nums):
                ans.append(path.copy())
                return

            # 加入当前 iterator 的数
            path.append(nums [iterator])
            backtracking(nums, iterator+1, path)
            path.pop()
            # backtracking(nums, iterator+1, path+[nums[iterator]])
            # >>>>>>>>>>>>>>>>>>>>>>>>>>>>> 此处为一个右值副本, 不是左值所以不是按引用传递

            # 不加入当前 iterator 的数
            backtracking(nums, iterator+1, path)
            # >>>>>>>>>>>>>>>>>>>>>>>>>>> 这里是左值, 所以是按引用传递

        backtracking(nums,0, [])

        return ans

90. 子集 II

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的 子集(幂集)。

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

示例 1:

输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]

示例 2:

输入:nums = [0]
输出:[[],[0]]

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
class Solution:
    def subsetsWithDup(self, nums: List [int]) -> List [List[int]]:
        # 首先进行排序获得以便利用顺序特性: 该题中的特性为, 按顺序来取的话子集中也只会是按顺序排列, 那么元组就会自动过滤相同的子集, 如果不排序, cnt 相同的元组可能会因为顺序不同不能过滤掉, 所以要先进行排列
        nums.sort()

        # 该题与子集 I 的不同点就在于需要去重:
        # 1.利用 set 过滤不同的元组(之所以是元组(tuple)是因为列表(list)不具有 hash 性不能加入 set)
        # 2.利用排序过滤 cnt 相同而顺序不同的数组
        ans_set = set()
        path = []

        def backtracking(nums, iterator, path):
            if iterator == len(nums):
                ans_set.add(tuple(path))
                return

            backtracking(nums, iterator+1, path)
            backtracking(nums, iterator+1, path+[nums[iterator]])

        backtracking(nums,0, [])
        return list(ans_set)
            

94. 二叉树的中序遍历

给定一个二叉树的根节点 root ,返回 它的 * 中序 * 遍历

示例 1:

img

输入:root = [1,null,2,3]
输出:[1,3,2]

示例 2:

输入:root = []
输出:[]

示例 3:

输入:root = [1]
输出:[1]

提示:

  • 树中节点数目在范围 [0, 100]
  • -100 <= Node.val <= 100

二叉树定义

# Definition for a binary tree node.
class TreeNode:
    def __init__(self, val = 0, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right

迭代法(需要记住, 手动维护一个栈)

class Solution:
    def inorderTraversal(self, root: Optional [TreeNode]) -> List [int]:
        stack = []
        ans = []
        cur = root

        while cur != None or stack != []:
            # 如果上一个节点存在子树则向左子树深入
            if cur != None:
                stack.append(cur)
                cur = cur.left

            # 如果不存在子树则回到上一个节点, 并将上一个节点加入答案, 向其右子树走一个节点
            else:
                cur = stack.pop()
                ans.append(cur.val)
                cur = cur.right

        return ans

递归法(简单)

class Solution:
    def inorderTraversal(self, root: Optional [TreeNode]) -> List [int]:
        def inorderTraversalHandler(node):
            # 退出条件
            if node == None:
                return

            # 如果本题是先序遍历 #################
            # ans.append(node.val)
            ####################################

            inorderTraversalHandler(node.left)
            ans.append(node.val)
            inorderTraversalHandler(node.right)

            # 如果本题是后序遍历 #################
            # ans.append(node.val)
            ####################################

        ans = []
        inorderTraversalHandler(root)
        return ans
        


102. 二叉树的层序遍历

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

示例 1:

img

输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]

示例 2:

输入:root = [1]
输出:[[1]]

示例 3:

输入:root = []
输出:[]

提示:

  • 树中节点数目在范围 [0, 2000]
  • -1000 <= Node.val <= 1000

简练做法(用 深度搜索+先序遍历 实现 层序遍历)

class Solution:
    def levelOrder(self, root: Optional [TreeNode]) -> List [List[int]]:
        
        def dfs(root, depth, ans):
            if root == None:
                return
            if depth >= len(ans):
                ans.append([])
            ans [depth].append(root.val)
            dfs(root.left,  depth+1 , ans)
            dfs(root.right, depth+1, ans)

        ans = []
        dfs(root, 0, ans)
        return ans
     

我的做法

import queue

class Solution:
    def levelOrder(self, root: Optional [TreeNode]) -> List [List[int]]:
        if root == None:
            return []
        elif root.left == None and root.right == None:
            return [[root.val]]
            
        my_queue = queue.Queue()
        my_queue.put(root)
        # 加入第一层标识符
        my_queue.put('-')
        path = []
        ans = [[root.val]]

        while not my_queue.empty():
            cur = my_queue.get()
            # 每一层结尾检测到标识符将 path 加入 ans, 清除之前的 path, 并重置标识位
            if cur == '-':
                if pre == '-':
                    break
                if path:
                    ans.append(path.copy())
                    path = []
                my_queue.put('-')
                pre = cur
                continue

            # 如果 cur 存在左右子树就将子树加入队列, 子树的值加入 path
            if cur.left:
                my_queue.put(cur.left)
                path.append(cur.left.val)
            if cur.right:
                my_queue.put(cur.right)
                path.append(cur.right.val)
            
            pre = cur

        return ans

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

给你二叉树的根节点 root ,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。

示例 1:

img

输入:root = [3,9,20,null,null,15,7]
输出:[[3],[20,9],[15,7]]

示例 2:

输入:root = [1]
输出:[[1]]

示例 3:

输入:root = []
输出:[]

提示:

  • 树中节点数目在范围 [0, 2000]
  • -100 <= Node.val <= 100
class Solution:
    def zigzagLevelOrder(self, root: Optional [TreeNode]) -> List [List[int]]:
        
        def dfs(root, depth, ans):
            if root == None:
                return
            if depth >= len(ans):
                ans.append([])

            if depth%2 == 0:
                ans [depth].append(root.val)
            else:
                ans [depth].insert(0, root.val)
                
            dfs(root.left,  depth+1 , ans)
            dfs(root.right, depth+1, ans)

        ans = []
        dfs(root, 0, ans)
        return ans

144. 二叉树的前序遍历

给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

示例 1:

输入: root = [1, null,2,3]

输出: [1,2,3]

解释:

img

示例 2:

输入: root = [1,2,3,4,5, null,8, null, null,6,7,9]

输出: [1,2,4,5,6,7,3,8,9]

解释:

img

示例 3:

输入: root = []

输出: []

示例 4:

输入: root = [1]

输出: [1]

提示:

  • 树中节点数目在范围 [0, 100]
  • -100 <= Node.val <= 100

以前写的递归写法

class TreeNode:
    def __init__(self, val = 0, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right

class Solution:
    def preorderTraversal(self, root: TreeNode) -> List [int]:
        result = []
        self.preorder(root, result)
        return result
    
    def preorder(self, node, result):
        if node:
            result.append(node.val)
            self.preorder(node.left, result)
            self.preorder(node.right, result)

200. 岛屿数量

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

示例 1:

输入:grid = [
  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
]
输出:1

示例 2:

输入:grid = [
  ["1","1","0","0","0"],
  ["1","1","0","0","0"],
  ["0","0","1","0","0"],
  ["0","0","0","1","1"]
]
输出:3

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 300
  • grid[i][j] 的值为 '0''1'

DFS 解法

237ms 击败 92.72%

class Solution:
    def numIslands(self, grid: List [List[str]]) -> int:
        rows = len(grid)
        cols = len(grid [0])
        ans  = 0

        def dfs(row, col):
            if row < 0 or row > rows-1 or col < 0 or col > cols-1 or grid [row][col] == "0":
                return
            else:
                grid [row][col] = "0"   # grid [row][col] = 0 一开始写成具体数值 0, 错误了
                dfs(row+1, col)
                dfs(row-1, col)
                dfs(row, col+1)
                dfs(row, col-1)
        
        for i in range(rows):
            for j in range(cols):
                if grid [i][j] == "1":
                    ans+= 1
                    dfs(i, j)

        return ans

209. 长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target

找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度 如果不存在符合条件的子数组,返回 0

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:

输入:target = 4, nums = [1,4,4]
输出:1

示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0

提示:

  • 1 <= target <= 109
  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 104

进阶:

  • 如果你已经实现 O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。

自己的写法(18/21 用例)

class Solution:
    def minSubArrayLen(self, target: int, nums: List [int]) -> int:
        n = len(nums)
        left = 0
        right = 0 
        ans = float('inf')

        # [left: right] 为左闭右开区间
        #当 right 小于数组长度时一直前进, 如果到了而且 sum(nums [left: right])小于 target 就可以退出了    
        while not (right >= n and sum(nums [left: right])< target):
            if sum(nums [left: right])< target:
                right+= 1
            else:
                ans = min(right-left, ans)
                left+= 1

        return ans if ans < 65536 else 0
        

问题

  1. 重复计算 sum(nums[left:right]):在每次循环中都调用 sum(nums[left:right]) 会导致性能瓶颈,因为这个操作的时间复杂度是 O(n),其中 n 是子数组的长度。
  2. 终止条件的问题:当前的终止条件 while not (right >= n and sum(nums[left:right])<target) 可能会导致逻辑错误,特别是在处理边界情况时。
  3. 返回值检查 ans if ans<65536 else 0:这种做法虽然可以工作,但不如直接比较 ans 是否仍为初始值 float('inf') 来判断是否有符合条件的子数组直观。

改进建议

为了提高效率并修正逻辑错误,我们可以使用滑动窗口的方法来避免重复计算子数组的和,并优化终止条件。下面是改进后的代码示例:

通义千问写法

from typing import List

class Solution:
    def minSubArrayLen(self, target: int, nums: List [int]) -> int:
        n = len(nums)
        left = 0
        total = 0
        min_len = float('inf')
        
        for right in range(n):
            total += nums [right]  # 增加右指针指向的元素到总和
            
            # 当总和大于等于目标值时,尝试缩小窗口大小
            while total >= target:
                min_len = min(min_len, right - left + 1)  # 更新最小长度
                total -= nums [left]  # 减去左指针指向的元素从总和
                left += 1  # 移动左指针
        
        return min_len if min_len != float('inf') else 0

关键点解释

  • 滑动窗口方法:通过维护一个窗口(由 leftright 两个指针定义),我们可以在遍历数组的同时动态调整窗口大小,以找到满足条件的最小子数组。
  • 避免重复计算:通过在每一步更新窗口的总和,而不是每次都重新计算整个子数组的和,大大提高了效率。
  • 终止条件:新的实现不再需要复杂的终止条件,而是自然地通过遍历数组完成任务。
  • 结果检查:如果 min_len 仍为初始值 float('inf'),说明没有找到符合条件的子数组,因此返回 0。

这种方法的时间复杂度是 O(n),每个元素最多被访问两次(一次通过右指针,一次通过左指针),空间复杂度是 O(1)。


407. 接雨水 II

给你一个 m x n 的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。

示例 1:

img

输入: heightMap = [[1,4,3,1,3,2],[3,2,1,3,2,4],[2,3,3,2,3,1]]
输出: 4
解释: 下雨后,雨水将会被上图蓝色的方块中。总的接雨水量为1+2+1=4。

示例 2:

img

输入: heightMap = [[3,3,3,3,3],[3,2,2,2,3],[3,2,1,2,3],[3,2,2,2,3],[3,3,3,3,3]]
输出: 10

提示:

  • m == heightMap.length
  • n == heightMap[i].length
  • 1 <= m, n <= 200
  • 0 <= heightMap[i][j] <= 2 * 104

我的解法( 19 / 42 )

class Solution:
    def trapRainWater(self, heightMap: List [List[int]]) -> int:
        rows = len(heightMap)
        cols = len(heightMap [0])
        ans  = 0

        pre_left  = [[0 for _ in range(cols)] for _ in range(rows)]
        suf_right = [[0 for _ in range(cols)] for _ in range(rows)]
        pre_up    = [[0 for _ in range(cols)] for _ in range(rows)]
        suf_down  = [[0 for _ in range(cols)] for _ in range(rows)]

        for i in range(rows):
            pre_left [i][0]   = heightMap [i][0]
            suf_right [i][-1] = heightMap [i][-1]
        for j in range(cols):
            pre_up [0][j]    = heightMap [0][j]
            suf_down [-1][j] = heightMap [-1][j]

        for i in range(rows):
            for j in range(1, cols):
                pre_left [i][j] = max(heightMap [i][j], pre_left [i][j-1])
                suf_right [i][(cols-1)-j] = max(heightMap [i][(cols-1)-j], suf_right [i][(cols-1)-(j-1)])
        for j in range(cols):
            for i in range(1, rows):
                pre_up [i][j] = max(heightMap [i][j], pre_up [i-1][j])
                suf_down [(rows-1)-i][j] = max(heightMap [(rows-1)-i][j], suf_down [(rows-1)-(i-1)][j])

        for i in range(rows):
            for j in range(cols):
                ans += min(pre_left [i][j], suf_right [i][j], pre_up [i][j], suf_down [i][j]) - heightMap [i][j]

        return ans

713. 乘积小于 K 的子数组

给你一个整数数组 nums 和一个整数 k ,请你返回子数组内所有元素的乘积严格 小于 k 的连续子数组的数目。

示例 1:

输入:nums = [10,5,2,6], k = 100
输出:8
解释:8 个乘积小于 100 的子数组分别为:[10]、[5]、[2]、[6]、[10,5]、[5,2]、[2,6]、[5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于 100 的子数组。

示例 2:

输入:nums = [1,2,3], k = 0
输出:0

提示:

  • 1 <= nums.length <= 3 * 104
  • 1 <= nums[i] <= 1000
  • 0 <= k <= 106

我的写法

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

主要问题

  1. 缩进不一致:Python 对缩进非常敏感,必须使用一致的缩进方式(全部使用空格或全部使用制表符)。你当前的代码在不同的地方混用了制表符和空格,导致了 TabError: inconsistent use of tabs and spaces in indentation 错误。
  2. 变量名拼写错误tmep_mul 应该是 temp_mul
  3. 逻辑错误
    • 在更新 ans 时,应该基于当前有效的子数组长度。
    • mul >= k 时,需要移动左指针来减少乘积,而不是通过临时变量来进行调整。

修正后的代码

通义千问写法

from typing import List

class Solution:
    def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
        if k <= 1:
            return 0
        
        n = len(nums)
        left = 0
        ans = 0
        mul = 1
        
        for right in range(n):
            mul *= nums[right]
            
            # 移动左指针以保证乘积小于k
            while mul >= k and left <= right:
                mul /= nums[left]
                left += 1
            
            # 当前有效子数组的数量是(right - left + 1)
            ans += right - left + 1
        
        return ans

关键点解释

  • 滑动窗口方法:我们使用两个指针 leftright 来表示当前窗口的边界。当窗口内所有元素的乘积大于等于 k 时,我们通过移动左指针来缩小窗口,直到乘积小于 k
  • 计算有效子数组数量:每当右指针向右移动一位时,所有以当前 right 结尾且乘积小于 k 的子数组数量为 (right - left + 1)。这是因为从 leftright 之间的每一个起点都构成一个有效的子数组。
  • 处理特殊情况:如果 k <= 1,那么没有任何子数组的乘积可以小于 k,因此直接返回 0。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值