資料結構: 樹
定義與術語
這沒什麼好說的,照搬書上的吧。
一棵樹 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);;
};
*由於removeFirst
和removeNext
函式在考慮整顆樹的情況下,比較複雜,所以暫時將其訪問許可權限制為 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
樹的另一種表示:父指標表示法
父指標表示樹,即結點只儲存父結點的指標。
但是很明顯,這樣的表示對於一些常用的操作,如查詢左兄弟結點並不支援。
這樣的表示是有特殊的目的的,它能解答以下的問題:
“給出兩個結點,它們是否在同一棵樹中?”
父指標表示法常常用來維護由一些不相交子集構成的集合。對於不相交的集合,希望提供如下兩種操作:
- 判斷兩個節點是否在同一集合中
- 歸併兩個集合。(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上找到