树的遍历方式 --- 前序遍历

前言

先给大家上一段代码。这段代码出自
LeetCode 144.Binary Tree Preorder Traversal
是二叉树的前序遍历的递归代码写法。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> res;
        helper(root, res);
        return res;
    }
    
    void helper(TreeNode *root, vector<int> &res) {
        if ( !root )
            return ;
        
        res.push_back(root->val);
        helper(root->left, res);
        helper(root->right, res);
    }
};

我们单独将这三行代码抽离出来,这也是二叉树的前序遍历的实现的主体部分:

res.push_back(root->val); // 这里就是对根进行访问的一个过程,在题目中具体化为一个入res的操作
helper(root->left, res);
helper(root->right, res);

很多最初被数据结构与算法蹂躏过的同学看到这段代码应该会有同样的反应:哇,好简单的代码!而且之后学习中序遍历与后序遍历后,发现只需要将res.push_back(root->val)这一行代码移动一下位置即可。这直接促成了很多人一开始对待二叉树的遍历的“不屑”,比如我。初学的时候被递归这妖精磨的死去活来,汉诺塔都半天走不出那个逻辑怪圈,树的遍历的递归代码居然只要这么简简单单三行代码,这就是递归嘛?真是有够好笑呢。因为树的遍历方式是根左右,所以先对根进行访问,再对左子树和右子树分别进行访问就好了,所以用递归就行了。
我承认这段代码是非常的简单,但如果各位只对这段代码抱有这样的态度和想法,那是远远不够的,因为我们重点要学习的并不是前序遍历本身,而是前序遍历的思想。
有些人也许会在看到一道关于树的递归的问题时无从下手,比如这道题需要叶结点的某些信息,需要记录父结点,需要翻转,最大深度…你也许会埋怨自己在解递归题方面没有天赋,永远被困在某一层递归中走不出来,那么我想给你一个建议,先去透彻的了解**树的前中序遍历方式!**为什么我要给你这样的建议,因为我就是这里的有些人。

前序遍历递归版本

我猜很多同学自己没有完整的推导过一棵二叉树的前序遍历到底是怎么样的,于是我先来带大家干一次这个事。就假定我们有这么一棵朴实无华的二叉树。先看下面两张图,最初的几个步骤理解起来是毫无难度的。
在这里插入图片描述
在这里插入图片描述
现在让我们把目光聚焦在最后一步,即判断结点4⃣️的左右子树均为空。所对应到的情况就是这样的:
在这里插入图片描述
通俗点讲,递归函数其实是在函数调用的时候做了一些插入结算,我们刚才看到的最后一步,对结点4⃣️的左右结点判断结束后,第三个方框这个插入结算的过程( helper(2⃣️->left) )也就此终止了,现在我们要做的就是再返回到第二个方框,继续调用helper(2⃣️->right)这个插入过程。
在这里插入图片描述
在这里插入图片描述
于是方框2这个插入过程也整体结算完毕,回到了结点1⃣️,方框1中的helper(1⃣️->left)这个过程也结算完毕了,继续执行helper(1⃣️->right)。本来这个函数正常的调用就是先调用helper(1⃣️->left),再调用helper(1⃣️->right),可是却出现了非常多的插入结算,这就是递归。
同时我们也不难发现一个规律,我们将每次插入结算视为一个方框的话,只有当这个方框中的所有运算结束了之后,我们才可以跳回到上一级的方框所调用的插入结算中。听起来有点绕,其实一个正确的递归程序也就是插入结算总有终止的时候,那么终止时回到调用插入结算的那个时间点即可。而这其实就是前序遍历的思想:
操作做在执行递归之前。
为什么会有“前序遍历根左右”这一说,就是因为在我访问完这个根结点后我开始往左子树递归,左子树的第一个结点被访问到又继续往这个结点的左子树递归,这是插入结算,所以每次这个左结点都成为了新的根结点,而当左子树递归不下去的时候再去递归右子树,右子树也递归不下去的时候,这个结算过程,也就是上面我们显性化的方框里面的内容执行结束了,该回到哪就回到哪,这个‘哪’即上次这个插入结算调用的位置,这是一种回溯,这也是根左右这种说法的由来。
这里最后还剩了一个helper(1⃣️->right)的过程我没有分析,留给你们当做作业好了。
在解释这个递归过程上费了这么多口舌,也许有同学会说:“多此一举,考试我会写这个代码就好了,我用得着这么透彻的理解嘛?”我之前就强调过,这个例子实质上是前序遍历思想的学习。这一点,会在我之后大家刷LeetCode的题目中详细说明,前序遍历思想到底在刷题中有什么样的应用。

前序遍历非递归版本

假设我们现在拥有一个栈存放访问过的结点,有一个结点cur去准备越俎代庖遍历这棵树。

vector<TreeNode *> node_stack;
TreeNode* cur = root;

最初我们一直将最左边的结点放入栈中,且将访问到的元素值加入res中。

while ( ... ) {
	res.push_back(cur->val);
	node_stack.push(cur);
	cur = cur->left;
}

现在我们待解决的问题就是如何在栈中体现诸如4⃣️结点这类结点的回溯。不难发现,这一步的插入结算在4⃣️结点的left判断为空后,开始判断起了4⃣️结点的right部分是否为空,而我们来观察这个栈,由于栈先进后出的特性,4⃣️结点就在栈顶的位置,我们最开始不断将左结点入栈,直到左结点入不动(为nullptr)的时候,现在我们对这个结点的右结点进行判断,即我们将栈顶元素移去,使其右结点入栈,然后继续做我们上述的判断,就可以达到我们需要的效果。
在这里插入图片描述

while ( ... ) {
	...
}

TreeNode* temp = node_stack.top(); // 判断栈顶元素
node_stack.pop(); // 移去栈顶元素
cur = temp->right; // 而新一轮判断的结点应该为这个栈顶元素的right

万里长征已经走了90%,最后的判断就是这个while循环的条件是什么?
这里我就不再卖关子了,这可以算是一个模版/套路:

// cur不为空 or 栈不为空 时 就循环
// 栈和cur同时为空要么是遍历时出了错误要么就是遍历结束
while ( !node_stack.empty() || cur ) {
	...
}

最后附上完整源码:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode *> node_stack;
        TreeNode *cur = root;

        while ( !node_stack.empty() || cur ) {
            while ( cur ) {
                res.push_back(cur->val);
                node_stack.push(cur);
                cur = cur->left;
            }

            TreeNode *temp = node_stack.top();
            node_stack.pop();
            cur = temp->right;
        }

        return res;
    }
};

总结

树的遍历真的不像各位想象的那么“脆弱”,其思想贯彻了整个LeetCode刷题!一定要重视起来。
给各位留一个作业:589.N-ary Tree preorder Traversal,请用递归和非递归两种方式对这道题进行解答,且递归的过程请务必自己推敲一遍!答案源码我将放在我的github上
https://github.com/18260036169/LeetCode-Tree/tree/master

本期的数据结构与算法就和大家聊到这里,谢谢各位。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章