1. 程式人生 > >資料結構——樹(5)——樹的先序,中序,後序,層次遍歷

資料結構——樹(5)——樹的先序,中序,後序,層次遍歷

樹的儲存結構

之前我們提到過堆是一棵特殊的樹,在堆的儲存方式中,我們選擇了陣列的方式去儲存。因為只要知道一個節點的位置我們就能找到其他的節點的位置。但是前提是堆是一棵完全二叉樹。樹的結構多種多樣,沒人規定說一個節點只能有一個孩子。
我們先看下圖,一個最簡單的二叉樹:
這裡寫圖片描述
怎麼表達?看到箭頭,我們應該立馬反應過來一個基本的工具,沒錯就是指標,應該立馬反應過來一個數據結構,那就是連結串列!
這裡寫圖片描述
對吧,把value換成我們的A B C,分別定義兩個指標,一個指向左邊的節點,一個指向右邊的節點,其他的節點類似,就能把一棵二叉樹表達出來了。很完美對吧。那麼我們怎麼用c++來描述這一個資料結構呢?程式碼如下:

struct Tree{
    string value;
    Tree *left;
    Tree *right;
};

程式碼對應的結構圖應該是這樣的:
這裡寫圖片描述

但肯定有人問,那麼三叉樹呢?n叉樹呢是不是也可以同樣的方式來定義呢?當然!就先說說三叉樹,其實三叉樹就是在中間多了一個往下的箭頭對吧,那麼我們的資料結構按上面的結構,多用一個指向中間的指標不就完事了?看下圖:
這裡寫圖片描述
那麼 n 叉樹呢?這個時候我們就發現如果按照上述的定義,太麻煩了,程式碼都得多幾十行,而且不好記憶。那麼我們可以把這些指標存進一個vector中,就像這樣:

struct Tree {
  string
value; Vector<Tree *> children; };

這個vector裡面儲存著指向他們孩子節點的指標。就像這樣:
這裡寫圖片描述

那麼樹的儲存結構在記憶體中是怎麼樣的呢?看下圖
這裡寫圖片描述

樹的四種遍歷方式

我們先看一顆樹:
這裡寫圖片描述
這是一棵簡單的二叉樹,現在我們來看看什麼叫樹的遍歷:
遍歷樹的節點並在每個節點執行一些操作的過程稱為遍歷或行走樹(traversing or walking the tree)。在許多情況下,將需要按照數值的的順序遍歷樹 這種處理在左右子樹的遞迴呼叫之間的節點的方法被稱為中序遍歷(inorder traversal)。 另外還經常發生另外兩種型別的樹遍歷,它們被稱為前序遍歷和後序遍歷(preorder and postorder traversals)

先序遍歷(preOrder)

遍歷樹的節點並在每個節點執行一些操作的過程稱為遍歷樹,這是我們講遍歷之前提到過的,那麼我們就以這個模式來講解這個部分的內容。操作我們就用最簡單的輸出資料來實現。這樣先序遍歷的步驟就是:
- Do something
- Go left
- Go right

結合程式碼跟上面的樹我們來講解一下具體的步驟

void preOrder(Tree *tree){
    if (tree == NULL) return; //如果樹為空,直接返回函式
    cout << tree -> value << endl;
    preOrder(tree -> left);
    preOrder(tree -> right);
}

首先,判斷樹是否為空,是,則直接返回,否則進行下一步。先執行操作(這裡是輸出節點的值),為Grumpy,然後繼續先序遍歷節點的左子樹,此時的tree指向的是doc節點,判斷這子樹是否為空?結果是否,因為doc下面還有子節點(孩子分別為Bashful和Dopey),判斷為否後執行操作,輸出Doc,繼續先序遍歷,此時tree指向Bashful,判斷這子樹是否為空,結果為否!(這是個葉子,沒有子節點,但是也是一棵孤立的樹),執行輸出操作,輸出Bashful。繼續執行先序遍歷操作,此時指標指向Bashful的左孩子,由於它沒有孩子,於是判斷這子樹為空,返回程式(return;)。此時指標往上回退,對右邊進行先序遍歷,輸出Dopey。判斷這個節點有無孩子,繼續執行上述操作。這樣子,我們可以看到先序遍歷是先遍歷左邊的子樹,再遍歷右邊的子樹。輸出為:
這裡寫圖片描述
這裡寫圖片描述

中序遍歷(inOrder)

中序遍歷的步驟為:
- Go left
- Do something
- Go right

看一段程式碼:

void inOrder(Tree *tree){
    if (tree == NULL) return;
    inOrder(tree -> left);
    cout << tree -> value << endl;
    inOrder(tree -> right);
}

首先判斷樹是否為空,是直接返回,否則繼續。繼續中序遍歷樹的左節點,在這個例子中,一開始的指標指向的是Grumpy,因為樹不為空,因此遍歷它的左節點,也就是這個時候指標指向Doc,樹仍然不為空,那麼繼續遍歷其左節點,此時指標指向Bashful,此時bashful是個孤立的樹葉,但仍是一棵子樹。繼續遍歷其左子樹,發現此時為空,程式直接返回上一級指標,也就是指向Bashful,並執行輸出操作。接下來遍歷其右節點,同理發現沒有,指標回退到Doc,由於是由inOrder(tree -> left);回退而來,所以接下來輸出Doc,然後執行inOrder(tree -> right),指標指向Doc的右節點,也就是Dopey。過程同Bashful。完成後指標就會退至Grumpy。由於是由inOrder(tree -> left);回退而來,所以接下來輸出Doc,然後執行inOrder(tree -> right),指標此時指向Sleepy,樹為空嗎,否,指標繼續往下,遍歷左節點,也就是指標此時指向Happy,此時Happy是個孤立的樹葉,但仍是一棵子樹。繼續遍歷其左子樹,發現此時為空,程式直接返回上一級指標,也就是指向Happy,並執行輸出操作。然後重複上述的操作,得出結果應該是這樣的:
這裡寫圖片描述

Bashful
Doc
Dopey
grumpy
Happy
Sleepy
Sneezy

後序遍歷(postOrder)

步驟為:
- Go left
- Go right
- Do something

程式碼為:

void postOrder(Tree * tree) {
    if(tree == NULL) return;
    postOrder(tree -> left);
    postOrder(tree -> right);
    cout<< tree->value << endl;
}

首先,指標指向Grumpy,判斷樹是否為空,是則返回,否則繼續。後序遍歷樹的左節點,此時指標指向Doc,樹為空嗎?否,指標繼續指向左子樹,也就是Bashful。樹仍然不為空,繼續往下,發現左右子樹都為空,立刻返回,然後輸出該節點的值,也就是Bashful。指標往上返回,指向Doc,然後執行postOrder(tree -> right);此時指標指向Dopey,同上操作,輸出Dopey。隨後指標回退,指向Grumpy,此時樹為空嗎?否,遍歷左邊,輸出Doc,然後遍歷右邊,指標指向Sleepy,此時樹為空嗎?否,繼續遍歷Happy,為空嗎?否,檢視樹的左節點,為空,立刻返回,輸出Happy,隨後指標返回Sleep中,檢視右節點,為空嗎?否,遍歷左右子樹,為空,立刻返回,然後輸出Sneezy,指標回退到Grumpy中,輸出Sleepy,最後輸出根節點。因此最終的結果為:
這裡寫圖片描述

Bashful
Dopey
Doc
Happy
Sneezy
Sleepy
Grumpy

層次遍歷(levelOrder)

考慮這樣一個問題,如果我們需要將樹的節點按順序從左往右,依次按順序輸出,我們應該怎麼辦呢?能否按上述的步驟進行遞迴實現呢?唔。。。似乎不太容易,但是按順序依次輸出似乎讓我們想到了什麼?我們前面介紹過一種順序資料結構,沒錯,就是我們的棧跟佇列,顯然這種情況我們選用佇列好多了。我們要做的步驟為:
1. 將樹裝進我們的佇列中,如果佇列未空執行下列操作
2. 將樹根移出佇列
3. 對節點進行操作
4. 如果有左節點,進行出佇列操作
5. 如果有右佇列,進行出佇列操作

這樣,出佇列的順序就是樹的層次順序。程式碼為:

void levelOrder(Tree *tree) {
    Queue<Tree *>treeQueue; //建立一個佇列儲存樹,型別為指向樹的指標
    treeQueue.enqueue(tree);//將樹裝進我們的佇列中
    while (!treeQueue.isEmpty()) { //判斷樹是否為空
        Tree *node = treeQueue.dequeue(); //定義一個Tree型別的指標,指向出隊的節點
        cout << node->value << " ";  //輸出該節點的值

       if (node->left != NULL) {     //如果存在左節點,將左節點移除
            treeQueue.enqueue(node->left);
        }
        if (node->right != NULL) {   //如果存在右節點,將右節點移除
            treeQueue.enqueue(node->right);
        }
    }
}

因此,按上述的例子,輸出為:
這裡寫圖片描述
Grumpy
Doc
Sleepy
Bashful
Dopey
Happy
Sneezy

好了,樹的四種遍歷方式就先講到這裡。不過個人感覺,再去做一下嚴蔚敏《資料結構(C語言版)》P129的那個表示式,能受益匪淺!!!