CD71.【C++ Dev】二叉樹的三種非遞歸遍歷方式

目錄

1.知識回顧

2.前序遍歷

分析

總結入棧的幾種可能

循環的條件

代碼

提交結果

3.中序遍歷

分析

代碼

提交結果

3.★后序遍歷

分析

問題:如何確定是第一次訪問到棧的元素還是第二次訪問到棧中的元素?

方法1:使用填充的內存(依賴于架構)

判斷計算機使用的架構和指針大小

代碼

提交結果

方法2:尋找規律,設置一個prev變量記錄上一個訪問的節點

代碼

運行結果

方法3:使用標記棧

代碼

運行結果


1.知識回顧

106.【C語言】數據結構之二叉樹的三種遞歸遍歷方式

L17.【LeetCode題解】前序遍歷

L18.【LeetCode題解】中序遍歷和后序遍歷

2.前序遍歷

https://leetcode.cn/problems/binary-tree-preorder-traversal/

給你二叉樹的根節點 root ,返回它節點值的?前序?遍歷。

示例 1:

輸入:root = [1,null,2,3]

輸出:[1,2,3]

解釋:

示例 2:

輸入:root = [1,2,3,4,5,null,8,null,null,6,7,9]

輸出:[1,2,4,5,6,7,3,8,9]

解釋:

示例 3:

輸入:root = []

輸出:[]

示例 4:

輸入:root = [1]

輸出:[1]

提示:

  • 樹中節點數目在范圍 [0, 100]
  • -100 <= Node.val <= 100

進階:遞歸算法很簡單,你可以通過迭代算法完成嗎?

分析

以這棵樹[8,3,10,1,6,null,null,null,null,4,7]為例分析:

前序遍歷必然最先訪問左路節點,其次是左路節點的右子樹,需要一直重復這個過程

那么可以分成兩部分:

(紅色是左路節點,藍色是左路節點的右子樹)

訪問順序:8→3→1→1的右子樹→3的右子樹→8的右子樹

會發現:記錄8→3→1遍歷順序 又要訪問1、3、8的右子樹(恰好是8、3、1倒過來的順序)

可以利用棧:先訪問左路節點(左路節點入前序遍歷數組),后左路節點入棧(入棧的是節點的指針),訪問左路節點的右子樹需要使用棧頂元素,彈出的節點記錄到前序遍歷的數組中,而且需要使用一個指針cur從根節點開始遍歷樹

1. cur指向一個節點意味著訪問一棵樹的開始
2. 棧里面的節點意味著要訪問它的右子樹

樹分成左路節點和右子樹 右子樹再分成左路節點和右子樹 右子樹再再分成左路節點和右子樹……

先入左節點:

1的左子樹為空,需要彈出棧頂元素,訪問右節點

訪問右子樹前要先彈出左子樹的所有節點:

1的左和右子樹為NULL,直接彈出:

3的右子樹不為NULL,彈出3后入右子樹:

6的左子樹為4,繼續入棧:

4的左和右子樹節點為NULL,直接彈出:

6的右子樹不為NULL,彈出6后入右子樹:

7的左和右子樹節點為NULL,直接彈出:

8的右子樹不為NULL,彈出8后入右子樹:

10的左和右子樹節點為NULL,直接彈出:

棧為空,循環結束

總結入棧的幾種可能

1.棧頂的左右子樹都為空,直接彈出

2.棧頂的左子樹不為空,將左子樹入棧

3.棧頂的右子樹不為空,彈出棧頂后,將右子樹入棧

//必然最先訪問左路節點
while(cur)
{st.push(cur);cur=cur->left;
}//左路節點的右子樹,需要取棧頂元素
auto elem=st.top();
st.pop();
ret.push_back(elem->val);
cur=elem->right;//子問題的方式訪問右子樹

循環的條件

顯然棧不為空就繼續循環,但如果只寫這個條件會導致有些測試用例過不了

原因是1的左子樹為空,將1出棧后,恰滿足棧為空的循環退出條件,導致2和3沒有訪問到,需要添加cur如果不為空,也可以繼續循環

而且"cur不為空"這個條件應該比"棧不為空"這個條件先判斷,可以使用短路運算的規則:

while (cur || ! !st.empty())
{//......
}

代碼

class Solution {
public:vector<int> preorderTraversal(TreeNode* root) {if (root==nullptr)return {};vector<int> ret;stack<TreeNode*> st;TreeNode* cur=root;while (cur || !st.empty()){//必然最先訪問左路節點while(cur){st.push(cur);ret.push_back(cur->val);cur=cur->left;}//訪問左路節點的右子樹,需要取棧頂元素auto elem=st.top();st.pop();cur=elem->right;// 子問題的方式訪問右子樹} return ret;}
};

提交結果

3.中序遍歷

https://leetcode.cn/problems/binary-tree-inorder-traversal/

給定一個二叉樹的根節點 root ,返回 它的 中序?遍歷

示例 1:

輸入:root = [1,null,2,3]
輸出:[1,3,2]

示例 2:

輸入:root = []
輸出:[]

示例 3:

輸入:root = [1]
輸出:[1]

提示:

  • 樹中節點數目在范圍 [0, 100]
  • -100 <= Node.val <= 100

進階:?遞歸算法很簡單,你可以通過迭代算法完成嗎?

分析

中序遍歷:左→根→右,即左子樹訪問完了才能訪問根和右子樹

反復執行這兩步:

1. 左路節點入棧
2. 訪問左路節點及左路節點的右子樹

????????從棧里面取到一個節點(pop)就意味這個節點左子樹已經訪問完了

稍微改改前序遍歷的代碼即可

代碼

class Solution {
public:vector<int> inorderTraversal(TreeNode* root) {if (root==nullptr)return {};vector<int> ret;stack<TreeNode*> st;TreeNode* cur=root;while (cur || !st.empty()){//先遍歷左子樹,但不訪問其中的節點,即不要push_back到ret中while(cur){st.push(cur);cur=cur->left;}//左子樹遍歷完了,那就訪問根和右子樹auto elem=st.top();st.pop();ret.push_back(elem->val);cur=elem->right;// 子問題的方式訪問右子樹} return ret;}
};

提交結果

3.★后序遍歷

https://leetcode.cn/problems/binary-tree-postorder-traversal/

給你一棵二叉樹的根節點 root ,返回其節點值的 后序遍歷

示例 1:

輸入:root = [1,null,2,3]

輸出:[3,2,1]

解釋:

示例 2:

輸入:root = [1,2,3,4,5,null,8,null,null,6,7,9]

輸出:[4,6,7,5,2,9,8,3,1]

解釋:

示例 3:

輸入:root = []

輸出:[]

示例 4:

輸入:root = [1]

輸出:[1]

提示:

  • 樹中節點的數目在范圍 [0, 100]
  • -100 <= Node.val <= 100

進階:遞歸算法很簡單,你可以通過迭代算法完成嗎?

分析

后序遍歷:左→右→根,即必須訪問完左子樹和右子樹才能訪問根

以這棵樹[8,3,10,1,6]為例分析:

仍然分成兩部分:

(紅色是左路節點,藍色是左路節點的右子樹)

左路節點入棧:

1的左和右子樹為NULL,直接彈出1,將1記錄到后序遍歷的數組中:

雖然3的右子樹不為空,但是不能彈出3,如果彈出3那就成前序遍歷了,應該等第二次訪問到棧里面的3才彈出3(即將3的左右子樹都訪問完才能訪問根節點3)

入3的右子樹

6的左和右子樹為NULL,直接彈出6,將6記錄到后序遍歷的數組中:

第二次遇到3,因為3的左右子樹都訪問完了,可以彈出3,將3記錄到后序遍歷的數組中:

雖然8的右子樹不為空,但是不能彈出8(理由見上),入8的右子樹:

10的左和右子樹為NULL,直接彈出10,將10記錄到后序遍歷的數組中:

第二次遇到8,因為8的左右子樹都訪問完了,可以彈出8,將8記錄到后序遍歷的數組中:

棧為空,循環結束

問題:如何確定是第一次訪問到棧的元素還是第二次訪問到棧中的元素?

上面提到了:"雖然3的右子樹不為空,但是不能彈出3,如果彈出3那就成前序遍歷了,應該等第二次訪問到棧里面的3才彈出3(即將3的左右子樹都訪問完才能訪問根節點3)"

方法1:使用填充的內存(依賴于架構)

得知題目條件"樹中節點的數目在范圍 [-100, 100] 內",而且TreeNode的val是用int存儲的,如果val>0,那么最多只會占用int的低7位,如果val<0,那么int所有位都會占用

顯然在val內部使用一個標志位來判斷是第一次訪問到棧的元素還是第二次訪問到棧中的元素是不切實際的

突破口:結構體的內存對齊

64位下,TreeNode結構體的內存分布圖:

會發現0x0004~0x007是填充的4個字節,沒有用處!

填充的字節可以借用其中一個字節來標記是第一次還是第二次訪問到棧中的元素,簡稱為標志字節

例如選這一字節:

怎么從標志字節哪里得知是第一次還是第二次訪問到棧中的元素呢?

對于填充的字節,編譯器會給初始值,LeetCode下的值是0xBE,Windows VS2022下的值是0xCD,下面padding_byte獲取到的就是初始字節(pad v.填充)

unsigned char padding_byte = *((unsigned char*)&(root->val) + 4);

那么第一次訪問過節點后可以設置標志字節為指定值,讓其不等于原來編譯器給的初始值就行了,例如可以設置成0x01,等到第二次訪問發現是0x01,就能讓棧頂元素出棧,寫入到后序遍歷的數組中

如果LeetCode是64位架構且沒有使用gcc的-m32選項,那么上述方法是可行的,可以用宏判斷:

判斷計算機使用的架構和指針大小
#if defined(__x86_64__) || defined(_M_X64)printf("This is a 64-bit x86 architecture\n");
#elif defined(__i386__)printf("This is a 32-bit x86 architecture\n");
#elif defined(__arm__) || defined(_M_ARM)printf("This is an ARM architecture\n");
#elif defined(__aarch64__) || defined(_M_AARCH64)printf("This is a 64-bit ARM architecture\n");
#elseprintf("Unknown architecture\n");
#endif//__SIZEOF_POINTER__是指針大小
if (__SIZEOF_POINTER__ == 4) 
{printf("This is a 32-bit architecture.\n");
} 
else if (__SIZEOF_POINTER__ == 8) 
{   printf("This is a 64-bit architecture.\n"); 
} 
else 
{printf("Unknown architecture.\n");
}

LeetCode輸出結果:

使用填充的內存的方法是可行的

代碼
class Solution {
public:vector<int> postorderTraversal(TreeNode* root){if (root == nullptr)return {};vector<int> ret;unsigned char padding_byte=*((unsigned char*)&(root->val)+4);stack<TreeNode*> st;TreeNode* cur = root;while (cur || !st.empty()){//先遍歷左子樹,但不訪問其中的節點,即不要push_back到ret中while (cur){st.push(cur);cur = cur->left;}//左子樹遍歷完了,那就訪問根和右子樹auto elem = st.top();unsigned char* flag_byte_ptr=(unsigned char*)&(elem->val)+4;if (elem->right == nullptr){ret.push_back(elem->val);st.pop();continue;}if (*flag_byte_ptr!=padding_byte){//第二次訪問到,說明左右子樹都訪問完了,必須重新設置curst.pop();ret.push_back(elem->val);continue;}else{//第一次訪問到,要設置標志字節*flag_byte_ptr=1;}cur = elem->right;// 子問題的方式訪問右子樹}return ret;}
};
提交結果

方法2:尋找規律,設置一個prev變量記錄上一個訪問的節點

訪問右子樹有兩種情況:右為空(不用管右直接拿出棧頂元素),右不為空(需要使用上一次訪問的節點)

例如下圖的節點的3會訪問到兩次:

由方法1的棧圖分析

1.如果3的右子樹沒有訪問過,上一次訪問的是3的左子樹的根1

↓?

2..如果3的右子樹訪問過,上一次訪問的是3的右子樹的根6

↓?

設置一個變量prev記錄cur訪問的前一個節點即可

代碼
class Solution {
public:vector<int> postorderTraversal(TreeNode* root){if (root == nullptr)return {};vector<int> ret;stack<TreeNode*> st;TreeNode* cur = root;TreeNode* prev = nullptr;while (cur || !st.empty()){//先遍歷左子樹,但不訪問其中的節點,即不要push_back到ret中while (cur){st.push(cur);cur = cur->left;}//左子樹遍歷完了,那就訪問根和右子樹auto elem = st.top();if (elem->right == nullptr){prev=elem;ret.push_back(elem->val);st.pop();}if (prev==elem->right){//第二次訪問到,說明左右子樹都訪問完了,必須重新設置curprev=elem;st.pop();ret.push_back(elem->val);}else//prev=cur->left{cur = elem->right;// 第一次訪問到,子問題的方式訪問右子樹}   }return ret;}
};
運行結果

方法3:使用標記棧

標記棧的作用是標記對應的節點有沒有訪問過,是否訪問過這個節點就看標記棧的棧頂值

代碼
class Solution {
public:vector<int> postorderTraversal(TreeNode* root){if (root == nullptr)return {};vector<int> ret;stack<TreeNode*> st;stack<TreeNode*> flag;TreeNode* cur = root;while (cur || !st.empty()){//先遍歷左子樹,但不訪問其中的節點,即不要push_back到ret中while (cur){st.push(cur);cur = cur->left;}//左子樹遍歷完了,那就訪問根和右子樹auto elem = st.top();if (elem->right == nullptr){ret.push_back(elem->val);st.pop();continue;}if (flag.size()&&flag.top()==elem){//第二次訪問到,說明左右子樹都訪問完了,必須重新設置curst.pop();flag.pop();ret.push_back(elem->val);continue;}if (flag.empty() || flag.top()!=elem){//第一次訪問到,要設置標志字節flag.push(elem);}cur = elem->right;// 子問題的方式訪問右子樹}return ret;}
};
運行結果

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/97311.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/97311.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/97311.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

音視頻學習(五十九):H264中的SPS

在 H.264 (也稱為 AVC, Advanced Video Coding) 視頻編碼標準中&#xff0c;SPS (Sequence Parameter Set) 是一個至關重要的 NALU (Network Abstraction Layer Unit) 類型&#xff0c;它承載著整個視頻序列共有的全局性配置信息。你可以把它理解為視頻文件的“基因”&#xff…

linux實時性研究

Linux 實時性研究旨在提升 Linux 系統對外部事件的響應速度和確定性,使其能夠滿足實時應用的需求。以下是關于 Linux 實時性研究的一些關鍵內容: Linux 實時性不足的原因 中斷優先級問題:在標準 Linux 內核中,中斷具有最高優先級,包括軟中斷,這使得實時任務的優先級得不到…

Java-面試八股文-Mysql篇

MySQL篇 1、Select 語句完整的執行順序 難度系數&#xff1a;?&#x1f4cc; SQL SELECT 語句書寫順序&#xff08;開發者寫的順序&#xff09; SELECT ... FROM ... JOIN ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT ...&#x1f4cc; 實際執行順序&#…

多代理系統架構:Supervisor 與 Swarm 架構詳解

多代理&#xff08;Multi-Agent&#xff09;系統正成為構建復雜 AI 應用的重要范式。本文將深入剖析兩種熱門的多代理架構模式——Supervisor&#xff08;主管模式&#xff09;與 Swarm&#xff08;群智模式&#xff09;&#xff0c;揭示它們的執行流程、適用場景及實現細節&am…

【深度學習】思維鏈(Chain of Thought, CoT):提升大模型推理能力的關鍵技術

思維鏈&#xff08;Chain of Thought, CoT&#xff09;&#xff1a;提升大模型推理能力的關鍵技術 文章目錄思維鏈&#xff08;Chain of Thought, CoT&#xff09;&#xff1a;提升大模型推理能力的關鍵技術1 什么是思維鏈&#xff08;Chain of Thought, CoT&#xff09;&#…

GitHub 宕機自救指南:打造韌性開發體系

一、引言1.1 GitHub 宕機事件回顧與影響剖析在軟件開發的廣袤版圖中&#xff0c;GitHub 宛如一座熠熠生輝的燈塔&#xff0c;為全球超 1 億開發者照亮前行之路&#xff0c;其重要性不言而喻。它集代碼托管、版本控制、協作開發以及項目管理等核心功能于一身&#xff0c;是無數開…

移動端網頁調試實戰,iOS WebKit Debug Proxy 的應用與替代方案

在移動端開發中&#xff0c;iOS WebView 的調試一直是個難題。不同于 Android 可以依賴 Chrome DevTools 和 ADB&#xff0c;iOS 的 WKWebView 只能通過 Safari 開發者工具調試&#xff0c;而這需要 Mac 環境和設備直連。為了彌補限制&#xff0c;社區出現了一個常用工具 —— …

煥新升級,Sermant 2.0.0 release版本重磅發布!

Sermant社區在6月底正式發布了2.0.0 release版本&#xff0c;這次更新中&#xff0c;Sermant進行了項目所屬組織調整并新增了基于xDS協議的服務發現能力、預過濾啟動加速機制、Sermant Backend的配置管理能力。所屬組織調整使得Sermant淡化廠商屬性&#xff0c;以全新的姿態更好…

sqli-labs通關筆記-第28a關GET字符注入(多重關鍵字過濾繞過 腳本法)

目錄 一、sqlmap之tamper腳本 二、源碼分析 1、代碼審計 2、SQL安全性分析 三、滲透實戰 1、進入靶場 2、tamper腳本 3、sqlmap滲透 SQLI-LABS 是一個專門為學習和練習 SQL 注入技術而設計的開源靶場環境&#xff0c;本小節對第28a關Less 28a基于GET字符型的SQL注入關卡…

聯想打印機2268w安裝

聯想打印機2268w是支持無線打印的。在某度搜索&#xff0c;掀起蓋子長按開機鍵&#xff0c;成功初始化。之后按說明應該能用手機搜索到打印機的熱點&#xff0c;反復搜索都沒有出現。最后沒辦法&#xff0c;之后好用我自己的方法安裝。找了個筆記本&#xff0c;開機連接到wifi,…

【LeetCode】動態規劃——72.編輯距離、10.正則表達式匹配

LeetCode題目鏈接 https://leetcode.cn/problems/edit-distance/description/ https://leetcode.cn/problems/regular-expression-matching/description/ 題解 72.編輯距離 本題要定義為長度為i、長度為j的字符串的最少編輯次數&#xff0c;每次判斷字符的下標為i-1、j-1。dp[i…

[親測可用]Android studio配置國內鏡像源 Kotlin DSL (build.gradle.kts)

一、更改gradle下載鏡像Android studio項目需要下載和更新 Gradle 及其依賴。由于網絡環境&#xff0c;直接從 Gradle 官網下載可能會遇到速度慢或超時的問題。這里需要更換為使用國內的鏡像站點來加速下載。官網地址&#xff08;較慢&#xff09;&#xff1a;https://services…

《跳出“技術堆砌”陷阱,構建可演進的軟件系統》

很多團隊陷入了“技術焦慮式開發”—盲目追逐熱門框架&#xff0c;將“使用微服務”“引入云原生”“集成AI組件”當作架構先進的標簽&#xff0c;卻忽視了業務與技術的底層匹配邏輯。某互聯網團隊為了“彰顯技術實力”&#xff0c;在內部協同工具中強行接入機器學習推薦模塊&a…

賦能你的應用:英超實時數據接入終極指南(API vs. WebSocket)

在當今數據驅動的時代&#xff0c;為您的應用程序注入實時、準確的英超賽事數據&#xff0c;是提升用戶體驗、打造差異化競爭力的關鍵。無論是開發一款球迷必備的比分追蹤App&#xff0c;一個深度專業的賽事分析平臺&#xff0c;還是一個充滿互動性的夢幻足球游戲&#xff0c;首…

計算機網絡:(poll、epoll)

一、select的不足1. 最大監聽數受限&#xff1a;FD_SETSIZE 默認 1024&#xff08;Linux&#xff09;2. 每次調用需重置 fd_set&#xff1a;內核會修改集合&#xff0c;必須每次重新 FD_SET3. 用戶態與內核態拷貝開銷大4. 返回后仍需遍歷所有 fd 才能知道哪個就緒5. 效率隨 fd …

網絡編程之設置端口復用

首先來說一下為什么要設置端口復用&#xff0c;有些時候在調試服務器代碼時勢必會經常啟動或結束服務器進程&#xff0c;這樣就會出現當再次啟動服務器時有可能會出現端口綁定失敗的情況&#xff0c;造成這個情況的原因是由于你上次關閉服務器時有連接尚未斷開等等其他原因&…

stargo縮擴容starrocks集群,實現節點服務器替換

1.背景在企業中可能需要&#xff0c;將starrocks的某一臺服務器下架&#xff0c;換上另一臺服務器&#xff0c;如何實現這個操作&#xff0c;本篇將進行介紹&#xff1b;節點hadoop101hadoop102hadoop103hadoop104集群原集群節點新節點fe???&#xff08;下線&#xff09;?&…

Linux -- 進程間通信【命名管道】

目錄 一、命名管道定義 二、命名管道創建 1、指令 2、系統調用 3、刪除 三、匿名管道和命名管道的區別 四、命名管道的打開規則 五、代碼示例 1、comm.hpp 2、server.cc 3、client.cc 一、命名管道定義 # 匿名管道存在以下核心限制&#xff1a; 僅限親緣關系進程&a…

LinuxC系統多線程程序設計

一.多線程程序設計1. 線程概述&#xff1a;1.1 什么是線程?線程是進程中的一個實體(組成單元),是系統進程調度的最小單元。一個進程至少具有一個線程&#xff0c;如果進程僅有一個線程&#xff0c;該線程就代表進程本身。把代表進程本身的線程稱為主線程&#xff0c;一個進程…

Vue3 + TS + MapboxGL.js 三維地圖開發項目

文章目錄 1. 安裝依賴 2. 新建 Map 組件(components/MapView.vue) 3. 在頁面中使用(views/Home.vue) 4. 效果說明 1. 安裝依賴 npm install mapbox-gl @types/mapbox-gl --save?? 注意:需要去 Mapbox 官網,申請一個 access token。 package.json {"name":…