二叉树的递归方式
博客文章主要目的是分享和记录。
分享给需要学习的同学,记录自己学习路上的点滴。
相信大家对于二叉树的定义以及结构,在本科《数据结构与算法》课程中或者在其他同学的博客中有所了解。在此,不在过多地回顾课本理论知识。
我们编码语言主要为C++,如下是二叉树在C++中类的表现形式,这边使用的是 struct 结构体,在C++中类和结构体的区别在于不标志 public , private 以及 protected 时, 成员变量和成员函数是 public 还是 private 以及默认的继承方式是 public 还是 private。使用 struct 的原因在于,结构体更想表达的是一种数据的组织形式,而不是ADT。
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode*(int x):val(x),left(nullptr),right(nullptr){}
};
并且配图可以更好地理解二叉树的结构。
二叉树的遍历方式
在线性结构中,例如,在数组中我们采用索引值(即,地址偏移)的方式去遍历整个数组;在链表中我们采用迭代的方式去遍历整个链表。而二叉树和线性的数组和链表的最大区别在于,二叉树的每一个节点并非只有一个后继节点。因此就有了许多有趣的遍历方式。
分类
按照遍历顺序可以分为三种:
- 前序
- 中序
- 后序
- 层序
按照遍历算法的写法可以分为:
- DFS(对应前中后的递归)
- BFS(对应层序)
还可以根据是否采用迭代写法分为:
- 迭代
- 递归
框架
递归框架(前序中序后序框架)
首先是递归,也是最简单书写的一种,其代码框架如下:
void order(TreeNode * root){
if(root == nullptr) return;
// 前序
order(root->left);
// 中序
order(root->right);
// 后序
}
优缺点
优点:递归很简单,我们只需要记住这一个代码框架就可以完成前中后序遍历;
缺点:递归过程中,从根节点出发不断递归左右节点,这一些函数调用的开销发生在栈空间,使得栈空间消耗巨大,甚至可能会导致栈溢出。
理解
那么,为什么这样就可以实现遍历呢?
-
递归写法必须有一个递归出口,也就是我们框架函数体内第一个语句,也就是我们递归到了叶子节点的时候,会调用
order(叶子节点的左孩子)
和order(叶子节点的右孩子)
。那么这两个调用我们就应该停止递归了,也就是遇到了root==nullptr
这个条件为true
,递归返回。 -
那么对于任意一个非空节点来说我们需要做什么?我们无非是要做 : 访问它,访问它的左孩子,访问它的右孩子这三件事。那么既然有三件事,一定会涉及到顺序问题。这三者的顺序不同,就分出了三种访问顺序:
- 前序:根-左-右
- 中序:左-根-右
- 后序:左-右-根
比如,我们要按照中序的顺序输出二叉树的节点值:
void order(TreeNode* root){ if(root == nullptr) return; order(root->left); /*中序 begin */ cout << root->val << ' '; //中序位置写执行语句,这一个区域写的root就是中序的节点 /*中序 end */ order(root->right); };
迭代框架(前序框架 与 中序框架)
迭代框架没有递归框架那么简单,既没有对前中后序以及层序的通用模板,也没有递归那么语句短小,接下来主要介绍:前序,中序,后序,层序的迭代框架(模板)。
vector<int> PreOrder(TreeNode *root) {
vector<int> res;
if (!root) return res;
stack<TreeNode *> s;
while (root || !s.empty()) {
if (root) {
s.push(root);
// res.push_back(root->val); // PreOrder
root = root->left;
} else {
root = s.top();
s.pop();
// res.push_back(root->val); // InOrder
root = root->right;
}
}
return res;
}
代码在上方,可以看到前序和中序的框架比较相似,在不同的位置执行操作,那么它的遍历顺序就可以不同。
理解
前序遍历,我们总是先读根节点(或者对根节点做一系列操作),如果左孩子存在,再去访问当前根节点的左孩子,那么我们要访问当前节点的左孩子,如果我们需要之后再访问右孩子,我们岂不是无法回到那个根节点了?(树的结构不支持子节点到父节点的指针)。所以,我们需要借助一个辅助栈来存放没有访问过右节点的根节点,等我们把左子树访问完毕之后,再提出之前的根节点,来访问他的右子树。
如图,我们访问了节点1,去访问了节点2,那么我们访问完节点5之后需要回到节点1,再去访问节点3,因此必须把1给保留下来,但是我们不能用单个变量来保存节点1,因为在访问过程中,访问了节点2之后,访问节点4,再之后需要通过节点2,再去访问节点5,这样又需要一个变量来保存节点2,因此再迭代过程中,这个节点记录是一个变长的,所以需要一个辅助栈。
迭代框架(后序框架)
前序和中序遍历的迭代框架是类似的,但是后序遍历的框架截然不同。后序遍历顺序:左-右-根。我们可以看到,后序的遍历顺序和前序遍历顺序(根-左-右)类似。我们可以稍微修改前序遍历的框架,再对数组进行反转,获得后序遍历顺序。
vector<int> PostOrder(TreeNode *root) {
vector<int> res;
if (!root) return res;
stack<TreeNode *> visit;
TreeNode *cur = root;
TreeNode *pre = nullptr;
while (cur != nullptr || !visit.empty()) {
while (cur != nullptr) {
visit.push(cur);
cur = cur->left;
}
cur = visit.top();
if (cur->right == nullptr || cur->right == pre) {
visit.pop();
res.push_back(cur->val);
pre = cur;
cur = nullptr;
} else {
cur = cur->right;
}
}
return res;
}
vector<int> PostOrder_2(TreeNode *root) {
vector<int> res;
if (!root) return res;
stack<TreeNode *> s;
TreeNode *p = root;
while (p != nullptr || !s.empty()) {
if (p != nullptr) {
res.push_back(p->val); //前序的位置
s.push(p);
p = p->right; //区别在于遍历顺序是 根-右-左
}
else if(!s.empty()){
p = s.top();
s.pop();
p = p->left;
}
}
reverse(res.begin(),res.end()); //得到 根-右-左, 反转之后得到 左-右-根
return res;
}
理解
可以看一下以上的代码:PostOrder(TreeNode* root)
是正常思路的后序遍历迭代写法,而 PostOrder_2(TreeNode* root)
是前序遍历反转的写法。个人感觉前序遍历反转的模板和前序中序模板类似,思想方式较为统一。
迭代框架(层序遍历)
层序遍历其实就是BFS的思想,遍历的顺序是按层从低到高来遍历。
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> res;
if(!root) return res;
deque<TreeNode*> q;
q.push_back(root);
while(!q.empty()){
int size = q.size();
res.push_back(vector<int>());
for(int i = 1;i <= size;++i){
TreeNode* node = q.front();q.pop_front();
res.back().push_back(node->val);
if(node->left) q.push_back(node->left);
if(node->right) q.push_back(node->right);
}
}
return res;
}
理解
层序遍历相较于前中后序来说,它访问到了当前节点的兄弟节点以后,我们无法访问前节点的孩子节点。因此,这里不同的是,需要一个辅助的队列来存放同层的节点。
总结
那么,二叉树的常见遍历写法都在这边啦。
递归写法因为每一个节点访问一次,所以时间复杂度为:O(N)
,而递归需要在栈空间里开辟许多空间,平均递归的深度是logN,因此空间复杂度也是:O(logN)
。
迭代写法因为每一个节点访问一次,所以时间复杂度为:O(N)
,而迭代需要一个辅助栈或者辅助队列,平均的情况是把logN的节点放入栈或队列,最差的情况是把大多数节点全部放入栈或队列,因此空间复杂度也是:O(logN)
。
而还有一个遍历写法叫做morris遍历,它能够实现空间复杂度为O(1)
。但是写法较为复杂,准备另外再写一篇关于morris遍历的模板。
如果本文对大家有所帮助,希望能够一键三连鸭!