C++構(gòu)建二叉樹問題

2023-09-20 09:23 更新

Question

給定一個二叉樹的前序遍歷 preorder 和中序遍歷 inorder ,請從中構(gòu)建二叉樹,返回二叉樹的根節(jié)點。

構(gòu)建二叉樹的示例數(shù)據(jù)

圖 12-5   構(gòu)建二叉樹的示例數(shù)據(jù)

判斷是否為分治問題

原問題定義為從 preorderinorder 構(gòu)建二叉樹,其是一個典型的分治問題。

  • 問題可以被分解:從分治的角度切入,我們可以將原問題劃分為兩個子問題:構(gòu)建左子樹、構(gòu)建右子樹,加上一步操作:初始化根節(jié)點。而對于每個子樹(子問題),我們?nèi)匀豢梢詮?fù)用以上劃分方法,將其劃分為更小的子樹(子問題),直至達到最小子問題(空子樹)時終止。
  • 子問題是獨立的:左子樹和右子樹是相互獨立的,它們之間沒有交集。在構(gòu)建左子樹時,我們只需要關(guān)注中序遍歷和前序遍歷中與左子樹對應(yīng)的部分。右子樹同理。
  • 子問題的解可以合并:一旦得到了左子樹和右子樹(子問題的解),我們就可以將它們鏈接到根節(jié)點上,得到原問題的解。

如何劃分子樹

根據(jù)以上分析,這道題是可以使用分治來求解的,但如何通過前序遍歷 preorder 和中序遍歷 inorder 來劃分左子樹和右子樹呢?

根據(jù)定義,preorderinorder 都可以被劃分為三個部分。

  • 前序遍歷:[ 根節(jié)點 | 左子樹 | 右子樹 ] ,例如圖 12-5 的樹對應(yīng) [ 3 | 9 | 2 1 7 ] 。
  • 中序遍歷:[ 左子樹 | 根節(jié)點 | 右子樹 ] ,例如圖 12-5 的樹對應(yīng) [ 9 | 3 | 1 2 7 ] 。

以上圖數(shù)據(jù)為例,我們可以通過圖 12-6 所示的步驟得到劃分結(jié)果。

  1. 前序遍歷的首元素 3 是根節(jié)點的值。
  2. 查找根節(jié)點 3 在 inorder 中的索引,利用該索引可將 inorder 劃分為 [ 9 | 3 | 1 2 7 ]
  3. 根據(jù) inorder 劃分結(jié)果,易得左子樹和右子樹的節(jié)點數(shù)量分別為 1 和 3 ,從而可將 preorder 劃分為 [ 3 | 9 | 2 1 7 ] 。

在前序和中序遍歷中劃分子樹

圖 12-6   在前序和中序遍歷中劃分子樹

基于變量描述子樹區(qū)間

根據(jù)以上劃分方法,我們已經(jīng)得到根節(jié)點、左子樹、右子樹在 preorderinorder 中的索引區(qū)間。而為了描述這些索引區(qū)間,我們需要借助幾個指針變量。

  • 將當(dāng)前樹的根節(jié)點在 preorder 中的索引記為 i 。
  • 將當(dāng)前樹的根節(jié)點在 inorder 中的索引記為 m 。
  • 將當(dāng)前樹在 inorder 中的索引區(qū)間記為 [l,r]

如表 12-1 所示,通過以上變量即可表示根節(jié)點在 preorder 中的索引,以及子樹在 inorder 中的索引區(qū)間。

表 12-1   根節(jié)點和子樹在前序和中序遍歷中的索引

根節(jié)點在 preorder 中的索引 子樹在 inorder 中的索引區(qū)間
當(dāng)前樹 i [l,r]
左子樹 i+1 [l,m?1]
右子樹 i+1+(m?l) [m+1,r]

請注意,右子樹根節(jié)點索引中的 (m?l) 的含義是“左子樹的節(jié)點數(shù)量”,建議配合圖 12-7 理解。

根節(jié)點和左右子樹的索引區(qū)間表示

圖 12-7   根節(jié)點和左右子樹的索引區(qū)間表示

4.   代碼實現(xiàn)?

為了提升查詢 m 的效率,我們借助一個哈希表 hmap 來存儲數(shù)組 inorder 中元素到索引的映射。

build_tree.cpp

/* 構(gòu)建二叉樹:分治 */
TreeNode *dfs(vector<int> &preorder, unordered_map<int, int> &inorderMap, int i, int l, int r) {
    // 子樹區(qū)間為空時終止
    if (r - l < 0)
        return NULL;
    // 初始化根節(jié)點
    TreeNode *root = new TreeNode(preorder[i]);
    // 查詢 m ,從而劃分左右子樹
    int m = inorderMap[preorder[i]];
    // 子問題:構(gòu)建左子樹
    root->left = dfs(preorder, inorderMap, i + 1, l, m - 1);
    // 子問題:構(gòu)建右子樹
    root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
    // 返回根節(jié)點
    return root;
}

/* 構(gòu)建二叉樹 */
TreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {
    // 初始化哈希表,存儲 inorder 元素到索引的映射
    unordered_map<int, int> inorderMap;
    for (int i = 0; i < inorder.size(); i++) {
        inorderMap[inorder[i]] = i;
    }
    TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorder.size() - 1);
    return root;
}

圖 12-8 展示了構(gòu)建二叉樹的遞歸過程,各個節(jié)點是在向下“遞”的過程中建立的,而各條邊(即引用)是在向上“歸”的過程中建立的。

構(gòu)建二叉樹的遞歸過程

built_tree_step2

built_tree_step3

built_tree_step4

built_tree_step5

built_tree_step6

built_tree_step7

built_tree_step8

built_tree_step9


圖 12-8   構(gòu)建二叉樹的遞歸過程

每個遞歸函數(shù)內(nèi)的前序遍歷 preorder 和中序遍歷 inorder 的劃分結(jié)果如圖 12-9 所示。

每個遞歸函數(shù)中的劃分結(jié)果

圖 12-9   每個遞歸函數(shù)中的劃分結(jié)果

設(shè)樹的節(jié)點數(shù)量為 n ,初始化每一個節(jié)點(執(zhí)行一個遞歸函數(shù) dfs() )使用 O(1) 時間。因此總體時間復(fù)雜度為 O(n) 。

哈希表存儲 inorder 元素到索引的映射,空間復(fù)雜度為 O(n) 。最差情況下,即二叉樹退化為鏈表時,遞歸深度達到 n ,使用 O(n) 的棧幀空間。因此總體空間復(fù)雜度為 O(n) 。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號