python 单词搜索(回溯-矩阵-字符串-中等)含源码(二十)

问题说明(含示例)

问题描述 :给定一个 m x n 的二维字符网格 board 和一个字符串 word,判断 word 是否存在于网格中。单词需按字母顺序,通过相邻(水平 / 垂直,无对角线)单元格的字母构成,且同一个单元格的字母不允许重复使用。

示例

输入 输出 解释
board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "ABCCED" true 路径为 (0,0)→(0,1)→(0,2)→(1,2)→(2,2)→(2,1),匹配所有字符
board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "SEE" true 路径为 (1,3)→(2,3)→(2,2),匹配所有字符
board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "ABCB" false 无法找到不重复使用单元格且相邻的路径,如第二个 B 无合法相邻单元格

解题关键

核心思路是回溯法 ,通过 "状态标记 - 相邻探索 - 状态恢复" 实现路径搜索,而回溯的底层依赖(Python 解释器的递归调用栈或手动模拟的列表栈)管理状态。关键步骤如下:

  1. 初始过滤 :若网格为空,直接返回 false;遍历网格,仅将与 word[0] 匹配的单元格作为回溯起始点(剪枝优化)。
  2. 方向定义 :用 (0,1)、(0,-1)、(-1,0)、(1,0) 表示右、左、上、下四个相邻方向,确保仅按规则探索。
  3. 栈式回溯
    • 递归实现:依赖 Python 底层递归调用栈自动管理状态(参数、局部变量、返回地址);
    • 迭代实现:用列表模拟栈手动存储状态(单元格坐标、匹配索引、已访问集合)。
  4. 状态管理 :通过修改网格字符(如用 $ 标记已访问)避免重复使用,探索失败后恢复原字符(回溯核心)。

核心逻辑 + 关键细节(含 Python 栈的内部设定)

一、回溯的核心逻辑:"标记 - 探索 - 恢复" 闭环

无论用递归栈还是手动栈,回溯的核心是通过栈记忆 "当前状态",确保探索失败后能回退到上一步,具体闭环如下:

  1. 标记 :将当前单元格标记为已访问(如 board[i][j] = '$'),避免同一路径重复使用;
  2. 探索:按四个方向遍历相邻单元格,若符合 "不越界、未访问、字符匹配",则进入下一层探索;
  3. 恢复 :若所有方向探索失败,恢复当前单元格的原字符(如 board[i][j] = temp),回退到上一步状态。

二、Python 栈的内部设定(两种实现方式)

栈的核心作用是 **"记忆状态 + 控制回退"**,Python 中存在两种栈的使用场景,内部设定差异显著:

1. 递归实现:依赖 Python 解释器的 "递归调用栈"(自动管理)
(1)栈的内部结构:栈帧(Stack Frame)

每次调用递归函数(如 backtrack(i, j, k)),Python 解释器会自动创建一个栈帧,压入递归调用栈。栈帧包含以下关键信息(用户无需手动定义,由解释器维护):

栈帧内容 作用 示例(backtrack(0,0,0)
函数参数 记录当前探索的单元格坐标(i,j)和匹配进度(k i=0, j=0, k=0
局部变量 存储当前单元格的原字符(temp),用于后续恢复 temp = 'A'board[0][0] 的原字符)
返回地址 记录函数执行完 return 后,需回到的上一层代码位置 主函数中 if backtrack(0,0,0): 这一行
(2)栈的操作时机(自动执行)
  • 压栈(Push) :调用递归函数时触发。例如,从 backtrack(0,0,0) 调用 backtrack(0,1,1),会将 (i=0,j=1,k=1, temp='B', 返回地址=backtrack(0,0,0)的for循环) 压入栈;
  • 弹栈(Pop) :函数执行 return 时触发。例如,backtrack(0,1,1) 探索失败返回 false,其栈帧会从栈中弹出,控制权回到上一层的返回地址(继续遍历下一个方向)。
(3)示例:递归栈的工作流程(匹配 "AB")
python 复制代码
1. 主函数调用 backtrack(0,0,0) → 压栈帧1(i=0,j=0,k=0,temp='A');
2. 探索右方向,调用 backtrack(0,1,1) → 压栈帧2(i=0,j=1,k=1,temp='B');
3. k=1 == len(word)-1(word="AB"),返回 true → 弹栈帧2;
4. 栈帧1接收到true,返回主函数 → 弹栈帧1;
5. 主函数返回 true,流程结束。
2. 迭代实现:用 "列表模拟栈"(手动管理)

若递归深度过大(如网格尺寸 1000x1000),可能触发栈溢出,此时需用列表手动模拟栈,内部设定完全由用户控制:

(1)栈的内部结构:自定义状态元组

列表中每个元素是一个状态元组 ,包含探索所需的全部信息(需手动定义),格式为 (i, j, k, visited)

状态字段 作用 示例
i,j 当前单元格坐标 (0,0)
k 当前匹配的 word 索引 0(匹配 word[0]
visited 已访问的单元格集合(避免重复) {(0,0)}
(2)栈的操作时机(手动执行)
  • 压栈(Push) :用 list.append() 实现,将符合条件的新状态加入列表尾部(栈顶);
  • 弹栈(Pop) :用 list.pop() 实现,从列表尾部取出最后压入的状态(栈顶),即 "回退到上一步"。
(3)示例:手动栈的工作流程(匹配 "AB")
python 复制代码
stack = [(0,0,0, {(0,0)})]  # 初始压栈:起始点状态
while stack:
    i,j,k,visited = stack.pop()  # 弹栈:取栈顶状态
    if k == 1:  # 匹配完"AB"
        return True
    for dx,dy in direction:
        new_i,new_j = i+dx,j+dy
        # 符合条件:不越界、未访问、字符匹配
        if 0<=new_i<n and 0<=new_j<m and (new_i,new_j) not in visited and board[new_i][new_j] == word[k+1]:
            new_visited = visited.copy()  # 复制集合,避免状态污染
            new_visited.add((new_i,new_j))
            stack.append((new_i,new_j,k+1,new_visited))  # 压栈新状态
return False

三、关键细节:避坑与优化

  1. 方向数组不可重复 :需确保四个方向不重复(如避免 (0,-1) 出现两次),否则会遗漏方向或重复探索;
  2. 边界判断需精准 :行索引范围是 0<=i<nn 为网格行数),列索引是 0<=j<mm 为网格列数),避免 i>n 这类错误(会导致越界访问);
  3. 状态恢复的必要性 :递归中若不恢复原字符(board[i][j] = temp),会导致后续路径无法复用单元格;迭代中若不复制 visited 集合(直接修改原集合),会导致状态污染;
  4. 剪枝优化 :仅从与 word[0] 匹配的单元格开始探索,减少无效递归 / 压栈操作。

对应代码(两种栈实现方式)

1. 递归版(依赖 Python 底层递归调用栈)

python 复制代码
from typing import List

class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        # 1. 初始过滤与变量定义
        n = len(board)
        if n == 0:
            return False
        m = len(board[0])
        direction = [(0,1), (0,-1), (-1,0), (1,0)]  # 右、左、上、下
        word_len = len(word)
        
        # 2. 回溯函数(依赖Python递归调用栈)
        def backtrack(i: int, j: int, k: int) -> bool:
            # 终止条件1:匹配完所有字符
            if k == word_len:
                return True
            # 终止条件2:越界、字符不匹配
            if i < 0 or i >= n or j < 0 or j >= m or board[i][j] != word[k]:
                return False
            
            # 标记:保存原字符,用$标记已访问(栈帧中存储temp)
            temp = board[i][j]
            board[i][j] = '$'
            
            # 探索四个方向(递归调用,自动压栈)
            for dx, dy in direction:
                new_i, new_j = i + dx, j + dy
                if backtrack(new_i, new_j, k + 1):
                    return True  # 找到有效路径,直接返回(弹栈后向上传递)
            
            # 恢复:回溯,恢复原字符(栈帧弹出前执行)
            board[i][j] = temp
            return False  # 所有方向失败,返回(弹栈)
        
        # 3. 遍历起始点,启动回溯
        for i in range(n):
            for j in range(m):
                if board[i][j] == word[0]:
                    if backtrack(i, j, 0):
                        return True
        return False

2. 迭代版(用列表手动模拟栈)

python 复制代码
from typing import List

class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        # 1. 初始过滤与变量定义
        n = len(board)
        if n == 0:
            return False
        m = len(board[0])
        direction = [(0,1), (0,-1), (-1,0), (1,0)]
        word_len = len(word)
        stack = []
        
        # 2. 初始化栈:压入所有匹配word[0]的起始点状态
        for i in range(n):
            for j in range(m):
                if board[i][j] == word[0]:
                    stack.append((i, j, 0, {(i, j)}))  # 手动压栈:(坐标, 匹配索引, 已访问集合)
        
        # 3. 迭代处理栈(手动弹栈、压栈)
        while stack:
            i, j, k, visited = stack.pop()  # 手动弹栈:取栈顶状态
            
            # 终止条件:匹配完所有字符
            if k == word_len:
                return True
            
            # 探索四个方向,手动压栈新状态
            for dx, dy in direction:
                new_i = i + dx
                new_j = j + dy
                # 检查:不越界、未访问、字符匹配
                if 0 <= new_i < n and 0 <= new_j < m:
                    if (new_i, new_j) not in visited and board[new_i][new_j] == word[k + 1]:
                        # 复制已访问集合,避免状态污染
                        new_visited = visited.copy()
                        new_visited.add((new_i, new_j))
                        # 手动压栈:新状态加入栈顶
                        stack.append((new_i, new_j, k + 1, new_visited))
        
        # 所有状态探索失败
        return False

对应的基础知识

1. Python 递归调用栈的内部机制

  • 自动管理:递归调用栈由 Python 解释器底层维护,用户无需手动操作,仅需编写递归逻辑;
  • 栈帧生命周期:函数调用时创建栈帧(压栈),函数返回时销毁栈帧(弹栈),栈帧中的局部变量仅在当前函数调用中有效;
  • 栈溢出风险 :Python 默认递归深度约为 1000(可通过 sys.setrecursionlimit() 修改,但不推荐),若网格尺寸过大(如 2000x2000),递归深度可能超过限制,触发 RecursionError

2. 列表模拟栈的原理

  • 数据结构匹配 :Python 列表的 append()(尾部添加)和 pop()(尾部删除)操作均为 O(1) 时间复杂度,符合栈 "先进后出(LIFO)" 的特性;
  • 状态独立性 :迭代中需复制 visited 集合(如 new_visited = visited.copy()),因为列表中的状态共享引用,直接修改会导致所有相关状态的 visited 被污染;
  • 灵活性:手动栈可自定义状态字段(如增加 "方向索引" 记录已探索的方向),避免重复探索,而递归栈无法直接控制。

3. 二维网格的索引操作

  • 行数与列数n = len(board) 表示网格行数(外层列表长度),m = len(board[0]) 表示网格列数(内层列表长度),需先判断 board 非空再获取 m
  • 相邻坐标计算 :通过方向数组 (dx, dy) 计算新坐标 new_i = i + dxnew_j = j + dy,避免硬编码(如 i+1j-1)导致的代码冗余。

对应的进阶知识

1. 两种栈实现的效率对比

对比维度 递归调用栈(自动) 列表模拟栈(手动)
代码复杂度 低(逻辑简洁,无需手动管理状态) 高(需手动处理压栈、弹栈、状态复制)
运行效率 较低(函数调用有栈帧创建 / 销毁开销) 较高(无函数调用开销,仅列表操作)
栈溢出风险 高(递归深度受限,大网格易溢出) 低(列表大小仅受内存限制,无深度限制)
适用场景 小规模网格(如 100x100 以内)、代码可读性优先 大规模网格、递归深度超限时

2. 时间与空间复杂度

  • 时间复杂度O(nm×3ᵏ)n= 行数,m= 列数,k=word 长度);每个单元格最多作为起始点一次(O(nm)),每个单元格探索时最多有 3 个有效方向(排除来时的方向),递归 / 迭代深度为 k,故每个起始点的时间为 O(3ᵏ)
  • 空间复杂度
    • 递归版:O(k)(递归栈深度为 k,网格标记无额外空间);
    • 迭代版:O(nm)(最坏情况下栈存储所有单元格状态,visited 集合最大为 nm)。

3. 优化策略:避免重复探索

  • 方向剪枝 :迭代版中,可在状态元组中增加 "方向索引"(如 (i,j,k,visited, dir_idx)),记录已探索的方向,下次弹栈时从 dir_idx+1 开始遍历,避免重复探索同一方向;
  • 原地标记 vs 额外集合 :递归版用 "原地修改网格" 标记已访问(空间 O(1)),迭代版用 "visited 集合"(空间 O(nm)),前者空间更优,但需确保状态恢复正确。

编程思维与启示

1. "栈" 是回溯的核心载体

回溯的本质是 "深度优先搜索 + 状态回退",而栈的 "先进后出" 特性完美匹配这一逻辑 ------ 栈记住 "当前路径的所有状态",回退时只需弹出栈顶状态,即可回到上一步,这是 "为什么回溯离不开栈" 的根本原因。

2. "就地修改" 与 "状态隔离" 的平衡

  • 递归版用 "就地修改网格"($ 标记)实现状态标记,通过 "恢复原字符" 实现状态隔离,空间效率高,但需确保每个修改都有对应的恢复;
  • 迭代版用 "复制 visited 集合" 实现状态隔离,无需修改网格,但空间效率低,需在 "空间" 与 "代码复杂度" 间权衡。

3. 问题拆解的简化思路

将 "单词搜索" 拆解为 "起始点筛选→相邻探索→状态管理" 三个子问题,每个子问题用对应的数据结构解决:

  • 起始点筛选:用网格遍历 + 字符匹配(剪枝);
  • 相邻探索:用方向数组 + 边界判断;
  • 状态管理:用栈(自动 / 手动)实现回溯。这种 "分而治之" 的思维可迁移到 "岛屿数量""路径总和" 等网格类问题。
相关推荐
徐同保4 小时前
Redux和@reduxjs/toolkit同时在Next.js项目中使用
开发语言·前端·javascript
~无忧花开~4 小时前
CSS学习笔记(二):CSS动画核心属性全解析
开发语言·前端·css·笔记·学习·css3·动画
AI 嗯啦4 小时前
深度学习——Python 爬虫原理与实战:从入门到项目实践
爬虫·python·深度学习
想要AC的sjh4 小时前
华为Java专业级科目一通过心得
java·开发语言·华为
浮灯Foden4 小时前
算法-每日一题(DAY18)多数元素
开发语言·数据结构·c++·算法·leetcode·面试
MediaTea5 小时前
Python 第三方库:Word Cloud(词云图生成)
开发语言·python
B站_计算机毕业设计之家5 小时前
python股票交易数据管理系统 金融数据 分析可视化 Django框架 爬虫技术 大数据技术 Hadoop spark(源码)✅
大数据·hadoop·python·金融·spark·股票·推荐算法
小龙报5 小时前
《算法每日一题(1)--- 第31场蓝桥算法挑战赛》
c语言·开发语言·c++·git·算法·学习方法
llz_1125 小时前
五子棋小游戏
开发语言·c++·算法