二叉树的遍历(前中后序)(模拟栈)(模拟队列)(递归法)

二叉树的遍历
从其他平台转载,详细学习请点击此链接
对于二叉树的遍历

  • 前序遍历为:中左右
  • 中序遍历为:左中右
  • 后序遍历为:左右中
  • 层序遍历则是一层一层地遍历,每一层按照层左往右的顺序遍历
    递归法
    前序遍历
    /**
  • Definition for a binary tree node.
  • public class TreeNode {
  • int val;
    
  • TreeNode left;
    
  • TreeNode right;
    
  • TreeNode() {}
    
  • TreeNode(int val) { this.val = val; }
    
  • TreeNode(int val, TreeNode left, TreeNode right) {
    
  •     this.val = val;
    
  •     this.left = left;
    
  •     this.right = right;
    
  • }
    
  • }
    /
    class Solution {
    List res=new ArrayList<>();
    public List preorderTraversal(TreeNode root) {
    if(root!=null){
    res.add(root.val);
    preorderTraversal(root.left);
    preorderTraversal(root.right);
    }
    return res;
    }
    }
    后序遍历
    /
    *
  • Definition for a binary tree node.
  • public class TreeNode {
  • int val;
    
  • TreeNode left;
    
  • TreeNode right;
    
  • TreeNode() {}
    
  • TreeNode(int val) { this.val = val; }
    
  • TreeNode(int val, TreeNode left, TreeNode right) {
    
  •     this.val = val;
    
  •     this.left = left;
    
  •     this.right = right;
    
  • }
    
  • }
    /
    class Solution {
    List res=new ArrayList<>();
    public List postorderTraversal(TreeNode root) {
    if(root!=null){
    postorderTraversal(root.left);
    postorderTraversal(root.right);
    res.add(root.val);
    }
    return res;
    }
    }
    中序遍历
    /
    *
  • Definition for a binary tree node.
  • public class TreeNode {
  • int val;
    
  • TreeNode left;
    
  • TreeNode right;
    
  • TreeNode() {}
    
  • TreeNode(int val) { this.val = val; }
    
  • TreeNode(int val, TreeNode left, TreeNode right) {
    
  •     this.val = val;
    
  •     this.left = left;
    
  •     this.right = right;
    
  • }
    
  • }
    /
    class Solution {
    List res=new ArrayList<>();
    public List inorderTraversal(TreeNode root) {
    if(root!=null){
    inorderTraversal(root.left);
    res.add(root.val);
    inorderTraversal(root.right);
    }
    return res;
    }
    }
    层序遍历
    /
    *
  • Definition for a binary tree node.
  • public class TreeNode {
  • int val;
    
  • TreeNode left;
    
  • TreeNode right;
    
  • TreeNode() {}
    
  • TreeNode(int val) { this.val = val; }
    
  • TreeNode(int val, TreeNode left, TreeNode right) {
    
  •     this.val = val;
    
  •     this.left = left;
    
  •     this.right = right;
    
  • }
    
  • }
    */
    class Solution {
    public List<List> resList=new ArrayList<List>();
    public List<List> levelOrder(TreeNode root) {
    checkFun01(root,0);
    return resList;
    }
    //递归方式
    public void checkFun01(TreeNode node,Integer deep){
    if(node==null){
    return;
    }
    deep++;
    if(resList.size()<deep){
    //当层级增加时,list的item也增加,利用list的索引值进行层级界定
    List item=new ArrayList();
    resList.add(item);
    }
    resList.get(deep-1).add(node.val);
    checkFun01(node.left,deep);
    checkFun01(node.right,deep);
    }
    }
    递归改进
    ChatGPT说上面的代码具有局限性:
    函数中定义了全局变量 List res 作为遍历结果存储对象,在多次调用该函数时会导致累加结果,无法得到每个二叉树节点单独的遍历结果列表。

这是更加好的代码:
/**

  • Definition for a binary tree node.
  • public class TreeNode {
  • int val;
    
  • TreeNode left;
    
  • TreeNode right;
    
  • TreeNode(int x) { val = x; }
    
  • }
    */

// 先序遍历(根左右)
public List preorderTraversal(TreeNode root) {
if (root == null) {
return new ArrayList<>();
}
List res = new ArrayList<>();
res.add(root.val);
res.addAll(preorderTraversal(root.left));
res.addAll(preorderTraversal(root.right));
return res;
}

// 中序遍历(左根右)
public List inorderTraversal(TreeNode root) {
if (root == null) {
return new ArrayList<>();
}
List res = new ArrayList<>();
res.addAll(inorderTraversal(root.left));
res.add(root.val);
res.addAll(inorderTraversal(root.right));
return res;
}

// 后序遍历(左右根)
public List postorderTraversal(TreeNode root) {
if (root == null) {
return new ArrayList<>();
}
List res = new ArrayList<>();
res.addAll(postorderTraversal(root.left));
res.addAll(postorderTraversal(root.right));
res.add(root.val);
return res;
}

一下是和ChatGPT的一段对话

  • List集合里面的add()和addAll()有什么区别?
  • List 集合类中的 add() 和 addAll() 方法都可以向列表中添加元素,但它们之间有很大的区别:
  1. add() 方法通常用于将一个单独的元素添加到列表中。例如,您可以使用 list.add(obj) 将对象 obj 添加到 list 中。
  2. addAll() 方法用于将一组元素(即另一个集合)添加到列表中。例如,如果您有一个 Collection 类型的列表 col,可以使用 list.addAll(col) 将所有 col 中的元素添加到 list 中。
    除此之外,还有一些其他的差别:
  3. add() 方法只需要添加一个参数,而 addAll() 方法需要添加一个集合作为参数。
  4. add() 只能添加一个元素,而 addAll() 可以将多个元素一次性添加到列表中。
  5. addAll() 可以用于将任何实现了 Collection 接口的类(比如 List、Set 或 Queue)添加到指定的列表中。
  • 所以你提供的递归遍历二叉树的代码里用了addAll()?
    是的,我在之前提供的代码中使用了 addAll() 方法。因为在二叉树递归遍历中,我们需要将左右子树遍历得到的节点值添加到当前节点的值之后,并返回整个遍历结果。
    在 Java 集合框架中,addAll() 方法可以方便地将一个集合中的所有元素添加到另一个集合中。因此,在我的实现中,我使用了 addAll() 方法将左右子树的遍历结果添加到当前节点值的后面,形成一个完整的遍历序列,最终返回整个遍历结果列表。
    举个例子,在先序遍历算法中,我们先访问根节点的值,然后再迭代访问左右子树中的节点值,并加入遍历结果列表中,最终按 “根-左子树-右子树” 的顺序返回一个完整的遍历结果列表。这样的话就需要在遍历过程中将根节点和左右子树得到的遍历结果列表按照正确的顺序组合起来。而 addAll() 方法就可以很好地帮助我们实现这个目的。
  • 还是不太明白
    非常抱歉,我的回答可能有点不够清晰,让您产生了困惑。
    以下是更详细的解释:
    比如在先序遍历算法中,我们需要按照 “根-左子树-右子树” 的顺序访问节点值。
    那么我们首先访问当前节点(根节点)的值,接着进入左子树递归调用,并加入遍历结果列表。因为这里的遍历结果列表是一个 List 集合类型,我们可以使用 add() 方法将当前节点值和左子树得到的遍历结果依次添加到列表的末尾。
    java复制代码
    res.add(root.val); // 添加当前节点值
    res.addAll(preorderTraversal(root.left)); // 将左子树遍历结果添加到末尾
    按照同样的方法递归访问右子树,将其遍历结果也添加到列表末尾中。最终,在递归过程结束时,返回包含根节点、左子树和右子树遍历结果的完整遍历结果列表。
    java复制代码
    res.add(root.val);
    res.addAll(preorderTraversal(root.left));
    res.addAll(preorderTraversal(root.right)); // 将右子树遍历结果添加到末尾
    而addAll() 方法能够将另一个集合中的所有元素依次添加到指定的列表或集合中。
    因此,在上述代码中,res.addAll(preorderTraversal(root.left)) 表示将左子树得到的遍历结果添加到当前节点值之后,并形成一个完整的遍历序列。
    希望这次能够更加清晰地解释答案了。如果还有疑问,请继续提问,我会尽力回答并帮助您理解。
  • res.addAll(preorderTraversal(root.left));
    这句代码还是不太清晰,能不能采用类比的方式、更加通俗易懂地、结合生活中的一些事物、浅显易懂地讲解一下
    好的,我考虑了一下,或许可以用采集水果作为类比来说明这段代码的意义。
    假设我们有一个果园,里面种着苹果树、梨树和桃子树。我们现在要去采集这些水果,并按照顺序装到一个篮子里面。
    首先,我们遇到了一棵苹果树,掉下了两个苹果,我们把它们装进篮子里。接着,我们发现在苹果树的左边还有一棵梨树。因此,我们必须离开目前的位置,向左移动,并从梨树上采集所有的梨子,然后把它们也放到篮子里。但是我们已经将两个苹果装进篮子中了,所以我们需要将所有的梨子都紧挨着两个苹果装进篮子里面(因为我们要按照顺序摆放水果)。
    在 Java 中,这个过程就可以用 addAll() 来实现。
    具体而言,res 是我们的篮子,一开始时里面放着两个苹果;preorderTraversal(root.left) 根据二叉树递归算法遍历到的左子树(即梨树),返回的是一个 List 集合类型,包含了左子树上的所有节点值。此时,我们需要将这个列表中的元素依次添加到篮子(res)当中。因此,我们使用 addAll() 方法,将梨树得到的遍历结果加入到篮子中。
    java复制代码
    res.addAll(preorderTraversal(root.left)); // 将左子树遍历结果添加到篮子中
    在这个例子中,addAll() 方法会将苹果和梨子这两种水果都按照正确的顺序添加到篮子里面,原因是它相当于把整个梨树上的每一个梨都放进了篮子里。同样的,对于递归遍历二叉树也一样,addAll() 方法可以方便地将所有左右子树上的节点值依次添加到当前节点值之后,并返回一个完整的遍历结果列表。
    希望这样解释能够更加清晰地说明答案。
    总体来说,add() 方法用于向列表添加单个元素,而 addAll() 方法用于将多个元素或另一个集合中的所有元素添加到列表中
    递归方法二叉树遍历总结以及原理理解
    对于层序遍历,使用递归会显得非常麻烦。当然,硬要做肯定也能做出来。
    总之,对于前中后序遍历使用递归,非常简单直观、通俗易懂、易于理解;但是对于层序遍历就先用别的方法把

通过以上的前中后序遍历的递归代码可以发现:

  1. 代码简短、容易记住,就像一套固定的模板
  2. 三个遍历的递归写法的代码几乎一样,除了方法体内三行代码的书写顺序
  3. 前序遍历:
    1. res.add(root.val);
    2. preorderTraversal(root.left);
    3. preorderTraversal(root.right);
  4. 后序遍历:
    1. postorderTraversal(root.left);
    2. postorderTraversal(root.right);
    3. res.add(root.val);
  5. 中序遍历:
    1. inorderTraversal(root.left);
    2. res.add(root.val);
    3. inorderTraversal(root.right);
    迭代法
    二叉树遍历的迭代法是指利用辅助数据结构(如栈或队列)来实现二叉树遍历的过程,而不使用递归函数进行调用的方法
    使用栈
    前序遍历
    public List preorderTraversal(TreeNode root) {
    List res = new ArrayList<>();
    if (root == null) {
    return res;
    }
Stack<TreeNode> stack = new Stack<>();
stack.push(root); // 先将根节点放入栈中

while (!stack.empty()) {
    TreeNode cur = stack.pop(); // 获取当前节点并弹出栈顶元素
    res.add(cur.val); // 将当前节点值加入遍历结果列表

    if (cur.right != null) { // 先将右子树入栈
        stack.push(cur.right);
    }
    if (cur.left != null) { // 将左子树入栈等待处理
        stack.push(cur.left);
    }
}

return res;

}
后序遍历
/**

  • Definition for a binary tree node.

  • public class TreeNode {

  • int val;
    
  • TreeNode left;
    
  • TreeNode right;
    
  • TreeNode() {}
    
  • TreeNode(int val) { this.val = val; }
    
  • TreeNode(int val, TreeNode left, TreeNode right) {
    
  •     this.val = val;
    
  •     this.left = left;
    
  •     this.right = right;
    
  • }
    
  • }
    */
    class Solution {
    public List postorderTraversal(TreeNode root) {
    List res=new ArrayList<>();
    if(root==null){
    return res;
    }
    Stack stark=new Stack<>();
    stark.push(root);
    while(!stark.isEmpty()){
    TreeNode cur=stark.pop();
    res.add(cur.val);
    if(cur.left!=null){
    stark.push(cur.left);
    }
    if(cur.right!=null){
    stark.push(cur.right);
    }
    }
    Collections.reverse(res);
    return res;
    }
    }
    讲一下Collections.reverse(res)这个代码
    Collections.reverse(res) 是 Java 标准库中的一个静态方法,它用于将集合或列表 res 中的所有元素从尾部开始反转。如果 res 为空,则直接返回空。
    这个方法能很方便地实现对集合或列表的逆序操作,常常被用于需要进行列表元素排序、数据交换等场合。
    在上面的代码中,我们首先按照根-右子树-左子树的遍历顺序处理了所有节点,并将它们加入到初始的遍历结果列表 res 中。但是,由于遍历顺序和后序遍历并不一致,因此得到的列表的顺序是错误的。
    为了获得真正的后序遍历结果,我们需要将 res 中贯穿整个遍历过程中得到的节点结果再次倒序排列。具体而言,调用 Collections.reverse(res) 方法可以直接对 List 类型的对象 res 进行 in-place 操作,即原地修改该集合对象的元素顺序,从而使得最终得到的列表(也就是遍历结果)的顺序符合后序遍历的要求。
    例如,如果我们有一个包含整数元素的列表 [1, 2, 3, 4, 5],那么调用 Collections.reverse(list) 方法之后,该列表中的元素顺序将会变成 [5, 4, 3, 2, 1]。
    需要注意的是,虽然这种方式可以方便地实现对集合元素的反转,但它并不一定具有最优的时间和空间复杂度。如果需要进行更高效的逆序操作,可以考虑使用其他数据结构和算法进行实现。
    中序遍历
    public List inorderTraversal(TreeNode root) {
    List res = new ArrayList<>();
    if (root == null) {
    return res;
    }

    Stack stack = new Stack<>();
    TreeNode cur = root; // 当前操作节点

    while (!stack.empty() || cur != null) {
    // 将当前节点以及其所有的左子节点推入栈中
    while (cur != null) {
    stack.push(cur);
    cur = cur.left;
    }
    // 弹出栈顶元素并进行处理
    cur = stack.pop();
    res.add(cur.val);
    // 处理右子节点
    cur = cur.right;
    }

    return res;
    }
    修正后的代码和原来的不同之处在于 while 循环条件变成了 !stack.empty() || cur != null,即只有当栈为空且当前搜索节点不为空时才退出。在内层 while 循环中,我们仍然按照 “左子树-根-右子树” 的顺序依次将当前节点以及所有左子树节点加入栈中。当栈非空时,则从栈顶弹出一个元素进行处理,并让当前节点指向其右子树的根节点。
    总结
    为了便于记忆,可以讲前中后序遍历的代码书写过程你以为以下四个步骤:

  1. 集合判空:创建一个空的 List 类型的遍历结果集合,并判断二叉树是否为空。
  2. 建栈入根:创建一个 Stack 对象,并将当前节点(即二叉树的根节点)推入栈中。
  3. while出根:循环操作,从栈顶取出一个节点进行处理。
  4. 根右左:处理当前节点,按照 “根-左子树-右子树” 的顺序,将该节点的右子节点和左子节点按顺序推入栈中等待处理。

对于前序遍历
这是一个在日常生活中广泛应用的例子。设想你需要将房间里的书籍以前序方式摆放好,现在你有一堆书排成了一个队列,你该如何按照前序方式摆放呢?
一种基本的方法就是使用栈。首先你将最开始的一个书放在桌子上(这个书是根节点),然后从队列头取出下一个书并检查其章节(表示左右子节点的状态),如果它有更晚的章节表示有子节点,那么就将这个书插入到前面这个书的左边;否则,说明前面这本书之后就没有更多的子节点了,这时候就弹出前面这本书,像讲故事一样读完它之后再去处理它的右侧,也就是下一个要摆放的书。依照这个流程不断重复,直到所有的书都被按前序方式摆放在桌子上。
这个过程中,你手里的搬运工具就像栈一样:每次将新的书压进去(push()操作),只要发现不能再往左继续摆放了,就弹出栈顶的书(pop()操作)并继续向右进行处理。最终,通过这种方式,你可以比较快速而方便地完成了书籍的前序摆放,同时也训练了你有效管理物品、善于规划顺序的能力。
使用队列
前序遍历
二叉树的前序遍历是一种基于深度优先搜索(DFS)的迭代算法,其实现过程和中序遍历类似。我们同样需要使用到栈来记录访问过的节点信息,并依次遍历所有节点。具体而言,在每次迭代过程中,我们首先将当前节点加入结果列表中,并在栈中存储下它的右子树节点、左子树节点。
public List preorderTraversal(TreeNode root) {
List res = new ArrayList<>();
if (root == null) {
return res;
}
Stack stack = new Stack<>();
TreeNode cur = root;
// 首先将当前节点压入栈中
stack.push(cur);

while (!stack.isEmpty()) {
    // 从栈顶取出一个节点并记录值
    cur = stack.pop();
    res.add(cur.val);
    // 将右孩子(如果存在)和左孩子(如果存在)以此顺序加入到栈内
    if (cur.right != null) {
        stack.push(cur.right);
    }
    if (cur.left != null) {
        stack.push(cur.left);
    }
}

return res;

}
在该示例代码中,我们同样需要检查根节点是否为空。如果不为空,则首先创建一个栈 stack 和初始化当前节点 cur 为根节点,并将其加入栈中。
在 while 循环体中,我们不断从栈顶取出待遍历的节点 cur 并将其值记录到结果集合中。然后,我们检查该节点是否存在右孩子(如果存在,则将其加入到栈中),以及是否存在左孩子(如果存在,则将其加入到栈中)。这样保证了前序遍历过程的正确性,并且即使是非递归实现,也可以按照根结点、左孩子和右孩子的顺序访问整个二叉树。
需要注意的是,该算法的时间复杂度为 O(n),其中 n 表示二叉树节点的数量。尽管这个方法非常依赖于栈操作,并且代码本身比较简单,但它是理解迭代实现方式的基础,最终目标是用更优秀、更高效的算法来解决相应问题。
后序遍历
二叉树的后序遍历是一种基于深度优先搜索(DFS)的迭代算法,和前序遍历、中序遍历略有不同。在后序遍历中,我们首先左右子节点均遍历完成之后,再遍历当前节点。因此,在实现时需要特别注意顺序性,并使用到两个栈(或者一个栈和队列)来存储已经访问过的节点信息。
public List postorderTraversal(TreeNode root) {
List res = new ArrayList<>();
if (root == null) {
return res;
}
Stack stack1 = new Stack<>();
Stack stack2 = new Stack<>();

TreeNode cur = root;
stack1.push(cur);

while (!stack1.isEmpty()) {
    // 每次从栈顶取出一个元素并加入到另一个空栈中
    cur = stack1.pop();
    stack2.push(cur);
    // 将该元素的左孩子和右孩子以及交替顺序加入到第一个栈中
    if (cur.left != null) {
        stack1.push(cur.left);
    }
    if (cur.right != null) {
        stack1.push(cur.right);
    }
}

while (!stack2.isEmpty()) {
    // 从第二个栈中取出每个元素并记录其值
    cur = stack2.pop();
    res.add(cur.val);
}

return res;

}
在该示例代码中,我们同样需要检查根节点是否为空。如果不为空,则首先创建两个栈 stack1 和 stack2,初始化当前节点 cur 为根节点,并将其加入到第一个栈 stack1 中。
在 while 循环体中,我们持续从栈 stack1 中取出已经访问过且还未加入第二个栈 stack2 中的节点,并加入到第二个栈 stack2 中,重复此过程直至所有节点都被处理完毕。因此,最终保存在第二个栈中的元素就是后序遍历中每个节点的遍历顺序的反向排序,我们只需要依次弹出并记录其值即可。
需要注意的是,该算法的时间复杂度也为 O(n),其中 n 表示二叉树节点的数量。与前序遍历、中序遍历相比,后续遍历的实现方式稍微麻烦一些,但我们仍然可以通过合理利用栈数据结构,并逐步增加功能、迭代次数等手段来逐渐优化这个过程。
中序遍历
二叉树的中序遍历是一种基于深度优先搜索(DFS)的迭代算法,它的核心思想是利用队列数据结构来模拟二叉树的节点遍历顺序。具体而言,我们需要首先将根节点入队,并依次一层一层地横向扫描整棵树,在扫描到每个节点时同时取出并记录其左子节点,并将其加入到队列的尾部,然后再将当前节点同样加入到结果列表中,最后对该节点的右子节点重复以上过程。
public List inorderTraversal(TreeNode root) {
List res = new ArrayList<>();
if (root == null) {
return res;
}
Queue queue = new LinkedList<>();
Stack stack = new Stack<>();

TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
    // 持续将左侧节点入栈,直至没有左节点
    while (cur != null) {
        stack.push(cur);
        cur = cur.left;
    }
    // 此时栈顶元素即为下一个要遍历的节点,将其弹出并记录值
    cur = stack.pop();
    res.add(cur.val);
    // 将当前节点的右子节点作为下一个待处理节点,进入到队列末尾
    cur = cur.right;
}

return res;

}
在该示例代码中,我们同样需要检查根节点是否为空。如果不为空,则首先创建一个队列 queue 和一个栈 stack,并初始化当前节点 cur 为根节点。
在 while 循环体中,我们持续将当前节点的左侧子节点(如果存在)加入到栈中,并将当前节点更新为其左孩子,直至没有更多的左孩子。这个过程可以理解为一直向左侧善处。如果此时栈顶元素的左孩子已经遍历完毕,则需要按照中序遍历的顺序将栈顶弹出并记录其值,并将当前节点更新为其右孩子,以等待后续处理。重复以上操作直至循环结束,最终返回二叉树的中序遍历结果集。
需要注意的是,该算法的时间复杂度为 O(n),其中 n 表示二叉树节点的数量。用迭代的方式实现中序遍历操作通常比较困难,因为我们必须随时记录当前未遍历的节点信息并对其进行压栈/出栈操作。因此,在实际编码中,我们可能会结合递归的方式来实现中序遍历,具体视情况而定。
层序遍历
二叉树的层序遍历(BFS,广度优先搜索)是一种基于队列实现的迭代算法。其核心思想是从二叉树的根节点开始,逐层向下处理每个节点,并将其所有子节点加入队列中。在维护好当前层的节点信息后,我们再将队首元素出队并处理,直到队列为空为止。
对于二叉树的层序遍历而言,实现其迭代法方式需要借助队列来模拟,因为栈并不适合对层序节点进行管理和调度。因此,使用栈来实现该算法可能会比较困难,而且效率也不如队列方式高。
public List levelOrder(TreeNode root) {
List res = new ArrayList<>();
if (root == null) {
return res;
}

Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);

while (!queue.isEmpty()) {
    TreeNode cur = queue.poll();
    res.add(cur.val);
    if (cur.left != null) {
        queue.offer(cur.left);
    }
    if (cur.right != null) {
        queue.offer(cur.right);
    }
}

return res;

}
这段代码首先创建了一个空的 List 类型的结果集合 res 和一个 Queue 类型的队列对象 queue,并判断当前输入的二叉树是否为空。有了这个预处理,可以构建一个循环,将所有属于同一层次的树节点全部弹出,放到 list 中记录值,并依次推入其所有子节点(如果存在)到队列末尾。
在 while 循环中,我们先取出队列头部节点 cur,接着将其值加入到结果集合 res 中。同时,我们依次处理该节点的两个子节点,如果左右子节点非空,则将其推入队列中。最后,重复以上操作直至队列为空。
需要注意的是,这里我们使用了 LinkedList 的 offer() 和 poll() 方法来分别实现队列尾部元素添加和队列头部元素弹出的功能。由于 Queue 接口本身是一个标记接口,因此无法直接进行实例化,在代码中我们需要使用LinkedList类来实现真正的队列。
总的来说, Java 内置的 LinkedList 已经足够支持一般规模的二叉树层序遍历操作,同时具备良好的可读性和易用性,可以方便地在日常工作和开发中使用。

猜你喜欢

转载自blog.csdn.net/qq_64744030/article/details/130312617