本文归纳学习总结自Carl大佬的公众号代码随想录关于二叉树的几篇推文。如若侵权,请联系删除!其中一篇推文链接
二叉树是一种重要的数据结构,在应用和面试中经常出现,这里对二叉树的几种遍历方法进行了总结。
总的来说二叉树的遍历分为深度遍历,广度遍历,方法也有迭代法和递归法。递归法易于理解,但一定要掌握迭代法。
深度遍历主要包括前序遍历、中序遍历和后序遍历,前中后都是指的节点的访问次序。
图引自代码随想录
写递归主要是把握住以下三个步骤:
1.确定递归函数的参数和返回值:确定那些参数需要“递”写入形参中,根据“归”的数据确定返回值类型。
2.确定终止条件:这是非常重要的一步,如果终止条件没写对,递归没有终止,系统的内存栈必然溢出。
3.确定单层递归操作:确定相应的逻辑操作,当然里面肯定要包括对自己的调用!
前序遍历
1.确定递归函数的参数和返回值
需要传入节点的指针和保存遍历结果的数组,同时注意为了不使用返回数组传递,这里需要对数组采用引用的方式。
void pretrval(Treenode* cur,vector
2.确定终止条件
当遇到空节点时,可认为终止,无需继续向下遍历。
if(node==nullptr)
{
return;//这里返回值类型为void,所以可以只写return
}
3.确定单层递归操作
单层的操作就是,将此节点的数值存入vec数组中,然后继续对左子树和右子树进行遍历。
vec.push_back(cur->val);//中
pretrval(cur->left);//左 这里不需要判断cur->val是否为空,因为执行此语句后带入后,终止条件会判断
pretrval(cur->right);//右
将三者结合起来,就是完整的代码了
class Solution {
public:
void pretravl(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
vec.push_back(cur->val); // 中
pretravl(cur->left, vec); // 左
pretravl(cur->right, vec); // 右
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
pretravl(root, result);
return result;
}
};
通过递归过程大家也能明白为什么叫深度搜索了吧,它会一直优先递归左子树,直到左子树为空(达到左子树最深处),才会”归“回上一步,继续递归右子树。
中序遍历
递归法一个很大的优点是便于理解学习,只要稍作改动就可以根据一种遍历次序,写出其他两种遍历算法。
class Solution {
public:
void midtravl(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
midtravl(cur->left, vec); // 左
vec.push_back(cur->val); // 中
midtravl(cur->right, vec); // 右
}
vector<int> midorderTraversal(TreeNode* root) {
vector<int> result;
midtravl(root, result);
return result;
}
};
后序遍历
class Solution {
public:
void lsttravl(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
lsttravl(cur->left, vec); // 左
lsttravl(cur->right, vec); // 右
vec.push_back(cur->val); // 中
}
vector<int> lstorderTraversal(TreeNode* root) {
vector<int> result;
lsttravl(root, result);
return result;
}
};
递归的底层实现就是栈,所以,是否可以试着用递归和栈实现以上的遍历呢???
栈是(FILO)型的数据结构,所以要仔细考虑入栈顺序。
前序遍历
class Solution
{
public:
vector<int> preorderTraversal(TreeNode* root)
{
stack<TreeNode*>st;
if(root)
{
st.push(root);
}
vector<int>result;
while(!st.empty())
{
TreeNode* node=st.top();
st.pop();
result.push_back(node->val);
if(node->right)
{
st.push(node->right);
}
if(node->left)
{
st.push(node->left);
}
}
return result;
}
};
上面压入栈中的确保了不为空指针,因为在加入左右孩子节点时都先进行了判断;当然还可以换另一种写法,在内部判断,不为空遍历此节点,为空时就pop()掉,继续下一次遍历。
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> result;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top(); // 中
st.pop();
if (node != NULL)
{
result.push_back(node->val);
}
else
{
continue;//continue不在执行加入节点操作,总会把之前加入的多个nullptr全部pop()掉,使st为空结束循环。
}
st.push(node->right); // 右
st.push(node->left); // 左
}
return result;
}
};
后序遍历
后序遍历如果直接用栈实现还是比较难处理的,这是因为访问顺序和处理顺序不一致。后序遍历时,会先访问节点(中),然后根据节点访问左右孩子(左右/右左),但使用栈时无法实现节点(中)在最后出栈。
细心的同学应该可以发现,使用栈只能实现”中“先出栈的情况,所以也可以实现中右左的出栈顺序,然后对结果反转就可以实现后序遍历左右中。
变换如下:
前序遍历(中左右)—>中右左—>左右中
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> result;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top(); // 中
st.pop();
if (node != NULL) result.push_back(node->val);
else continue;
st.push(node->left); // 左
st.push(node->right); // 右
}
reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了
return result;
}
};
中序遍历
中序遍历放在最后是因为,它无法通过前序遍历稍作修改实现。
迭代过程主要有两个操作:
1.处理:将元素放进result数组中
2.访问:遍历节点
因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。
但中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。
小结:用栈(先处理后访问)的方式只能实现前序遍历(中左右)和伪前序遍历(中右左),后序遍历之所以能够实现的原因是在可以由伪前序遍历反转得到。
使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
中序遍历的迭代法容易忘!多学习
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
TreeNode* cur = root;
while (cur != NULL || !st.empty()) {
if (cur != NULL) { // 指针来访问节点,访问到最底层
st.push(cur); // 讲访问的节点放进栈
cur = cur->left; // 左
} else {
cur = st.top(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
st.pop();
result.push_back(cur->val); // 中
cur = cur->right; // 右
}
}
return result;
}
};
上面迭代法其实是分成了两类,这里可以使用nullptr标记法实现稍作统一的迭代法,只需要稍微修改就可以任意更改为前、中、后序遍历。
前序遍历
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if(root) st.push(root);//先访问的中
while (!st.empty())
{
TreeNode* cur=st.top();
st.pop();//需要弹出中,访问过,但弹出未处理,一会再加入时需要标记
if(cur!=nullptr)
{
if (cur->right)
{
st.push(cur->right);
}
if (cur->left)
{
st.push(cur->left);
}
st.push(cur);
st.push(nullptr);//前序遍历需要中节点最先出栈,所以弹出后加入左右孩子后在重新入栈
}
else
{
cur=st.top();
st.pop(); // 把标记nullptr弹出后,在从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
result.push_back(cur->val); // 中
}
}
return result;
}
};
中序遍历
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if(root) st.push(root);//先访问的中
while (!st.empty())
{
TreeNode* cur=st.top();
st.pop();//需要弹出中,访问过,但弹出未处理,一会再加入时需要标记
if(cur!=nullptr)
{
if (cur->right)
{
st.push(cur->right);
}
st.push(cur);
st.push(nullptr);
if (cur->left)
{
st.push(cur->left);
}
}
else
{
cur=st.top();
st.pop(); // 把标记nullptr弹出后,在从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
result.push_back(cur->val); // 中
}
}
return result;
}
};
后序遍历
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if(root) st.push(root);//先访问的中
while (!st.empty())
{
TreeNode* cur=st.top();
st.pop();//需要弹出中,访问过,但弹出未处理,一会再加入时需要标记
if(cur!=nullptr)
{
st.push(cur);
st.push(nullptr);
if (cur->right)
{
st.push(cur->right);
}
if (cur->left)
{
st.push(cur->left);
}
}
else
{
cur=st.top();
st.pop(); // 把标记nullptr弹出后,在从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
result.push_back(cur->val); // 中
}
}
return result;
}
};
上面三种方法是不是有一种和谐统一之美啊!
层序遍历需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而是用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> que;
if (root != NULL) que.push(root);
vector<vector<int>> result;
while (!que.empty()) {
int size = que.size();
vector<int> vec;
// 这里一定要使用固定大小size,不要使用que.size(),因为que.size是不断变化的
for (int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
vec.push_back(node->val);
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
result.push_back(vec);
}
return result;
}
};
会了二叉树,N叉树自然就会了,不过是在添加节点时不只有左右孩子,还可能有三个或多个孩子,只需要判断孩子的个数,添加一个循环即可。
可以用leetcodde429练习!