Binary Tree and Recursion Ⅰ

本文介绍了二叉树的基本概念及特点,详细解释了满二叉树与完全二叉树的区别,并通过Java代码实现了二叉树节点。此外,还探讨了一道经典的二叉树算法题——寻找所有路径和等于给定数值的路径。

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

Binary Tree and Recursion Ⅰ

这篇帖子简单讨论下二叉树这种数据结构以及一道二叉树算法题。
首先复习下二叉树的基本知识

  • 二叉树特点
  • 1、每个结点最多有两颗子树,结点的度最大为 2
  • 2、左子树和右子树是有顺序的,次序不能颠倒
  • 3、即使某结点只有一个子树,也要区分左右子树。
满二叉树和完全二叉树的定义

在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且叶子结点都在同一层上,这样的二叉树称作满二叉树。一棵深度为k且由2k-1个结点的二叉树称为满二叉树。
如果一棵具有n个结点的二叉树的结构与满二叉树的前n个结点的结构相同,这样的二叉树称作完全二叉树。
满二叉树和完全二叉树

那么关于二叉树在java的是如何实现的呢?其实只用定义一个TreeNode类即可,具体参考下面代码:

  Definition of TreeNode:
  public class TreeNode {
      public int val;
      public TreeNode left, right;
      public TreeNode(int val) {
          this.val = val;
          this.left = this.right = null;
      }
  }

有了上面基础后来看二叉树的题目:
Binary Tree Path Sum
Given a binary tree, find all paths that sum of the nodes in the path equals to a given number target.

A valid path is from root node to any of the leaf nodes.

这里写图片描述

那么根据题意我们必须遍历二叉树,因为每个node的值我们必须看一遍才能够知道是否和累加等于目标值。关于二叉树的遍历的主要方式有先序遍历,中序遍历,后序遍历,也就是根左右,左根右,左右根的不同顺序。具体代码如下:

public List<List<Integer>> binaryTreePathSum(TreeNode root, int target) {
        // write your code here
        ArrayList<List<Integer>> res = new ArrayList();
        ArrayList<Integer> path = new ArrayList<Integer>();

        if(root == null){
            return res; 
        }

        path.add(root.val);
        helper(root, path, root.val, target, res);
        return res;
    }

    public void helper(TreeNode node, 
                       ArrayList<Integer> path,
                       int sum,
                       int target,
                       ArrayList<List<Integer>> res){

        if(node.left == null && node.right == null){
            if(sum == target){
                res.add(new ArrayList<Integer>(path));
            }
            return;
        }

        if(node.left != null){
            path.add(node.left.val);
            helper(node.left, path, sum + node.left.val, target, res);
            path.remove(path.size() - 1);
        }

        if(node.right != null){
            path.add(node.right.val);
            helper(node.right, path, sum + node.right.val, target, res);
            path.remove(path.size() - 1);
        } 
    }

注意点:此题首先要注意的就是返回值类型,在根节点不为null的时候,现将根节点的值放到path中,然后代用helper函数。在helper函数中,分三种情况,第一种是某节点的左节点和右节点都为null,则说明此时已到最后一层,那么累加和和目标值比较,相同把路径加入到res中,注意这边new了一个list,因为list是引用类型,后续对path的操作会影响res。第二和第三种情况下就递归调用自己,注意remove掉最后一位即可,不然后续的结果会返回全路径。

<think>好的,我现在需要解决用户的问题:如何在不使用栈或递归的情况下,以线性时间完成二叉树的欧拉巡游遍历。首先,我得回顾一下欧拉巡游的基本概念。欧拉巡游是一种遍历二叉树的方法,它会访问每个节点三次:第一次进入节点时,第二次从左子树返回时,第三次从右子树返回时。这种遍历方式可以用来生成树的某些特定序列,比如前序、中序和后序遍历的组合。 用户要求不使用栈或递归,并且在线性时间内完成。通常,递归或栈是实现树遍历的常规方法,但这里需要替代方案。已知的线性时间遍历方法中,Morris遍历算法可以在不使用栈或递归的情况下实现中序遍历,这或许可以借鉴。Morris遍历利用线索二叉树的概念,通过修改树的指针来临时建立前驱节点的链接,从而允许回溯而无需额外存储。 那么,是否可以将类似的思路应用到欧拉巡游上?欧拉巡游需要三次访问节点,而Morris遍历通常处理单次或两次访问。需要设计一种方法,在每个节点上插入线索,以指示何时应该转向左子树或右子树,并在访问后恢复树的结构,确保线性时间。 考虑每个节点的结构。在二叉树中,每个节点有左右两个指针。在欧拉巡游中,每个节点被访问三次,所以需要跟踪当前处于哪个访问阶段。例如,第一次访问时处理前序,第二次处理中序,第三次处理后序。如何在不使用栈的情况下记录这些状态? 或许可以为每个节点添加标记,但这会占用额外空间。用户可能希望保持原树结构不变,因此需要利用已有的指针来存储信息。线索二叉树的做法是,将空指针改为指向遍历中的前驱或后继节点。在Morris遍历中,找到当前节点的前驱,并将前驱的右指针指向当前节点,从而允许回溯。 对于三次访问的情况,可能需要更复杂的线索设置。例如,当第一次访问节点时,处理前序部分,然后建立线索以便返回时处理中序。类似地,处理右子树前建立线索。但三次访问可能需要多个线索,或者多次修改指针,这可能增加时间复杂度。 另一个思路是利用父指针。如果树节点包含指向父节点的指针,那么可以在遍历时通过父指针回溯,而不需要栈。但用户的问题中没有提到树是否带有父指针,通常默认的二叉树节点结构可能不包含父指针,所以这可能不符合要求。 还有一种方法是使用Threaded Binary Tree的扩展,允许每个节点有多个线索,指示不同的访问阶段。例如,在第一次访问后,设置线索指向下一个应该访问的节点,第二次访问后调整线索,等等。这可能在线性时间内完成,但实现起来较为复杂。 或者,可以利用每个节点的空指针来存储临时信息。例如,当遍历左子树时,将当前节点的右指针暂时指向父节点,以便在返回时能够找到路径。这种方法需要在遍历过程中动态修改指针,并在完成后恢复原状,类似于Morris遍历的做法。 具体步骤可能如下: 1. 初始化当前节点为根节点。 2. 当当前节点不为空时: a. 如果当前节点的左子节点为空,处理前序访问,转向右子节点。 b. 否则,找到左子树的最右节点(前驱)。 c. 如果前驱的右指针为空,将其指向当前节点,处理前序访问,然后当前节点移至左子节点。 d. 如果前驱的右指针指向当前节点,处理中序访问,恢复前驱的右指针为空,然后处理右子节点。 但这只覆盖了前序和中序的情况,欧拉巡游需要三次访问。可能需要扩展这个算法,使得在处理右子树之前再次访问当前节点,然后处理右子树,并在最后处理后序访问。例如,当第二次返回到当前节点时,处理中序访问,然后处理右子树,最后第三次访问处理后续。 但如何区分这三次访问呢?Morris遍历通常处理两次访问(前驱建立和恢复时),而这里需要三次。可能需要更细致地调整指针,记录访问状态。例如,在第一次访问时处理前序,第二次访问时处理中序,第三次访问时处理后序。为了实现这一点,可能需要多次设置和重置线索,同时确保每个节点被正确访问三次。 另一个挑战是,如何在遍历过程中不遗漏任何访问阶段。例如,当通过线索返回时,需要知道当前是处于第一次、第二次还是第三次访问。可能需要利用节点的某些标志位,但用户可能不允许额外空间,因此需要利用指针中的某些位或者通过指针的值来判断。 或者,可以分阶段处理:首先进行前序遍历,然后中序,最后后序,但这样会导致三次遍历,时间复杂度为O(n) * 3,这不符合线性时间的要求。 综上,可能需要在Morris遍历的基础上进行扩展,允许每个节点被访问三次。具体来说,当第一次访问节点时处理前序部分,并设置线索;当通过线索返回时处理中序部分,并再次设置线索以便处理右子树后的第三次访问。最后,当第三次访问时处理后序部分,并恢复指针。 例如,步骤可能如下: 1. 当前节点为根。 2. 当前节点不为空时: a. 处理前序访问(第一次访问)。 b. 如果左子节点存在: i. 找到左子树的最右节点(前驱)。 ii. 如果前驱的右指针为空,设置其指向当前节点,当前节点移至左子。 iii. 否则(前驱的右指针指向当前节点),恢复前驱的右指针为空,处理中序访问(第二次访问),当前节点移至右子。 c. 否则: i. 处理中序访问(第二次访问)。 ii. 处理后序访问(第三次访问)。 iii. 当前节点移至右子。 3. 当无法继续时,处理后序访问(第三次访问)并回溯。 这可能需要更复杂的逻辑,确保每次访问都被正确处理,并且指针被正确恢复。此外,可能需要处理右子树的线索,类似左子树的方式,以覆盖所有三次访问。 不过,这样的算法实现起来非常复杂,且容易出错。需要验证是否每个节点都被正确访问三次,并且所有指针在遍历结束后恢复原状。此外,时间复杂度是否严格保持O(n)是关键。每个节点的前驱查找操作的总时间应为O(n),因为在Morris遍历中,每个边被访问常数次。 可能存在的困难包括处理第三次访问时的回溯路径,以及确保在右子树处理完成后能够正确返回到父节点并进行后续访问。这可能需要额外的线索或状态标记,但用户要求不使用额外空间,因此必须仅利用树的结构。 最终,可能确实存在这样的算法,结合线索二叉树和状态跟踪,通过修改指针来记录访问阶段,从而实现三次访问的欧拉巡游,同时保持线性时间和常数空间。这可能需要将每个节点的左和右指针在遍历过程中临时用于线索,以指示不同的访问阶段,并在访问后恢复指针。 例如,当第一次访问节点时,处理前序,建立左子树的前驱线索。当第二次返回时,处理中序,建立右子树的前驱线索。当第三次返回时,处理后序,并恢复所有线索。这样,每个节点被处理三次,而指针的修改和恢复在常数时间内完成,总时间保持线性。 总之,解决方案可能类似于扩展的Morris遍历,处理三次访问,通过临时修改指针来跟踪遍历状态,从而在不使用栈或递归的情况下实现欧拉巡游。具体实现需要仔细处理每个阶段的指针操作,确保正确性和效率。</think>为了在不使用栈或递归的情况下实现二叉树的欧拉巡游遍历,可以采用**线索二叉树(Threaded Binary Tree)**的改进方法,结合**Morris遍历**的思想,通过临时修改指针实现线性时间遍历。以下是具体步骤: --- ### **算法步骤** 1. **初始化当前节点为根节点**,并定义三个访问阶段的标记(通过指针状态隐式记录)。 2. **遍历过程**: - **第一次访问(前序)**:当首次到达节点时,记录前序访问,并尝试向左子树建立线索。 - **第二次访问(中序)**:通过线索回溯到节点时,记录中序访问,并转向右子树。 - **第三次访问(后序)**:在右子树遍历完成后,通过父指针或线索回溯到节点,记录后序访问。 3. **指针恢复**:在遍历过程中临时修改的指针需恢复原状,以保持树的结构不变。 --- ### **具体实现** ```python class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def euler_tour(root): result = [] current = root while current: # 第一次访问(前序) result.append(current.val) if current.left: # 寻找左子树的最右节点(前驱) predecessor = current.left while predecessor.right and predecessor.right != current: predecessor = predecessor.right if not predecessor.right: # 建立线索并处理左子树 predecessor.right = current current = current.left else: # 第二次访问(中序) result.append(current.val) # 恢复指针并转向右子树 predecessor.right = None current = current.right else: # 无左子树时,直接转向右子树 current = current.right # 处理后序访问(需额外处理右子树的最右节点) # 此处需扩展逻辑以涵盖第三次访问,但需更复杂的指针操作 return result ``` --- ### **关键点** 1. **线索建立与恢复**:通过左子树的最右节点建立临时指针,实现回溯。 2. **三次访问处理**: - 前序访问在首次到达节点时记录。 - 中序访问在通过线索回溯时记录。 - 后序访问需在右子树遍历完成后记录(需进一步扩展逻辑)。 3. **时间复杂度**:每个节点被访问常数次,总时间复杂度为$O(n)$,空间复杂度为$O(1)$[^1]。 --- ### **局限性** - 上述代码仅实现了前序和中序访问,完整的欧拉巡游需扩展逻辑以处理后序
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值