前言
二叉樹翻轉是一道經典的面試編程題,經常出現在各大公司的招聘筆試面試環節。
這裏還有個趣事,Homebrew 的作者 Max Howell 某天去 Google 面試,面試官出了一道反轉二叉樹的題目,然而 Max Howell 沒答上來,結果被拒。面試官的評語是:“我們 90% 的工程師使用您編寫的軟件,但是您卻無法在面試時在白板上寫出翻轉二叉樹這道題,所以滾蛋吧”。
可見,在求職面試過程中,即使你是一位優秀的程序員,如果答不上算法題,那麼在算法方面的能力將被面試官認爲是不及格的,甚至無法被聘用。
問題描述
給定一個二叉樹,輸出其鏡像。
Input:
4
/ \
2 7
/ \ / \
1 3 6 9
Output:
4
/ \
7 2
/ \ / \
9 6 3 1
遞歸實現
- 問題分析
翻轉一個二叉樹,直觀上看,就是把二叉樹的每一層左右順序倒過來。比如問題中的例子,第三層 1-3-6-9 經過變換後變成了9-6-3-1,順序反過來就對了。
再仔細觀察一下,對於上面的例子,根結點的左子結點及其所有的子孫結點構成根節點的左子樹,同樣的,根結點的右子結點及其所有的子孫節點構成根結點的右子樹。因此翻轉一個二叉樹,就是把根結點的左子樹翻轉一下,同樣的把右子樹翻轉一下,在交換左右子樹就可以了。
當然,翻轉左子樹和右子樹的過程和當前翻轉二叉樹的過程沒有區別,就是遞歸的調用當前的函數就可以了。
因此,翻轉二叉樹的步驟可總結如下:
(1)交換根結點的左子結點與右子結點;
(2)翻轉根結點的左子樹(遞歸調用當前函數);
(3)翻轉根結點的右子樹(遞歸調用當前函數)。
- 具體實現
// 二叉樹結點結構體
struct BinaryTreeNode
{
int m_key;
BinaryTreeNode* m_pLeft;
BinaryTreeNode* m_pRight;
BinaryTreeNode() {
m_key = 0;
m_pLeft = m_pRight = nullptr;
}
};
// @brief: 翻轉二叉樹
// @param: root 二叉樹根結點
// @ret: 翻轉後的二叉樹根結點
BinaryTreeNode* invertBT(BinaryTreeNode* root) {
if (root == nullptr) return root;
// 交換左右孩子結點
auto tmp = root->m_pLeft;
root->m_pLeft = root->m_pRight;
root->m_pRight = tmp;
// 遞歸處理左右子樹
invertBT(root->m_pLeft);
invertBT(root->m_pRight);
return root;
}
- 驗證實現
驗證代碼涉及二叉樹的創建和遍歷,驗證如下:
// @brief: 前序遞歸遍歷
void preorderRecursion(BinaryTreeNode* root) {
if (root == nullptr) return;
cout << " " << root->m_key;
preorderRecursion(root->m_pLeft);
preorderRecursion(root->m_pRight);
}
// @brief: 根據前序序列和中序序列構建二叉樹
// @param: preOrder:前序序列; midOrder:中序序列; len:結點數
// @ret: 二叉樹根結點
BinaryTreeNode* constructPreMid(int* preOrder, int* midOrder, int len) {
if (preOrder == nullptr || midOrder == nullptr || len <= 0) return nullptr;
// 前序遍歷的第一個值就是根節點
int rootKey = preOrder[0];
BinaryTreeNode* root = new BinaryTreeNode;
root->m_key = rootKey;
// 只有一個結點
if (len == 1 && *preOrder == *midOrder) return root;
// 在中序序列中找到根結點
int* rootMidOrder = midOrder;
// 左子樹結點數
int leftLen = 0;
while (*rootMidOrder != rootKey && rootMidOrder <= (midOrder + len - 1)) {
++rootMidOrder;
++leftLen;
}
// 在中序序列未找到根結點,輸入錯誤
if (*rootMidOrder != rootKey) return nullptr;
// 構建左子樹
if (leftLen>0) {
root->m_pLeft = constructPreMid(preOrder + 1, midOrder, leftLen);
}
// 構建右子樹
if (len - leftLen - 1>0) {
root->m_pRight = constructPreMid(preOrder + leftLen + 1, rootMidOrder + 1, len - leftLen - 1);
}
return root;
}
int main() {
// 前序+中序構建二叉樹
int preorder[] = {4,2,1,3,7,6,9};
int midorder[] = {1,2,3,4,6,7,9};
auto root = constructPreMid(preorder, midorder, 7);
preorderRecursion(root);
cout << endl;
// 翻轉二叉樹
auto invertRoot = invertBT(root);
cout << "--- after invert ---" << endl;
preorderRecursion(invertRoot); // 4,7,9,6,2,3,1
}
運行輸出:
4 2 1 3 7 6 9
--- after invert ---
4 7 9 6 2 3 1
非遞歸實現
- 問題分析
二叉樹反轉,實際上是遍歷二叉樹的每一個結點,對其左右結點進行交換。那麼我們可以採用層序遍歷,使用隊列來存放待遍歷的結點。
具體步驟如下:
(1)首先把二叉樹的根結點送入隊列;
(2)訪問隊首結點,把它的左子結點和右子結點分別入隊列,然後交換其左右子結點,最後隊首結點出隊列;
(3)重複上面兩步操作,直至隊列空。
- 具體實現
// @brief: 非遞歸翻轉二叉樹
// @param: 二叉樹根結點
// @ret: 翻轉後的二叉樹根結點
BinaryTreeNode* invertBTNonrecu(BinaryTreeNode* root) {
if (root == nullptr) return root;
queue<BinaryTreeNode*> queue;
queue.push(root);
while (!queue.empty()){
// 取隊首結點
BinaryTreeNode* cur = queue.front();
// 左右子結點入隊列
if (cur->m_pLeft != nullptr) queue.push(cur->m_pLeft);
if (cur->m_pRight != nullptr) queue.push(cur->m_pRight);
// 交換左右子結點
auto tmp = cur->m_pLeft;
cur->m_pLeft = cur->m_pRight;
cur->m_pRight = tmp;
// 隊首結點出隊列
queue.pop();
}
return root;
}
- 驗證實現
int main(){
// 前序+中序構建二叉樹
int preorder[] = {4,2,1,3,7,6,9};
int midorder[] = {1,2,3,4,6,7,9};
BinaryTreeNode* root = constructPreMid(preorder, midorder, 7);
preorderRecursion(root);
cout << endl;
// 非遞歸翻轉二叉樹
cout << "--- after non-recursive invert ---" << endl;
auto invertRoot = invertBTNonrecu(root);
preorderRecursion(invertRoot); // 4,7,9,6,2,3,1
}
運行輸出:
4 2 1 3 7 6 9
--- after non-recursive invert ---
4 7 9 6 2 3 1