c++智能指针和二叉树(1): 图解层序遍历和逐层打印二叉树

二叉树是极为常见的数据结构,关于如何遍历其中元素的文章更是数不胜数。

然而大多数文章都是讲解的前序/中序/后序遍历,有关逐层打印元素的文章并不多,已有文章的讲解也较为晦涩读起来不得要领。本文将用形象的图片加上清晰的代码帮助你理解层序遍历的实现,同时我们使用现代c++提供的智能指针来简化树形数据结构的资源管理。

那么现在让我们进入正题。

使用智能指针构建二叉树

我们这里所要实现的是一个简单地模拟了二叉搜索树的二叉树,提供符合二叉搜索树的要求的插入功能个中序遍历。同时我们使用shared_ptr来管理资源。

现在我们只实现insertldr两个方法,其余方法的实现并不是本文所关心的内容,不过我们会在后续的文章中逐个介绍:

struct BinaryTreeNode: public std::enable_shared_from_this<BinaryTreeNode> {
    explicit BinaryTreeNode(const int value = 0)
    : value_{value}, left{std::shared_ptr<BinaryTreeNode>{}}, right{std::shared_ptr<BinaryTreeNode>{}}
    {}

    void insert(const int value)
    {
        if (value < value_) {
            if (left) {
                left->insert(value);
            } else {
                left = std::make_shared<BinaryTreeNode>(value);
            }
        }

        if (value > value_) {
            if (right) {
                right->insert(value);
            } else {
                right = std::make_shared<BinaryTreeNode>(value);
            }
        }
    }

    // 中序遍历
    void ldr()
    {
        if (left) {
            left->ldr();
        }

        std::cout << value_ << "\n";

        if (right) {
            right->ldr();
        }
    }

    // 分层打印
    void layer_print();

    int value_;
    // 左右子节点
    std::shared_ptr<BinaryTreeNode> left;
    std::shared_ptr<BinaryTreeNode> right;

private:
    // 层序遍历
    std::vector<std::shared_ptr<BinaryTreeNode>> layer_contents();
};

我们的node对象继承自enable_shared_from_this,通常这不是必须的,但是为了在层序遍历时方便操作,我们需要从this构造智能指针,因此这步是必须的。insert会将比root小的元素插入左子树,比root大的插入到右子树;ldr则是最为常规的中序遍历,这里实现它是为了以常规方式查看tree中的所有元素。

值得注意的是,对于node节点我们最好使用make_shared进行创建,而不是将其初始化为全局/局部对象,否则在层序遍历时会因为shared_ptr的析构进而导致对象被销毁,从而引发未定义行为。

现在假设我们有一组数据:[3, 1, 0, 2, 5, 4, 6, 7],将第一个元素作为root,将所有数据插入我们的树中会得到如下的一棵二叉树:

auto root = std::make_shared<BinaryTreeNode>(3);
root->insert(1);
root->insert(0);
root->insert(2);
root->insert(5);
root->insert(4);
root->insert(6);
root->insert(7);

可以看到节点一共分成了四层,现在我们需要逐层打印,该怎么做呢?

层序遍历

其实思路很简单,我们采用广度优先的思路,先将节点的孩子都打印,然后再去打印子节点的孩子。

以上图为例,我们先打印根节点的值3,然后我们再打印它的所有子节点的值,是15,然后是左右子节点的子节点,以此类推。。。。。。

说起来很简单,但是代码写起来却会遇到麻烦。我们不能简单得像中序遍历时那样使用递归来解决问题(事实上可以用改进的递归算法),因为它会直接来到叶子节点处,这不是我们想要的结果。不过不要紧,我们可以借助于队列,把子节点队列添加到队列末尾,然后从队列开头也就是根节点处遍历,将其子节点添加进队列,随后再对第二个节点做同样的操作,遇到一行结束的地方,我们使用nullptr做标记。

先看具体的代码:

std::vector<std::shared_ptr<BinaryTreeNode>>
BinaryTreeNode::layer_contents()
{
    std::vector<std::shared_ptr<BinaryTreeNode>> nodes;
    // 先添加根节点,根节点自己就会占用一行输出,所以添加了作为行分隔符的nullptr
    // 因为需要保存this,所以这是我们需要继承enable_shared_from_this是理由
    // 同样是因为这里,当返回的结果容器析构时this的智能指针也会析构
    // 如果我们使用了局部变量则this的引用计数从1减至0,导致对象被销毁,而使用了make_shared创建的对象引用计数是从2到1,没有问题
    nodes.push_back(shared_from_this());
    nodes.push_back(nullptr);
    // 我们使用index而不是迭代器,是因为添加元素时很可能发生迭代器失效,处理这一问题将会耗费大量精力,而index则无此烦恼
    for (int index = 0; index < nodes.size(); ++index) {
        if (!nodes[index]) {
            // 子节点打印完成或已经遍历到队列末尾
            if (index == nodes.size()-1) {
                break;
            }

            nodes.push_back(nullptr); // 添加分隔符
            continue;
        }

        if (nodes[index]->left) { // 将当前节点的子节点都添加进队列
            nodes.push_back(nodes[index]->left);
        }
        if (nodes[index]->right) {
            nodes.push_back(nodes[index]->right);
        }
    }

    return nodes;
}

代码本身并不复杂,重要的是其背后的思想。

算法图解

如果你第一遍并没有读懂这段代码也不要紧,下面我们有请图解上线:

首先是循环开始时的状态,第一行的内容已经确定了(^代表空指针):

然后我们从首元素开始遍历,第一个遍历到的是root,他有两个孩子,值分别是1和5:

接着索引值+1,这次遍历到的是nullptr,因为不是在队列末尾,所以我们简单添加一个nullptr在队列末尾,这样第二行的节点就都在队列中了:

随后我们开始遍历第二行的节点,将它们的子节点作为第三行的内容放入队列,最后加上一个行分隔符,以此类推:

简单来说,就是通过队列来缓存上一行的所有节点,然后再根据上一行的缓存得到下一行的所有节点,循环往复直到二叉树的最后一层。当然不只是二叉树,其他多叉树的层序遍历也可以用类似的思想实现。

好了,知道了如何获取每一行的内容,我们就能逐行处理节点了:

void BinaryTreeNode::layer_print()
{
    auto nodes = layer_contents();
    for (auto iter = nodes.begin(); iter != nodes.end(); ++iter) {
        // 空指针代表一行结束,这里我们遇到空指针就输出换行符
        if (*iter) {
            std::cout << (*iter)->value_ << " ";
        } else {
            std::cout << "\n";
        }
    }
}

如你所见,这个方法足够简单,我们把节点信息保存在额外的容器中是为了方便做进一步的处理,如果只是打印的话大可不必这么麻烦,不过简单通常是有代价的。对于我们的实现来说,分隔符的存在简化了我们对层级之间的区分,然而这样会导致浪费至少log2(n)+1个vector的存储空间,某些情况下可能引起性能问题,而且通过合理得使用计数变量可以避免这些额外的空间浪费。当然具体的实现读者可以自己挑战一下,原理和我们上面介绍的是类似的因此就不在赘述了,也可以参考园内其他的博客文章。

测试

最后让我们看看完整的测试程序,记住要用make_shared创建root实例:

int main()
{
    auto root = std::make_shared<BinaryTreeNode>(3);
    root->insert(1);
    root->insert(0);
    root->insert(2);
    root->insert(5);
    root->insert(4);
    root->insert(6);
    root->insert(7);
    root->ldr();
    std::cout << "\n";
    root->layer_print();
}

输出:

可以看到上半部分是中序遍历的结果,下半部分是层序遍历的输出,而且是逐行打印的,不过我们没有做缩进。所以不太美观。

另外你可能已经发现了,我们没有写任何有关资源释放的代码,没错,这就是智能指针的威力,只要注意资源的创建,剩下的事都可以放心得交给智能指针处理,我们可以把更多的精力集中在算法和功能的实现上。

智能指针和层序遍历的内容到这里就结束了,在下一篇文章中我们还将看到智能指针和二叉树的更多操作。

如有错误和疑问欢迎指出!

原文地址:https://www.cnblogs.com/apocelipes/p/10758692.html

时间: 2024-07-30 09:37:32

c++智能指针和二叉树(1): 图解层序遍历和逐层打印二叉树的相关文章

SDUT 3344 数据结构实验之二叉树五:层序遍历

数据结构实验之二叉树五:层序遍历 Time Limit: 1000MS Memory Limit: 65536KB Submit Statistic Problem Description 已知一个按先序输入的字符序列,如abd,,eg,,,cf,,,(其中,表示空结点).请建立二叉树并求二叉树的层次遍历序列. Input 输入数据有多行,第一行是一个整数t (t<1000),代表有t行测试数据.每行是一个长度小于50个字符的字符串. Output 输出二叉树的层次遍历序列. Example I

按层打印二叉树--每行打印一层

一,问题介绍 给定一棵二叉树,按照层序遍历的顺序打印二叉树.但是要求,每一行打印一层数据. 二,算法分析 借助二叉树的层序遍历来实现.但是需要额外两个变量.一个变量用来保存当前层 还未打印的结点个数,另一个变量保存下一层待打印的结点个数. 二叉树层序遍历参考:http://www.cnblogs.com/hapjin/p/5409921.html 层序打印参考: 1 public void printTree(BinaryNode<T> root){ 2 if(root == null) 3

C++培训 C++ 智能指针详解

C++培训 C++ 智能指针详解 一.简介 由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete.程序员忘记 delete,流程太复杂,最终导致没有 delete,异常导致程序过早退出,没有执行 delete 的情况并不罕见. 用智能指针便可以有效缓解这类问题,本文主要讲解参见的智能指针的用法.包括:std::auto_ptr.boost::scoped_ptr.boost::shared_ptr.boost::scoped_array.boost::sh

剑指Offer对答如流系列 - 从上往下打印二叉树

面试题32:从上往下打印二叉树 题目描述 树的结构定义如下: public class Node{ int e; Node left; Node right; Node(int x) { e = x; } } (一)不分行从上到下打印二叉树 从上往下打印出二叉树的每个结点,同一层的结点按照从左到右的顺序打印. 比如下面二叉树,输出顺序为 8 6 10 5 7 9 11 (二)分行从上到下打印二叉树 从上到下按层打印二叉树,同一层的结点按从左到右的顺序打印,每一层打印到一行. 比如下面二叉树,输出

C++11 智能指针unique_ptr使用 -- 以排序二叉树为例

用智能指针可以简化内存管理.以树为例,如果用普通指针,通常是在插入新节点时用new,在析构函数中调用delete:但有了unique_ptr类型的智能指针,就不需要在析构函数中delete了,因为当unique_ptr类型的指针P生命结束时(比如对于局部变量,程序执行到局部变量的作用域范围之外),P会自动delete它拥有的资源(指针指向的空间).对于shared_ptr,情况更加复杂一些,shared_ptr会维护一个use count,即有多少个指针共享这一资源,当use count为0时,

C++智能指针剖析(下)boost::shared_ptr&amp;其他

1. boost::shared_ptr 前面我已经讲解了两个比较简单的智能指针,它们都有各自的优缺点.由于 boost::scoped_ptr 独享所有权,当我们真真需要复制智能指针时,需求便满足不了了,如此我们再引入一个智能指针,专门用于处理复制,参数传递的情况,这便是如下的boost::shared_ptr. boost::shared_ptr 属于 boost 库,定义在 namespace boost 中,包含头文件#include<boost/smart_ptr.hpp> 便可以使

c++智能指针以及循环引用问题(转)

解决循环引用: 在知道存在循环引用的条件下,使用boost::weak_ptr,即弱引用来代替循环引用中的某个强引用,从而打破循环引用的环. 由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete,比如流程太复杂,最终导致没有 delete,异常导致程序过早退出,没有执行 delete 的情况并不罕见,并造成内存泄露.如此c++引入 智能指针 . c++ 智能指针主要包括:unique_ptr,shared_ptr, weak_ptr, 这三种,其中auto

二叉树层序遍历的实现

我们可以很容易的使用队列来实现二叉树的层序遍历,代码如下: 1 #include <stdio.h> 2 #include <stdlib.h> 3 #define MAX 10 4 5 6 //二叉树存储结构定义 7 typedef char Item; 8 typedef struct node *link; 9 struct node {Item item; link l, r;}; 10 11 int Create(link *tp); 12 void show(link

编程之美问题之二叉树层序遍历多种解法

二叉树的层序遍历(要求区分层,例如每层遍历完输出换行) 单单层序遍历非常简单,一个队列就搞定了,但是区分层则要麻烦些.总的思路无非就是在每次print的时候,要能通过某个东西 区分出当前节点是否是一层最后一个节点,或者下一层的最后一个节点,感觉有点类似于机器学习中找个区分度明显的特征: 1.自己的解法,在单队列基础上,输入队列的数据添加一个标志 ,LevelHeaded,同时,在后面插入两个孩子的时候,判断是否这次输出的是队头,如果是的话,先插 个队头标志,再插入孩子,而且插入一次之后,后面不能