根據中序、前序遍歷重建二叉樹

文章目錄

  • 題目
  • 遞歸
    • 思路
    • 細節
    • 易錯
    • 代碼
    • 復雜度分析
  • 迭代
    • 思路
    • 細節
    • 易錯
    • 代碼
    • 復雜度分析


題目

輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重復的數字。

例如,給出

前序遍歷 preorder = [3,9,20,15,7]
中序遍歷 inorder = [9,3,15,20,7]

返回如下的二叉樹:

在這里插入圖片描述

限制:

0 <= 節點個數 <= 5000


遞歸

思路

首先要明確最重要的一個知識:

對于任意一顆樹而言,前序遍歷的形式總是

[ 根節點, [左子樹的前序遍歷結果], [右子樹的前序遍歷結果] ]

即根節點總是前序遍歷中的第一個節點。而中序遍歷的形式總是

[ [左子樹的中序遍歷結果], 根節點, [右子樹的中序遍歷結果] ]

顯然:

  • 對前序遍歷來講,找到左右子樹的遍歷結果分界線是困難的,找到根節點是簡單的
  • 對中序遍歷來講,找到根節點是困難的,但找到根節點之后,左右兩側自然分成左右兩棵子樹

根據上面的特性,我們可以做出互補:

  1. 通過前序遍歷的結果數組的首元素確定根節點
  2. 根據找到的根節點結合中序遍歷數組確定左右子樹的節點數目

重復上述過程,我們也就可以通過將每個節點視作根節點,不斷遞歸生成左右子樹,無法再生成左右子樹。很顯然生成左右子樹的過程可以用遞歸思想來實現。


細節

思路有了,仍需解決幾個問題:

  1. 即使通過前序遍歷找到根節點,怎樣確定根節點在中序遍歷中的位置?
  2. 遞歸生成左右子樹的細節操作是什么?

先解決第一個問題:

普通的方法當然是拿著根節點的值,從中序遍歷結果數組inorder [0]開始遍歷,但是每次在生成根節點時都進行遍歷的話,時間復雜度較高(O(N))。因此可以使用哈希表來建設中序遍歷數組值到下標鍵值對映射

在構造二叉樹的過程之前,我們可以對中序遍歷的列表進行一遍掃描(O(N)),就可以構造出這個哈希映射。

在此后構造二叉樹的過程中,我們就只需要 O(1)的時間對根節點進行定位了。(一次O(N),N次O(1));否則我們必須每次都遍歷一遍中序遍歷結果數組定位根節點(N次O(N))。

再來說第二個問題:

遞歸生成左右子樹這種說法聽起來還是太“模糊”了,其實我們實際做的操作是不停的生成根節點,再進入這個根節點的左右子樹,在每個子樹中生成當前子樹的根節點,直到這個”根節點“沒有子樹為止。

易錯

寫代碼的時候沒有子樹應該返回空指針——return nullptr;,粗心大意寫成了return null;,null和nullptr是有區別的。

代碼

class Solution {
private:map<int, int> index; // 映射值給定值對應的下標
public:TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {int num = inorder.size();for (int i = 0; i < num; i++){index[inorder[i]] = i; // 建立中序遍歷數組 值到下標 的鍵值對映射,快速定位根節點}return buildRoot(preorder, inorder, 0, num - 1, 0, num - 1);}// 把每個節點都當作它自身的“根節點”,進入到每個節點遍歷生成它的左右子樹、及根節點本身   TreeNode* buildRoot(vector<int>& pre, vector<int>& in, int pre_left, int pre_right, int in_left, int in_right) {if (pre_left > pre_right) { // 沒有子樹return nullptr;}int pre_root = pre_left; // 前序遍歷的根節點就是左邊界int in_root = index[pre[pre_left]]; // 根據映射關系確定中序遍歷中的根節點TreeNode* root = new TreeNode(in[in_root]); // 建立根節點// 等價于TreeNode root = TreeNode(in[in_root]);// TreeNode *proot = &root;// 但沒必要這樣寫,可能便于理解但是過于繁瑣int lefttree_num = in_root - in_left; // 確定左子樹中節點數目// 前序遍歷 根節點(左邊界)+1 到 根節點+左子樹數量 的范圍為左子樹// 中序遍歷根節點左側為左子樹root->left = buildRoot(pre, in, pre_left + 1, pre_left + lefttree_num, in_left, in_root - 1); // 前序遍歷 根節點+左子樹數量+1 到 右邊界 的范圍為右子樹// 中序遍歷根節點右側為右子樹root->right = buildRoot(pre, in, pre_left + 1 + lefttree_num, pre_right, in_root + 1, in_right);return root;}
};

復雜度分析

時間復雜度:O(n),其中 n 是樹中的節點個數。

空間復雜度:O(n),除去返回的答案需要的 O(n)空間之外,我們還需要使用 O(n) 的空間存儲哈希映射,以及 O(h)(其中 h 是樹的高度)的空間表示遞歸時棧空間。這里 h < n,所以總空間復雜度為 O(n)。



迭代

思路

前序遍歷的相鄰節點 u 和 v 有如下關系:

  1. v 是 u 的左兒子;
  2. u 沒有左兒子。則 v 是 u 或者 u 祖先節點的右兒子。

以此樹為例:
在這里插入圖片描述
它的前序遍歷和中序遍歷分別為

preorder = [3, 9, 8, 5, 4, 10, 20, 15, 7]
inorder = [4, 5, 8, 10, 9, 3, 15, 20, 7]

可以看到,對于3,9,8,5,4它們之間滿足第一種關系(例如:8是9的左兒子),對于4,10它們滿足第二種關系,10是4祖父節點的右兒子。

也就是前序遍歷會

  1. 從根節點開始,一直遍歷左子樹
  2. 直到左子樹遍歷完了,開始遍歷右子樹
  3. 如果當前的節點沒有右子樹,則會回溯遍歷祖先節點的右子樹。

那么我們可以根據這一特性,我們可以用一個棧來存儲祖先節點和左子樹,直到左子樹被遍歷完,(本例中,將3,9,8,5,4依次入棧,直到遇到10)此時開始尋找當前節點(10)是誰的右兒子。

細節

思路有了,仍需解決幾個問題:

  1. 當開始遍歷右子樹,怎么確定當前節點是誰的右兒子呢?

這時來看中序遍歷,我們可以發現,中序遍歷結果數組的首元素是——根節點不斷往左走達到的最終節點。 根據這一特性,我們可以創建一個指針 index 指向當前的最左子樹

首先我們將根節點 3 入棧,再初始化 index 指向的節點為 4,隨后對于前序遍歷中的每個節點,我們依次判斷它是棧頂節點的左兒子,還是棧中某個節點的右兒子。

  1. 我們遍歷 9。9 一定是棧頂節點 3 的左兒子。我們使用反證法,假設 9 是 3 的右兒子,那么 3 沒有左兒子,index 應該恰好指向 3,但實際上為 4,因此產生了矛盾。所以我們將 9 作為 3 的左兒子,并將 9 入棧。

stack = [3, 9]
index -> inorder[0] = 4

  1. 我們遍歷 8,5 和 4。同理可得它們都是上一個節點(棧頂節點)的左兒子,所以它們會依次入棧。

stack = [3, 9, 8, 5, 4]
index -> inorder[0] = 4

  1. 當我們遍歷到 10,這時情況就不一樣了。我們發現此時 index 指向的節點和當前的棧頂節點一樣,都為 4,也就是說 4 沒有左兒子,那么 10 必須為棧中某個節點的右兒子。 那么如何找到這個節點呢?棧中的節點的順序和它們在前序遍歷中出現的順序是一致的,而且每一個節點的右兒子都還沒有被遍歷過, 那么這些節點的順序和它們在中序遍歷中出現的順序一定是相反的(原因如下)。

這是因為棧中的任意兩個相鄰的節點,前者都是后者的某個祖先。并且我們知道,棧中的任意一個節點的右兒子還沒有被遍歷過(前序遍歷順序——中左右),說明后者一定是前者左兒子,那么后者就先于前者出現在中序遍歷中(中序遍歷順序——左中右)。

因此我們可以先把此時的棧頂元素保存并彈出, 然后把 index 不斷向右移動,并與棧頂節點進行比較。如果 index 對應的元素恰好等于棧頂節點,那么說明上一個被彈出的節點沒有右子樹,且其本身是當前節點的左子樹, 所以重復將棧頂節點保存并彈出,然后將 index 增加 1 的過程,直到 index 對應的元素不等于棧頂節點,此時 index 對應的元素就是上一個被保存且彈出的棧頂節點的右子樹。按照這樣的過程,我們彈出的最后一個節點 x 就是 10 的父節點,這是因為 10 出現在了 xx在棧中的下一個節點的中序遍歷之間,因此 10 就是 x 的右兒子(根據中序遍歷順序——左中右,x是中,10是右,x在棧中的下一個節點x的父節點)。

回到我們的例子,我們會依次從棧頂彈出 4,5 和 8,并且將 index 向右移動了三次。我們將 10 作為最后彈出的節點 8 的右兒子,并將 10 入棧。

stack = [3, 9, 10]
index -> inorder[3] = 10

  1. 我們遍歷 20時。index 恰好指向當前棧頂節點 10,那么我們會依次從棧頂彈出 10,9 和 3,并且將 index 向右移動了三次。我們將 20 作為最后彈出的節點 3 的右兒子,并將 20 入棧。

stack = [20]
index -> inorder[6] = 15

  1. 我們遍歷 15,將 15 作為棧頂節點 20 的左兒子,并將 15 入棧。

stack = [20, 15]
index -> inorder[6] = 15

  1. 我們遍歷 7。index 恰好指向當前棧頂節點 15,那么我們會依次從棧頂彈出 15 和 20,并且將 index 向右移動了兩次。我們將 7 作為最后彈出的節點 20 的右兒子,并將 7 入棧。

stack = [7]
index -> inorder[8] = 7

此時遍歷結束,我們構造出了正確的二叉樹。

總結來講就是,遍歷前序遍歷結果數組并將其壓到棧中:

  1. 棧頂元素不等于index指向的元素時,將當前元素作為棧頂元素左兒子,然后當前元素入棧成為新棧頂
  2. 棧頂元素等于index指向的元素時,彈出并保存棧頂元素,同時將index遞增1,再判斷棧頂元素和index指向的元素之間的關系,相等則重復上述操作,不相等則將當前元素作為最后一個被彈出的棧頂元素右兒子,然后將當前元素入棧成為新棧頂

易錯

  1. 通過判斷前序遍歷或中序遍歷的結果數組是否為空,來確定二叉樹是否為空。
  2. 二叉樹不為空時,在建立左右子樹的循環操作之前,先將根節點入棧。因為根節點的建立操作與其他左右子樹不同,放到循環里面要單獨處理,反而繁瑣。
  3. 注意保存彈出的棧頂元素。
  4. 在生成右子樹的時候,棧不為空也應該是重要的循環判定條件之一。

代碼

class Solution2 { // 迭代
public:TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {if (!preorder.size()) {return nullptr;}stack<TreeNode*> st;TreeNode* root = new TreeNode(preorder[0]); // 建立根節點st.push(root); // 根節點入棧// 否則無法進行將節點歸為左兒子或者右兒子的操作// 因為進行上面的操作需要訪問棧頂元素的left或者rightint index = 0;for (size_t i = 1; i < preorder.size(); i++) {int pre = preorder[i];int in = inorder[index];auto node = st.top();if (node->val != in) { // 如果前序遍歷i位置的數和中序遍歷index位置的數不相等// 說明i位置的數是二叉樹的左子樹node->left = new TreeNode(pre);st.push(node->left);}else {while (!st.empty() && in == st.top()->val) {in = inorder[++index];node = st.top(); // 保存彈出的節點// 當跳出while時,pre的值即為該節點右子樹st.pop();}node->right = new TreeNode(pre);st.push(node->right);}}return root;}
};

復雜度分析

時間復雜度:O(n),其中 n 是樹中的節點個數。

空間復雜度:O(n),除去返回的答案需要的 O(n) 空間之外,我們還需要使用 O(h)(其中 h 是樹的高度)的空間存儲棧。這里 h < n,所以(在最壞情況下)總空間復雜度為 O(n)。

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

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

相關文章

深搜+剪枝

文章目錄題目思路注意代碼復雜度分析題目 給定一個 m x n 二維字符網格 board 和一個字符串單詞 word 。如果 word 存在于網格中&#xff0c;返回 true &#xff1b;否則&#xff0c;返回 false 。 單詞必須按照字母順序&#xff0c;通過相鄰的單元格內的字母構成&#xff0c…

搜索+回溯問題(DFS\BFS詳解)

文章目錄題目思路DFS思路代碼復雜度分析BFS思路代碼復雜度分析題目 地上有一個m行n列的方格&#xff0c;從坐標 [0,0] 到坐標 [m-1,n-1] 。一個機器人從坐標 [0, 0] 的格子開始移動&#xff0c;它每次可以向左、右、上、下移動一格&#xff08;不能移動到方格外&#xff09;&am…

剪繩子(動規、數論、貪心)

文章目錄題目數論思路代碼復雜度分析動規一思路代碼動規二思路代碼對最終結果取模1e97思路代碼題目 給你一根長度為 n 的繩子&#xff0c;請把繩子剪成整數長度的 m 段&#xff08;m、n都是整數&#xff0c;n>1并且m>1&#xff09;&#xff0c;每段繩子的長度記為 k[0],…

快速冪實現pow函數(從二分和二進制兩種角度理解快速冪)

文章目錄迭代實現快速冪思路int的取值范圍快速冪從二進制的角度來理解從二分法的角度來理解代碼復雜度分析進階——超級次方思路倒序快速冪正序快速冪代碼復雜度分析迭代實現快速冪 實現 pow(x, n) &#xff0c;即計算 x 的 n 次冪函數&#xff08;即&#xff0c;xn&#xff0…

備份MySQL數據庫的命令

備份MySQL數據庫的命令 mysqldump -hhostname -uusername -ppassword databasename > backupfile.sql 備份MySQL數據庫為帶刪除表的格式 備份MySQL數據庫為帶刪除表的格式&#xff0c;能夠讓該備份覆蓋已有數據庫而不需要手動刪除原有數據庫。 mysqldump -–add-drop-…

n位數的全排列(需要考慮大數的情況)

文章目錄題目思路代碼題目 輸入數字 n&#xff0c;按順序打印出從 1 到最大的 n 位十進制數。比如輸入 3&#xff0c;則打印出 1、2、3 一直到最大的 3 位數 999。 示例 1: 輸入: n 1 輸出: [1,2,3,4,5,6,7,8,9] 說明&#xff1a; 用返回一個整數列表來代替打印 n 為正整數 …

正則表達式匹配(動規)

文章目錄題目思路轉移方程特征再探 i 和 j代碼題目 請實現一個函數用來匹配包含 . 和 * 的正則表達式。模式中的字符 . 表示任意一個字符&#xff0c;而 * 表示它前面的字符可以出現任意次&#xff08;含0次&#xff09;。在本題中&#xff0c;匹配是指字符串的所有字符匹配整…

在循環遞增一次的數組中插入元素

文章目錄題目思路如何建立左右區間&#xff1f;如何查找最高點&#xff1f;那我們怎么判斷 num 到底處于什么樣的位置呢&#xff1f;如何確定插入位置&#xff1f;插入元素代碼題目 給一個只循環遞增一次的數組 res&#xff0c;res 滿足首元素大于等于尾元素&#xff0c;形如&…

Unable to find 'struts.multipart.saveDir' property setting.

Unable to find struts.multipart.saveDir property setting. 今天在做工作的時候遇到了這個問題&#xff0c;后來經過百度&#xff0c;終于知道了原因&#xff0c;現在記錄下來&#xff0c;以備以后查看。 1.struts.multipart.saveDir沒有配置 2.struts.multipart.saveDir用…

表示數值的字符串(有限狀態自動機與搜索)

文章目錄題目思路一代碼一思路二代碼二題目 思路一 考察有限狀態自動機&#xff08;參考jyd&#xff09;&#xff1a; 字符類型&#xff1a; 空格 「 」、數字「 0—9 」 、正負號 「 」 、小數點 「 . 」 、冪符號 「 eE 」 。 狀態定義&#xff1a; 按照字符串從左到右的…

樹的子結構

文章目錄題目深搜深搜代碼廣搜廣搜代碼題目 輸入兩棵二叉樹A和B&#xff0c;判斷B是不是A的子結構。(約定空樹不是任意一個樹的子結構) B是A的子結構&#xff0c; 即 A中有出現和B相同的結構和節點值。 例如: 給定的樹 A: 給定的樹 B&#xff1a; 返回 true&#xff0c;因為…

寫題過程中碰見的小問題

文章目錄和--vector二維vector的初始化數組中最大的數max_element()數組所有元素之和accumulate()vector數組去重對pair類型的vector排序對元素都為正整數的vector利用sort默認的升序排列進行降序排列一維數組轉二維數組size_t和int如何不用臨時變量交換兩個數?將類函數的形參…

LeetCode——二叉樹序列化與反序列化

文章目錄題目思路問題一問題二代碼實現題目 請實現兩個函數&#xff0c;分別用來序列化和反序列化二叉樹。 設計一個算法來實現二叉樹的序列化與反序列化。不限定序列 / 反序列化算法執行邏輯&#xff0c;你只需要保證一個二叉樹可以被序列化為一個字符串并且將這個字符串反序…

jsp中生成的驗證碼和存在session里面的驗證碼不一致的處理

今天在調試項目的時候發現&#xff0c;在提交表單的時候的驗證碼有問題&#xff0c;問題是這樣的&#xff1a;就是通過debug模式查看得知&#xff1a;jsp頁面生成的驗證碼和表單輸入的頁面輸入的一樣&#xff0c;但是到后臺執行的時候&#xff0c;你會發現他們是不一樣的&#…

求1~n這n個整數十進制表示中1出現的次數

文章目錄題目思路代碼復雜度分析題目 輸入一個整數 n &#xff0c;求1&#xff5e;n這n個整數的十進制表示中1出現的次數。 例如&#xff0c;輸入12&#xff0c;那么1&#xff5e;12這些整數中包含1 的數字有1、10、11和12。可得1一共出現了5次。 思路 將個位、十位……每位…

求數字序列中的第n位對應的數字

文章目錄題目思路代碼復雜度分析致謝題目 數字以0123456789101112131415…的格式序列化到一個字符序列中。在這個序列中&#xff0c;第5位&#xff08;從下標0開始計數&#xff09;是5&#xff0c;第13位是1&#xff0c;第19位是4&#xff0c;等等。 請寫一個函數&#xff0c…

一學就廢的歸并排序

文章目錄其他與排序有關的文章原理代碼實現復雜度分析其他與排序有關的文章 一學就廢的三種簡單排序【冒泡、插入、選擇】 原理 歸并排序&#xff08;Merge sort&#xff09;&#xff1a; 歸并排序對元素 遞歸地 進行 逐層折半分組&#xff0c;然后從最小分組開始&#xff0c…

神奇的x -x,Lowbit函數的實現方式!

文章目錄-xx & -x&#xff0c;當x為偶數時x & -x&#xff0c;當x為奇數時x&-x 的實際用途-x -x 在二進制里表示對 x 的二進制按位取反(~x)之后再加 1 &#xff0c;即 -x ~x1x & -x&#xff0c;當x為偶數時 在執行 x & -x 時&#xff0c;若 x 為偶數&am…

JAVA實現把指定文件夾下的所有文件壓縮成zip包

1.代碼如下&#xff1a; package cn.gov.csrc.base.util;import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import…

樹狀數組的相關知識 及 求逆序對的運用

文章目錄樹狀數組概念前綴和和區間和樹狀數組原理區間和——單點更新前綴和——區間查詢完整代碼離散化sort函數unique函數去重erase函數僅保留不重復元素通過樹狀數組求逆序對樹狀數組概念 樹狀數組又名二叉索引樹&#xff0c;其查詢與插入的復雜度都為 O(logN)&#xff0c;其…