本文原載於知乎專欄“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; } };