1. 程式人生 > >【資料結構和演算法】全面剖析樹的各類遍歷方法

【資料結構和演算法】全面剖析樹的各類遍歷方法

面試中常考到樹的前序,中序,後序和層序遍歷,這篇博文就帶你深度剖析一下二叉樹的各類遍歷演算法的實現

二叉樹的遍歷主要有四種,前序、中序、後序和層序

遍歷的實現方式主要是:遞迴和非遞迴

遞迴遍歷的實現非常容易,非遞迴的實現需要用到棧,難度係數要高一點。

一、二叉樹節點的定義

二叉樹的每個節點由節點值、左子樹和右子樹組成。

class TreeNode{
public:
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
}

二、二叉樹的遍歷方式

前序遍歷:先訪問根節點,再訪問左子樹,最後訪問右子樹

中序遍歷:先訪問左子樹,再訪問根節點,最後訪問右子樹

後序遍歷:先訪問左子樹,再訪問右子樹,最後訪問根節點

層序遍歷:每一層從左到右訪問每一個節點。

舉例說明:(以下面的二叉樹來說明這四種遍歷)

前序遍歷:ABDFGHIEC
中序遍歷:FDHGIBEAC
後序遍歷:FHIGDEBCA
層序遍歷:ABCDEFGHI

大家可以根據這個例子先熟悉一下這四種遍歷,如有不懂的,建議先去google一下,再接著閱讀本文

三、前序遍歷

遞迴版本

按照遍歷的順序很容易就能寫出下列程式碼:

以下程式碼均在leetcode測試通過,二叉樹前序遍歷的原題連結:戳我!leetcode直通車!上車啦!

vector<int> preorderTraversal(TreeNode* root){
    vector<int> ret;
    dfsPreOrder(root,ret);
    return ret;
}
void dfsPreOrder(TreeNode* root,vector<int> &ret){
    if(root==NULL) return;
    ret.push_back(root->val);//儲存根節點
if(root->left!=NULL) dfsPreOrder(root->left,ret);//訪問左子樹 if(root->right!=NULL) dfsPreOrder(root->right,ret);//訪問右子樹 }

非遞迴版本

非遞迴版本需要利用輔助棧來實現

  • 1.首先把根節點壓入棧中
  • 2.此時棧頂元素即為當前根節點,彈出並訪問即可
  • 3.把當前根節點的右子樹和左子樹分別入棧,考慮到棧是先進後出,所以必須右子樹先入棧,左子樹後入棧
  • 4.重複2,3步驟,直到棧為空為止
vector<int> preorderTraversal(TreeNode* root) {
    vector<int> ret;
    if (root==NULL) return ret;
    stack<TreeNode*> st;
    st.push(root);
    while(!st.empty())
    {
        TreeNode* tp = st.top();//取出棧頂元素
        st.pop();
        ret.push_back(tp->val);//先訪問根節點
        if(tp->right!=NULL) st.push(tp->right);//由於棧時先進後出,考慮到訪問順序,先將右子樹壓棧
        if(tp->left!=NULL) st.push(tp->left);//將左子樹壓棧
    }
    return ret;
}

四、中序遍歷

遞迴版本

中序遍歷的訪問順序依次是左子樹->根節點->右子樹,按照遞迴的思想依次訪問即可

以下程式碼均在leetcode測試通過,二叉樹中序遍歷的原題連結:戳我!leetcode直通車!上車啦!

vector<int> inorderTraversal(TreeNode* root) {
    vector<int> ret;
    inorder(root,ret);
    return ret;
}
void inorder(TreeNode* p,vector<int>& ret)
{
    if(p==NULL) return;
    inorder(p->left,ret);//訪問左子樹
    ret.push_back(p->val);//訪問根節點
    inorder(p->right,ret);//訪問右子樹
}

非遞迴版本

中序遍歷的非遞迴版本比前序稍微複雜一點,除了用到輔助棧之外,還需要一個指標p指向下一個待訪問的節點

  • 如果p非空,則將p入棧,p指向p的左子樹
  • 如果p為空,說明此時左子樹已經訪問到盡頭了,彈出當前棧頂元素,進行訪問,並把p設定成p的右子樹的左子樹,即下一個待訪問的節點
vector<int> inorderTraversal(TreeNode* root) {
    vector<int> ret;
    TreeNode* p = root;
    stack<TreeNode*> st;
    while(!st.empty()||p!=NULL){
        if(p){//p非空,代表還有左子樹,繼續
            st.push(p);
            p=p->left;
        }
        else{//如果為空,代表左子樹已經走到盡頭了
            p = st.top();
            st.pop();
            ret.push_back(p->val);//訪問棧頂元素
            if(p->right) {
                st.push(p->right);//如果存在右子樹,將右子樹入棧
                p = p->right->left;//p始終為下一個待訪問的節點
            }
            else p=NULL;
        }
    }
    return ret;
}

五、後序遍歷

遞迴版本

遞迴版本還是一樣,按照訪問順序來寫程式碼即可。

以下程式碼均在leetcode測試通過,二叉樹後序遍歷的原題連結:戳我!leetcode直通車!上車啦!

vector<int> inorderTraversal(TreeNode* root) {
    vector<int> ret;
    inorder(root,ret);
    return ret;
 }
void inorder(TreeNode* p,vector<int>& ret)
{
    if(p==NULL) return;
    inorder(p->left,ret);//訪問左子樹
    inorder(p->right,ret);//訪問右子樹
    ret.push_back(p->val);//訪問根節點
}

非遞迴版本

採用一個輔助棧和兩個指標p和r,p代表下一個需要訪問的節點,r代表上一次需要訪問的節點

1、如果p非空,則將p入棧,p指向p的左子樹

2、如果p為空,代表左子樹到了盡頭,此時判斷棧頂元素

  • 如果棧頂元素存在右子樹且沒有被訪問過(等於r代表被訪問過),則右子樹入棧,p指向右子樹的左子樹
  • 如果棧頂元素不存在或者已經被訪問過,則彈出棧頂元素,訪問,然後p置為null,r記錄上一次訪問的節點p
vector<int> postorderTraversal(TreeNode* root) {
    vector<int> ret;
    TreeNode* p = root;
    stack<TreeNode*> st;
    TreeNode* r = NULL;
    while(p||!st.empty())
    {
        if(p)
        {
            st.push(p);
            p = p -> left;
        }
        else
        {
            p = st.top();
            if(p->right&&p->right!=r)
            {
                p = p->right;
                st.push(p);
                p = p->left;
            }
            else 
            {
                p = st.top();
                st.pop();
                ret.push_back(p->val);
                r= p;
                p = NULL;
            }
        }
    }
    return ret;
}

還有另一種解法,大家可以看看前序遍歷的非遞迴版本,訪問順序依次是根節點->左子樹->右子樹,如果將壓棧順序改動一下,可以很容易得到根節點->右子樹->左子樹,觀察這個順序和後序遍歷左子樹->右子樹->根節點正好反序。

vector<int> postorderTraversal(TreeNode* root) {
    vector<int> ret;
    if(root==NULL) return ret;
    stack<TreeNode*> st;
    st.push(root);
    while(!st.empty())
    {
       TreeNode* tmp = st.top();
       ret.push_back(tmp->val);//先訪問根節點
       st.pop();
       if(tmp->left!=NULL) st.push(tmp->left);//再訪問左子樹
       if(tmp->right!=NULL) st.push(tmp->right);//最後訪問右子樹
    }
    reverse(ret.begin(),ret.end());//將結果反序輸出
    return ret;
}

六、層序遍歷

層序遍歷,即按層序從左到右輸出二叉樹的每個節點。如例子中的A(第一層)BC(第二層)DE(第三層)FG(第四層)HI(第五層)

層序遍歷需要藉助佇列queue來完成,因為要滿足先進先去的訪問順序。具體思路看程式碼:

以下程式碼均在leetcode測試通過,二叉樹層序遍歷的原題連結:戳我!leetcode直通車!上車啦!

vector<vector<int>> levelOrder(TreeNode* root) {
    vector<vector<int>> ret;
    if(root==NULL) return ret;
    queue<TreeNode*> que;
    que.push(root);
    while(!que.empty())
    {
        vector<int> temp;
        queue<TreeNode*> tmpQue;//儲存下一層需要訪問的節點
        while(!que.empty())//從左到右依次訪問本層
        {
            TreeNode* tempNode = que.front();
            que.pop();
            temp.push_back(tempNode->val);
            if(tempNode->left!=NULL) tmpQue.push(tempNode->left);//左子樹壓入佇列
            if(tempNode->right!=NULL) tmpQue.push(tempNode->right);//右子樹壓入佇列
        }
        ret.push_back(temp);
        que=tmpQue;//訪問下一層
    }
    return ret;
}

七、其他經典考題

根據前序和中序遍歷來構造二叉樹

前序遍歷的順序是:根節點->左子樹->右子樹,中序遍歷的順序時:左子樹->根節點->右子樹。

在前序遍歷中第一個節點為根節點,然後去中序遍歷中找到根節點,則其左邊為左子樹,右邊為右子樹

例如前序遍歷ABC,中序遍歷BAC,在前序遍歷中找到根節點A,在中序遍歷中A的左邊B為左子樹,右邊C為右子樹。

然後一次遞迴下去,就可以把整棵數構造出來了。

以下程式碼均在leetcode測試通過,構造二叉樹的原題連結:戳我!leetcode直通車!上車啦!

typedef vector<int>::iterator vi;
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
    if(preorder.empty()||inorder.empty()) return (TreeNode*)NULL;
    vi preStart = preorder.begin();
    vi preEnd = preorder.end()-1;
    vi inStart = inorder.begin();
    vi inEnd = inorder.end()-1;
    return constructTree(preStart,preEnd,inStart,inEnd);
}
TreeNode* constructTree(vi preStart,vi preEnd,vi inStart,vi inEnd)
{
    if(preStart>preEnd||inStart>inEnd) return NULL;
    //前序遍歷的第一個節點為根節點
    TreeNode* root = new TreeNode(*preStart);
    if(preStart==preEnd||inStart==inEnd) return root;
    vi rootIn = inStart;
    while(rootIn!=inEnd){//在中序遍歷中找到根節點
        if(*rootIn==*preStart) break;
        else ++rootIn;
    }
    root->left = constructTree(preStart+1,preStart+(rootIn-inStart),inStart,rootIn-1);//遞迴構造左子樹
    root->right = constructTree(preStart+(rootIn-inStart)+1,preEnd,rootIn+1,inEnd);//遞迴構造右子樹
    return root;
} 

根據中序和後序遍歷構造二叉樹

與上面的題目比較相似,後序遍歷中最後一個節點為根節點,然後在中序遍歷中找到根節點,左邊為左子樹,右邊為右子樹。

以下程式碼均在leetcode測試通過,構造二叉樹的原題連結:戳我!leetcode直通車!上車啦!

TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
    if(inorder.empty()||postorder.empty()) return NULL;
    return constructTree(inorder,postorder,0,inorder.size()-1,0,postorder.size()-1);
}
TreeNode* constructTree(vector<int>& inorder, vector<int>& postorder, int inStart,int inEnd,int postStart,int postEnd)
{
    if(postStart>postEnd||inStart>inEnd) return NULL;
    TreeNode* root = new TreeNode(postorder[postEnd]);
    if(postStart==postEnd||inStart==inEnd) return root;
    int i ;
    for(i = inStart ;i<inEnd;i++)//在中序遍歷中找到根節點
    {
        if(inorder[i]==postorder[postEnd]) break;
    }
    root->left = constructTree(inorder,postorder,inStart,i-1,postStart,postStart+i-inStart-1);//遞迴構造左子樹
    root->right = constructTree(inorder,postorder,i+1,inEnd,postStart+i-inStart,postEnd-1);//遞迴構造右子樹
    return root;
}

求二叉樹的深度

採用深度優先搜尋,可以很容易計算出深度

以下程式碼均在leetcode測試通過,二叉樹的深度的原題連結:戳我!leetcode直通車!上車啦!

int maxDepth(TreeNode* root) {
    return DfsTree(root);
}
int DfsTree(TreeNode* root){
    if(root==NULL) return 0;
    int left = DfsTree(root->left);//左子樹的深度
    int right = DfsTree(root->right);//右子樹的深度
    return left>right?left+1:right+1;//比較左右子樹的深度,取最大值
}

判斷是否為平衡二叉樹

利用上面求深度的思想,求出左右子樹的深度,判斷它們相差是否大於1,如果大於則返回false。

以下程式碼均在leetcode測試通過,判斷平衡二叉樹的原題連結:戳我!leetcode直通車!上車啦!

bool isBalanced(TreeNode* root) {
    return dfsTree(root)!=-1;
}
int dfsTree(TreeNode* root)
{
    if(root==NULL) return 0;
    int left = dfsTree(root->left);//求左子樹的深度
    if(left == -1) return -1;//返回-1代表左子樹不平衡
    int right = dfsTree(root->right);//求右子樹的深度
    if(right== -1) return -1;//返回-1代表右子樹不平衡
    if(abs(left-right)>1) return -1;//如果左右子樹均平衡,則判斷它們是否相差小於等於1
    return max(left,right)+1;//返回該根節點樹的深度
}

本篇部落格到這裡就結束了,當然二叉樹的變種考題還有很多中,後序也會陸續收集進來,敬請關注我的小站

如有任何疑問,可以在下方留言去留言,也可以直接發郵件給我,詳見聯絡方式