二叉樹的遍歷
二叉樹是一種常用並且重要的數據結構,其是每個節點最多有兩個子樹的樹結構。通常子樹被稱作“左子樹”和“右子樹”。二叉樹中一個比較常用的操作是遍歷二叉樹。
所謂遍歷(Traversal)是指沿着某條搜索路線,依次對樹中每個結點均做一次且僅做一次訪問。訪問結點所做的操作依賴於具體的應用問 題。
遍歷是二叉樹上最重要的運算之一,是二叉樹上進行其它運算之基礎。
———-[百度百科]
本文中使用的二叉樹的節點結構如下:
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
二叉樹的遍歷根據訪問節點的順序不同分爲三種:前序遍歷(Preorder),中序遍歷(Inorder),後序遍歷(PostOrder)。代碼可以分成遞歸和非遞歸兩種實現方式。
前序遍歷
前序遍歷是對於一個節點,我們先訪問該節點,再訪問該節點的左子樹,最後訪問該節點的右子樹。
遞歸的實現方式如下:
void preorder(TreeNode *root, vector<int> &result)
{
if(root)
{
result.push_back(root->val);
if(root->left)
preorder(root->left,result);//遞歸左子樹
if(root->right)
preorder(root->right,result);//遞歸右子樹
}
}
vector<int> preorderTraversal(TreeNode *root) {
vector<int> result;
preorder(root,result);
return result;
}
非遞歸實現方式如下:
vector<int> preorderTraversal(TreeNode *root) {
vector<int> result;
if(!root) return result;
stack<TreeNode*> s;
s.push(root);
while(!s.empty())
{
TreeNode* n = s.top();
result.push_back(n->val);
s.pop();
//根據棧後進先出的特點先存入右子樹,再存入左子樹,確保 了每次都先訪問節點的左子樹,然後是右子樹
if(n->right) s.push(n->right);
if(n->left) s.push(n->left);
}
return result;
}
非遞歸的核心是利用棧,先將根節點放入棧中,然後不斷對棧中元素訪問,每出棧一個元素,就可看成訪問了該元素,然後根據棧後進先出的特點先存入節點右子樹,再存入左子樹,確保了每次都先訪問節點的左子樹,然後是右子樹。
中序遍歷
中序遍歷是對於一個節點,我們先訪問該節點的左子樹,再訪問該節點,最後訪問該節點的右子樹。
遞歸的實現方式
void inorder(TreeNode *root,vector<int> &ret)
{
if(root)
{
inorder(root->left,ret);
ret.push_back(root->val);
inorder(root->right,ret);
}
}
vector<int> inorderTraversal(TreeNode *root) {
vector<int> ret;
inorder(root,ret);
return ret;
}
非遞歸的實現方式
vector<int> inorderTraversal(TreeNode *root) {
vector<int> ret;
stack<TreeNode *> s;
TreeNode *p = root;
do
{
while (p != NULL)
{
s.push(p);
p = p->left;
}
if (!s.empty())
{
p = s.top();
s.pop();
ret.push_back(p->val);
p = p->right;
}
} while (!s.empty() || p != NULL);
return ret;
}
非遞歸的實現也是利用了棧,當遇到一個節點時,先遍歷到其左子樹的最左邊,途中不斷地入棧,則每當出棧一個元素時,其左子樹已經完全被遍歷,然後訪問該節點,之後指向該節點的右子樹。
後序遍歷
後序遍歷是對於一個節點,我們先訪問該節點的左子樹,再訪問該節點的右子樹,最後訪問該節點。
遞歸方式:
void postorder(TreeNode *root,vector<int> &result)
{
if(root)
{
postorder(root->left,result);
postorder(root->right,result);
result.push_back(root->val);
}
}
vector<int> postorderTraversal(TreeNode *root) {
vector<int> result;
postorder(root,result);
return result;
}
非遞歸方式:
vector<int> postorderTraversal(TreeNode *root) {
vector<int> result;
if(!root) return result;
stack<TreeNode*> stack;
stack.push(root);
while(!stack.empty())
{
TreeNode* node = stack.top();
result.push_back(node->val);
stack.pop();
if(node->left) stack.push(node->left);
if(node->right) stack.push(node->right);
}
reverse(result.begin(), result.end());
return result;
}
後序遍歷要是直接用非遞歸的方式實現會有點麻煩,我們可以按照先節點,再右子樹,再左子樹的順便遍歷,而這個順序的實現方式我們可以參照先序遍歷,只不過是左右子樹的入棧順序變一下,之後再將遍歷的節點逆序即可。
層次遍歷
層次遍歷是一層層遍歷的樹,如下圖:
該樹的層次遍歷結果是:3,9,20,15,7.
遞歸實現方式,也可以看成是DFS(深度優先搜索):
void solve(int dep,TreeNode *root,vector<vector<int>> &result)
{
if(!root)
return;
if(dep>=result.size())
{
vector<int> res;
res.push_back(root->val);
result.push_back(res);
}else{
result[dep].push_back(root->val);
}
solve(dep+1,root->left,result);
solve(dep+1,root->right,result);
}
vector<vector<int> > levelOrder(TreeNode *root) {
vector<vector<int>> result;
solve(0,root,result);
return result;
}
非遞歸實現方式,可看成是BFS(廣度優先搜素)
vector<vector<int> > levelOrder(TreeNode *root) {
vector<vector<int>> result;
if(root == NULL) return result;
queue<TreeNode*> q;
queue<int> queue_level;
q.push(root);
queue_level.push(0);
vector<int> elem;
result.push_back(elem);
while(q.size() > 0){
TreeNode* x = q.front();
q.pop();
int l = queue_level.front();
queue_level.pop();
if(l > (result.size()-1) ){ //需要新增一個層級
vector<int> elem;
result.push_back(elem);
}
result[l].push_back(x->val);
//將左右節點加入到隊列中,層級需要加1
if(x->left != NULL){
q.push(x->left);
queue_level.push(l+1);
}
if(x->right != NULL){
q.push(x->right);
queue_level.push(l+1);
}
}
return result;
}
廣度優先搜素需要維護兩個隊列,一個用來保存節點,一個來保存節點的層級,兩者在兩個隊列裏一一對應,根節點是0層。每次取兩個隊列的頭元素,判斷層級和結果集result的大小,做出對應的操作(和遞歸一樣的操作)。之後將節點的左右節點加入到隊列中,直到遍歷所有節點。