根据中序、前序遍历重建二叉树
文章目錄
- 題目
- 遞歸
- 思路
- 細(xì)節(jié)
- 易錯(cuò)
- 代碼
- 復(fù)雜度分析
- 迭代
- 思路
- 細(xì)節(jié)
- 易錯(cuò)
- 代碼
- 復(fù)雜度分析
題目
輸入某二叉樹的前序遍歷和中序遍歷的結(jié)果,請重建該二叉樹。假設(shè)輸入的前序遍歷和中序遍歷的結(jié)果中都不含重復(fù)的數(shù)字。
例如,給出
前序遍歷 preorder = [3,9,20,15,7]
中序遍歷 inorder = [9,3,15,20,7]
返回如下的二叉樹:
限制:
0 <= 節(jié)點(diǎn)個(gè)數(shù) <= 5000
遞歸
思路
首先要明確最重要的一個(gè)知識(shí):
對于任意一顆樹而言,前序遍歷的形式總是
[ 根節(jié)點(diǎn), [左子樹的前序遍歷結(jié)果], [右子樹的前序遍歷結(jié)果] ]
即根節(jié)點(diǎn)總是前序遍歷中的第一個(gè)節(jié)點(diǎn)。而中序遍歷的形式總是
[ [左子樹的中序遍歷結(jié)果], 根節(jié)點(diǎn), [右子樹的中序遍歷結(jié)果] ]
顯然:
- 對前序遍歷來講,找到左右子樹的遍歷結(jié)果分界線是困難的,找到根節(jié)點(diǎn)是簡單的
- 對中序遍歷來講,找到根節(jié)點(diǎn)是困難的,但找到根節(jié)點(diǎn)之后,左右兩側(cè)自然分成左右兩棵子樹
根據(jù)上面的特性,我們可以做出互補(bǔ):
重復(fù)上述過程,我們也就可以通過將每個(gè)節(jié)點(diǎn)視作根節(jié)點(diǎn),不斷遞歸生成左右子樹,無法再生成左右子樹。很顯然生成左右子樹的過程可以用遞歸思想來實(shí)現(xiàn)。
細(xì)節(jié)
思路有了,仍需解決幾個(gè)問題:
先解決第一個(gè)問題:
普通的方法當(dāng)然是拿著根節(jié)點(diǎn)的值,從中序遍歷結(jié)果數(shù)組inorder [0]開始遍歷,但是每次在生成根節(jié)點(diǎn)時(shí)都進(jìn)行遍歷的話,時(shí)間復(fù)雜度較高(O(N))。因此可以使用哈希表來建設(shè)中序遍歷數(shù)組值到下標(biāo)的鍵值對映射。
在構(gòu)造二叉樹的過程之前,我們可以對中序遍歷的列表進(jìn)行一遍掃描(O(N)),就可以構(gòu)造出這個(gè)哈希映射。
在此后構(gòu)造二叉樹的過程中,我們就只需要 O(1)的時(shí)間對根節(jié)點(diǎn)進(jìn)行定位了。(一次O(N),N次O(1));否則我們必須每次都遍歷一遍中序遍歷結(jié)果數(shù)組定位根節(jié)點(diǎn)(N次O(N))。
再來說第二個(gè)問題:
遞歸生成左右子樹這種說法聽起來還是太“模糊”了,其實(shí)我們實(shí)際做的操作是不停的生成根節(jié)點(diǎn),再進(jìn)入這個(gè)根節(jié)點(diǎn)的左右子樹,在每個(gè)子樹中生成當(dāng)前子樹的根節(jié)點(diǎn),直到這個(gè)”根節(jié)點(diǎn)“沒有子樹為止。
易錯(cuò)
寫代碼的時(shí)候沒有子樹應(yīng)該返回空指針——return nullptr;,粗心大意寫成了return null;,null和nullptr是有區(qū)別的。
代碼
class Solution { private:map<int, int> index; // 映射值給定值對應(yīng)的下標(biāo) public:TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {int num = inorder.size();for (int i = 0; i < num; i++){index[inorder[i]] = i; // 建立中序遍歷數(shù)組 值到下標(biāo) 的鍵值對映射,快速定位根節(jié)點(diǎn)}return buildRoot(preorder, inorder, 0, num - 1, 0, num - 1);}// 把每個(gè)節(jié)點(diǎn)都當(dāng)作它自身的“根節(jié)點(diǎn)”,進(jìn)入到每個(gè)節(jié)點(diǎn)遍歷生成它的左右子樹、及根節(jié)點(diǎn)本身 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; // 前序遍歷的根節(jié)點(diǎn)就是左邊界int in_root = index[pre[pre_left]]; // 根據(jù)映射關(guān)系確定中序遍歷中的根節(jié)點(diǎn)TreeNode* root = new TreeNode(in[in_root]); // 建立根節(jié)點(diǎn)// 等價(jià)于TreeNode root = TreeNode(in[in_root]);// TreeNode *proot = &root;// 但沒必要這樣寫,可能便于理解但是過于繁瑣int lefttree_num = in_root - in_left; // 確定左子樹中節(jié)點(diǎn)數(shù)目// 前序遍歷 根節(jié)點(diǎn)(左邊界)+1 到 根節(jié)點(diǎn)+左子樹數(shù)量 的范圍為左子樹// 中序遍歷根節(jié)點(diǎn)左側(cè)為左子樹root->left = buildRoot(pre, in, pre_left + 1, pre_left + lefttree_num, in_left, in_root - 1); // 前序遍歷 根節(jié)點(diǎn)+左子樹數(shù)量+1 到 右邊界 的范圍為右子樹// 中序遍歷根節(jié)點(diǎn)右側(cè)為右子樹root->right = buildRoot(pre, in, pre_left + 1 + lefttree_num, pre_right, in_root + 1, in_right);return root;} };復(fù)雜度分析
時(shí)間復(fù)雜度:O(n),其中 n 是樹中的節(jié)點(diǎn)個(gè)數(shù)。
空間復(fù)雜度:O(n),除去返回的答案需要的 O(n)空間之外,我們還需要使用 O(n) 的空間存儲(chǔ)哈希映射,以及 O(h)(其中 h 是樹的高度)的空間表示遞歸時(shí)棧空間。這里 h < n,所以總空間復(fù)雜度為 O(n)。
迭代
思路
前序遍歷的相鄰節(jié)點(diǎn) u 和 v 有如下關(guān)系:
以此樹為例:
它的前序遍歷和中序遍歷分別為
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它們之間滿足第一種關(guān)系(例如:8是9的左兒子),對于4,10它們滿足第二種關(guān)系,10是4祖父節(jié)點(diǎn)的右兒子。
也就是前序遍歷會(huì)
那么我們可以根據(jù)這一特性,我們可以用一個(gè)棧來存儲(chǔ)祖先節(jié)點(diǎn)和左子樹,直到左子樹被遍歷完,(本例中,將3,9,8,5,4依次入棧,直到遇到10)此時(shí)開始尋找當(dāng)前節(jié)點(diǎn)(10)是誰的右兒子。
細(xì)節(jié)
思路有了,仍需解決幾個(gè)問題:
這時(shí)來看中序遍歷,我們可以發(fā)現(xiàn),中序遍歷結(jié)果數(shù)組的首元素是——根節(jié)點(diǎn)不斷往左走達(dá)到的最終節(jié)點(diǎn)。 根據(jù)這一特性,我們可以創(chuàng)建一個(gè)指針 index 指向當(dāng)前的最左子樹。
首先我們將根節(jié)點(diǎn) 3 入棧,再初始化 index 指向的節(jié)點(diǎn)為 4,隨后對于前序遍歷中的每個(gè)節(jié)點(diǎn),我們依次判斷它是棧頂節(jié)點(diǎn)的左兒子,還是棧中某個(gè)節(jié)點(diǎn)的右兒子。
stack = [3, 9]
index -> inorder[0] = 4
stack = [3, 9, 8, 5, 4]
index -> inorder[0] = 4
這是因?yàn)闂V械娜我鈨蓚€(gè)相鄰的節(jié)點(diǎn),前者都是后者的某個(gè)祖先。并且我們知道,棧中的任意一個(gè)節(jié)點(diǎn)的右兒子還沒有被遍歷過(前序遍歷順序——中左右),說明后者一定是前者左兒子,那么后者就先于前者出現(xiàn)在中序遍歷中(中序遍歷順序——左中右)。
因此我們可以先把此時(shí)的棧頂元素保存并彈出, 然后把 index 不斷向右移動(dòng),并與棧頂節(jié)點(diǎn)進(jìn)行比較。如果 index 對應(yīng)的元素恰好等于棧頂節(jié)點(diǎn),那么說明上一個(gè)被彈出的節(jié)點(diǎn)沒有右子樹,且其本身是當(dāng)前節(jié)點(diǎn)的左子樹, 所以重復(fù)將棧頂節(jié)點(diǎn)保存并彈出,然后將 index 增加 1 的過程,直到 index 對應(yīng)的元素不等于棧頂節(jié)點(diǎn),此時(shí) index 對應(yīng)的元素就是上一個(gè)被保存且彈出的棧頂節(jié)點(diǎn)的右子樹。按照這樣的過程,我們彈出的最后一個(gè)節(jié)點(diǎn) x 就是 10 的父節(jié)點(diǎn),這是因?yàn)?10 出現(xiàn)在了 x 與 x在棧中的下一個(gè)節(jié)點(diǎn)的中序遍歷之間,因此 10 就是 x 的右兒子(根據(jù)中序遍歷順序——左中右,x是中,10是右,x在棧中的下一個(gè)節(jié)點(diǎn)是x的父節(jié)點(diǎn))。
回到我們的例子,我們會(huì)依次從棧頂彈出 4,5 和 8,并且將 index 向右移動(dòng)了三次。我們將 10 作為最后彈出的節(jié)點(diǎn) 8 的右兒子,并將 10 入棧。
stack = [3, 9, 10]
index -> inorder[3] = 10
stack = [20]
index -> inorder[6] = 15
stack = [20, 15]
index -> inorder[6] = 15
stack = [7]
index -> inorder[8] = 7
此時(shí)遍歷結(jié)束,我們構(gòu)造出了正確的二叉樹。
總結(jié)來講就是,遍歷前序遍歷結(jié)果數(shù)組并將其壓到棧中:
易錯(cuò)
代碼
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]); // 建立根節(jié)點(diǎn)st.push(root); // 根節(jié)點(diǎn)入棧// 否則無法進(jìn)行將節(jié)點(diǎn)歸為左兒子或者右兒子的操作// 因?yàn)檫M(jìn)行上面的操作需要訪問棧頂元素的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位置的數(shù)和中序遍歷index位置的數(shù)不相等// 說明i位置的數(shù)是二叉樹的左子樹node->left = new TreeNode(pre);st.push(node->left);}else {while (!st.empty() && in == st.top()->val) {in = inorder[++index];node = st.top(); // 保存彈出的節(jié)點(diǎn)// 當(dāng)跳出while時(shí),pre的值即為該節(jié)點(diǎn)右子樹st.pop();}node->right = new TreeNode(pre);st.push(node->right);}}return root;} };復(fù)雜度分析
時(shí)間復(fù)雜度:O(n),其中 n 是樹中的節(jié)點(diǎn)個(gè)數(shù)。
空間復(fù)雜度:O(n),除去返回的答案需要的 O(n) 空間之外,我們還需要使用 O(h)(其中 h 是樹的高度)的空間存儲(chǔ)棧。這里 h < n,所以(在最壞情況下)總空間復(fù)雜度為 O(n)。
總結(jié)
以上是生活随笔為你收集整理的根据中序、前序遍历重建二叉树的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 建行分期通申请评分不足什么情况?这里有解
- 下一篇: 长春金桥计算机学校,金桥学校2017年招