c++智能指針和二叉樹(1): 圖解層序遍歷和逐層打印二叉樹

二叉樹是極爲常見的數據結構,關於如何遍歷其中元素的文章更是數不勝數。html

然而大多數文章都是講解的前序/中序/後序遍歷,有關逐層打印元素的文章並很少,已有文章的講解也較爲晦澀讀起來不得要領。本文將用形象的圖片加上清晰的代碼幫助你理解層序遍歷的實現,同時咱們使用現代c++提供的智能指針來簡化樹形數據結構的資源管理。node

那麼如今讓咱們進入正題。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);

能夠看到節點一共分紅了四層,如今咱們須要逐層打印,該怎麼作呢?this

層序遍歷

其實思路很簡單,咱們採用廣度優先的思路,先將節點的孩子都打印,而後再去打印子節點的孩子。3d

以上圖爲例,咱們先打印根節點的值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();
}

輸出:

能夠看到上半部分是中序遍歷的結果,下半部分是層序遍歷的輸出,並且是逐行打印的,不過咱們沒有作縮進。因此不太美觀。

另外你可能已經發現了,咱們沒有寫任何有關資源釋放的代碼,沒錯,這就是智能指針的威力,只要注意資源的建立,剩下的事均可以放心得交給智能指針處理,咱們能夠把更多的精力集中在算法和功能的實現上。

智能指針和層序遍歷的內容到這裏就結束了,在下一篇文章中咱們還將看到智能指針和二叉樹的更多操做。

若有錯誤和疑問歡迎指出!

相關文章
相關標籤/搜索