1. 程式人生 > >資料結構: 樹

資料結構: 樹

定義與術語

這沒什麼好說的,照搬書上的吧。
一棵樹 T 是由一個或一個以上結點組成的有限集,其中有一個特定的結點 R 稱為 T 的根結點。如果集合 (T -{R}) 非空,那麼集合中的這些結點被劃分為 n 個不相交的子集 T0, T1, ……, Tn, 其中每個子集都是樹,並且其相應的根結點 R0, R1, ……, Rn是 R 的子結點。
結點的出度(out degree)定位該結點的子結點數目。

結點的ADT

//General Tree Node
template<typename E>
class GTNode
{
    protected:
    virtual
void removeFirst(); virtual void removeNext(); public: virtual ~GTNode() {} virtual E value() const; virtual GTNode* leftMostChild() const; virtual GTNode* rightSibling() const; virtual void setValue(const E& e); virtual void insertFirst(GTNode* infirst); virtual
void insertNext(GTNode* innext);; };

*由於removeFirstremoveNext函式在考慮整顆樹的情況下,比較複雜,所以暫時將其訪問許可權限制為 protected

樹的遍歷

類似二叉樹,樹的遍歷有前序,中序,後序三種。
前序遍歷和後序遍歷很好定義,而中序遍歷卻並不是確定的。常用的定義是先遍歷最左子結點,然後遍歷根結點,然後遍歷其他子結點。
實現:

template<typename E>
void preorderTree(E* root, void (*visit)(E*) )
{
    if(root==nullptr)   return;
    visit(root);
    auto temp = root->leftMostChild();
    while(temp!=nullptr)
    {
        preorderTree(temp, visit);
        temp = temp->rightSibling();
    }
} template<typename E> void inorderTree(E* root, void(*visit)(E*) ) { if(root==nullptr) return; auto temp = root->leftMostChild(); inorderTree(temp, visit); visit(root); while(temp!=nullptr) { temp = temp->rightSibling(); inorderTree(temp, visit); } } template<typename E> void postorderTree(E* root, void(*visit)(E*) ) { if(root==nullptr) return; auto temp = root->leftMostChild(); while(temp!=nullptr) { postorderTree(temp, visit); temp = temp->rightSibling(); } visit(root); }

樹的回收其實是後序遍歷的一種,實現如下:

template<typename E>
void clear(E* root)
{
    if(root == nullptr) return;
    auto temp = root->leftMostChild();
    while(temp!=nullptr)
    {
        auto next = temp->rightSibling();
        clear(temp);
        temp = next;
    }
    delete root;
}

一個簡單的例子

這裡寫圖片描述
按照書上的這顆樹,建立如下:

#include<iostream>
#include"Tree.h"
using namespace std;

template<typename E>
void visit(E* root)
{
    cout<<root->value()<<"  ";
}

int main()
{
    char c[] = { 'R', 'A', 'B', 'C', 'D', 'E', 'F'};
    int i=0;
    GTNode<char>* root = new GTNode<char>(c[i++]);//R
    root->insertFirst( new GTNode<char>(c[i++]));
    root->leftMostChild()->insertNext( new GTNode<char>(c[i++]));

    auto temp = root->leftMostChild();
    temp->insertFirst( new GTNode<char>(c[i++]));
    temp = temp->leftMostChild();
    temp->insertNext( new GTNode<char>(c[i++])); temp = temp->rightSibling();
    temp->insertNext( new GTNode<char>(c[i++])); temp = temp->rightSibling();

    temp = root->leftMostChild()->rightSibling();
    temp->insertFirst(new GTNode<char>(c[i++]));   


    preorderTree(root, visit);  cout<<endl;
    inorderTree(root, visit);   cout<<endl;
    postorderTree(root, visit);
    cout<<endl;

    clear(root);    root = nullptr;
}

輸出:

R A C D E B F
C A D E R F B
C D E A F B R

樹的另一種表示:父指標表示法

父指標表示樹,即結點只儲存父結點的指標。
但是很明顯,這樣的表示對於一些常用的操作,如查詢左兄弟結點並不支援。
這樣的表示是有特殊的目的的,它能解答以下的問題:
“給出兩個結點,它們是否在同一棵樹中?”
父指標表示法常常用來維護由一些不相交子集構成的集合。對於不相交的集合,希望提供如下兩種操作:

  1. 判斷兩個節點是否在同一集合中
  2. 歸併兩個集合。(UNION)

歸併兩個集合的過程常常稱為”UNION”,且整個操作旨在通過歸併找出兩個結點是否在同一個集合中,因此以“並查演算法”(UNION/FIND,也稱並查集)命令。
下面是實現:

class ParPtrTree
{
    int* array;
    int len;

    enum{ ROOT = -1 };

    int find(int curr)
    {
        if(array[curr] == ROOT) return curr;
        array[curr] = find(array[curr]);
        return array[curr];
    }
    int getChildNum(int curr)
    {
        int sum = 0;
        for(int i = 0;i<len;++i)
            if(array[i] == curr)    ++sum;
        return sum;
    }
    public:
    ParPtrTree(int n)
    {
        array = new int[n];
        len = n;
        for(int i=0;i<len;++i)
            array[i] = ROOT;
    }

    virtual ~ParPtrTree()
    {
        delete[] array;
    }
    void Union(int a, int b)
    {
        a = find(a);
        b = find(b);
        if(getChildNum(b) > getChildNum(a) )
            array[a] = b;
        else
            array[b] = a;

    }
    bool differ(int a, int b)
    {
        return find(a)!=find(b);
    }
};

*在課本上找不到完整的實現程式碼,這裡的實現僅是我個人的實現,僅供參考
array 陣列儲存的是相應結點的父結點的下標。
find 函式使用了遞迴。這是一種路徑壓縮的方法,查詢到當前結點的父結點,並把當前結點的所有祖先結點的父指標都指向根結點。
並查演算法的輸入是一系列等價對,考慮以下包含兩個連通分支的圖:
這裡寫圖片描述

等價對可以是 A-H 或者 C-H .
因為傳遞性, A-C 也是等價對。

樹的實現

樹的實現有幾種形式,這裡只講方法(將用到課本的圖),並不給出程式碼實現。

子結點表表示法

子結點表表示法在陣列中儲存樹的結點,每個結點包括結點值,一個父指標(或索引)及一個指向子結點連結串列的指標,順序是從左到右。
這裡寫圖片描述

左子結點/右兄弟結點表示法

左子結點/右兄弟結點表示法,顧名思義,就是表中記錄了一個結點的左子結點和右兄弟
這裡寫圖片描述

動態結點表示法

每個結點儲存了結點的值和一個動態陣列,動態陣列中儲存的是子節點的指標域和陣列的大小。
這裡寫圖片描述

另外一種利用連結串列的實現更加靈活:
這裡寫圖片描述

動態左子結點/右兄弟結點表示法

在二叉樹中,有左右子樹。擴充套件到樹的表示,即可變成左子節點表示樹的左子節點,右子結點表示樹的右兄弟結點。
這個表示方法只有兩個指標域。
還可以擴充套件的表示森林。即根結點互為兄弟。
一般的表示如下:
這裡寫圖片描述

樹的順序表示法

儲存一系列結點的值,其中包含了儘可能少,但是對於重建樹結構必不可少的資訊,這種方法稱為順序樹表示法。它的優點是節省空間。
考慮如下二叉樹,可以根據先序遍歷記錄下來,其中 / 表示NULL
這裡寫圖片描述
那麼這棵樹的順序表示如下:

AB/D//CEG///FH//I//

上面的表示並不區分內部結點和葉子結點。
我們可以用另外一種方法表示,標記處內部結點,用以區分葉子結點。

A’B’/DC’E’G’/F’HI

這樣的表示更省空間,但是我們還要額外記錄一個標識資訊。
標識資訊可以使用位來記錄,假如儲存結點值的是 int 整型欄位,而結點的值是正數,那麼我們可以使用最高位的符號位來表示內部結點。
我們還可以在書中額外記錄一個位向量。上面的位向量如下:

11001110100

然而使用順序表示法來表示樹的話,還需要記錄結構更多的顯示資訊,如子結點的數目。
作為一種替代方法,可以在記錄子結點表結束的位置。
下面使用了特殊的標記來表示子結點表的結束”)”.
考慮如下的樹:
這裡寫圖片描述
它的順序表示法是:

RAC)D)E))BF)))

其中,F 後面跟了三個括號,因此表示 F 子結點、B 子結點、R 子結點的結束。
則重建樹的程式碼可以如下:

GTNode<char>* rebuildTree(const char* str, int& i)
{
    if(str[i] == ')' || str[i] == '\0')  return nullptr;
    GTNode<char>* root = new GTNode<char>(str[i]);
    root->insertFirst(rebuildTree(str, ++i));
    root->insertNext(rebuildTree(str, ++i));
    return root;
}

所有實現均可在github上找到