二叉樹遍歷(前序、中序、后序、層次、深度優先、廣度優先遍歷)


轉載:二叉樹遍歷(前序、中序、后序、層次、深度優先、廣度優先遍歷)

【數據結構和算法】全面剖析樹的各類遍歷方法

二叉樹

概念

二叉樹是一種非常重要的數據結構,非常多其他數據結構都是基於二叉樹的基礎演變而來的。對於二叉樹,有深度遍歷和廣度遍歷,深度遍歷有前序、中序以及后序三種遍歷方法,廣度遍歷即我們尋常所說的層次遍歷。由於樹的定義本身就是遞歸定義,因此採用遞歸的方法去實現樹的三種遍歷不僅easy理解並且代碼非常簡潔,而對於廣度遍歷來說,須要其他數據結構的支撐。比方堆了。所以。對於一段代碼來說,可讀性有時候要比代碼本身的效率要重要的多。

四種基本的遍歷思想

前序遍歷:根結點 ---> 左子樹 ---> 右子樹

中序遍歷:左子樹---> 根結點 ---> 右子樹

后序遍歷:左子樹 ---> 右子樹 ---> 根結點

層次遍歷:僅僅需按層次遍歷就可以

比如。求以下二叉樹的各種遍歷

前序遍歷:1  2  4  5  7  8  3  6 

中序遍歷:4  2  7  5  8  1  3  6

后序遍歷:4  7  8  5  2  6  3  1

層次遍歷:1  2  3  4  5  6  7  8

一、前序遍歷

1)依據上文提到的遍歷思路:根結點 ---> 左子樹 ---> 右子樹,非常easy寫出遞歸版本號:

public void preOrderTraverse1(TreeNode root) {  
        if (root != null) {  
            System.out.print(root.val+"  ");  
            preOrderTraverse1(root.left);  
            preOrderTraverse1(root.right);  
        }  
    }  

2)如今討論非遞歸的版本號:

依據前序遍歷的順序,優先訪問根結點。然后在訪問左子樹和右子樹。所以。對於隨意結點node。第一部分即直接訪問之,之后在推斷左子樹是否為空,不為空時即反復上面的步驟,直到其為空。若為空。則須要訪問右子樹。注意。在訪問過左孩子之后。須要反過來訪問其右孩子。所以,須要棧這樣的數據結構的支持。對於隨意一個結點node,詳細過程例如以下:

a)訪問之,並把結點node入棧。當前結點置為左孩子;

b)推斷結點node是否為空,若為空。則取出棧頂結點並出棧,將右孩子置為當前結點;否則反復a)步直到當前結點為空或者棧為空(能夠發現棧中的結點就是為了訪問右孩子才存儲的)

代碼例如以下:

public void preOrderTraverse2(TreeNode root) {  
        Stack<TreeNode> stack = new Stack<>();  
        TreeNode pNode = root;  
        while (pNode != null || !stack.isEmpty()) {  
            if (pNode != null) {  
                System.out.print(pNode.val+"  ");  
                stack.push(pNode);  
                pNode = pNode.left;  
            } else { //pNode == null && !stack.isEmpty()  
                TreeNode node = stack.pop();  
                pNode = node.right;  
            }  
        }  
    } 

二、中序遍歷

1)依據上文提到的遍歷思路:左子樹 ---> 根結點 ---> 右子樹,非常easy寫出遞歸版本號:

public void inOrderTraverse1(TreeNode root) {  
        if (root != null) {  
            inOrderTraverse1(root.left);  
            System.out.print(root.val+"  ");  
            inOrderTraverse1(root.right);  
        }  
    }  

2)非遞歸實現,有了上面前序的解釋,中序也就比較簡單了。同樣的道理。僅僅只是訪問的順序移到出棧時。代碼例如以下:

public void inOrderTraverse2(TreeNode root) {  
        Stack<TreeNode> stack = new Stack<>();  
        TreeNode pNode = root;  
        while (pNode != null || !stack.isEmpty()) {  
            if (pNode != null) {  
                stack.push(pNode);  
                pNode = pNode.left;  
            } else { //pNode == null && !stack.isEmpty()  
                TreeNode node = stack.pop();  
                System.out.print(node.val+"  ");  
                pNode = node.right;  
            }  
        }  
    }  

三、后序遍歷

1)依據上文提到的遍歷思路:左子樹 ---> 右子樹 ---> 根結點。非常easy寫出遞歸版本號:

public void postOrderTraverse1(TreeNode root) {  
        if (root != null) {  
            postOrderTraverse1(root.left);  
            postOrderTraverse1(root.right);  
            System.out.print(root.val+"  ");  
        }  
    }  

2)

后序遍歷的非遞歸實現是三種遍歷方式中最難的一種。由於在后序遍歷中,要保證左孩子和右孩子都已被訪問而且左孩子在右孩子前訪問才干訪問根結點,這就為流程的控制帶來了難題。以下介紹兩種思路。

      第一種思路:對於任一結點P,將其入棧,然后沿其左子樹一直往下搜索。直到搜索到沒有左孩子的結點,此時該結點出如今棧頂,可是此時不能將其出棧並訪問,因此其右孩子還為被訪問。

所以接下來依照同樣的規則對其右子樹進行同樣的處理,當訪問完其右孩子時。該結點又出如今棧頂,此時能夠將其出棧並訪問。這樣就保證了正確的訪問順序。能夠看出,在這個過程中,每一個結點都兩次出如今棧頂,僅僅有在第二次出如今棧頂時,才干訪問它。因此須要多設置一個變量標識該結點是否是第一次出如今棧頂。

void postOrder2(BinTree *root)    //非遞歸后序遍歷
{
    stack<BTNode*> s;
    BinTree *p=root;
    BTNode *temp;
    while(p!=NULL||!s.empty())
    {
        while(p!=NULL)              //沿左子樹一直往下搜索。直至出現沒有左子樹的結點 
        {
            BTNode *btn=(BTNode *)malloc(sizeof(BTNode));
            btn->btnode=p;
            btn->isFirst=true;
            s.push(btn);
            p=p->lchild;
        }
        if(!s.empty())
        {
            temp=s.top();
            s.pop();
            if(temp->isFirst==true)     //表示是第一次出如今棧頂 
             {
                temp->isFirst=false;
                s.push(temp);
                p=temp->btnode->rchild;    
            }
            else                        //第二次出如今棧頂 
             {
                cout<<temp->btnode->data<<" ";
                p=NULL;
            }
        }
    }    
} 

另外一種思路:要保證根結點在左孩子和右孩子訪問之后才干訪問,因此對於任一結點P。先將其入棧。假設P不存在左孩子和右孩子。則能夠直接訪問它;或者P存在左孩子或者右孩子。可是其左孩子和右孩子都已被訪問過了。則相同能夠直接訪問該結點。若非上述兩種情況。則將P的右孩子和左孩子依次入棧。這樣就保證了每次取棧頂元素的時候,左孩子在右孩子前面被訪問。左孩子和右孩子都在根結點前面被訪問。

void postOrder3(BinTree *root)     //非遞歸后序遍歷
{
    stack<BinTree*> s;
    BinTree *cur;                      //當前結點 
    BinTree *pre=NULL;                 //前一次訪問的結點 
    s.push(root);
    while(!s.empty())
    {
        cur=s.top();
        if((cur->lchild==NULL&&cur->rchild==NULL)||
           (pre!=NULL&&(pre==cur->lchild||pre==cur->rchild)))
        {
            cout<<cur->data<<" ";  //假設當前結點沒有孩子結點或者孩子節點都已被訪問過 
              s.pop();
            pre=cur; 
        }
        else
        {
            if(cur->rchild!=NULL)
                s.push(cur->rchild);
            if(cur->lchild!=NULL)    
                s.push(cur->lchild);
        }
    }    
}

四、層次遍歷

層次遍歷的代碼比較簡單。僅僅須要一個隊列就可以。先在隊列中增加根結點。之后對於隨意一個結點來說。在其出隊列的時候,訪問之。同一時候假設左孩子和右孩子有不為空的。入隊列。代碼例如以下:

public void levelTraverse(TreeNode root) {  
        if (root == null) {  
            return;  
        }  
        LinkedList<TreeNode> queue = new LinkedList<>();  
        queue.offer(root);  
        while (!queue.isEmpty()) {  
            TreeNode node = queue.poll();  
            System.out.print(node.val+"  ");  
            if (node.left != null) {  
                queue.offer(node.left);  
            }  
            if (node.right != null) {  
                queue.offer(node.right);  
            }  
        }  
    }  

五、深度優先遍歷

事實上深度遍歷就是上面的前序、中序和后序。可是為了保證與廣度優先遍歷相照顧,也寫在這。代碼也比較好理解,事實上就是前序遍歷,代碼例如以下:

public void depthOrderTraverse(TreeNode root) {  
        if (root == null) {  
            return;  
        }  
        Stack<TreeNode> stack = new Stack<>();  
        stack.push(root);  
        while (!stack.isEmpty()) {  
            TreeNode node = stack.pop();  
            System.out.print(node.val+"  ");  
            if (node.right != null) {  
                stack.push(node.right);  
            }  
            if (node.left != null) {  
                stack.push(node.left);  
            }  
        }  
    }  

六、后序遍歷的簡單思路

前序遍歷的非遞歸版本,訪問順序依次是根節點->左子樹->右子樹,如果將壓棧順序改動一下,可以很容易得到根節點->右子樹->左子樹,觀察這個順序和后序遍歷左子樹->右子樹->根節點正好反序。

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;
}

 

public void postOrderTraverse(Node node) {
        if (node == null) {
            return;
        }
        Node temp = node;
        Stack<Node> stack = new Stack<>();
        List<Node> nodes = new ArrayList<>();
        while (temp != null || !stack.isEmpty()) {
            if (temp != null) {
                nodes.add(temp);
                stack.push(temp);
                temp = temp.rightChild;
            } else {
                Node popNode = stack.pop();
                temp = popNode.leftChild;
            }
        }
        
        for (int i = nodes.size() - 1; i >= 0; i--) {
            System.out.print(nodes.get(i).data + " ");
        }
    }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM