樹和二叉樹 C語言實現
1、基本概念
樹是樹型結構的簡稱,它是一種重要的非線性資料結構。
樹的表示:通常使用廣義表表示方法,即每棵樹的根作為由子樹構成的表的名字而放在表的前面,如下圖的樹對應的廣義表表示為:
A(B(D,E(H,I),F),C(G))
結點的度:樹中每個結點具有的非空子樹數或者說後繼結點數被定義為該結點的度。(如上圖中,B結點度為3,A和E結點度都為2,C結點度為1,其餘結點度均為0)
樹的度:樹中所有結點的度的最大值被定義為該樹的度。(如上圖中樹的度為3)
葉子結點:度等於0的結點稱為葉子結點或終端結點。
分支結點:度大於0的結點稱為分支結點或非終端結點。每個結點的分支數就是該結點的度數。
在一棵樹中,每個結點的子樹的根(或者說每個結點的後繼)被稱為孩子結點,該結點被稱為父親結點。
結點的層數從樹根開始定義,根結點為第一層,它的孩子結點為第二層,以此類推。樹中結點最大層數稱為樹的深度或高度。上圖樹的深度為4.
二叉樹:是指樹的度為2的有序樹。
滿二叉樹:當二叉樹中的每一層都滿時(結點數為2^(i-1)),則稱此樹為滿二叉樹。
完全二叉樹:二叉樹中,除最後一層外,其餘層都是滿的,並且最後一層或者是滿的,或者是在最右邊缺少連續若干個結點,則稱此樹為完全二叉樹。
理想平衡二叉樹:二叉樹中,除最後一層外,其餘層都是滿的,並且最後一層的結點可以任意分佈,則稱此樹為理想平衡二叉樹。理想平衡二叉樹包含滿二叉樹和完全二叉樹。
2、二叉樹的儲存結構
a、順序儲存結構
順序儲存一棵二叉樹,首先對每個結點編號,然後以編號為下標,把各結點儲存到一堆陣列中。
如下圖為一個二叉樹及其對應的順序儲存結構
二叉樹順序儲存結構對於儲存完全二叉樹是合適的,但對於一般二叉樹來說不合適,浪費儲存空間,故一般使用下面的連結儲存結構。
b、連結儲存結構
每個結點設定三個域:左指標域、值域、右指標域。
結點型別定義:
struct BTreeNode //結點型別定義
{
ElemType data; //值域
struct BTreeNode* left; //左指標域
struct BTreeNode* right; //右指標域
};
如下圖為一個二叉樹及其對應的連結儲存結構
3、二叉樹的遍歷
a、前序遍歷:訪問根結點的操作在遍歷左、右子樹之前,上圖前序遍歷序列為:A,B,C,D,E,F,G
b、中序遍歷:訪問根結點的操作在遍歷左子樹之後和右子樹之前,上圖中序遍歷序列為:C,B,D,A,E,G,F
c、後序遍歷:訪問根結點的操作在遍歷左、右子樹之後,上圖後序遍歷序列為:C,D,B,G,F,E,A
d、按層遍歷演算法:即按照從上到下、同一層從左到右的次序訪問各結點,上圖按層遍歷次序為:A,B,E,C,D,F,G
具體實現見後面的詳細程式。
4、二叉樹的操作和運算
下面通過詳細的程式展現二叉樹的操作和運算,按上圖二叉樹作為例子,廣義表表示為:A(B(C,D),E(,F(G)))
#include<stdio.h>
#include<stdlib.h>
#define QueueMaxSize 20 //定義佇列陣列長度
#define StackMaxSize 10 //定義棧陣列長度
typedef char ElemType; //定義ElemType型別
struct BTreeNode //結點型別定義
{
ElemType data; //值域
struct BTreeNode* left; //左指標域
struct BTreeNode* right; //右指標域
};
//1、初始化二叉樹
void InitBTree(struct BTreeNode** BT)
{
*BT = NULL; //把樹根指標置空
}
//2、建立二叉樹,採用廣義表表示的輸入法,如:A(B(C,D),E(,F(G)))
void CreateBTree(struct BTreeNode** BT, char* string)
{
struct BTreeNode* p;
struct BTreeNode* s[StackMaxSize]; //定義s陣列作為儲存根結點的指標的棧使用
int top = -1; //棧頂指標置為-1,表示空棧
int k; //k作為處理結點的標誌,k=1處理左子樹,k=2處理右子樹
int i = 0; //用i掃描陣列string中儲存的二叉樹廣義表字符串,初值為0
*BT = NULL; //把樹根指標置空,即從空樹開始建立二叉樹
while (string[i])
{
switch (string[i])
{
case ' ':break;
case '(':
{
if (top == StackMaxSize - 1)
{
printf("棧空間太小,需增加StackMaxSize!\n");
exit(1);
}
top++;
s[top] = p;
k = 1;
break;
}
case ')':
{
if (top == -1)
{
printf("二叉樹廣義表字符串錯!\n");
exit(1);
}
top--;
break;
}
case ',':k = 2;break;
default:
{
p = malloc(sizeof(struct BTreeNode));
p->data = string[i];
p->left = p->right = NULL;
if (*BT == NULL)
*BT = p;
else
{
if (k == 1)
s[top]->left = p;
else
s[top]->right = p;
}
}
}//switch end
i++;
}//while end
}
//3、檢查二叉樹是否為空
int BTreeEmpty(struct BTreeNode* BT)
{
if (BT == NULL)
return 1;
else
return 0;
}
//4、求二叉樹深度
int BTreeDepth(struct BTreeNode* BT)
{
if (BT == NULL)
return 0;
else
{
int dep1 = BTreeDepth(BT->left); //計算左子樹深度
int dep2 = BTreeDepth(BT->right);//計算右子樹深度
if (dep1 > dep2)
return dep1 + 1;
else
return dep2 + 1;
}
}
//5、從二叉樹中查詢值為x的結點,若存在則返回元素儲存位置,否則返回空值(演算法類似於前序遍歷)
ElemType* FindBTree(struct BTreeNode* BT, ElemType x)
{
if (BT == NULL)
return NULL;
else
{
if (BT->data == x)
return &(BT->data);
else
{
ElemType* p;
if (p = FindBTree(BT->left, x))
return p;
if (p = FindBTree(BT->right, x))
return p;
return NULL;
}
}
}
//6、輸出二叉樹,可在前序遍歷的基礎上修改。採用廣義表輸出格式:A(B(C,D),E(,F(G)))
void PrintBTree(struct BTreeNode* BT)
{
if (BT != NULL)
{
printf("%c", BT->data); //輸出根結點的值
if (BT->left != NULL || BT->right != NULL)
{
printf("(");
PrintBTree(BT->left); //輸出左子樹
if (BT->right != NULL)
printf(",");
PrintBTree(BT->right); //輸出右子樹
printf(")");
}
}
}
//7、清除二叉樹,使之變為一棵空樹,演算法類似於後序遞迴遍歷
void ClearBTree(struct BTreeNode** BT)
{
if (*BT != NULL)
{
ClearBTree(&((*BT)->left));//刪除左子樹
ClearBTree(&((*BT)->right));//刪除右子樹
free(*BT); //釋放根結點
*BT = NULL; //置根指標為空
}
}
//8、前序遍歷
void Preorder(struct BTreeNode* BT)
{
if (BT != NULL)
{
printf("%c,", BT->data);
Preorder(BT->left);
Preorder(BT->right);
}
}
//9、中序遍歷
void Inorder(struct BTreeNode* BT)
{
if (BT != NULL)
{
Inorder(BT->left);
printf("%c,", BT->data);
Inorder(BT->right);
}
}
//10、後序遍歷
void Postorder(struct BTreeNode* BT)
{
if (BT != NULL)
{
Postorder(BT->left);
Postorder(BT->right);
printf("%c,", BT->data);
}
}
//11、按層遍歷
//按層遍歷演算法需要使用一個佇列,開始時把整個樹的根結點入隊,然後每從佇列中刪除一個結點並輸出該結點時,
//都把它的非空的左右孩子結點入隊,當佇列為空時演算法結束。
//演算法中,佇列的最大長度不會超過二叉樹中相鄰兩層的最大結點數,
//所以在提前在程式開始處定義最大佇列長度QueueMaxSize大於佇列的最大長度,就無需考慮隊滿溢位的事了
void Levelorder(struct BTreeNode* BT)
{
struct BTreeNode* p;
struct BTreeNode* q[QueueMaxSize];//定義佇列所使用的陣列空間,元素型別為指向結點的指標型別
int front = 0;
int rear = 0;
if (BT != NULL)//將樹根指標入隊
{
q[rear] = BT;
rear = (rear + 1) % QueueMaxSize;
}
while (front != rear)//當佇列非空時執行迴圈
{
p = q[front];//儲存隊首元素
front = (front + 1) % QueueMaxSize;//刪除隊首元素,使隊首指標指向隊首元素
printf("%c,", p->data);//輸出隊首元素
if (p->left != NULL)//若結點存在左孩子,則左孩子結點指標入隊
{
q[rear] = p->left;
rear = (rear + 1) % QueueMaxSize;
}
if (p->right != NULL)//若結點存在右孩子,則左孩子結點指標入隊
{
q[rear] = p->right;
rear = (rear + 1) % QueueMaxSize;
}
}
}
//主函式
void main()
{
struct BTreeNode* bt;
char b[50];
ElemType x, *px;
InitBTree(&bt);
printf("輸入二叉樹廣義表字符串:\n");
scanf("%s", b);
CreateBTree(&bt, b);
PrintBTree(bt);
printf("\n");
printf("前序:");
Preorder(bt);
printf("\n");
printf("中序:");
Inorder(bt);
printf("\n");
printf("後序:");
Postorder(bt);
printf("\n");
printf("按層:");
Levelorder(bt);
printf("\n");
printf("輸入一個待查詢的字元:\n");
scanf(" %c", &x); //格式串中的空格可以跳過任何空白符
px = FindBTree(bt, x);
if (px)
printf("查詢成功:%c\n", *px);
else
printf("查詢失敗\n");
printf("二叉樹的深度為:");
printf("%d\n", BTreeDepth(bt));
ClearBTree(&bt);
}
輸出結果:
程式中的建立二叉樹和按層遍歷部分用到了廣義表、棧和佇列的知識,而且細節性較強,需額外細心。
5、樹的基本概念及儲存結構
這裡指度大於等於3的樹,通常稱為多元樹或多叉樹。
樹的順序儲存適合滿樹的情況,否則將非常浪費儲存空間。所以通常使用連結儲存結構。
樹的連結儲存結構通常採用如下三種方式:
(1) 標準方式
在這種方式中,樹中的每個結點除了包含有儲存資料元素的值域外,還包含有k個指標域,用來分別指向k個孩子結點,或者說,用來分別連結k棵子樹,其中k為樹的度。結點的型別可定義為:
struct GTreeNode
{
ElemType data;//結點值域
struct GTreeNode* t[k];//結點指標域t[0]~t[k-1]
};
(2) 廣義標準方式
廣義標準方式是在標準方式的每個結點中增加一個指向其雙親結點的指標域。結點的型別可定義為:
struct PGTreeNode
{
ElemType data; //結點值域
struct PGTreeNode* t[k]; //結點指標域t[0]~t[k-1]
struct PGTreeNode* parent; //雙親指標域
};
(3)二叉樹形式
這種表示首先將樹轉換為對應的二叉樹形式,然後再採用二叉連結串列儲存這棵二叉樹。但無法表示任一結點中缺少前面孩子,又存在後面孩子那樣的有序樹。
一般情況下使用標準方式儲存樹。
下面是一個三叉樹及其對應的連結儲存結構(標準方式),廣義表表示為:a(b(,e,f(,j)),c,d(g(k,,l),h,i))
(注意:在缺少前面子樹而存在後面子樹的情況時,廣義表表示時,空子樹後面的逗號不能省略)
6、樹的操作和運算
樹的操作和運算與二叉樹類似,可以在熟練二叉樹的操作和運算後進行比較。
下面用一個例項程式具體展現上圖中樹的操作和運算
#include<stdio.h>
#include<stdlib.h>
#define kk 3 //定義樹的度
#define MS 10 //定義符號常量在建立樹儲存結構時指定棧空間的大小
#define MQ 10 //定義符號常量在樹的按層遍歷演算法中指定佇列空間的大小
typedef char ElemType;
struct GTreeNode //樹的連結儲存標準方式的結點型別定義
{
ElemType data; //結點值域
struct GTreeNode* t[kk]; //結點指標域t[0]~t[kk-1]
};
//1、建立樹的儲存結構,採用廣義表的表示法,如:a(b(,e,f(,f)),c,d(g(k,,l),h,i))
//假定結點值仍為字元型別char,演算法與二叉樹類似
//設定兩個棧,s棧用來儲存指向根結點的指標,以便孩子結點向雙親結點連結之用,
//d棧用來儲存待連結的孩子結點的序號,以便能正確地連結到雙親結點的指標域
//下面是根據廣義表字符串string所給出的k叉樹建立對應的儲存結構
void CreateGTree(struct GTreeNode** GT, char* string)
{
struct GTreeNode* s[MS]; //MS常量為儲存結點指標的棧陣列長度
int d[MS];
int top = -1; //top作為兩個棧的棧頂指標,初始值0表示棧空
struct GTreeNode* p; //定義p為指向樹結點的指標
int i = 0, j; //用i指示掃描字串陣列a中的當前字元位置
*GT = NULL; //初始將樹根指標置空
while (string[i])
{
switch(string[i])
{
case ' ':break;
case '(':
{
top++;
s[top] = p; //p指標進s棧,0進d棧,
d[top] = 0; //表明待掃描的孩子結點將被連結到s棧頂元素所指結點的第一個指標域
break;
}
case ')':top--;break; //讓s和d退棧
case ',':d[top]++;break;//讓待讀入的孩子結點連結到s棧頂元素所指結點的下一個指標域
default:
{
p = malloc(sizeof(struct GTreeNode));
p->data = string[i];
for (j = 0; j < kk; j++) //kk表示樹的深度
p->t[j] = NULL;
if (*GT == NULL) //使p結點成為樹根結點(if)或連結到雙親結點對應的指標域(else)
*GT = p;
else
s[top]->t[d[top]] = p;
}
}//switch end;
i++; //準備處理下一個字元
}
}
//2、樹的先根遍歷
void PreRoot(struct GTreeNode* GT)//先根遍歷一棵k叉樹
{
int i;
if (GT != NULL)
{
printf("%c,", GT->data); //訪問根結點
for (i = 0; i< kk ; i++)
PreRoot(GT->t[i]); //遞迴遍歷每一個子樹
}
}
//3、樹的後根遍歷
void PostRoot(struct GTreeNode* GT)
{
int i;
if (GT != NULL)
{
for (i = 0; i < kk; i++)
PostRoot(GT->t[i]); //遞迴遍歷每一個子樹
printf("%c,", GT->data); //訪問根結點
}
}
//4、樹的按層遍歷
void LayerOrder(struct GTreeNode* GT)//按層遍歷由GT指標所指向的k叉樹
{
struct GTreeNode* p;
int i;
struct GTreeNode* q[MQ]; //定義一個佇列用的陣列q,MQ常量為佇列陣列長度
int front = 0, rear = 0; //定義隊首指標和隊尾指標,初始均置0表示空隊
if (GT != NULL) //將樹根指標進隊
{
q[rear] = GT;
rear = (rear + 1) % MQ;
}
while (front != rear)//當佇列非空時執行迴圈
{
p= q[front];
front = (front + 1) % MQ; //使隊首指標指向隊首元,素刪除隊首元素
printf("%c,",p->data); //輸出隊首元素所指結點的值
for (i = 0; i < kk; i++) //非空的孩子結點指標依次進隊
if (p->t[i] != NULL)
{
q[rear] = p->t[i];
rear = (rear + 1) % MQ;
}
}
}
//5、從樹中查詢結點值
ElemType* FindGTree(struct GTreeNode* GT, ElemType x)
{
if (GT==NULL)
return NULL; //樹空返回空指標
else
{
ElemType* p;
int i;
if (GT->data == x)
return &(GT->data); //查詢成功返回結點值域的地址
for (i = 0; i < kk; i++) //向每棵子樹繼續查詢,返回得到的值域地址
if (p = FindGTree(GT->t[i], x))
return p;
return NULL;//查詢不成功返回空指標
}
}
//6、輸出樹,可在先根遍歷的基礎上修改。採用廣義表輸出格式:a(b(,e,f(,f)),c,d(g(k,,l),h,i))
void PrintGTree(struct GTreeNode* GT)
{
if (GT != NULL)
{
int i;
printf("%c", GT->data); //輸出根結點的值
for (i = 0; i < kk; i++)//判斷GT結點是否有孩子
if (GT->t[i] != NULL)
break;
if (i < kk) //有孩子時執行遞迴
{
printf("(");
PrintGTree(GT->t[0]); //輸出第一棵樹
for (i = 1; i < kk; i++)//輸出其餘子樹
{
printf(",");
PrintGTree(GT->t[i]);
}
printf(")");
}
}
}
//7、求樹深度
int GTreeDepth(struct GTreeNode* GT)
{
int i, max; //max用來儲存已求過的子樹中的最大深度
if (GT == NULL)
return 0;
max = 0;
for (i = 0; i < kk; i++)
{
int d = GTreeDepth(GT->t[i]); //計算一棵子樹深度
if (d > max)
max = d;
}
return max + 1; //返回非空樹的深度,它等於各子樹的最大深度加1
}
//8、清除二叉樹,使之變為一棵空樹,演算法類似於後序遞迴遍歷
void ClearGTree(struct GTreeNode** GT)
{
if (*GT != NULL)
{
int i;
for (i = 0; i < kk; i++)
ClearGTree(&((*GT)->t[i]));//刪除子樹
free(*GT); //釋放根結點
*GT = NULL; //置根指標為空
}
}
//主函式
void main()
{
char ch;
struct GTreeNode* gt = NULL;
char b[50]; //定義一個用於存放k叉樹廣義表的字元陣列
printf("輸入一棵%d叉樹的廣義表字符串:\n", kk);
scanf("%s", b);
CreateGTree(>, b);
printf("先根遍歷結果:");
PreRoot(gt);
printf("\n");
printf("後根遍歷結果:");
PostRoot(gt);
printf("\n");
printf("按層遍歷結果:");
LayerOrder(gt);
printf("\n");
printf("按廣義表形式輸出的樹為:");
PrintGTree(gt);
printf("\n");
printf("樹的深度為:%d\n", GTreeDepth(gt));
printf("輸入待查詢的一個字元:");
scanf(" %c", &ch);//格式串中的空格可以跳過任何空白符
if (FindGTree(gt, ch))
printf("查詢成功!\n");
else
printf("查詢失敗!\n");
ClearGTree(>);
}
執行結果: