博客 | LeetCode 617. Merge Two Binary Trees

本文原載於知乎專欄“LeetCode從易到難”

在日常的業務系統開發中,通常架構設計>數據結構設計>算法設計,架構設計,重在理解業務場景,考慮用戶規模和系統適配性的基礎上,想清楚每個模塊的職責,剩下的就是利用公司的基礎組件,比如:分佈式Cache和RPC框架,組合起來即可。數據結構設計,重在理清數據流轉的基礎上,能實現高效存取即可,最常使用的是map,高級點就是bitset,即可滿足絕大多數場景需求。而算法設計,業務開發平時真的用不上,雖然在往年的網易雲課堂上,參加了王宏志老師的《算法設計與分析》入門篇和進階篇,並順利結課,但因常年沒有使用和複習,基本也原路退還,但仍懷有“我有基礎,有能力解決常見算法問題”的妄念當中。

直到連續倒在3個60%左右接受率的Easy Tree上,才發現問題的嚴重性。下決心一定要徹底理解Easy Tree的實現邏輯,保證這是最後一篇Easy難度的Tree文章。不保證碰到Medium的時候依舊不會解~嘿嘿~

打開這個問題後:

1,第一個最直觀的感受是,可以嘗試使用遞歸求解(樹問題的銀彈),但因爲本身對遞歸的不熟悉,遞歸解法都是靠靈感,解題失敗;(因爲一個點沒想到,後面會講)

2,反而想到二叉樹通常可以使用數組表示,只需將2顆樹分別以“完美二叉樹”的順序遍歷到數組內,nullptr的子節點賦0,接着再按元素相加求和,最後再重新構造一顆二叉樹即可;

3,數組的思路,首先遇到的問題就是,如何退出循環。即,如何知道當前節點是樹的最後一個有效節點。我的第一思路是,求樹高,再按二叉樹子節點的公式,以“完美二叉樹”遍歷後退出即可;

4,所以樹高?emmm...,因爲常見的樹遞歸解法,通常先使用遞歸接觸到它最左的葉子節點,返回葉子節點的求解值,在函數入口分支返回,再考慮非葉子節點情況下,目標值如何通過葉子節點順着子節點向上傳遞,在函數尾部返回目標值即可。樹高比較“簡單”(不久前才陣亡過),遍歷到葉子節點返回0,否則分別遞歸該節點左,右子樹,返回左右子樹的樹高,則該節點的高度就是左右子樹中較大的樹高加1,返回即可。所以,任何樹遞歸問題的核心,就是找到目標值傳遞的方式。《669. Trim a Binary Search Tree》的核心比較容易想:首先是一棵搜索樹,其次在邊界條件整棵子樹都能砍掉;(其實,Merge的核心思路也類似,一時間沒聯想到)

5,樹高完成後,該問題順利AC,但憑經驗肯定是山路十八彎。再回到最初的遞歸解法,因爲有樹高的熱身,再次回到問題本身的時候,竟然思路順利很多。但仍然在函數入口跳出循環的步驟上百思不得其解。直到閃電劃過腦海,發現如果2棵樹的節點中,一棵樹的某個節點不存在,那麼,合併後的樹在這個節點,甚至這個節點的所有子樹,都可以複用另一顆樹的該節點,因爲它理論上也包含了合併後樹的所有子樹!(類似砍掉整棵樹,不受影響)想通了這一點,問題就解決了80%,剩下的20%就是當節點同時存在時,新建節點,然後類似樹高的解法一樣,新建節點的左右子樹分別遞歸2棵樹的左右節點即可。再次AC。

6,遞歸解法完成後,理論上我也知道任何遞歸解法都有對應的迭代解法,同時迭代解法一定可以用棧來實現。因爲平時後臺業務開發中,棧使用極少,更多時候是用隊列,一開始也沒想清楚棧要如何實現。

7,上網搜索遞歸轉迭代的方法論,思路是:(1)設計數據結構,想清楚棧中元素需要存儲的字段,至少包含遞歸解法中遞歸函數的參數,它也是核心,其次就是其他標記變量;(2)在主函數入口壓棧,模擬第一次進入函數,判斷棧空進入循環,模擬不斷壓棧,直至邊界的過程;(3)循環入口至遞歸調用前的邏輯,模擬函數真正的處理邏輯,棧元素的其他標記變量再此大顯身手;(4)遞歸調用的地方,就是壓棧的位置,即函數遞歸的本質;(5)Over,生搬硬套的遞歸轉迭代方法論。

8,代碼如下,老天保佑以後再也不要用Easy Tree的專欄文章!

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    
    // 棧元素
    struct SkItem
    {
        // 父節點
        TreeNode* node = nullptr;
        // 的左右節點
        bool isLeft = false;
        // 當前比較對
        pair<TreeNode*, TreeNode*> item;
        
        SkItem(TreeNode* node, bool isLeft, pair<TreeNode*, TreeNode*> item)
            : node(node), isLeft(isLeft), item(item) {}
    };
    
    TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {
        
        // 遞歸法
        /*
        if (t1 == nullptr) return t2;
        if (t2 == nullptr) return t1;
        
        TreeNode* node = new TreeNode(t1->val + t2->val);
        
        node->left = mergeTrees(t1->left, t2->left);
        node->right = mergeTrees(t1->right, t2->right);
        
        return node;
        */
        
        // 迭代法
        
        // 入口首先保護
        if (t1 == nullptr && t2 == nullptr) return nullptr;
        else if (t1 != nullptr && t2 == nullptr) return t1;
        else if (t1 == nullptr && t2 != nullptr) return t2;
        
        // 否則,堆棧調用
        stack<SkItem> sk;
        
        // 頭節點
        TreeNode* node = new TreeNode(t1->val + t2->val);
        
        // 壓棧左節點
        SkItem a(node, true, make_pair(t1->left, t2->left));
        sk.push(a);
        // 壓棧右節點
        SkItem b(node, false, make_pair(t1->right, t2->right));
        sk.push(b);
        
        while (!sk.empty())
        {
            // 取出棧頂元素,模仿函數調用
            SkItem p = sk.top();
            sk.pop();
            
            if (p.item.first == nullptr && p.item.second != nullptr)
            {
                if (p.isLeft)
                {
                    p.node->left = p.item.second;
                }
                else
                {
                    p.node->right = p.item.second;
                }
            }
            else if (p.item.first != nullptr && p.item.second == nullptr)
            {
                if (p.isLeft)
                {
                    p.node->left = p.item.first;
                }
                else
                {
                    p.node->right = p.item.first;
                }
            }
            else if (p.item.first != nullptr && p.item.second != nullptr)
            {
                // 子節點
                TreeNode* child = new TreeNode(p.item.first->val + p.item.second->val);

                // 壓棧左節點
                SkItem a(child, true, make_pair(p.item.first->left, p.item.second->left));
                sk.push(a);
                // 壓棧右節點
                SkItem b(child, false, make_pair(p.item.first->right, p.item.second->right));
                sk.push(b);
                
                if (p.isLeft)
                {
                    p.node->left = child;
                }
                else
                {
                    p.node->right = child;
                }
            }
        }
        
        return node;
    }
};
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章