二叉树的遍历--递归实现与非递归实现

二叉树的表示

在研究二叉树的遍历之前,我们需要先看看二叉树的表示方式。

一般来说,我们使用自定义的数据结构或是数组来表示二叉树。

  • 二叉树的数据结构:
public class TreeNode {
    public int val;
    // 左孩子
    public TreeNode left;
    // 右孩子
    public TreeNode right;
}
  • 数组形式表现二叉树

    当我们使用数组形式表现二叉树时,我们将数组第一个节点的索引置为「1」,也就是根节点,如果我们通用性的将其当为「x」,那么它的左孩子节点的索引就是「2*x」,右孩子节点的索引为「2*x+1」。

    例如下面的二叉树,我们可以使用数组[null, 1, 2, 3, null, 4, 5]来表示(因为根节点的索引需要为1,因此数组第一个元素我们将其置为null)

二叉树的遍历

二叉树中一般有四种遍历方式:前序遍历中序遍历后序遍历层序遍历。它们都可以通过「递归」或是「非递归」实现,递归便于理解但是效率低下且不安全,可能会出现栈溢出;非递归,即采用循环,通常采用栈来完成,会复杂一些,但能够提高效率,降低遍历的消耗。其中『层序遍历』本质上是图的「广度优先搜索」,与另外三种有较大差异,故我们这里只讨论前序、中序和后序遍历方式,层序遍历放在之后进行讨论。

在下面的实现中,我们用一个名为「result」的队列来模拟遍历的顺序

前序遍历

访问顺序:根 → 左 → 右

递归实现

public class PreorderTraversal {
    public List<Integer> preorderTraversal(TreeNode root) {
        //add()和remove()方法在失败的时候会抛出异常(不推荐),应使用add()和poll()代替
        List<Integer> result = new LinkedList<>();
        // 进行前序遍历
        _preorderTraversal(root, result);
        return result;
    }

    private void _preorderTraversal(TreeNode node, List<Integer> result) {
        if (null != node) {
            // 先输出根节点的值,然后再顺序处理左孩子和右孩子
            result.add(node.val);
            _preorderTraversal(node.left, result);
            _preorderTraversal(node.right, result);
        }
    }
}

非递归实现

实现思路

非递归实现时前序遍历时,我们需要借助栈(Stack)这一数据结构来完成,根据前序遍历「根-左-右」的特点,我们用以下思路来解决:

  • 预处理:将根节点 push 到栈中
  • 通过循环遍历树
    1. 检测栈是否为空,空则结束循环;若非空,则 pop 栈顶元素,并输出栈顶元素的值
    2. 判断栈顶元素「右孩子」是否为 null,非空则将其 push 到栈中
    3. 判断栈顶元素「左孩子」是否为 null,非空则将其 push 到栈中

在上面思路中,因为栈这一数据结构是「先进后出」的,所以我们先压入右孩子,后压入左孩子,这样最终访问时能够先访问左孩子。

参考代码

这里对于前序、中序和后序遍历的非递归实现,都会给出两种代码,第一种代码较为直观,易于理解;第二种代码属于模版类型的代码,形式统一,便于记忆。

这里先给出符合上述思路,便于理解的代码

public class PreorderTraversal {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> result = new LinkedList<>();
        // 1. 将根节点push到栈
        Stack<TreeNode> tempStack = new Stack<>();
        tempStack.push(root);
        // 2. 通过循环遍历树
        while (!tempStack.empty()) {
            TreeNode node = tempStack.pop();
            result.add(node.val)
            if (null != node.right) {
                tempStack.push(node.right);
            }
            if (null != node.left) {
                tempStack.push(node.left);
            }
        }
        return result;
    }
}

便于记忆的模版代码

这里为什么给出模版代码?

因为二叉树这东西,看着思路理解很快,但是实际写起来容易忘这忘那,所以这里给出模版代码,我们可以多看几遍模版代码先记下如何使用,在熟练之后就可以得心应手的使用二叉树了。

模版代码的思路

  • 预处理

    在遍历二叉树的模版中,我们需要引入一个『辅助节点 curr』保存「当前正在访问的节点」,初始化为根节点,保证栈顶元素始终是 curr 的根节点;每次循环时判断条件为当前栈非空或辅助节点 curr 不为空。

  • 使用循环前序遍历二叉树

    对于这个模版,我们要牢记其在循环中的两种情况,这是根据辅助节点 curr 是否为null进行判断的

    1. curr 存在时,表示栈顶元素存在左孩子,将 curr 压入栈并输出 curr 的值,最后将 curr 节点的值指向其左孩子节点
    2. 如果 curr 为空(即不存在),表示栈顶的节点不存在左子树,这是我们弹出栈顶的节点,将 curr 指向被弹出栈顶节点的右孩子节点

模版代码

public class PreorderTraversal {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> result = new LinkedList<>();

        // 1. 初始化辅助节点curr
        Stack<TreeNode> tempStack = new Stack<>();
        TreeNode curr = root;

        // 2. 通过循环遍历树
        while (!tempStack.empty() || null != curr) {
            if (null != curr) {
                tempStack.push(curr);
                result.add(curr.val);
                curr = curr.left;
            }
            if (null == curr) {
                // 如果下一次循环中 curr 也为空,则表示对于本次弹出后的栈,其栈顶节点左子树已经遍历完毕了,仍然是弹出栈顶节点并对其右子树遍历
                curr = tempStack.pop().right;
            }
        }
        return result;
    }
}

中序遍历

访问顺序:左 → 根 → 右

递归实现

递归代码和前序遍历类似,只是这里我们需要先递归访问到整棵树的最左叶节点

public class InorderTraversal {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> result = new LinkedList<>();
        _inorderTraversal(root, result);
        return result;
    }

    private void _inorderTraversal(TreeNode node, List<Integer> result) {
        if (null != node) {
            _inorderTraversal(node.left, result);
            // 输出完左孩子就输出根节点,之后再处理右孩子
            result.add(node.val);
            _inorderTraversal(node.right, result);
        }
    }
}

非递归实现

实现思路

同样的,我们使用栈(Stack)来实现树的中序遍历,我们需要牢记中序遍历的顺序是「左-根-右」。也就是在打印当前节点之前,要保证该节点的左子树不存在或已经遍历完毕,所以我们同样需要一个『辅助节点 curr』来保存「正在访问的节点信息」,在循环开始时保证它指向栈顶节点的左孩子,在 curr 为null时我们便认为栈顶元素的左子树已无需遍历, 而循环跳出条件也需要变成当前栈非空或 curr 非空。

  • 预处理:将辅助节点 curr 初始化为根节点
  • 通过循环遍历树
    1. 将当前节点 curr 的所有左孩子节点压入栈
    2. 弹出栈顶的元素,让 curr 指向该元素并输出
    3. 让当前节点 curr 指向其自身的右孩子

参考代码

public class InorderTraversal {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> result = new LinkedList<>();

        Stack<TreeNode> tempStack = new Stack<>();
        TreeNode curr = root;

        while (null != curr || !tempStack.empty()) {
            while (null != curr) {
                tempStack.push(curr);
                curr = curr.left;
            }
            // 此时可以保证栈顶元素不存在左子树,弹出并输出,最后再对其右孩子节点进行迭代判定
            curr = tempStack.pop();
            result.add(curr.val);
            curr = curr.right;
        }

        return result;
    }
}

便于记忆的模版代码

实现思路

  • 预处理:将辅助节点初始化为根节点
  • 通过循环实现遍历

    既然是模版,那么形式上和之前前序遍历的模版是类似的,不同的仅是对于辅助节点 curr 是否为null的两种情况的处理:

    1. 当前节点curr 存在,此时将 curr 压入栈之后仅让 curr 指向其左孩子
    2. 当前节点curr 为null,此时栈顶元素的左子树为空,我们将栈顶元素弹出并输出,然后让当前节点 curr 指向其右孩子

模版代码

public class InorderTraversal {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> result = new LinkedList<>();

        Stack<TreeNode> tempStack = new Stack<>();
        TreeNode curr = root;

        while (null != curr || !tempStack.empty()) {
            if (null != curr) {
                tempStack.push(curr);
                curr = curr.left;
            }
            if (null == curr) {
                curr = tempStack.pop();
                result.add(curr.val);
                // 如果当前节点curr是叶子节点,则curr的右孩子一定为null,下次循环时会再次弹出栈顶元素(也就是当前访问节点curr的根节点)进行输出
                curr = curr.right;
            }
        }
        return result;
    }
}

后序遍历

访问顺序:左 → 右 → 根

递归实现

在后序遍历时,我们需要先访问整棵树的最左子节点,再访问与这个最左子节点根节点的右节点

参考代码

public class postorderTraversal {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> result = new LinkedList<>();

        Stack<TreeNode> tempStack = new Stack<>();
        _postorderTraversal(root, result);

        return result;
    }

    private void _postorderTraversal(TreeNode node, List<Integer> result) {
        if (null != node) {
            _postorderTraversal(node.left, result);
            _postorderTraversal(node.right, result);
            // 输出了左孩子和右孩子之后,才输出根节点
            result.add(node.val);
        }
    }
}

非递归实现

实现思路

后序遍历的非递归实现依然依靠栈(Stack)实现,这里它的遍历顺序是「左-右-根」。类似于中序遍历要保证当前节点的左子树已无需遍历,对于后序遍历,我们输出一个节点之前,要保证它的左子树和右子树都已经无需遍历。

在之前的中序遍历,我们使用『curr』表示当前节点,并令其起到判断当前栈顶元素的左子树是否需要遍历的作用(即判断 curr 是否为null);而后序遍历我们还要保证右子树是否被遍历,因此我们这里再引入一个节点『right』表示「上次遍历的右节点」,帮助我们判断栈顶元素的右子树是否需要遍历。

参考代码

public class postorderTraversal {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> result = new LinkedList<>();
        Stack<TreeNode> tempStack = new Stack<>();
        TreeNode curr = root;
        TreeNode right = null;

        while (null != curr || !tempStack.empty()) {
            while (null != curr) {
                tempStack.push(curr);
                curr = curr.left;
            }
            // 这时可以保证栈顶元素的左子树为空或已被遍历了
            curr = tempStack.peek();

            // 如果栈顶元素的右节点存在且未被访问,则需要先对其右节点进行遍历
            if (null != curr.right && curr.right != right) {
                curr = curr.right;
                right = null;
                continue;
            }
            // 右子树不存在或者被遍历过,则输出当前节点
            result.add(curr.val);
            // curr可能是栈顶元素的右孩子
            right = curr;
            // 将curr置为空保证下一次循环时直接取出栈顶元素(即这时curr的根元素)
            tempStack.pop();
            curr = null;
        }
        return result;
    }
}

便于记忆的模版代码

实现思路

我们知道后序遍历的顺序是「左-右-根」,再看一下之前前序遍历的顺序:「根-左-右」。在遍历时,我们将遍历结果顺序压入队列中,考虑如下两个操作:

  1. 前序遍历时,我们将结果压入队列尾部;如果我们现在将每次遍历的结果压入队列头部,那么我们从队列中得到的顺序就变成了「右-左-根」
  2. 在前序遍历时,为什么可以保证先访问左节点,再访问右节点?因为我们每次遍历时优先寻找当前节点的左孩子;如果我们现在每次遍历时,优先寻找右孩子,那么在『1』的基础上,队列中保存的顺序就变成了「左-右-根」,这就是后序遍历的顺序了。

在上述思路和前序遍历模版代码的基础上,我们可以知道代码的方式如下:

  • 预处理:将辅助节点初始化为根节点,循环跳出条件依然是「当前节点curr为空」或「栈为空」
  • 通过循环实现遍历
    1. curr 存在时,将 curr 压入栈,将 curr 的值压入队列头部,并令 curr 指向其右孩子节点
    2. 如果 curr 为空,弹出栈顶元素,将 curr 指向被弹出栈顶节点的左孩子节点

模版代码

public class PostorderTraversal {
    public List<Integer> postorderTraversal(TreeNode root) {
        // 注意这里要指定为LinkedList(双向队列),不然无法使用addFirst()方法
        LinkedList<Integer> result = new LinkedList<>();
        Stack<TreeNode> tempStack = new Stack<>();
        TreeNode curr = root;

        while (null != curr || !tempStack.empty()) {
            if (null != curr) {
                tempStack.push(curr);
                // 这里需要使用addFirst()将元素压入队列头部
                result.addFirst(curr.val);
                curr = curr.right;
            }
            if (null == curr) {
                curr = tempStack.pop().left;
            }
        }

        return result;
    }
}


参考文章:

动画理解二叉树遍历

非递归实现二叉树前序遍历

非递归实现二叉树中序遍历

原文地址:https://www.cnblogs.com/Bylight/p/11566974.html

时间: 2024-10-03 20:54:37

二叉树的遍历--递归实现与非递归实现的相关文章

【算法导论】二叉树的前中后序非递归遍历实现

二叉树的递归遍历实现起来比较简单,而且代码简洁:而非递归遍历则不那么简单,我们需要利用另一种数据结构---栈来实现.二叉树的遍历又可以分为前序.中序和后序三种,它们是按照根结点在遍历时的位置划分的,前序遍历则根结点先被遍历,中序则根结点在左右叶子节点之间被遍历,后序则是根结点最后被遍历.三种非递归遍历中,前序和中序都不是太复制,而后序遍历则相对较难. 一.前序遍历 我们这里前序遍历按照"根-左-右"的顺序来遍历.这里按照"递归--非递归"的次序来研究,之后的几种亦是

递归如何转换为非递归

递归算法实际上是一种分而治之的方法,它把复杂问题分解为简单问题来求解.递归的特点包括:递归过程简洁.易编.易懂:递归过程效率低.重复计算多. 考虑递归的执行效率低,可以尝试将递归过程转换为非递归过程.本文就是来探讨怎么转换的. 将递归算法转换为非递归算法有两种方法,一种是直接求值(迭代/循环),不需要回溯:另一种是不能直接求值,需要回溯.前者使用一些变量保存中间结果,称为直接转换法:后者使用栈保存中间结果,称为间接转换法,下面分别讨论这两种方法. 一.直接转换法 直接转换法通常用来消除尾递归和单

【C语言】求斐波那契(Fibonacci)数列通项(递归法、非递归法)

意大利的数学家列昂那多·斐波那契在1202年研究兔子产崽问题时发现了此数列.设一对大兔子每月生一对小兔子,每对新生兔在出生一个月后又下崽,假若兔子都不死亡.   问:一对兔子,一年能繁殖成多少对兔子?题中本质上有两类兔子:一类是能生殖的兔子,简称为大兔子:新生的兔子不能生殖,简称为小兔子:小兔子一个月就长成大兔子.求的是大兔子与小兔子的总和. 月     份  ⅠⅡ  Ⅲ  Ⅳ  Ⅴ Ⅵ  Ⅶ  Ⅷ Ⅸ Ⅹ  Ⅺ  Ⅻ大兔对数 1  1   2   3   5  8  13  21 34 55 

二叉树的非递归遍历(借鉴递归思想实现非递归遍历)

1 // 树结点定义 2 typedef struct TNode 3 { 4 int value; 5 TNode *left; 6 TNode *right; 7 }*PTNode; 1. 前序遍历的非递归实现(借鉴递归思想实现) 思想: 访问到一结点时,先将其入栈,假设入栈节点为P. 访问P,将P的右孩子和左孩子依次入栈,这样就保证了每次左孩子在右孩子前面被访问. 1 void preOrderNoneRecursion(PTNode root) 2 { 3 if(root == NULL

二叉树序言、为了、经过非递归措辞预订透彻的分析

前言 前两篇文章二叉树和二叉搜索树中已经涉及到了二叉树的三种遍历.递归写法,仅仅要理解思想,几行代码.但是非递归写法却非常不easy.这里特地总结下,透彻解析它们的非递归写法.当中.中序遍历的非递归写法最简单,后序遍历最难.我们的讨论基础是这种: //Binary Tree Node typedef struct node { int data; struct node* lchild; //左孩子 struct node* rchild; //右孩子 }BTNode; 首先.有一点是明白的:非

【数据结构】线索化二叉树中序线索化的递归写法和非递归写法

二叉树是一种非线性结构,遍历二叉树几乎都是通过递归或者用栈辅助实现非递归的遍历.用二叉树作为存储结构时,取到一个节点,只能获取节点的左孩子和右孩子,不能直接得到节点的任一遍历序列的前驱或者后继. 为了保存这种在遍历中需要的信息,我们利用二叉树中指向左右子树的空指针来存放节点的前驱和后继信息.所以引入了线索化二叉树.下面我们讲一下线索化二叉树中序线索化的两种实现方法: (1).递归实现中序线索化二叉树 首先我们先看一下线索化二叉树的结构 enum PointerTag{ THREAD, LINK 

数据结构--Avl树的创建,插入的递归版本和非递归版本,删除等操作

AVL树本质上还是一棵二叉搜索树,它的特点是: 1.本身首先是一棵二叉搜索树. 2.带有平衡条件:每个结点的左右子树的高度之差的绝对值最多为1(空树的高度为-1). 也就是说,AVL树,本质上是带了平衡功能的二叉查找树(二叉排序树,二叉搜索树). 对Avl树进行相关的操作最重要的是要保持Avl树的平衡条件.即对Avl树进行相关的操作后,要进行相应的旋转操作来恢复Avl树的平衡条件. 对Avl树的插入和删除都可以用递归实现,文中也给出了插入的非递归版本,关键在于要用到栈. 代码如下: #inclu

手写栈(递归转化为非递归)

递归的本质是通过栈来保存状态,然后再次调用自己进入新的状态,然后函数返回的时候回到上次保存的状态. 如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的.当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归.尾递归函数的特点是在回归过程中不用做任何操作,就是没有回溯过程,所以我们可以直接将尾递归写成循环 更一般的递归,想要转化为非递归,就需要模拟栈(手写栈)的行为. 遍历的递归和非递归实现: #include<cstdio>

3.4.4 利用栈将递归转换成非递归的方法

在函数执行时系统需要设立一个“递归工作栈”存储第一层递归所需的信息,此工作栈是递归函数执行的辅助空间,所以可以看出,递归程序在执行时需要系统提供隐式栈这种数据结构来实现,对于一般的递归过程,仿照递归算法执行过程中递归工作栈的状态变化可直接写出相应的非递归算法.这种利用栈消除递归过程的步骤如下. (1)设置一个工作栈存放递归工作记录(包括实参.返回地址及局部变量等) (2)进入非递归调用入口(即被调用程序开始处)将调用程序传来的实在参数和返回地址入栈(递归程序不可以作为主程序,因而可认为初始是被某