樹的遍歷方式 --- 前序遍歷

前言

先給大家上一段代碼。這段代碼出自
LeetCode 144.Binary Tree Preorder Traversal
是二叉樹的前序遍歷的遞歸代碼寫法。

/**
 * 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:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> res;
        helper(root, res);
        return res;
    }
    
    void helper(TreeNode *root, vector<int> &res) {
        if ( !root )
            return ;
        
        res.push_back(root->val);
        helper(root->left, res);
        helper(root->right, res);
    }
};

我們單獨將這三行代碼抽離出來,這也是二叉樹的前序遍歷的實現的主體部分:

res.push_back(root->val); // 這裏就是對根進行訪問的一個過程,在題目中具體化爲一個入res的操作
helper(root->left, res);
helper(root->right, res);

很多最初被數據結構與算法蹂躪過的同學看到這段代碼應該會有同樣的反應:哇,好簡單的代碼!而且之後學習中序遍歷與後序遍歷後,發現只需要將res.push_back(root->val)這一行代碼移動一下位置即可。這直接促成了很多人一開始對待二叉樹的遍歷的“不屑”,比如我。初學的時候被遞歸這妖精磨的死去活來,漢諾塔都半天走不出那個邏輯怪圈,樹的遍歷的遞歸代碼居然只要這麼簡簡單單三行代碼,這就是遞歸嘛?真是有夠好笑呢。因爲樹的遍歷方式是根左右,所以先對根進行訪問,再對左子樹和右子樹分別進行訪問就好了,所以用遞歸就行了。
我承認這段代碼是非常的簡單,但如果各位只對這段代碼抱有這樣的態度和想法,那是遠遠不夠的,因爲我們重點要學習的並不是前序遍歷本身,而是前序遍歷的思想。
有些人也許會在看到一道關於樹的遞歸的問題時無從下手,比如這道題需要葉結點的某些信息,需要記錄父結點,需要翻轉,最大深度…你也許會埋怨自己在解遞歸題方面沒有天賦,永遠被困在某一層遞歸中走不出來,那麼我想給你一個建議,先去透徹的瞭解**樹的前中序遍歷方式!**爲什麼我要給你這樣的建議,因爲我就是這裏的有些人。

前序遍歷遞歸版本

我猜很多同學自己沒有完整的推導過一棵二叉樹的前序遍歷到底是怎麼樣的,於是我先來帶大家幹一次這個事。就假定我們有這麼一棵樸實無華的二叉樹。先看下面兩張圖,最初的幾個步驟理解起來是毫無難度的。
在這裏插入圖片描述
在這裏插入圖片描述
現在讓我們把目光聚焦在最後一步,即判斷結點4⃣️的左右子樹均爲空。所對應到的情況就是這樣的:
在這裏插入圖片描述
通俗點講,遞歸函數其實是在函數調用的時候做了一些插入結算,我們剛纔看到的最後一步,對結點4⃣️的左右結點判斷結束後,第三個方框這個插入結算的過程( helper(2⃣️->left) )也就此終止了,現在我們要做的就是再返回到第二個方框,繼續調用helper(2⃣️->right)這個插入過程。
在這裏插入圖片描述
在這裏插入圖片描述
於是方框2這個插入過程也整體結算完畢,回到了結點1⃣️,方框1中的helper(1⃣️->left)這個過程也結算完畢了,繼續執行helper(1⃣️->right)。本來這個函數正常的調用就是先調用helper(1⃣️->left),再調用helper(1⃣️->right),可是卻出現了非常多的插入結算,這就是遞歸。
同時我們也不難發現一個規律,我們將每次插入結算視爲一個方框的話,只有當這個方框中的所有運算結束了之後,我們纔可以跳回到上一級的方框所調用的插入結算中。聽起來有點繞,其實一個正確的遞歸程序也就是插入結算總有終止的時候,那麼終止時回到調用插入結算的那個時間點即可。而這其實就是前序遍歷的思想:
操作做在執行遞歸之前。
爲什麼會有“前序遍歷根左右”這一說,就是因爲在我訪問完這個根結點後我開始往左子樹遞歸,左子樹的第一個結點被訪問到又繼續往這個結點的左子樹遞歸,這是插入結算,所以每次這個左結點都成爲了新的根結點,而當左子樹遞歸不下去的時候再去遞歸右子樹,右子樹也遞歸不下去的時候,這個結算過程,也就是上面我們顯性化的方框裏面的內容執行結束了,該回到哪就回到哪,這個‘哪’即上次這個插入結算調用的位置,這是一種回溯,這也是根左右這種說法的由來。
這裏最後還剩了一個helper(1⃣️->right)的過程我沒有分析,留給你們當做作業好了。
在解釋這個遞歸過程上費了這麼多口舌,也許有同學會說:“多此一舉,考試我會寫這個代碼就好了,我用得着這麼透徹的理解嘛?”我之前就強調過,這個例子實質上是前序遍歷思想的學習。這一點,會在我之後大家刷LeetCode的題目中詳細說明,前序遍歷思想到底在刷題中有什麼樣的應用。

前序遍歷非遞歸版本

假設我們現在擁有一個棧存放訪問過的結點,有一個結點cur去準備越俎代庖遍歷這棵樹。

vector<TreeNode *> node_stack;
TreeNode* cur = root;

最初我們一直將最左邊的結點放入棧中,且將訪問到的元素值加入res中。

while ( ... ) {
	res.push_back(cur->val);
	node_stack.push(cur);
	cur = cur->left;
}

現在我們待解決的問題就是如何在棧中體現諸如4⃣️結點這類結點的回溯。不難發現,這一步的插入結算在4⃣️結點的left判斷爲空後,開始判斷起了4⃣️結點的right部分是否爲空,而我們來觀察這個棧,由於棧先進後出的特性,4⃣️結點就在棧頂的位置,我們最開始不斷將左結點入棧,直到左結點入不動(爲nullptr)的時候,現在我們對這個結點的右結點進行判斷,即我們將棧頂元素移去,使其右結點入棧,然後繼續做我們上述的判斷,就可以達到我們需要的效果。
在這裏插入圖片描述

while ( ... ) {
	...
}

TreeNode* temp = node_stack.top(); // 判斷棧頂元素
node_stack.pop(); // 移去棧頂元素
cur = temp->right; // 而新一輪判斷的結點應該爲這個棧頂元素的right

萬里長征已經走了90%,最後的判斷就是這個while循環的條件是什麼?
這裏我就不再賣關子了,這可以算是一個模版/套路:

// cur不爲空 or 棧不爲空 時 就循環
// 棧和cur同時爲空要麼是遍歷時出了錯誤要麼就是遍歷結束
while ( !node_stack.empty() || cur ) {
	...
}

最後附上完整源碼:

/**
 * 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:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode *> node_stack;
        TreeNode *cur = root;

        while ( !node_stack.empty() || cur ) {
            while ( cur ) {
                res.push_back(cur->val);
                node_stack.push(cur);
                cur = cur->left;
            }

            TreeNode *temp = node_stack.top();
            node_stack.pop();
            cur = temp->right;
        }

        return res;
    }
};

總結

樹的遍歷真的不像各位想象的那麼“脆弱”,其思想貫徹了整個LeetCode刷題!一定要重視起來。
給各位留一個作業:589.N-ary Tree preorder Traversal,請用遞歸和非遞歸兩種方式對這道題進行解答,且遞歸的過程請務必自己推敲一遍!答案源碼我將放在我的github上
https://github.com/18260036169/LeetCode-Tree/tree/master

本期的數據結構與算法就和大家聊到這裏,謝謝各位。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章