根据前序遍历和中序遍历序列构建二叉树

本文介绍了如何根据二叉树的前序遍历和中序遍历序列来构建二叉树。解法一采用递归,通过先序遍历获取根节点,并在中序遍历中找到根节点的位置来构建左右子树。解法二使用迭代,借助栈来实现,通过比较先序遍历和中序遍历中的节点匹配关系确定子树的位置。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目描述(中等难度)

根据二叉树的先序遍历和中序遍历还原二叉树。

解法一 递归

先序遍历的顺序是根节点,左子树,右子树。中序遍历的顺序是左子树,根节点,右子树。

所以我们只需要根据先序遍历得到根节点,然后在中序遍历中找到根节点的位置,它的左边就是左子树的节点,右边就是右子树的节点。

生成左子树和右子树就可以递归的进行了。

比如上图的例子,我们来分析一下。

preorder = [3,9,20,15,7]
inorder = [9,3,15,20,7]
首先根据 preorder 找到根节点是 3
然后根据根节点将 inorder 分成左子树和右子树
左子树
inorder [9]
右子树
inorder [15,20,7]
把相应的前序遍历的数组也加进来
左子树
preorder[9] 
inorder [9]
右子树
preorder[20 15 7] 
inorder [15,20,7]
现在我们只需要构造左子树和右子树即可,成功把大问题化成了小问题
然后重复上边的步骤继续划分,直到 preorder 和 inorder 都为空,返回 null 即可

事实上,我们不需要真的把 preorder 和 inorder 切分了,只需要用分别用两个指针指向开头和结束位置即可。注意下边的两个指针指向的数组范围是包括左边界,不包括右边界。

对于下边的树的合成。

左子树

右子树

public TreeNode buildTree(int[] preorder, int[] inorder) {
    return buildTreeHelper(preorder, 0, preorder.length, inorder, 0, inorder.length);
}
private TreeNode buildTreeHelper(int[] preorder, int p_start, int p_end, int[] inorder, int i_start, int i_end) {
    // preorder 为空,直接返回 null
    if (p_start == p_end) {
        return null;
    }
    int root_val = preorder[p_start];
    TreeNode root = new TreeNode(root_val);
    //在中序遍历中找到根节点的位置
    int i_root_index = 0;
    for (int i = i_start; i < i_end; i++) {
        if (root_val == inorder[i]) {
            i_root_index = i;
            break;
        }
    }
    int leftNum = i_root_index - i_start;
    //递归的构造左子树
    root.left = buildTreeHelper(preorder, p_start + 1, p_start + leftNum + 1, inorder, i_start, i_root_index);
    //递归的构造右子树
    root.right = buildTreeHelper(preorder, p_start + leftNum + 1, p_end, inorder, i_root_index + 1, i_end);
    return root;
}

上边的代码很好理解,但存在一个问题,在中序遍历中找到根节点的位置每次都得遍历中序遍历的数组去寻找,参考这里 ,我们可以用一个HashMap把中序遍历数组的每个元素的值和下标存起来,这样寻找根节点的位置就可以直接得到了。

public TreeNode buildTree(int[] preorder, int[] inorder) {
    HashMap<Integer, Integer> map = new HashMap<>();
    for (int i = 0; i < inorder.length; i++) {
        map.put(inorder[i], i);
    }
    return buildTreeHelper(preorder, 0, preorder.length, inorder, 0, inorder.length, map);
}
private TreeNode buildTreeHelper(int[] preorder, int p_start, int p_end, int[] inorder, int i_start, int i_end,
                                 HashMap<Integer, Integer> map) {
    if (p_start == p_end) {
        return null;
    }
    int root_val = preorder[p_start];
    TreeNode root = new TreeNode(root_val);
    int i_root_index = map.get(root_val);
    int leftNum = i_root_index - i_start;
    root.left = buildTreeHelper(preorder, p_start + 1, p_start + leftNum + 1, inorder, i_start, i_root_index, map);
    root.right = buildTreeHelper(preorder, p_start + leftNum + 1, p_end, inorder, i_root_index + 1, i_end, map);
    return root;
}

本以为已经完美了,在 这里-without-map>) 又看到了令人眼前一亮的思路,就是 StefanPochmann 大神,经常逛 Discuss 一定会注意到他,拥有 3 万多的赞。

他也发现了每次都得遍历一次去找中序遍历数组中的根节点的麻烦,但他没有用 HashMap就解决了这个问题,下边来说一下。

pre变量保存当前要构造的树的根节点,从根节点开始递归的构造左子树和右子树,in变量指向当前根节点可用数字的开头,然后对于当前pre有一个停止点stop,从instop表示要构造的树当前的数字范围。

public TreeNode buildTree(int[] preorder, int[] inorder) {
    return buildTreeHelper(preorder,  inorder, (long)Integer.MAX_VALUE + 1);
}
int pre = 0;
int in = 0;
private TreeNode buildTreeHelper(int[] preorder, int[] inorder, long stop) {
    //到达末尾返回 null
    if(pre == preorder.length){
        return null;
    }
    //到达停止点返回 null
    //当前停止点已经用了,in 后移
    if (inorder[in] == stop) {
        in++;
        return null;
    }
    int root_val = preorder[pre++];
    TreeNode root = new TreeNode(root_val);   
    //左子树的停止点是当前的根节点
    root.left = buildTreeHelper(preorder,  inorder, root_val);
    //右子树的停止点是当前树的停止点
    root.right = buildTreeHelper(preorder, inorder, stop);
    return root;
}

代码很简洁,但如果细想起来真的很难理解了。

把他的原话也贴过来吧。

Consider the example again. Instead of finding the 1 in inorder, splitting the arrays into parts and recursing on them, just recurse on the full remaining arrays and stop when you come across the 1 in inorder. That’s what my above solution does. Each recursive call gets told where to stop, and it tells its subcalls where to stop. It gives its own root value as stopper to its left subcall and its parent`s stopper as stopper to its right subcall.

本来很想讲清楚这个算法,但是各种画图,还是太难说清楚了。这里就画几个过程中的图,大家也只能按照上边的代码走一遍,理解一下了。

      3
    /   \
   9     7
  / \
 20  15
前序遍历数组和中序遍历数组
preorder = [ 3, 9, 20, 15, 7 ]
inorder = [ 20, 9, 15, 3, 7 ]   
p 代表 pre,i 代表 in,s 代表 stop
首先构造根节点为 3 的树,可用数字是 i 到 s
s 初始化一个树中所有的数字都不会相等的数,所以代码中用了一个 long 来表示
3, 9, 20, 15, 7 
^  
p
20, 9, 15, 3, 7
^              ^
i              s
考虑根节点为 3 的左子树,                考虑根节点为 3 的树的右子树,
stop 值是当前根节点的值 3                只知道 stop 值是上次的 s
新的根节点是 9,可用数字是 i 到 s 
不包括 s
3, 9, 20, 15, 7                       3, 9, 20, 15, 7                
   ^                                    
   p
20, 9, 15, 3, 7                       20, 9, 15, 3, 7                     
^          ^                                         ^
i          s                                         s
递归出口的情况
3, 9, 20, 15, 7 
       ^  
       p
20, 9, 15, 3, 7
^    
i   
s   
此时 in 和 stop 相等,表明没有可用的数字,所以返回 null,并且表明此时到达了某个树的根节点,所以 i 后移。

总之他的思想就是,不再从中序遍历中寻找根节点的位置,而是直接把值传过去,表明当前子树的结束点。不过总感觉还是没有 get 到他的点,in 和 stop 变量的含义也是我赋予的,对于整个算法也只是勉强说通,大家有好的想法可以和我交流。

解法二 迭代 栈

参考 这里,我们可以利用一个栈,用迭代实现。

假设我们要还原的树是下图

      3
    /   \
   9     7
  / \
 20  15

首先假设我们只有先序遍历的数组,如果还原一颗树,会遇到什么问题。

preorder = [3, 9, 20, 15, 7 ]

首先我们把 3 作为根节点,然后到了 9 ,就出现一个问题,9 是左子树还是右子树呢?

所以需要再加上中序遍历的数组来确定。

inorder = [ 20, 9, 15, 3, 7 ]

我们知道中序遍历,首先遍历左子树,然后是根节点,最后是右子树。这里第一个遍历的是 20 ,说明先序遍历的 9 一定是左子树,利用反证法证明。

假如 9 是右子树,根据先序遍历 preorder = [ 3, 9, 20, 15, 7 ],说明根节点 3 的左子树是空的,

左子树为空,那么中序遍历就会先遍历根节点 3,而此时是 20,假设不成立,说明 9 是左子树。

接下来的 20 同理,所以可以目前构建出来的树如下。

      3
    /   
   9    
  / 
 20

同时,还注意到此时先序遍历的 20 和中序遍历 20 相等了,说明什么呢?

说明中序遍历的下一个数 15 不是左子树了,如果是左子树,那么中序遍历的第一个数就不会是 20

所以 15 一定是右子树了,现在还有个问题,它是 20 的右子树,还是 9 的右子树,还是 3 的右子树?

我们来假设几种情况,来想一下。

  1. 如果是 3 的右子树, 20 和 9 的右子树为空,那么中序遍历就是20 9 3 15

  2. 如果是 9 的右子树,20 的右子树为空,那么中序遍历就是20 9 15

  3. 如果是 20 的右子树,那么中序遍历就是20 15

之前已经遍历的根节点是 3 9 20把它倒过来,即20 9 3,然后和上边的三种中序遍历比较,会发现 15 就是最后一次相等的节点的右子树。

第 1 种情况,中序遍历是20 9 3 15,和20 9 3 都相等,所以 15 是3 的右子树。

第 2 种情况,中序遍历是20 9 15,只有20 9 相等,所以 15 是 9 的右子树。

第 3 种情况,中序遍历就是20 15,只有20 相等,所以 20 是 15 的右子树。

而此时我们的中序遍历数组是inorder = [ 20, 9 ,15, 3, 7 ]20 匹配,9匹配,最后一次匹配是 9,所以 15 是 9的右子树。

     3
    /   
   9    
  / \
 20  15

综上所述,我们用一个栈保存已经遍历过的节点,遍历前序遍历的数组,一直作为当前根节点的左子树,直到当前节点和中序遍历的数组的节点相等了,那么我们正序遍历中序遍历的数组,倒着遍历已经遍历过的根节点(用栈的 pop 实现),找到最后一次相等的位置,把它作为该节点的右子树。

上边的分析就是迭代总体的思想,代码的话还有一些细节注意一下。用一个栈保存已经遍历的节点,用 curRoot 保存当前正在遍历的节点。

public TreeNode buildTree(int[] preorder, int[] inorder) {
    if (preorder.length == 0) {
        return null;
    }
    Stack<TreeNode> roots = new Stack<TreeNode>();
    int pre = 0;
    int in = 0;
    //先序遍历第一个值作为根节点
    TreeNode curRoot = new TreeNode(preorder[pre]);
    TreeNode root = curRoot;
    roots.push(curRoot);
    pre++;
    //遍历前序遍历的数组
    while (pre < preorder.length) {
        //出现了当前节点的值和中序遍历数组的值相等,寻找是谁的右子树
        if (curRoot.val == inorder[in]) {
            //每次进行出栈,实现倒着遍历
            while (!roots.isEmpty() && roots.peek().val == inorder[in]) {
                curRoot = roots.peek();
                roots.pop();
                in++;
            }
            //设为当前的右孩子
            curRoot.right = new TreeNode(preorder[pre]);
            //更新 curRoot
            curRoot = curRoot.right;
            roots.push(curRoot);
            pre++;
        } else {
            //否则的话就一直作为左子树
            curRoot.left = new TreeNode(preorder[pre]);
            curRoot = curRoot.left;
            roots.push(curRoot);
            pre++;
        }
    }
    return root;
}

总结

用常规的递归和 HashMap 做的话这道题是不难的,用 stop 变量省去 HashMap 的思想以及解法二的迭代可以了解一下吧,不是很容易想到。

<think>好的,我现在需要帮助用户解决关于Dify循环实现的问题。首先,我需要明确用户的具体需求。他们提到“dify 循环实现”或“dify 如何处理循环”,但Dify本身可能没有直接称为“循环”的功能,所以需要结合现有文档中的信息来推断可能的解决方案。 根据用户提供的引用内容,Dify工作流中有迭代节点[^1]。这可能类似于编程中的循环结构,用于重复执行某个任务。例如,在处理列表数据或需要多次执行同一操作时,迭代节点可以派上用场。我需要详细解释迭代节点的配置和使用步骤,确保用户能够理解如何设置循环次数或遍历数据集。 另外,用户可能对如何处理循环中的错误或性能问题感兴趣。引用[2]提到在Dify中处理PEM文件的问题,这可能涉及在循环中正确处理数据格式,避免错误积累。需要提醒用户在循环中注意错误处理和资源管理,确保每次迭代的稳定性。 引用[3]中的例子展示了如何结合FastAPI和Selenium进行实时监控并生成GIF,这可能涉及循环截取浏览器内容。虽然Dify本身可能不直接控制这类循环,但可以通过外部脚本或定时任务触发工作流,模拟循环执行。这部分需要说明如何将外部循环机制与Dify结合使用。 接下来,我需要组织回答结构。首先介绍迭代节点的基本概念和配置步骤,然后讨论循环中的数据处理和错误处理,最后补充外部触发循环的方法。同时,生成相关问题以帮助用户进一步探索,比如优化循环性能或处理复杂数据结构。 需要确保回答符合格式要求,使用正确的中文表达,行内公式用$...$,独立公式用$$...$$,但当前问题不涉及数学公式,所以可能不需要使用。同时,引用标识要自然地添加到相关段落末尾,如、[^2]、[^3]。 最后,检查是否有遗漏点,比如Dify的其他循环相关功能或用户可能遇到的常见问题,确保回答全面且易于理解。</think>在Dify中处理循环逻辑主要通过**迭代节点**实现,以下是具体实现方式和应用场景的解析: ### 一、Dify循环实现机制 Dify通过**工作流设计器**中的迭代节点处理循环需求,其核心原理类似编程中的`for循环`。迭代节点可遍历以下数据类型: - 数组列表:`["A","B","C"]` - 字典集合:`{"key1":"value1", "key2":"value2"}` - 数值范围:通过`range()`函数生成序列 配置示例: ```python # 模拟迭代节点的数据输入 input_data = { "dataset": [1,2,3,4,5], "process_logic": "item * 2" # 对每个元素执行乘以2的操作 } ``` ### 二、迭代节点的关键配置步骤 1. **数据源绑定**:将数组/字典类型变量连接到迭代节点的输入端口 2. **循环变量命名**:设定当前元素的变量名(默认为`item`) 3. **子流程设计**:在迭代节点内部构建需要重复执行的逻辑模块 4. **结果聚合**:通过`outputs`收集所有迭代结果,支持数组或对象格式 $$ \text{总耗时} = \sum_{i=1}^{n}(单次迭代时间_i) + 系统开销 $$ ### 三、循环中的特殊处理 1. **错误中断控制**: - 启用`continueOnError`参数可跳过失败迭代 - 通过`try-catch`模块包裹敏感操作 2. **并行优化**: ```python # 伪代码示例 Parallel.forEach(dataset, lambda item: process(item)) ``` 3. **结果过滤**: ```python filtered = filter(lambda x: x%2==0, processed_results) ``` ### 四、应用场景案例 1. **批量文件处理**:遍历存储桶中的文件列表进行格式转换 2. **数据清洗**:对数据库查询结果集进行逐条校验 3. **API轮询**:定时循环调用第三方接口直到满足特定条件
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值