【LeetCode高频手撕题 2】面试中常见的手撕算法题(小红书)

【备注】:本blog主要用于记录博主本人在准备leetcode的算法题的过程,其中选出的题主要来源于leetcode hot100,大部分为企业真实面试中的高频面试手撕题目,建议全代码熟练到背诵程度。

目录

1、长度最小的子数组(209题-中等)

题目描述:

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

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

解题思路:

滑动窗口求解。用left 与 right记录窗口的左右端点,先滑动右侧端点同时计算窗口内的和s,当s>=target时进入判断,将当前长度(right-left+1)与答案进行比较,更新最小的为答案,然后s需要减去左端点的值,然后left需要右移,再判断s是否大于等于target。

代码实现:

python 复制代码
class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        # 滑动窗口求解。用left 与 right记录窗口的左右端点,先滑动右侧端点同时计算窗口内的和s,当s>=target时进入判断,记录当前长度并与答案进行比较,更新最小的为答案,然后left需要右移,同时s需要减去左端点的值。
        n = len(nums)
        ans = n+1
        left = 0
        right = 0
        s = 0
        for right, x in enumerate(nums):
            s += x
            while s >= target:
                ans = min(ans, right-left+1)
                s -= nums[left]
                left += 1
                
        return ans if ans <=n else 0

复杂度分析

时间复杂度:O(n),其中 n 为 nums 的长度。虽然写了个二重循环,但是内层循环中对 left 加一的总执行次数不会超过 n 次,所以总的时间复杂度为 O(n)。

空间复杂度:O(1),仅用到若干额外变量。

答疑:

请评论区留言。

2、二叉树中的最大路径和(124题-困难)

题目描述:

二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给你一个二叉树的根节点 root ,返回其 最大路径和 。

解题思路:

树形DP,与543题的数的最大直径类似。dfs(node)表示求解节点node的最大路径和,可以分解为求左右子树的最大路径和,然后加上node.val表示该节点的最大路径和。递归边界:空节点时返回0。

本题有两个关键概念:

  • 链:从下面的某个节点(不一定是叶子)到当前节点的路径。把这条链的节点值之和,作为 dfs 的返回值。如果节点值之和是负数,则返回 0(和 0 取最大值)。这个思想和 53. 最大子数组和 是一样的,如果左侧子数组的元素和是负数,就不和当前元素拼起来。
  • 直径:等价于由两条(或者一条)链拼成的路径。我们枚举每个 node,假设直径在这里「拐弯」,也就是计算由左右两条从下面的某个节点(不一定是叶子)到 node 的链的节点值之和,去更新答案的最大值。
    ⚠注意:dfs 返回的是链的节点值之和,不是直径的节点值之和。

代码实现:

python 复制代码
# 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 maxPathSum(self, root: Optional[TreeNode]) -> int:
        # 思路:树形DP,与543题的数的最大直径类似。dfs(node)表示求解节点node的最大路径和,可以分解为求左右子树的最大路径和,然后加上node.val表示该节点的最大路径和。递归边界:空节点时返回0。
        ans = -inf
        def dfs(node):
            if node is None:
                return 0
            l_val = dfs(node.left)
            r_val = dfs(node.right)
            nonlocal ans
            ans = max(ans, l_val + r_val + node.val)		# 求出以节点node为直径的最大路径和
            return max(max(l_val, r_val) + node.val, 0)      # 返回节点node的最大链和,为后续的递归回父节点做记录(注意这里和 0 取最大值了)

        dfs(root)
        return ans

复杂度分析

时间复杂度:O(n),其中 n 为二叉树的节点个数。

空间复杂度:O(n)。最坏情况下,二叉树退化成一条链,递归需要 O(n) 的栈空间。

答疑:

请评论区留言。

3、会议室 II(253题-中等)

题目描述:

给你一个会议时间安排的数组 intervals ,每个会议时间都会包括开始和结束的时间 intervals[i] = [starti, endi] ,返回 所需会议室的最小数量 。

示例 1:

输入:intervals = [[0,30],[5,10],[15,20]]

输出:2

解题思路:

把会议开始时间与结束时间视为独立的时间,会议开始用+1表示,会议结束用-1表示。将所有会议开始与结束时间统计好然后排序,用cur来表示当前进行的会议数量,遇到会议开始cur+1,遇到会议结束cur-1,同时不断更新最大的会议峰值ans。

代码实现:

python 复制代码
class Solution:
    def minMeetingRooms(self, intervals: List[List[int]]) -> int:
        # 思路:把会议开始时间与结束时间视为独立的时间,会议开始用+1表示,会议结束用-1表示。将所有会议开始与结束时间统计好然后排序,用cur来表示当前进行的会议数量,遇到会议开始cur+=1,遇到会议结束cur -= 1,不断更新最大的会议峰值ans。
        ans = cur = 0
        envent = [(itv[0], 1) for itv in intervals] + [(itv[1], -1) for itv in intervals]			# 将所有会议开始与结束的事件合并起来
        envent.sort()				#  必须要进行排序

        for _, e in envent:
            cur += e                    # 统计当前进行会议的事件数量
            ans = max(ans, cur)         # 更新会议的最大峰值

        return ans

复杂度分析

时间复杂度为 O(nlogn),主要由事件排序主导,因为创建和扫描事件仅需 O(n)。空间复杂度为O(n),因为需要将 n 个会议拆分为 2n 个事件存储,占用空间与会议数量成正比。

答疑:

请评论区留言。

4、 无重叠区间(435题-中等)

题目描述:

给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。

注意 只在一点上接触的区间是 不重叠的。例如 [1, 2] 和 [2, 3] 是不重叠的。

示例 1:

输入: intervals = [[1,2],[2,3],[3,4],[1,3]]

输出: 1

解释: 移除 [1,3] 后,剩下的区间没有重叠

解题思路:

贪心策略。根据区间的右端点进行排序,选取区间右端点最小的作为开始节点,不断遍历集合,比较当前遍历区间的左端点l是否比上个入选的区间右端点pre_r小,若pre_r<=l则该区间入选,更新入选区间的右端点pre_r,同时ans+1(ans记录的是入选区间的个数);否则不选择该区间。遍历完成返回len(intervals)-ans;

代码实现:

python 复制代码
class Solution:
    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        # 思路:贪心策略。根据区间的右端点进行排序,选取区间右端点最小的作为开始节点,不断遍历集合,比较当前遍历区间的左端点l是否比上个选区的区间右端点pre_r小,若pre_r>l则不选该区间,不更新pre_r;否则更新pre_r=r(当前区间的右端点),同时ans+1(ans记录的是入选区间的个数);遍历完成返回len(intervals)-ans;
        ans = 0             # 记录入选区间的个数
        pre_r = -inf        # 初始化右端点=为负无穷
        intervals.sort(key= lambda x:x[1])      # 根据区间的右端点进行排序
        for l, r in intervals:
            if pre_r <= l:          # 当入选的区间右端点小于当前区间的左端点,则不重叠,入选当前区间
                ans += 1
                pre_r = r           # 更新入选区间的右端点
        
        return len(intervals) - ans       # 返回需要去除的区间个数

复杂度分析

时间复杂度:O(nlogn),其中 n 是 intervals 的长度。瓶颈在排序上。

空间复杂度:O(1)。不计入排序的栈开销。

答疑:

(1)问:为什么要排序?为什么要按照右端点排序?

答:选择右端点最小的区间 A 后,左端点小于 A 的右端点的区间都与 A 相交,都不能选。因此,为了方便计算下一个可以选的区间,按照右端点从小到大排序。排序后,intervals[0] 一定可以选,并且下一个可以选的区间是第一个左端点 ≥intervals[0][1] 的区间。

5、最长递增子序列(253题-中等)

题目描述:

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

输入:nums = [10,9,2,5,3,7,101,18]

输出:4

解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

解题思路:

贪心+二分查找。即维护一个g[]数组(该数组是递增数组),g[n]以长度为n+1的递增子序列的末尾元素的最小值,然后二分遍历nums来更新g数组(设遍历nums位置为j),用二分查找第一个大于nums[j]的位置,若j长度等于g[]数组长度,即x不在数组g中(即x比g中所有元素都大),则直接将x添加到g的末尾,否则将g[j]更新为x=nums[j]的值(贪心思想,为了让后续稍微大的元素都可以插入该数组)

代码实现:

python 复制代码
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        # 思路:贪心+二分查找。即维护一个g[]数组(该数组是递增数组),g[n]以长度为n+1的递增子序列的末尾元素的最小值,然后二分遍历nums来更新g数组(设遍历nums位置为j),用二分查找第一个大于nums[j]的位置,若j长度等于g[]数组长度,即x不在数组g中(即x比g中所有元素都大),则直接将x添加到g的末尾,否则将g[j]更新为x=nums[j]的值(贪心思想,为了让后续稍微大的元素都可以插入该数组)
        g = []
        for x in nums:
            j = bisect_left(g, x)   # bisect()表示二分查找x插入的位置,_left表示第一个大于等于x的位置,_right表示大于x的位置
            if j == len(g):     # 若j长度等于g[]数组长度,即x还未在数组g中,则直接将x添加到g的末尾。
                g.append(x)
            else:
                g[j] = x	# 在g中找到比x大的位置,用贪心思想更新g数组为更小值,以便后续可以插入更多元素
        
        return len(g)

复杂度分析

时间复杂度:O(nlogn),其中 n 为 nums 的长度。

空间复杂度:O(1)。

答疑:

6、N皇后(51题-困难)

题目描述:

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。

解题思路:

n皇后只能在不同行与不同列,问题转换为每一行的皇后应该选择哪一列的排列问题。注意到在(r,c)位置的皇后的对角线:左上右下的r-c都相同,右上左下的r+c值都相同,不能放其他皇后。dfs®表示在递归放置第r行的皇后,用queue记录放置皇后的位置其中queue[i]表示第i行的皇后应该放置在值queue[i]的位置,用on_path记录当前列是否被选过了,用diag记录两侧对角线位置是否放置了皇后,若当前列on_path未被选过且左上与右上位置未放置皇后,则选择该位置放置皇后。递归遍历下一行dfs(i+1)并还原递归现场,避免后续回溯时影响其他递归情况。若第c不满足条件时则继续遍历下一列是否满足。

代码实现:

python 复制代码
class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        # 思路:n皇后只能在不同行与不同列,问题转换为每一行的皇后应该选择哪一列的排列问题。注意到在(r,c)位置的皇后的对角线:左上右下的r-c都相同,右上左下的r+c值都相同,不能放其他皇后。dfs(r)表示在递归放置第r行的皇后,用queue记录放置皇后的位置其中queue[i]表示第i行的皇后应该放置在值queue[i]的位置,用on_path记录当前列是否被选过了,用diag记录两侧对角线位置是否放置了皇后,若当前列on_path未被选过且左上与右上位置未放置皇后,则选择该位置放置皇后。递归遍历下一行dfs(i+1)并还原递归现场,避免后续回溯时影响其他递归情况。若第c不满足条件时则继续遍历下一列是否满足。
        ans = []
        queens = [0]*n
        on_path = [False]*n
        diag1 = [False] * (2*n -1)  # 记录r+c
        diag2 = [False] * (2*n -1)  # 记录r-c

        def dfs(r):		# 放置当前第r行的皇后
            # 递归边界条件,当所有列的皇后已经放置完成,则开始写入答案中。
            if r == n:
                ans.append(['.'*c + 'Q' + '.'*(n-c-1) for c in queens])		# queue[c]表示皇后应该放置在第c列,所以'Q'前边右c个'.',后边有n-c-1个'.'.
                return				# 退出递归

            for c in range(n):				# 对于每次递归(第r行)而言,遍历所有列寻找合适位置进行放置皇后
                if not on_path[c] and not diag1[r+c] and not diag2[r-c]:		# 当前列与对角线都没有皇后,可以放置皇后
                    queens[r] = c
                    on_path[c] = diag1[r+c] = diag2[r-c] = True
                    dfs(r+1)		# 开始递归下一列
                    on_path[c] = diag1[r+c] = diag2[r-c] = False    # 恢复现场,避免后续回溯时影响其他递归情况。
            
        dfs(0)
        return ans

复杂度分析

时间复杂度:O(n^2 * n!)。搜索树中至多有 O(n!) 个叶子,每个叶子生成答案每次需要 O(n^2) 的时间,所以时间复杂度为 O(n^2 * n!)。

空间复杂度:O(n)。返回值的空间不计入。

答疑:

评论区留言。

7、数组中的最长山脉(845题-中等)

题目描述:

把符合下列属性的数组 arr 称为 山脉数组 :

  • arr.length >= 3
  • 存在下标 i(0 < i < arr.length - 1),满足:
    arr[0] < arr[1] < ... < arr[i - 1] < arr[i]
    arr[i] > arr[i + 1] > ... > arr[arr.length - 1]
    给出一个整数数组 arr,返回最长山脉子数组的长度。如果不存在山脉子数组,返回 0 。

示例 1:

输入:arr = [2,1,4,7,3,2,5]

输出:5

解释:最长的山脉子数组是 [1,4,7,3,2],长度为 5。

解题思路:

找山顶,然后从山顶往两端进行拓展。如何判断一个节点是否可以作为山顶?:当arr[i]>arr[l] and arr[i]>arr[r](即山顶元素值大于左右两端值);然后遍历左右两端:当arr[l]>arr[l-1]时l-=1、arr[r]>arr[r+1]时l+=1;随后更新答案ans为最大的ans=max(ans, r-l+1)。

代码实现:

python 复制代码
class Solution:
    def longestMountain(self, arr: List[int]) -> int:
        # 思路:找山顶,然后从山顶往两端进行拓展。如何判断一个节点是否可以作为山顶?:当arr[i]>arr[l] and arr[i]>arr[r](即山顶元素值大于左右两端值);然后遍历左右两端:当arr[l]>arr[l-1]时l-=1、arr[r]>arr[r+1]时l+=1;随后更新答案ans为最大的ans=max(ans, r-l+1)。
        n = len(arr)
        ans = 0
        for i in range(1, n-1):      # 从1开始到n-2结束,遍历所有数判断是否可以作为山顶。
            l = i-1
            r = i+1
            if arr[i] > arr[l] and arr[i] > arr[r]:     # 当arr[i]可以作为山顶时
                while l>0 and arr[l] > arr[l-1]:        # 左边为递减时继续遍历
                    l -= 1
                while r < n-1 and arr[r] > arr[r+1]:     # 右边为递减时继续遍历
                    r += 1

                ans = max(ans, r-l+1)                     # 更新答案为最大山脉
        
        return ans

复杂度分析

时间复杂度 O(n²):最坏情况每个山顶向两侧扩展 O(n)。

空间复杂度 O(1)。

答疑:

请评论区留言。

8、螺旋遍历二维数组(146题-简单)

题目描述:

给定一个二维数组 array,请返回「螺旋遍历」该数组的结果。

螺旋遍历:从左上角开始,按照 向右、向下、向左、向上 的顺序 依次 提取元素,然后再进入内部一层重复相同的步骤,直到提取完所有元素。

示例 1:

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

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

解题思路:

按照右、下、左、上的顺序走,步数变化(m表示行n表示列)为n、m-1、n-1、m-2;第二轮为n-2、m-3、n-3、m-4,所以列的变化是n→n-1、行的变化是m-1→m-2;所以每次转弯时转90°时,令n, m = m-1, n即可实现步长控制。直到答案的长度等于矩阵大小。

代码实现:

python 复制代码
DIR = (0, 1), (1, 0), (0, -1), (-1, 0)          # 分别表示右、下、左、上(第一维dx控制上下, 第二维dy控制左右)
class Solution:
    def spiralArray(self, array: List[List[int]]) -> List[int]:
        # 思路:按照右、下、左、上的顺序走,步数变化(m表示行n表示列)为n、m-1、n-1、m-2;第二轮为n-2、m-3、n-3、m-4;每次转弯时转90°。直到答案的长度等于矩阵大小。
        if not array:
            return []
        ans = []
        m, n = len(array), len(array[0])
        size = m*n
        i, j, di = 0, -1, 0         # 表示初始从(0,-1)开始向右进行移动
        
        while len(ans) < size:      # ans长度未到达size时继续遍历
            dx, dy = DIR[di]        # 根据di方向进行移动
            for numb in range(n):
                i += dx
                j += dy             # 先走一步
                ans.append(array[i][j])
            di = (di + 1)%4          # 转弯90°
            n, m = m-1, n            # 步数变化n → m-1 → n-1 → m-2,下一轮也是如此变化

        return ans

复杂度分析

时间复杂度:O(mn),其中 m 和 n 分别为 array 的行数和列数。

空间复杂度:O(1)。返回值不计入。

答疑:

请评论区留言。

9、两数相加(236题-中等)

题目描述:

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

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

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

解题思路:

用carry当作进位数,若两个数组非空或者carry不为0时,计算当前数组之和。然后取余数作为新的节点,取进数作为carry的新值。直到两个链表都为空或者carry为空。

代码实现:

python 复制代码
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
        # 思路:用carry当作进位数,若两个数组非空或者carry不为0时,计算当前数组之和。然后取余数作为新的节点,取进数作为carry的新值。直到两个链表都为空或者carry为空。
        carry = 0   # 初始化进位数为0
        cur = dummy = ListNode()
        while l1 or l2 or carry != 0:   # 考虑carry不为0为了防止最后一次计算时需要进位,此时进位需要创建新节点。
            if l1:      # 当l1不为空链表时,计算进位数与当前节点之和
                carry += l1.val
                l1 = l1.next
            if l2:		# 当l1不为空链表时,计算进位数与当前节点之和
                carry += l2.val
                l2 = l2.next
            cur.next = ListNode(carry % 10)       # 取当前和的余数作为新节点
            cur = cur.next
            carry //= 10                # 取当前和的进位数作为新的进位数,为下轮更新做准备
        return dummy.next

复杂度分析

时间复杂度:O(n),其中 n 为 l1长度和 l2长度的最大值。

空间复杂度:O(1)。返回值不计入。

答疑:

请评论区留言。

10、无重复字符的最长子串(3题-中等)

题目描述:

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

示例 1:

输入: s = "abcabcbb"

输出: 3

解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。注意 "bca" 和 "cab" 也是正确答案。

解题思路:

滑动窗口求解。用一个字典cnt记录当前当前字符串x的个数,用ans记录无重复字符串的最长长度,右窗口不断滑动,当遇到cnt[x]>=2时说明当前窗口有重复的字符串,左窗口需要右移同时cnt[s[left]] -= 1 ,移动到cnt[x]不满足>=2的条件为止, 然后更新答案为最大值。

代码实现:

python 复制代码
class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        # 滑动窗口求解。用一个字典cnt记录当前当前字符串x的个数,用ans记录无重复字符串的最长长度,右窗口不断滑动,当遇到cnt[x]>=2时说明当前窗口有重复的字符串,左窗口需要右移同时cnt[left] -= 1 ,移动到cnt[x]不满足>=2的条件为止, 然后更新答案为最大值。
        cnt = defaultdict(int)
        left = 0
        ans = 0
        for right, c in enumerate(s):
            cnt[c] += 1
            while cnt[c] >= 2:
                cnt[s[left]] -= 1
                left += 1   
            ans = max(ans, right - left + 1)
        
        return ans

复杂度分析

时间复杂度:O(n),其中 n 为 s 的长度。注意 left 至多增加 n 次,所以整个二重循环至多循环 O(n) 次。

空间复杂度:O(∣Σ∣),其中 ∣Σ∣ 为字符集合的大小,本题中字符均为 ASCII 字符,所以 ∣Σ∣≤128。

答疑:

请评论区留言。

相关推荐
邪神与厨二病2 小时前
Problem L. ZZUPC
c++·数学·算法·前缀和
愚昧之山绝望之谷开悟之坡2 小时前
合格境外投资者
笔记
软件测试媛3 小时前
软件测试常见的面试题(46道)
功能测试·面试·职场和发展
梯度下降中3 小时前
LoRA原理精讲
人工智能·算法·机器学习
IronMurphy3 小时前
【算法三十一】46. 全排列
算法·leetcode·职场和发展
czlczl200209253 小时前
力扣1911. 最大交替子序列和
算法·leetcode·动态规划
靴子学长4 小时前
Decoder only 架构下 - KV cache 的理解
pytorch·深度学习·算法·大模型·kv
寒秋花开曾相惜4 小时前
(学习笔记)3.8 指针运算(3.8.3 嵌套的数组& 3.8.4 定长数组)
java·开发语言·笔记·学习·算法
Гений.大天才4 小时前
2026年计算机领域的年度主题与范式转移
算法