前言
樹是n(n>=0)個結點的有限集。在任意一棵非空樹中:1.有且只有一個特定的稱爲根的結點 2.當n>1時,其餘結點可分爲m(m>=0)個互不相交的有限集,並且稱爲根的子樹。可以發現,在樹的定義中,又用到了樹的概念,樹的結構定義是一個遞歸的定義。
森林則是m(m>=0)棵互不相交的樹的集合。對樹中的每一個結點而言,其子樹的集合即爲森林。因此,也可以用森林和樹相互遞歸的定義來描述樹。
樹的應用很廣泛,但是由於樹的形狀可以任意,顯然對樹的處理會複雜許多。
樹的存儲結構
雙親表示法
在雙親表示法中,結點擁有一個數據域和一個指針域,其中指針域指向結點的雙親結點。
(注:”雙親“結點即父結點,並不意味着兩個有結點,搞不懂爲啥要這樣翻譯)
這裏我們以順序存儲爲例子,假設用一個數組來保存整棵樹,那麼數組下標就是結點的位置。
如果用這種方式,那麼結點的結構如下:
typedef struct PTNode{
//數據域
ElemType data;
//指針域
int parent;
}PTNode;
使用這種表示法的缺點也很明顯:假如想得到結點的孩子,需要遍歷整個結構才能得到。
孩子表示法
顧名思義,孩子表示法即指針域指向的是結點的孩子結點。但是與二叉樹不同,一棵樹可以擁有多個結點,那麼指針域該如何設計呢?
第一種方法是採用固定指針域數量的結點,也就是說結點的格式是一致的。
typedef struct CTNode{
//數據域
ElemType data;
//指針域
int child1;
int child2;
//...
int childd;
}CTNode;
這種方式的弊端很明顯:假設某個結點的孩子結點數量很多,而其他結點的孩子結點數量很少時,由於採取了統一格式,所有結點的指針域數量必須同最大的數量一致,這樣就造成了浪費。
因此可以採取第二種方式:增加一個數據域用於記錄結點的度,根據度來確定指針域的個數。
typedef struct CTNode{
//數據域
ElemType data;
//結點的度
int degree;
//指針域
int child1;
//...
}CTNode;
顯然,這種方法雖然能節約空間,但是每個結點的結構不一樣,操作上很不方便。
有一種改進方法是,將每個結點的孩子結點排列起來,看成一個線性表,以單鏈表作爲存儲結構。
這樣,我們需要設計一個孩子結點,作爲該鏈表存儲的對象。
//孩子結點
typedef struct ChildNode{
//孩子結點的位置
int child;
//指向下一個孩子結點
struct ChildNode* next;
}ChildNode;
樹的結點:
//樹的結點
typedef struct CTNode{
//數據域
ElemType data;
//指針域-孩子結點
ChildNode* firstChild;
}CTNode;
採用這種方式很容易找到孩子結點,但是不容易找到父結點。可以與雙親表示法結合起來,使操作更簡便。
孩子兄弟表示法
孩子兄弟表示法又稱爲二叉樹表示法,即用二叉鏈表作爲樹的存儲結構。這樣,樹的結點一定有兩個指針域,規定將左結點的指針域指向該結點的第一個孩子結點,右結點的指針域指向結點的下一個兄弟結點,得到孩子兄弟表示法的結點結構如下:
typedef struct CSNode{
//數據域
ElemType data;
//指針域-左結點指向第一個孩子結點
struct CSNode* firstChild;
//指針域-右結點指向下一個兄弟結點
struct CSNode* nextSibling;
}CSNode;
用孩子兄弟表示法來表示樹,可以看做是將一棵樹轉換爲二叉樹。
樹、森林與二叉樹的轉換
樹轉換爲二叉樹
前面提到,孩子兄弟表示法即使用二叉鏈表來存儲樹。
作爲二叉鏈表,除了數據域外,它必定只有兩個指針域,指向其左結點和右結點。
在二叉樹中,左結點指向的是左子樹,右結點指向的是右子樹。
在線索二叉樹中,左結點指向左子樹或者前驅,右結點指向右子樹或後繼。
在樹中,沒有左右子樹的說法,那麼用左結點指向它的第一個孩子結點,右結點指向它的下一個兄弟結點。
示例:
- 首先存儲R,其第一個孩子結點爲A,則R的左結點爲A;R無兄弟結點,右結點爲空
- A的第一個孩子結點爲D,則A的左結點爲D;A的下一個兄弟節點爲B,則其右結點爲B
- D無孩子結點,則其左結點爲空;D的下一個兄弟結點爲E,則其右結點爲E
- E無孩子結點,則其左結點爲空;E無下一個兄弟,則其右結點也爲空
- B無孩子結點,則其左結點爲空;B的下一個兄弟結點爲C,則其右結點爲C
- C的第一個孩子結點爲F,則其左結點爲F;C無下一個兄弟,則其右結點爲空
- F的第一個孩子結點爲G,則其左結點爲G;F無兄弟,則其右結點爲空
- G無孩子結點,則其左結點爲空;G的下一個兄弟結點爲H,則其右結點爲H
- H無孩子結點,則其左結點爲空;H的下一個兄弟結點爲K,則其右結點爲K
- K無孩子結點,則其左結點爲空;K無下一個兄弟,則其右孩子爲空
由此可見,在物理結構上,二叉樹與孩子兄弟表示法的樹是一致的,只是解釋方法不同,含義就不同了。
由於根結點沒有兄弟,因此根結點的右結點必爲空。
森林轉換爲二叉樹
森林是由若干棵樹組成的,因此每一棵樹都能各自轉換爲二叉樹。同時可以認爲,森林中的每一棵樹都是兄弟,即根結點的兄弟結點是下一棵樹的根結點。基於這兩點,森林轉換爲二叉樹是很容易的。
首先,將第一棵樹轉換爲二叉樹。
- A爲根結點,第一個孩子結點爲B,則其左結點爲B;下一棵樹的根結點爲E,則其右結點爲E
- B無孩子結點,則其左結點爲空;下一個兄弟結點爲C,則其右結點爲C
- C無孩子結點,則其左結點爲空;下一個兄弟結點爲D,則其右結點爲D
- D無孩子結點,無下一個兄弟結點,左右結點均爲空
然後轉換第二棵樹
- E爲根結點,第一個孩子結點爲F,則其左結點爲F;下一棵樹的根結點爲G,則其右結點爲G
- F無孩子結點,無下一個兄弟結點,左右結點均爲空
轉換第三棵樹
- G爲根結點,第一個孩子結點爲H,則其左結點爲H;無下一棵樹,則其右結點爲空
- H無孩子結點,則其左結點爲空;下一個兄弟結點爲I,則其右結點爲I
- I第一個孩子結點爲J,則其左結點爲J;無下一個兄弟結點,則其右結點爲空
- J無孩子結點,無下一個兄弟結點,左右結點均爲空
二叉樹也可以再次轉換爲樹或森林,只要知道其原理,逆過來做很容易,就不說明了。
孩子兄弟樹的實現
由於孩子兄弟樹實際上就是一顆二叉樹,其實現與二叉樹並沒有太大的差別,這裏就不一一解釋了。需要注意的是,由於右結點不再是結點的子樹而是結點的兄弟,在進行一些操作時需要格外注意。
以尋找結點的父結點爲例。
假如按照二叉樹的方法,查找結點B的父結點,得到的是結點A,這顯然是錯誤的。
全部代碼
Tree.hpp
#ifndef Tree_hpp
#define Tree_hpp
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#define ElemType char
typedef struct TreeNode{
ElemType data;
struct TreeNode *FirstChild;
struct TreeNode *nextSibling;
}TreeNode;
typedef struct Tree{
TreeNode * root;
ElemType refValue;
}Tree;
//初始化
void InitTree(Tree *tree,ElemType ref);
//創建
void CreateTree(Tree *tree,char *str);
void CreateTree(Tree *tree,TreeNode *&t,char *&str);
//得到根結點
TreeNode* Root(Tree *tree);
//得到第一個孩子
TreeNode* FirstChild(Tree *tree);
TreeNode* FirstChild(TreeNode *&t);
//得到第一個兄弟
TreeNode* NextSibling(Tree *tree);
TreeNode* NextSibling(TreeNode *&t);
//查找某個結點
TreeNode* Find(Tree *tree,ElemType key);
TreeNode* Find(TreeNode *&t,ElemType key);
//查找某個結點的父結點
TreeNode* Parent(Tree *tree,TreeNode *p);
TreeNode* Parent(TreeNode *&t,TreeNode *p);
#endif /* Tree_hpp */
Tree.cpp
#include "Tree.hpp"
void InitTree(Tree *tree, ElemType ref){
tree->root = NULL;
tree->refValue = ref;
}
//創建
void CreateTree(Tree *tree,char *str){
CreateTree(tree,tree->root, str);
}
void CreateTree(Tree *tree,TreeNode *&t,char *&str){
if (*str == tree->refValue) {
t = NULL;
}else{
t = (TreeNode *)malloc(sizeof(TreeNode));
assert(t != NULL);
t->data = *str;
CreateTree(tree, t->FirstChild, ++str);
CreateTree(tree, t->nextSibling, ++str);
}
}
//
TreeNode* Root(Tree *tree){
return tree->root;
}
//得到第一個孩子
TreeNode* FirstChild(Tree *tree){
return FirstChild(tree->root);
}
TreeNode* FirstChild(TreeNode *&t){
return t == NULL ? t : t->FirstChild;
}
//得到第一個兄弟
TreeNode* NextSibling(Tree *tree){
return NextSibling(tree->root);
}
TreeNode* NextSibling(TreeNode *&t){
return t == NULL ? t : t->nextSibling;
}
//查找某個結點
TreeNode* Find(Tree *tree,ElemType key){
return Find(tree->root, key);
}
TreeNode* Find(TreeNode *&t,ElemType key){
//判斷是否是本結點
if (t == NULL || t->data == key) {
return t;
}
//不是,先在孩子結點中查找
TreeNode *p = Find(t->FirstChild, key);
if (p == NULL) {
//子樹中沒有,在兄弟結點中查找
p = Find(t->nextSibling, key);
}
return p;
}
TreeNode* Parent(Tree *tree,TreeNode *p){
return Parent(tree->root, p);
}
TreeNode* Parent(TreeNode *&t,TreeNode *p){
if (t == NULL || p == NULL || p == t) {
return NULL;
}
TreeNode *q = t->FirstChild;
TreeNode *parent;
while (q != NULL && q != p) {
//先在孩子結點中查找
parent = Parent(q,p);
if(parent != NULL){
return parent;
}
//孩子結點中沒有,在兄弟結點中查找
q = q->nextSibling;
}
if (q != NULL && q == p) {
return t;
}
return NULL;
}
main.cpp
#include "Tree.hpp"
int main(int argc, const char * argv[]) {
char *str = "RAD#E##B#CFG#H#K#####";
Tree tree;
InitTree(&tree, '#');
CreateTree(&tree,str);
TreeNode *p = Find(&tree, 'H');
printf("%c\n",p->data);
TreeNode *parent = Parent(&tree, p);
printf("%c\n",parent->data);
return 0;
}