二叉树的三序遍历

二叉树的三序遍历

前言

今天复习王道数据结构的时候注意到二叉树的遍历需要掌握迭代的方式,故去力扣刷了一下发现官方解法与民间解法都不太令我满意,不是难以理解就是代码统一度不够高,代码不够清爽。故给出我的示例代码和理解。

方法主要有两种方式

  1. 递归

  2. 迭代

要明白遍历的本质,本质并不是数字输出的顺序,遍历的本质而是以什么样的顺序去访问节点。

递归方式实现遍历

首先就是递归,递归其实感觉没啥好讲的,就是直接暴力递归,遵循树遍历的规则就好了,如果不会建议直接立即推。

前序遍历

力扣

根左右

    void inorder_traversal(TreeNode* root) {
        //递归的结束条件
        if(root == NULL) return;
        //访问根节点
        v.push_back(root->val);
        //左递归
        inorder_traversal(root->left);
        //右递归
        inorder_traversal(root->right);
    }

中序遍历

力扣

左根右

void inorder_traversal(TreeNode* root) {
    //递归的结束条件
    if(root == NULL) return;
    //左递归
    inorder_traversal(root->left);
    //访问根节点
    v.push_back(root->val);
    //右递归
    inorder_traversal(root->right);
}

后序遍历

力扣

左右根

void inorder_traversal(TreeNode* root) {
    //递归的结束条件
    if(root == NULL) return;
    //左递归
    inorder_traversal(root->left);
    //右递归
    inorder_traversal(root->right);
    //访问根节点
    v.push_back(root->val);
}

迭代方式实现遍历

迭代其实和递归方式是一样的只不过是由程序员手动去维护递归所需的方法栈

前序遍历

    vector<int> preorderTraversal(TreeNode* root) {
        //接收遍历结果的数组
        vector<int> v;
        //栈
        stack<TreeNode*> stk;
        //前面的root!=NULL其实就是排除空树的干扰并且把第一个根节点放入栈当中,这样写可以少写两行代码
        //后面栈不为空说明遍历未结束
        while(root || !stk.empty()) {
            //如果是个非空结点就访问他并放入栈当中并左递归
            if(root) {
                //入栈
                stk.push(root);
                //访问当前节点
                v.push_back(root->val);
                //相当于左递归
                root = root->left;
            }
            //如果是个空结点就说明左递归到头了得右递归
            else {
                //拿到当前结点
                root = stk.top();
                //出栈
                stk.pop();
                //相当于右递归
                root = root ->right;
            }
        }
        return v;
    }

中序遍历

和先序遍历相似只不过将遍历操作放到左递归之后右递归之前

    vector<int> inorderTraversal(TreeNode* root) {
        //接收遍历结果的数组
        vector<int> v;
        //栈
        stack<TreeNode*> stk;
        //前面的root!=NULL其实就是排除空树的干扰并且把第一个根节点放入栈当中,这样写可以少写两行代码
        //后面栈不为空说明遍历未结束
        while(root || !stk.empty()) {
            //如果是个非空结点就访问他并放入栈当中并左递归
            if(root) {
                //入栈
                stk.push(root);
                //相当于左递归
                root = root->left;
            }
            //如果是个空结点就说明左递归到头了得右递归
            else {
                //拿到当前结点
                root = stk.top();
                //访问当前节点
                v.push_back(root->val);
                //出栈
                stk.pop();
                //相当于右递归
                root = root ->right;
            }
        }
        return v;
    }

后序遍历

后序遍历和先序与中序遍历略有不同,你不能简单的把访问根结点操作放到右递归后面,因为要保证其左右子树均被访问过才能访问该节点其实就是说当我访问这个结点时以这个结点为根结点的子树必须全部访问完了。

这里有个比较讨巧的方法:后序遍历为左右根,那么我可以遍历为根右左然后进行一次反转就是最终答案。

    vector<int> postorderTraversal(TreeNode* root) {
        //接收遍历结果的数组
        vector<int> v;
        //栈
        stack<TreeNode*> stk;
        //前面的root!=NULL其实就是排除空树的干扰并且把第一个根节点放入栈当中,这样写可以少写两行代码
        //后面栈不为空说明遍历未结束
        while(root || !stk.empty()) {
            //如果是个非空结点就访问他并放入栈当中并右递归
            if(root) {
                //入栈
                stk.push(root);
                //访问当前节点
                v.push_back(root->val);
                //相当于右递归
                root = root->右;
            }
            //如果是个空结点就说明右递归到头了得左递归
            else {
                //拿到当前结点
                root = stk.top();
                //出栈
                stk.pop();
                //相当于左递归
                root = root ->right;
            }
        }
        reverse(v.begin(),v.end());        
        return v;
    }

上述代码用一个比较巧妙的方式避免了直接实现后序遍历,曲线救国。但我们思考一个问题遍历的本质到底是什么?难道只是简单的输出数据通过案例作对题目吗?显然不是的。遍历的本质并不是数字输出的顺序,而是以什么样的顺序去访问节点。故下面的操作才是正解。

后序遍历的本质其实就是说当我访问这个结点时以这个结点为根结点的子树必须全部访问完了,因此只需在上述代码中添加这个判断即可

    vector<int> postorderTraversal(TreeNode* root) {
        //接收遍历结果的数组
        vector<int> v;
        //栈
        stack<TreeNode*> stk;
        //用来记录上一个访问结点,用来判断是否重复访问        
        TreeNode* recent = NULL; 
    //前面的root!=NULL其实就是排除空树的干扰并且把第一个根节点放入栈当中,这样写可以少写两行代码
        //后面栈不为空说明遍历未结束
        while(root || !stk.empty()) {
            //如果是个非空结点就访问他并放入栈当中并左递归
            if(root) {
                //入栈
                stk.push(root);
                //相当于左递归
                root = root->left;
            }
            //如果是个空结点就说明左递归到头了得右递归
            else {
                root = stk.top();
                //如果右节点存在且未被访问过
                if(root->right && root->right != recent) {
                    root = root->right;
                }
                //如果右结点存在且已经被访问过
                else {
                    v.push_back(root->val);
                    stk.pop();
                    recent = root;
                    //每次栈访问完一个结点相当于访问完该结点为子树的全部结点,故需要将其重置为NULL
                    root = NULL;
                }
            }
        }
        return v;
    }
从前序遍历序列中还原二叉树的过程可以通过递归来实现。具体的步骤如下: 1. 创建一个全局变量,用于记录当前遍历到的节点在前序遍历序列中的索引,初始值为0。 2. 创建一个递归函数,函数参数包括前序遍历序列和当前子树的边界索引(start和end)。 3. 在递归函数中,首先判断边界索引是否合法(start大于end),如果不合法,则返回null。 4. 创建一个新节点,将前序遍历序列中当前索引位置的值赋给新节点。 5. 将全局索引值加1。 6. 在递归函数中,将当前子树的边界索引分为左子树和右子树的边界,并分别递归调用递归函数,传入左子树和右子树的边界索引。 7. 将递归调用返回的左子树和右子树连接到新节点上。 8. 返回新节点作为当前子树的根节点。 以下是使用Python示例代码实现从前序遍历得到二叉树的过程: ```python class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def buildTree(preorder, start, end): # 边界索引不合法,返回null if start > end: return None # 创建当前子树的根节点 root = TreeNode(preorder[buildTree.index]) buildTree.index += 1 # 划分左子树和右子树的边界索引,并递归调用 root.left = buildTree(preorder, start, buildTree.index-1) root.right = buildTree(preorder, buildTree.index, end) return root def buildTreeFromPreorder(preorder): buildTree.index = 0 # 初始化全局索引为0 return buildTree(preorder, 0, len(preorder)-1) # 示例调用 preorder = [3, 9, 20, 15, 7] root = buildTreeFromPreorder(preorder) ``` 以上代码中,通过定义一个全局索引变量 `buildTree.index` 来记录当前遍历到的节点位置,初始值为0。然后通过递归调用 `buildTree()` 函数来构建二叉树。最后返回根节点即可。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值