【LeetCode】之字形顺序打印二叉树(层序遍历 / 双端队列 / 双栈),清晰推演过程

一、题目

请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。

例如:
给定二叉树: [3,9,20,null,null,15,7],

  3
 / \
9  20
   /  \
  15   7

返回其层次遍历结果:

[
	[3],
	[20,9],
	[15,7]
]

二、思路分析

这个题目考察的是二叉树的层序遍历的问题。比起普通的层序遍历,它增加了新的要求:按照之子形的顺序来打印。下面我们分别介绍三种实现思路。

1. 层序遍历 + 反转数组

我们在上一篇文章BFS和DFS两种方式实现二叉树的层序遍历中专门介绍了基于广度优先搜索和深度优先搜索来分别实现层序遍历。经过层序遍历得到的结果是这样的:

[
	[3],
	[9,20],
	[15,7]
]

那么我们在此基础上,只需要实现奇数层的数组保持不变,偶数层的数组进行反转。就可以得到我们最终希望得到的结果:

[
	[3],
	[20,9],
	[15,7]
]

那么,只剩下一个问题,如何判断二叉树的一层是奇数层还是偶数层?

最直接的想法是设置一个flag,初始设置为false 表示奇数层;下一层对它取反变为true,表示偶数层;

但还有一个更优雅的方法,不需要设置变量,而是根据最终输出的数组res的元素个数:当元素个数为偶数个数时,当前层是奇数层;当元素个数为奇数个数时,当前层为偶数层。

复杂度分析:

  • 时间复杂度:
    • 每个节点元素进队和出队操作各一次,时间复杂度为 O ( N ) O(N) O(N)
    • 偶数层的元素做反转,即总共有小于N的元素的反转,时间复杂度为 O ( N ) O(N) O(N)
    • 总的时间复杂度为 O ( N ) O(N) O(N)
  • 空间复杂度:队列最大存储个数小于等于N,空间复杂度为 O ( N ) O(N) O(N)

代码实现:

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {

        vector<vector<int>> result;
        if(!root){
            return result;
        }
        
        queue<TreeNode*> q;
        q.push(root);
        while(!q.empty()){
            vector<int> tmp;
            for(int i = q.size(); i > 0; i--){
                TreeNode* node = q.front();
                q.pop();
                tmp.emplace_back(node->val);
                if(node->left) q.push(node->left);
                if(node->right) q.push(node->right);
            }
            // 偶数层的数组进行反转
            if(result.size() % 2 == 1)
                std::reverse(tmp.begin(), tmp.end());
            result.emplace_back(tmp);
        }
        return result;
    } .
};

2. 层序遍历 + 辅助两个栈

我们知道,层序遍历通常是采用队列作为容器来实现的。这是因为队列的特性是先进先出,符合我们按照顺序打印每层节点元素的需求。

但如果我们的需求变成了题目中要求的“按照之字形顺序打印二叉树”,我们就需要思考什么样特性的容器能满足这种要求。

2.1 思考1: 如果仍然使用一个队列,是否可行?

  • 首先放入根节点A到队列,此时队列元素为【A】
  • 根节点A出队列,为了满足第二层是逆序打印,所以我们得先把C放入队列,再把B放入队列。此时队列元素为【C,B】
  • 节点C出队列,同时让子节点G和F入队;节点B出队列,同时让子节点E和D入队;此时队列元素为【G,F,E,D】。这就造成了第三层也只能是逆序打印。无法满足题目中要求的子字形打印的目的。

2.2 思考2:使用一个栈(特性是后进先出),是否可行?

  • 首先放入根节点A到栈,此时栈内元素为【A】
  • 栈顶元素根节点A出栈,同时将子节点B和子节点C入栈。此时栈内元素为【B, C】
  • 按照后进先出,栈顶元素节点C出栈,此时将节点C的两个子节点入栈。就会造成一个问题:第三层的两个子节点入栈后,它们出栈的顺序就会在第二层的节点B前面。那么打印的顺序就会乱了。也不符合题目中的要求。

2.3 思考3:采用两个栈,是否可行?

经过2.1和2.2 的分析,普通的一个队列或者一个栈,都是无法直接满足题目要求的。

在2.2中出现的问题是,第三层的结点入栈导致第二层的结点无法及时出栈。那么如果我们采用两个栈,分别存储奇数层的节点和偶数层的节点,是否可行呢?下面我们来推演一下。

  • 设置两个栈s1和s2
  • 首先根节点入栈s1, 此时s1栈内元素为【A】,s2栈内元素为空。
  • s1栈顶元素A出栈,同时将子节点B和子节点C 分别放入s2栈内。此时s1栈内元素为空,s2栈内元素为【B, C】。
  • s2栈顶元素C出栈,同时将子节点G和子节点F分别放入s1栈内;接着,s2栈顶元素B出栈,同时将子节点E和子节点D分别放入s1栈内。此时s1栈内元素为【G, F, E, D】,s2栈内元素为空。
  • …持续这个过程,直到栈s1和栈s2均为空时结束。

可见,两个栈是可行的。

复杂度分析:

  • 时间复杂度:每个节点元素进栈和出栈操作各一次,时间复杂度为 O ( N ) O(N) O(N)
  • 空间复杂度:栈s1和栈s2, 两个栈分别存储奇数层的节点和偶数层的节点,即存储最大容量也不超过两层节点的个数,小于N。因此空间复杂度为 O ( N ) O(N) O(N)

代码实现:

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> res;
        if(!root) return res;

        stack<TreeNode*> s1;
        stack<TreeNode*> s2;
        s1.push(root);

        while(!s1.empty() || !s2.empty()){
            vector<int> tmp;
            if(!s1.empty()){
                while(!s1.empty()){
                    TreeNode* node = s1.top();
                    s1.pop();
                    tmp.emplace_back(node->val);
                    if(node->left){
                        s2.push(node->left);
                    }
                    if(node->right){
                        s2.push(node->right);
                    }
                }
            }else{
                while(!s2.empty()){
                    TreeNode* node = s2.top();
                    s2.pop();
                    tmp.emplace_back(node->val);
                    if(node->right){
                        s1.push(node->right);
                    }
                    if(node->left){
                        s1.push(node->left);
                    }
                }
            }
            res.emplace_back(tmp);
        }
        return res;

        
    }
};

3. 层序遍历 + 双端队列

接着上文2.2 小节的内容继续分析,我们知道了普通的一个队列或者一个栈,都是无法直接满足题目要求的。那么我们可以使用一个特殊的队列——双端队列。双端队列的特性是队列头部和尾部都可以入队和出队。

实现思路是:

  • 对于二叉树的奇数层,从队首取元素,下一层的节点都放到队尾。放的顺序是先左子节点,后右子节点。
  • 对于二叉树的偶数层,从队尾取元素,下一层的节点都放到队首。放的顺序是先右子节点,后左子节点。

复杂度分析:

  • 时间复杂度:每个节点元素进队和出队操作各一次,时间复杂度为 O ( N ) O(N) O(N)
  • 空间复杂度:队列最大存储个数小于等于N,空间复杂度为 O ( N ) O(N) O(N)
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> res;
        if(!root) return res;

        deque<TreeNode*> q;
        q.push_back(root);

        while(!q.empty()){
            vector<int> tmp;
            if(res.size()%2==0){
                for(int i=q.size(); i>0 ; i--){
                    TreeNode* node = q.front();
                    q.pop_front();
                    tmp.emplace_back(node->val);
                    if(node->left){
                        q.push_back(node->left);
                    }
                    if(node->right){
                        q.push_back(node->right);
                    }
                }
            }else{
                for(int i=q.size(); i>0 ; i--){
                    TreeNode* node = q.back();
                    q.pop_back();
                    tmp.emplace_back(node->val);
                    if(node->right){
                        q.push_front(node->right);
                    }
                    if(node->left){
                        q.push_front(node->left);
                    }
                }
            }
            res.emplace_back(tmp);
        }
        return res;

    }
};

三、C++ 知识扩展

在之前文章剑指 Offer 32 - I. 从上到下打印二叉树中我们介绍了C++标准库中队列queue的基础知识。在本文中,又使用了栈和双堆队列这两种数据结构。另外本文针对数组反转,也使用了C++库的函数reserve。下面我们展开介绍一下这些内容。

1. 栈stack

头文件为

#include<stack>

定义

stack<type> s;   // 这里type可以是int,float,结构体等数据类型

常用操作:

s.push(e)               //入栈元素e
s.pop()                 //弹出栈顶元素
s.top()                 //获取栈顶元素
s.size()                //返回栈内元素个数
s.empty()               //判断栈是否为空

2. 双端队列

头文件为

#include<deque>

定义

deque<type> deq;   // 这里type可以是int,float,结构体等数据类型

常用操作:

deq.push_front(e)               //队列头部插入元素e
deq.pop_front()					//弹出队列头部元素
deq.push_back(e)                //队列尾部插入元素e
deq.pop_back()					//弹出队列尾部元素
deq.front()						//返回队列头部元素
deq.back()						//返回队列尾部元素
deq.size()						//获取队列元素个数
deq.empty()						//盘对队列是否为空

3. reserve函数

功能:反转在一个区间[first, last)内元素的顺序。注意:这个区间是前开后闭区间,包括first指向的元素,不包括last指向的元素。

#include <iostream>     // std::cout
#include <algorithm>    // std::reverse
#include <vector>       // std::vector

int main () {
  std::vector<int> myvector;
  // set some values:
  for (int i=1; i<10; ++i) myvector.push_back(i);   // 1 2 3 4 5 6 7 8 9
  // reverse
  std::reverse(myvector.begin(),myvector.end());    // 9 8 7 6 5 4 3 2 1
  return 0;
}

四、总结

本文使用了三种方式实现了按照之字形顺序打印二叉树,包括标准的层序遍历与反转数组的结合、两个栈实现的层序遍历、双端队列实现的层序遍历。

也介绍了在算法实现过程中使用到的stack、deque、reverse 这样的C++语法,使得后续能够更熟练的利用C++语言来实现自己的算法。

如果大家在阅读过程中,觉得哪里有疑问的地方,欢迎留言讨论。
在这里插入图片描述
关注【CV面试宝典】,专注于经典论文、工业实践、面试真题、竞赛技巧的分享交流。

后台回复【opencv】下载二十余部经典OpenCV书籍和配套源码!

后台回复【c++】下载C++ primer、C++编程规范、Accelerated C++等经典书籍!

还有若干深林火情、城市烟火、吸烟、横幅、安全帽、车牌、object 365等数据集不便直接分享,学习用途的话可以后台和我留言沟通

猜你喜欢

转载自blog.csdn.net/u010414589/article/details/115273284