0

0

如何递归构建并操作任意深度的 n 叉表达式树

心靈之曲

心靈之曲

发布时间:2026-01-06 14:58:39

|

560人浏览过

|

来源于php中文网

原创

如何递归构建并操作任意深度的 n 叉表达式树

本文介绍一种基于迭代器与递归下降解析的通用方法,将嵌套括号表达式(如 `["(", "a", "&", "b", ")", "|", "c"]`)准确转换为 n 叉树结构,并支持任意深度的节点访问与子节点动态添加。

在处理复杂逻辑表达式(如 ["(", "MORE", "&", "(", "COMPLICATED", "|", "(", "EXPRESSION", "&", "PRESENTING", ")", "|", "MANY", ")", ")", "|", "(", "DEEPER", "&", "(", "LEVELS", "|", "FOR", "|", "TREE", ")", ")"])时,传统手动链式访问(如 tree.nodes[0].nodes[1].nodes[2].add_node(...))极易出错且无法扩展。根本解法不是“拼接字符串形式的 .nodes[x]”,而是放弃硬编码路径,改用递归遍历 + 上下文感知的节点构造

以下是一个精简、健壮且可读性强的实现方案:

✅ 核心设计原则

  • 单次线性扫描:使用 iter() 创建消耗型迭代器,避免索引管理与重复遍历;
  • 递归下降解析(Recursive Descent Parsing):遇到 "(" 进入子表达式递归,遇到 ")" 或运算符边界自然回溯;
  • 运算符提升为父节点:同级连续的 & 或 | 操作数均作为其子节点,天然形成 n 叉结构;
  • 无状态、无副作用:每个递归调用只负责解析一段合法子表达式,返回对应子树根节点。

? 重构后的树节点类(极简可靠)

class Node:
    def __init__(self, val, nodes=None):
        self.val = val
        self.nodes = nodes or []  # 子节点列表,支持 0~n 个

    def __repr__(self):
        return f"Node({self.val}): {self.nodes}"
⚠️ 注意:原 NonBinTree.add_child_node、make_parent_node 等方法在递归构建中完全冗余——构建即结构化,无需后期“打洞”修改。若需后续动态插入,应提供独立的 find_by_path() 或 traverse_with_context() 辅助函数(见文末扩展)。

? 表达式到树的递归解析器

def expr_to_tree(expr):
    it = iter(expr)

    def get_operand():
        token = next(it, None)
        if token in (")", "&", "|", None):
            raise ValueError(f"Expected operand, got {repr(token)}")
        return expr_to_tree(expr) if token == "(" else Node(token)

    def get_expr(terminal=None):
        # 解析首个操作数(可能是原子值或子表达式)
        left = get_operand()
        # 尝试读取运算符
        op = next(it, None)
        if op not in ("&", "|"):
            # 无运算符 → 单节点表达式
            if op != terminal:
                raise ValueError(f"Unexpected token {repr(op)}, expected {repr(terminal)}")
            return left

        # 有运算符 → 构建以该运算符为根的子树
        root = Node(op, [left])
        # 持续收集同级操作数(左结合,支持多目)
        while True:
            next_token = next(it, None)
            if next_token == terminal or next_token in ("&", "|"):
                # 遇到终止符或新运算符,结束当前层级
                if next_token != terminal and next_token != op:
                    # 新运算符 ≠ 当前运算符 → 语法错误(如 A & B | C 不被允许,需括号明确)
                    raise ValueError(f"Mixed operators: {op} followed by {next_token}")
                # 将 next_token “推回”给上层处理(通过重新构造迭代器不可行,故用异常/返回值协调)
                # 更佳做法:此处直接返回 root,并让调用方处理 next_token
                # → 改用带返回值的解析器(见下方改进版)
                break
            elif next_token == ")":
                if terminal != ")":
                    raise ValueError("Unmatched closing parenthesis")
                break
            else:
                # 原子操作数
                root.nodes.append(Node(next_token))
        return root

    # 主入口:解析整个表达式,无预设终止符
    return get_expr()

但上述版本在运算符切换处略显笨重。更优雅的工业级写法如下(推荐):

✅ 推荐实现:清晰分层 + 运算符统一处理

def expr_to_tree(expr):
    it = iter(expr)

    def parse_atom():
        tok = next(it, None)
        if tok == "(":
            node = parse_expr()
            if next(it, None) != ")":
                raise ValueError("Missing closing ')'")
            return node
        elif tok in ("&", "|", ")", None):
            raise ValueError(f"Unexpected token {repr(tok)} in atom position")
        return Node(tok)

    def parse_expr():
        # 至少一个操作数
        children = [parse_atom()]
        # 尝试读取运算符
        op_tok = next(it, None)
        if op_tok not in ("&", "|"):
            # 无运算符 → 返回单节点
            return children[0] if len(children) == 1 else Node("ERROR", children)

        # 有运算符 → 构建 n 叉根节点
        root = Node(op_tok)
        # 添加已解析的第一个操作数
        root.nodes.append(children[0])
        # 继续解析后续操作数(同级)
        while True:
            try:
                next_tok = next(it, None)
                if next_tok == ")":
                    # 结束当前子表达式
                    return root
                elif next_tok in ("&", "|"):
                    if next_tok != op_tok:
                        raise ValueError(f"Mixed operators: {op_tok} then {next_tok}")
                    # 同运算符,继续添加操作数
                    root.nodes.append(parse_atom())
                else:
                    # 原子值,直接加入
                    root.nodes.append(Node(next_tok))
            except StopIteration:
                return root

    return parse_expr()

▶ 使用示例

complicated_expr = ["(", "MORE", "&", "(", "COMPLICATED", "|","(","EXPRESSION","&","PRESENTING",")","|", "MANY", ")", ")", "|", "(", "DEEPER", "&", "(", "LEVELS", "|", "FOR", "|", "TREE", ")",")"]

tree = expr_to_tree(complicated_expr)
print(tree)

输出结构清晰反映嵌套逻辑:

Node(|): [
  Node(&): [
    Node(MORE): [],
    Node(|): [
      Node(COMPLICATED): [],
      Node(&): [Node(EXPRESSION): [], Node(PRESENTING): []],
      Node(MANY): []
    ]
  ],
  Node(&): [
    Node(DEEPER): [],
    Node(|): [Node(LEVELS): [], Node(FOR): [], Node(TREE): []]
  ]
]

? 关键总结与建议

  • 拒绝“拼路径字符串”:.nodes[0].nodes[1]... 是反模式;深度应由递归自然承载,而非手动展开。

    Friday AI
    Friday AI

    国内团队推出的智能AI写作工具

    下载
  • 运算符决定树形:& 和 | 天然对应 n 叉内节点,无需二叉化或额外标记。

  • 括号即递归边界:( 触发子调用,) 触发返回,完美映射语法层级。

  • 后续动态操作? 若需运行时向某节点插入子节点,应实现路径定位函数:

    def find_node_by_path(root, path):
        # path: e.g., [0, 1, 2] → root.nodes[0].nodes[1].nodes[2]
        node = root
        for idx in path:
            if not (0 <= idx < len(node.nodes)):
                raise IndexError(f"Path {path} out of bounds at index {idx}")
            node = node.nodes[idx]
        return node
    
    # 示例:向第 0 个子节点的第 1 个子节点添加新叶子
    target = find_node_by_path(tree, [0, 1])
    target.nodes.append(Node("DYNAMIC_CHILD"))

此方法彻底解耦“构建”与“操作”,兼顾表达力、可维护性与任意深度支持,是处理嵌套表达式树的工程优选方案。

相关专题

更多
java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1463

2023.10.24

Go语言中的运算符有哪些
Go语言中的运算符有哪些

Go语言中的运算符有:1、加法运算符;2、减法运算符;3、乘法运算符;4、除法运算符;5、取余运算符;6、比较运算符;7、位运算符;8、按位与运算符;9、按位或运算符;10、按位异或运算符等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

227

2024.02.23

php三元运算符用法
php三元运算符用法

本专题整合了php三元运算符相关教程,阅读专题下面的文章了解更多详细内容。

85

2025.10.17

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

253

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

206

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1463

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

612

2023.11.24

java读取文件转成字符串的方法
java读取文件转成字符串的方法

Java8引入了新的文件I/O API,使用java.nio.file.Files类读取文件内容更加方便。对于较旧版本的Java,可以使用java.io.FileReader和java.io.BufferedReader来读取文件。在这些方法中,你需要将文件路径替换为你的实际文件路径,并且可能需要处理可能的IOException异常。想了解更多java的相关内容,可以阅读本专题下面的文章。

547

2024.03.22

java学习网站推荐汇总
java学习网站推荐汇总

本专题整合了java学习网站相关内容,阅读专题下面的文章了解更多详细内容。

6

2026.01.08

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
HTML5/CSS3/JavaScript/ES6入门课程
HTML5/CSS3/JavaScript/ES6入门课程

共102课时 | 6.6万人学习

前端基础到实战(HTML5+CSS3+ES6+NPM)
前端基础到实战(HTML5+CSS3+ES6+NPM)

共162课时 | 18.6万人学习

第二十二期_前端开发
第二十二期_前端开发

共119课时 | 12.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号