8种算法题目类型的【三问三答结构化思考】

可上 欧弟OJ系统 练习华子OD、大厂真题
绿色聊天软件戳 od1441了解算法冲刺训练(备注【CSDN】否则不通过)

大家好,我是闭老师。

在 LeetCode 上刷题时,很多题目看似复杂,但实际上,许多问题可以归纳为几种常见的算法模式。

如果你掌握了这些模式,就能高效地解决大量问题

以下是 8 个常见的模式和它们在 LeetCode 中的应用案例,学会这些模式,你就能轻松应对 80% 的 LeetCode 问题。

1、滑动窗口:优化子数组和子字符串问题

滑动窗口是一种常用的技巧,特别适合解决子数组和子字符串相关的问题。滑动窗口的核心思想是在一个可变大小的窗口中维护一些信息,并通过窗口的移动来缩小问题的范围。

一般来说,可以通过三问三答的形式进行思考:

1、对于每一个右指针 right 所指的元素 ch ,做什么操作?

2、什么时候要令左指针 left 右移?left对应的元素做什么操作?【循环不变量】

3、什么时候进行ans的更新?

这三个问题,本质上对应了滑窗问题需要考虑的三个基本要素:

1、right 指针以及所对应的元素 ch

2、left 指针以及所对应的元素 left_ch

3、ans 答案变量

以 LeetCode 3. 无重复字符的最长子串 为例,题目给定一个字符串,请你找出其中不含有重复字符的最长子串的长度。

class Solution:
		def lengthOfLongestSubstring(self,s: str-> int:
				ans = 0
				hash_set = set()
				left = 0

				for right, ch in enumerate(s) :
						# Q1:对于每一个右指针 right 所指的元素ch,做什么操作?
						# A1:若 ch 不在哈希集合中,将ch 加入哈希集合
						if ch not in hash_set:
								hash_set.add(ch)
								#Q3:什么时候进行ans的更新?
								# A3:若 ch 不在哈希集合中,ans 更新
								ans = max(ans,right-left+1else:
								# Q2:什么时候要令左指针 left 右移?
								# left对应的元素做什么操作?【循环不变量】
								# A2: ch 在哈希集合中,left右移直到 ch 不在哈希集合中
								while(ch in hash_set):
										left_ch = s[left]
										hash_set.remove(left_ch)
										left += 1
								# A1:若 ch 在哈希集合中,left 右移完毕后,将ch 加入哈希集
								hash_set.add(ch)
					return ans

2、二分查找:寻找分割点

二分查找是一种高效的查找方法,特别适合用于有序数组或具有一定规律的数据结构中。二分查找的核心思想是每次将搜索区间缩小一半,以此快速逼近答案。

一般来说,可以通过三问三答的形式进行思考:

  1. 对于每个中间点 mid,我们应该如何处理?
  2. 如何调整左右指针 left 和 right 以缩小搜索范围?
  3. 在什么条件下可以直接返回答案?

这三个问题,本质上对应了二分查找问题需要考虑的三个基本要素:

  1. mid 指针以及所对应的元素 mid_val
  2. 左右指针 left 和 right 及其变化
  3. 返回答案的条件

以 LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置 为例,题目要求在一个升序排列的数组中,找到一个目标值出现的起始和结束位置。

class Solution:
    def searchRange(self, nums: List[int], target: int) -> List[int]:
        def findFirst(nums, target):
            left, right = 0, len(nums) - 1
            while left <= right:
                mid = left + (right - left) // 2
                # Q1:对于每个中间点 mid,我们应该如何处理?
                # A1:如果 mid_val 大于等于目标值,右边界收缩
                if nums[mid] >= target:
                    right = mid - 1
                else:
                    left = mid + 1
            # Q3:返回条件
            # A3:返回左指针,代表第一个满足条件的位置
            return left
        
        def findLast(nums, target):
            left, right = 0, len(nums) - 1
            while left <= right:
                mid = left + (right - left) // 2
                # A1:如果 mid_val 小于等于目标值,左边界收缩
                if nums[mid] <= target:
                    left = mid + 1
                else:
                    right = mid - 1
            # A3:返回右指针,代表最后一个满足条件的位置
            return right
        
        # 找第一个出现位置
        first = findFirst(nums, target)
        # 如果第一个位置超出范围或不等于目标值,说明目标值不存在
        if first >= len(nums) or nums[first] != target:
            return [-1, -1]
        # 找最后一个出现位置
        last = findLast(nums, target)
        return [first, last]

3、堆:寻找 top-k 元素

堆是一种适合解决 top-k 问题的数据结构。通过维护一个 k 大小的堆,可以高效地找到数组中的前 k 个最大或最小元素。

一般来说,可以通过三问三答的形式进行思考:

  1. 如何初始化堆?
  2. 如何处理新的元素以维护堆的性质?
  3. 如何从堆中提取最终答案?

这三个问题,本质上对应了堆问题需要考虑的三个基本要素:

  1. 堆的初始化
  2. 堆的维护操作
  3. 结果的提取

以 LeetCode 215. 数组中的第 K 个最大元素 为例,题目要求找到数组中第 k 个最大的元素。

import heapq

class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        # Q1:如何初始化堆?
        # A1:构建一个最小堆,初始时包含前 k 个元素
        heap = nums[:k]
        heapq.heapify(heap)

        # Q2:如何处理新的元素以维护堆的性质?
        # A2:对于剩余元素,如果大于堆顶,则替换堆顶并重新调整堆
        for num in nums[k:]:
            if num > heap[0]:
                heapq.heapreplace(heap, num)
        
        # Q3:如何从堆中提取最终答案?
        # A3:堆顶即为第 k 大的元素
        return heap[0]

4、深度优先搜索(DFS):遍历图和树的基础

深度优先搜索(DFS)是一种遍历或搜索图、树结构的算法,常用于解决全路径、连通性等问题。

一般来说,可以通过三问三答的形式进行思考:

  1. 如何处理当前节点?
  2. 如何递归到子节点?
  3. 如何处理递归返回后的结果?

这三个问题,本质上对应了 DFS 问题需要考虑的三个基本要素:

  1. 当前节点的处理
  2. 递归调用的处理
  3. 递归返回后的结果处理

以 LeetCode 104. 二叉树的最大深度 为例,题目要求计算二叉树的最大深度。

class Solution:
    def maxDepth(self, root: TreeNode) -> int:
        # Q1:如何处理当前节点?
        # A1:如果节点为空,直接返回 0
        if not root:
            return 0
        
        # Q2:如何递归到子节点?
        # A2:分别递归计算左子树和右子树的深度
        left_depth = self.maxDepth(root.left)
        right_depth = self.maxDepth(root.right)
        
        # Q3:如何处理递归返回后的结果?
        # A3:返回左子树和右子树深度的较大者,再加 1 表示当前层级
        return max(left_depth, right_depth) + 1

5、拓扑排序:任务调度与依赖

拓扑排序用于解决具有依赖关系的任务排序问题,通常在有向无环图(DAG)中应用。

一般来说,可以通过三问三答的形式进行思考:

  1. 如何构建图和入度数组?
  2. 如何处理入度为 0 的节点?
  3. 如何更新其他节点的入度?

这三个问题,本质上对应了拓扑排序问题需要考虑的三个基本要素:

  1. 图的构建
  2. 入度为 0 的节点处理
  3. 入度更新

以 LeetCode 207. 课程表 为例,题目要求判断是否可能完成所有课程。

from collections import deque, defaultdict

class Solution:
    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        # Q1:如何构建图和入度数组?
        # A1:构建邻接表和入度数组
        graph = defaultdict(list)
        in_degree = [0] * numCourses
        
        for dest, src in prerequisites:
            graph[src].append(dest)
            in_degree[dest] += 1
        
        # Q2:如何处理入度为 0 的节点?
        # A2:将所有入度为 0 的节点入队列,并开始遍历
        queue = deque([i for i in range(numCourses) if in_degree[i] == 0])
        completed_courses = 0
        
        while queue:
            course = queue.popleft()
            completed_courses += 1
            
            # Q3:如何更新其他节点的入度?
            # A3:减少邻接节点的入度,如果入度变为 0,加入队列
            for neighbor in graph[course]:
                in_degree[neighbor] -= 1
                if in_degree[neighbor] == 0:
                    queue.append(neighbor)
        
        return completed_courses == numCourses

6、广度优先搜索(BFS):层级遍历与最短路径

广度优先搜索(BFS)是一种层级遍历算法,特别适合用于搜索最短路径或进行层级遍历的问题。

一般来说,可以通过三问三答的形式进行思考:

  1. 如何处理当前层的节点?
  2. 如何扩展到下一层的节点?
  3. 如何确定搜索是否完成?

这三个问题,本质上对应了 BFS 问题需要考虑的三个基本要素:

  1. 当前层节点的处理
  2. 扩展到下一层的处理
  3. 搜索完成的条件

以 LeetCode 102. 二叉树的层序遍历 为例,题目要求返回二叉树的层序遍历结果。

from collections import deque

class Solution:
    def levelOrder(self, root: TreeNode) -> List[List[int]]:
        if not root:
            return []
        
        # Q1:如何处理当前层的节点?
        # A1:使用队列存储当前层的所有节点
        queue = deque([root])
        result = []
        
        while queue:
            level_size = len(queue)
            level = []
            
            # Q2:如何扩展到下一层的节点?
            # A2:遍历当前层所有节点,将其子节点加入队列
            for _ in range(level_size):
                node = queue.popleft()
                level.append(node.val)
                
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            
            # Q3:搜索完成的条件
            # A3:将当前层结果加入最终结果集
            result.append(level)
        
        return result

7、双指针:高效遍历与查找

双指针技巧适用于数组和字符串的遍历与查找问题,尤其是在需要高效处理对称性或前后关系时。

一般来说,可以通过三问三答的形式进行思考:

  1. 如何初始化两个指针?
  2. 如何移动指针以满足条件?
  3. 如何更新结果或停止移动?

这三个问题,本质上对应了双指针问题需要考虑的三个基本要素:

  1. 指针的初始化
  2. 指针的移动逻辑
  3. 结果的更新或停止条件

以 LeetCode 167. 两数之和 II - 输入有序数组 为例,题目要求在一个已升序排列的数组中,找到两个数使得它们的和等于目标值。

class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        # Q1:如何初始化两个指针?
        # A1:初始化左指针指向开头,右指针指向结尾
        left, right = 0, len(numbers) - 1
        
        while left < right:
            # Q2:如何移动指针以满足条件?
            # A2:根据当前和与目标值的关系,调整指针位置
            current_sum = numbers[left] + numbers[right]
            if current_sum == target:
                # Q3:如何更新结果或停止移动?
                # A3:找到目标值,返回结果
                return [left + 1, right + 1]
            elif current_sum < target:
                left += 1
            else:
                right -= 1
        
        return []

8、子集与组合:子集生成与组合求解

子集与组合问题涉及生成所有子集、组合或排列,通常使用回溯法进行求解。

一般来说,可以通过三问三答的形式进行思考:

  1. 如何选择当前元素加入子集?
  2. 如何递归生成子集或组合?
  3. 如何处理递归返回后的结果?

这三个问题,本质上对应了子集与组合问题需要考虑的三个基本要素:

  1. 当前元素的选择
  2. 递归生成的处理
  3. 递归返回的结果处理

以 LeetCode 78. 子集 为例,题目要求返回一个数组的所有可能子集。

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        result = []
        
        def backtrack(start, path):
            # Q3:如何处理递归返回后的结果?
            # A3:将当前路径添加到结果集中
            result.append(path[:])
            
            for i in range(start, len(nums)):
                # Q1:如何选择当前元素加入子集?
                # A1:将当前元素添加到路径中
                path.append(nums[i])
                # Q2:如何递归生成子集或组合?
                # A2:递归处理下一个元素
                backtrack(i + 1, path)
                path.pop()
        
        backtrack(0, [])
        return result

万变不离其宗,通过这样的结构化思考模式,你可以更清晰地理解和掌握不同算法模式的应用场景,进而提高解题效率和准确性。


华为OD算法/大厂面试高频题算法练习冲刺训练

  • 华为OD算法/大厂面试高频题算法冲刺训练目前开始常态化报名!目前已服务300+同学成功上岸!

  • 课程讲师为全网50w+粉丝编程博主@吴师兄学算法 以及小红书头部编程博主@闭着眼睛学数理化

  • 每期人数维持在20人内,保证能够最大限度地满足到每一个同学的需求,达到和1v1同样的学习效果!

  • 60+天陪伴式学习,40+直播课时,300+动画图解视频,300+LeetCode经典题,200+华为OD真题/大厂真题,还有简历修改、模拟面试、专属HR对接将为你解锁

  • 可上全网独家的欧弟OJ系统练习华子OD、大厂真题

  • 可查看链接 大厂真题汇总 & OD真题汇总(持续更新)

  • 绿色聊天软件戳 od1336了解更多

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

闭着眼睛学算法

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值