搜尋
首頁後端開發C#.Net教程c++ 圖解層序遍歷與逐層列印智慧指標所建造的二元樹

二元樹是極為常見的資料結構,關於如何遍歷其中元素的文章更是無數。然而大多數文章都是講解的前序/中序/後序遍歷,有關逐層打印元素的文章並不多,已有文章的講解也較為晦澀讀起來不得要領。本文將以形象的圖片加上清晰的程式碼幫助你理解層序遍歷的實現,同時我們使用現代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);

c++ 圖解層序遍歷與逐層列印智慧指標所建造的二元樹

可以看到節點一共分成了四層,現在我們需要逐層列印,該怎麼做呢?

層序遍歷

其實思路很簡單,我們採用廣度優先的思路,先將節點的孩子都列印,然後再去列印子節點的孩子。

以上圖為例,我們先列印根節點的值3,然後我們再列印它的所有子節點的值,是1 5,然後是左右子節點的子節點,以此類推。 。 。 。 。 。

說起來很簡單,但程式碼寫起來卻會遇到麻煩。我們不能簡單得像中序遍歷時那樣使用遞歸來解決問題(事實上可以用改進的遞歸演算法),因為它會直接來到葉子節點處,這不是我們想要的結果。不過不要緊,我們可以藉助於隊列,把子節點隊列添加到隊列末尾,然後從隊列開頭也就是根節點處遍歷,將其子節點添加進隊列,隨後再對第二個節點做同樣的操作,遇到一行結束的地方,我們使用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;
}

程式碼本身並不複雜,重要的是背後的想法。

演算法圖解

如果你第一遍並沒有讀懂這段程式碼也不要緊,下面我們有請圖解上線:

先是迴圈開始時的狀態,第一行的內容已經確定了(^代表空指標):

c++ 圖解層序遍歷與逐層列印智慧指標所建造的二元樹

然後我們從首元素開始遍歷,第一個遍歷到的是root,他有兩個孩子,值分別是1和5:

c++ 圖解層序遍歷與逐層列印智慧指標所建造的二元樹

接著索引值1,這次遍歷到的是nullptr,因為不是在佇列末尾,所以我們簡單地加入一個nullptr在佇列末尾,這樣第二行的節點就都在佇列中了:

c++ 圖解層序遍歷與逐層列印智慧指標所建造的二元樹

然後我們開始遍歷第二行的節點,將它們的子節點當作第三行的內容放入佇列,最後加上一個行分隔符,以此類推:

c++ 圖解層序遍歷與逐層列印智慧指標所建造的二元樹

#簡單來說,就是透過佇列來快取上一行的所有節點,然後再根據上一行的快取得到下一行的所有節點,循環往復直到二元樹的最後一層。當然不只是二元樹,其他多叉樹的層序遍歷也可以用類似的想法來實現。

好了,知道如何取得每一行的內容,我們就能逐行處理節點了:

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();
}

输出:

c++ 圖解層序遍歷與逐層列印智慧指標所建造的二元樹

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

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

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

以上是c++ 圖解層序遍歷與逐層列印智慧指標所建造的二元樹的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文轉載於:博客园。如有侵權,請聯絡admin@php.cn刪除
C#和.NET運行時:它們如何一起工作C#和.NET運行時:它們如何一起工作Apr 19, 2025 am 12:04 AM

C#和.NET運行時緊密合作,賦予開發者高效、強大且跨平台的開發能力。 1)C#是一種類型安全且面向對象的編程語言,旨在與.NET框架無縫集成。 2).NET運行時管理C#代碼的執行,提供垃圾回收、類型安全等服務,確保高效和跨平台運行。

C#.NET開發:入門的初學者指南C#.NET開發:入門的初學者指南Apr 18, 2025 am 12:17 AM

要開始C#.NET開發,你需要:1.了解C#的基礎知識和.NET框架的核心概念;2.掌握變量、數據類型、控制結構、函數和類的基本概念;3.學習C#的高級特性,如LINQ和異步編程;4.熟悉常見錯誤的調試技巧和性能優化方法。通過這些步驟,你可以逐步深入C#.NET的世界,並編寫高效的應用程序。

c#和.net:了解兩者之間的關係c#和.net:了解兩者之間的關係Apr 17, 2025 am 12:07 AM

C#和.NET的關係是密不可分的,但它們不是一回事。 C#是一門編程語言,而.NET是一個開發平台。 C#用於編寫代碼,編譯成.NET的中間語言(IL),由.NET運行時(CLR)執行。

c#.net的持續相關性:查看當前用法c#.net的持續相關性:查看當前用法Apr 16, 2025 am 12:07 AM

C#.NET依然重要,因為它提供了強大的工具和庫,支持多種應用開發。 1)C#結合.NET框架,使開發高效便捷。 2)C#的類型安全和垃圾回收機制增強了其優勢。 3).NET提供跨平台運行環境和豐富的API,提升了開發靈活性。

從網絡到桌面:C#.NET的多功能性從網絡到桌面:C#.NET的多功能性Apr 15, 2025 am 12:07 AM

C#.NETisversatileforbothwebanddesktopdevelopment.1)Forweb,useASP.NETfordynamicapplications.2)Fordesktop,employWindowsFormsorWPFforrichinterfaces.3)UseXamarinforcross-platformdevelopment,enablingcodesharingacrossWindows,macOS,Linux,andmobiledevices.

C#.NET與未來:適應新技術C#.NET與未來:適應新技術Apr 14, 2025 am 12:06 AM

C#和.NET通過不斷的更新和優化,適應了新興技術的需求。 1)C#9.0和.NET5引入了記錄類型和性能優化。 2).NETCore增強了雲原生和容器化支持。 3)ASP.NETCore與現代Web技術集成。 4)ML.NET支持機器學習和人工智能。 5)異步編程和最佳實踐提升了性能。

c#.net適合您嗎?評估其適用性c#.net適合您嗎?評估其適用性Apr 13, 2025 am 12:03 AM

c#.netissutableforenterprise-levelapplications withemofrosoftecosystemdueToItsStrongTyping,richlibraries,androbustperraries,androbustperformance.however,itmaynotbeidealfoross-platement forment forment forment forvepentment offependment dovelopment toveloperment toveloperment whenrawspeedsportor whenrawspeedseedpolitical politionalitable,

.NET中的C#代碼:探索編程過程.NET中的C#代碼:探索編程過程Apr 12, 2025 am 12:02 AM

C#在.NET中的編程過程包括以下步驟:1)編寫C#代碼,2)編譯為中間語言(IL),3)由.NET運行時(CLR)執行。 C#在.NET中的優勢在於其現代化語法、強大的類型系統和與.NET框架的緊密集成,適用於從桌面應用到Web服務的各種開發場景。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SecLists

SecLists

SecLists是最終安全測試人員的伙伴。它是一個包含各種類型清單的集合,這些清單在安全評估過程中經常使用,而且都在一個地方。 SecLists透過方便地提供安全測試人員可能需要的所有列表,幫助提高安全測試的效率和生產力。清單類型包括使用者名稱、密碼、URL、模糊測試有效載荷、敏感資料模式、Web shell等等。測試人員只需將此儲存庫拉到新的測試機上,他就可以存取所需的每種類型的清單。

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )專業的PHP整合開發工具

Atom編輯器mac版下載

Atom編輯器mac版下載

最受歡迎的的開源編輯器

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

強大的PHP整合開發環境