樹:
樹是n(n>=0)個節(jié)點的有限集。n=0時稱為空樹。在任一棵非空樹中:
(1)有且僅有一個特定的稱為根的節(jié)點;
(2)當n>1時其余節(jié)點可分為m(m>0)個互不相交的有限集T1、T2、……、Tm,其中每一個集合本身又是一棵樹,并且稱為根的子樹。
節(jié)點分類:
節(jié)點擁有的子樹數(shù)稱為節(jié)點的度。度為0的節(jié)點稱為葉節(jié)點或終端節(jié)點;度不為0的節(jié)點稱為非終端節(jié)點或分支節(jié)點。除根節(jié)點之外,分支節(jié)點也稱為內部節(jié)點。樹的度是樹內各節(jié)點的度的最大值。
節(jié)點間關系:
節(jié)點的子樹的根稱為該節(jié)點的孩子,相應地,該節(jié)點稱為孩子的雙親。同一個雙親的孩子之間互稱兄弟。節(jié)點的祖先是從根到該節(jié)點所經(jīng)分支上的所有節(jié)點
其他概念:
節(jié)點的層次從根開始定義起,根為第一層,根的孩子為第二層·。其雙親在同一層的節(jié)點互為堂兄弟。樹中節(jié)點的最大層次稱為樹的深度或高度。
若將樹中節(jié)點的各子樹看成從左至右是有次序的,不能互換的,則稱該樹為有序樹,否則稱為無序樹。
森林是(m>=0)棵互不相交的集合。
如圖:
對比線性表與樹的結構:
線性結構:
1.第一個數(shù)據(jù)元素:無前驅
2.最后一個數(shù)據(jù)元素:無后繼
3.中間元素:一個前驅一個后繼
樹結構:
1.根節(jié)點:無雙親,唯一
2.葉節(jié)點:無孩子,可以多個
3.中間節(jié)點:一個雙親,多個孩子
樹的存儲結構的表示方法:
1.雙親表示法(在每個節(jié)點中,附設一個指示器指示其雙親節(jié)點在數(shù)組中的位置);
2.孩子表示法(多重鏈表,即每個節(jié)點有多個指針域,其中每個指針指向一棵子樹的根節(jié)點),由于難以尋找雙親,可改進為雙親孩子表示法);
3.//孩子兄弟表示法(任意一棵樹,它的節(jié)點的第一個孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,我們設置兩個指針,分別指向該節(jié)點的第一個孩子和此節(jié)點的右兄弟)也可加parent指針域來解決快速查找雙親的問題。這個表示法的最大好處是他把一顆復雜的樹變成了一顆二叉樹。
//樹的孩子兄弟表示法結構定義
struct CSNode
{
????int data;
????CSNode* firstchild;
????CSNode* rightsib;
};
二叉樹
二叉樹是n(n>=0)個節(jié)點的有限集合,該集合或者為空集(稱為空二叉樹),或者由一個根節(jié)點和兩棵互不相交的、分別稱為根節(jié)點的左子樹和右子樹的二叉樹組成。
二叉樹特點:
1.每個節(jié)點最多有兩棵子樹,所以二叉樹中不存在度大于2的節(jié)點。注意不是只有兩棵子樹,而是最多有。沒有子樹或者有一棵子樹都是可以的。
2.左子樹和右子樹是有順序的,次序不能任意顛倒。
3.即使樹中某節(jié)點只有一棵子樹,也要區(qū)分它是左子樹還是右子樹。
二叉樹有5種基本形態(tài):
1.空二叉樹;
2.只有一個根節(jié)點;
3.根節(jié)點只有左子樹;
4.根節(jié)點只有右子樹;
5.根節(jié)點既有左子樹悠悠右子樹。
特殊二叉樹:
1.斜樹:所有的節(jié)點都只有左子樹的二叉樹叫左斜樹;所有節(jié)點都是只有右子樹的二叉樹叫右斜樹。
2.滿二叉樹:在一棵二叉樹中,如果所有分支節(jié)點都存在左子樹和右子樹,并且所有葉子都在同一層上,這樣的二叉樹稱為滿二叉樹。
特點:
(1)葉子只能出現(xiàn)在最下一層;
(2)非葉子節(jié)點的度一定是2;
(3)在同樣深度的二叉樹中,滿二叉樹的節(jié)點個數(shù)最多,葉子數(shù)最多。
3.完全二叉樹:
對一棵具有n個節(jié)點的二叉樹按層序編號,如果編號為i(1<=i<=n)的節(jié)點與同樣深度的滿二叉樹編號為i的節(jié)點在二叉樹中位置完全相同,則這棵二叉樹稱為完全二叉樹。滿二叉樹一定是一棵完全二叉樹,但完全二叉樹不一定是滿的。
特點:
(1):葉子節(jié)點只能出現(xiàn)在最下兩層;
(2):最下層的葉子一定集中在左部連續(xù)位置;
(3):倒數(shù)二層,若有葉子節(jié)點,一定都在右部連續(xù)位置;
(4):如果節(jié)點度為1,則該節(jié)點只有左孩子,即不存在只有右子樹的情況;
(5):同樣節(jié)點數(shù)的二叉樹,完全二叉樹的深度最小。
二叉樹性質:
1.在二叉樹的第i層上至多有2^(i-1)個節(jié)(i>=1);
2.深度為k的二叉樹至多有個節(jié)點(k>=1);
3.對任何一棵二叉樹T,如果其終端節(jié)點數(shù)為n0,度為2的節(jié)點數(shù)為n2,則n0=n2+1;
4.具有n個節(jié)點的完全二叉樹的深度為[log2n]+1([x]表示不大于x的最大整數(shù));
5.如果對一棵有n個節(jié)點的完全二叉樹(其深度為[log2n]+1)的節(jié)點按層序編號(從第1層到第[log2n]+1層,每層從左到右),對任一節(jié)點i(1<=i<=n)有:
(1).如果i=1,則節(jié)點i是二叉樹的根,無雙親;如果i>1,則其雙親是節(jié)點[i/2];
(2).如果2i>n,則節(jié)點i無左孩子(節(jié)點i為葉子節(jié)點);否則其左孩子是節(jié)點2i;
(3).如果2i+1>n,則節(jié)點i無右孩子;否則其右孩子是節(jié)點2i+1;
二叉樹的存儲結構:
1.順序存儲:
考慮一種極端的情況,一棵深度為k的右斜樹,他只有k個節(jié)點,卻要分配-1個存儲單元空間,這顯然是對存儲空間的浪費。所以,順序存儲結構一般只用于完全二叉樹。
2.二叉鏈表:
//二叉樹的二叉鏈表節(jié)點結構定義
struct BitNode
{
????int data;
????BitNode* lchild;
????BitNode* rchild;
};
二叉樹的遍歷:
二叉樹的遍歷是指從根節(jié)點出發(fā),按照某種次序依次訪問二叉樹中所有節(jié)點,使得每個節(jié)點被訪問一次且僅被訪問一次。
方法:
1.前序遍歷:若二叉樹為空,則空操作返回,否則先訪問根節(jié)點,然后前序遍歷左子樹,再前序遍歷右子樹;
//二叉樹的前序遍歷遞歸算法
void PreOrderTraverse(BitNode* T)
{
????if (T == NULL)
????????return;
????printf("%d ", T->data);
????PreOrderTraverse(T->lchild);
????PreOrderTraverse(T->rchild);
}
2.中序遍歷:若二叉樹為空,則空操作返回,否則從根節(jié)點開始(注意并不是先訪問根節(jié)點),中序遍歷根節(jié)點的左子樹,然后是訪問根節(jié)點,最后中序遍歷右子樹;
//二叉樹的中序遍歷遞歸算法
void InOrderTraverse(BitNode* T)
{
????if (T == NULL)
????????return;
????InOrderTraverse(T->lchild);
????printf("%d ", T->data);
????InOrderTraverse(T->rchild);
}
3.后續(xù)遍歷:若二叉樹為空,則空操作返回,否則從左到右先葉子后節(jié)點的方式遍歷訪問左右子樹,最后是訪問根節(jié)點。
//二叉樹的后序遍歷遞歸算法
void postOrderTraverse(BitNode* T)
{
????if (T == NULL)
????????return;
? ? postOrderTraverse(T->lchild);
????postOrderTraverse(T->rchild);
????printf("%d ", T->data);
}
4.層序遍歷:若二叉樹為空,則空操作返回,否則從樹的第一層,特就是根節(jié)點開始訪問,從上而下逐層遍歷,在同一層中,按從左到右的順序對節(jié)點逐個訪問。
二叉樹遍歷的性質:
1.已知前序遍歷和中序遍歷,可以唯一確定一棵二叉樹;
2.已知后序遍歷和中序遍歷,可以唯一確定一棵二叉樹;
3.已知前序遍歷和后序遍歷,不能確定一棵二叉樹。
二叉樹的建立:
為了能讓每個節(jié)點確認是否有左右孩子,我們對它進行了擴展,如下圖,也就是將二叉樹中每個節(jié)點的空指針引出一個虛節(jié)點,其值為一特定值,比如“#”。我們稱這種處理后的二叉樹為擴展二叉樹。擴展二叉樹就可以做到一個遍歷序列確定一棵二叉樹了。
//常用操作:
//構造空二叉樹
int InitBiTree(BitNode** T)
{
????*T = NULL;
????return 0;
}
//初始條件 :二叉樹T存在。操作結果:銷毀二叉樹T
void DestroyBiTree(BitNode** T)
{
????if (*T)
????{
????????if ((*T)->lchild)//有左孩子
????????????DestroyBiTree(&(*T)->lchild);
????????if ((*T)->rchild)//有右孩子
????????????DestroyBiTree(&(*T)->rchild);
????????free(*T);//釋放根節(jié)點
????????*T = NULL;//空指針賦07
????}
}
//按前序輸入二叉樹中節(jié)點的值(一個字符)
//“#”表示空樹,構造二叉鏈表表示二叉樹T
void CreateBiTree(BitNode** T)
{
????char ch;
????/*printf("請輸入:");
????scanf("%c", &ch);*/
????ch = str[index++];
????if (ch == '#')
????*T = NULL;
????else
????{
????????*T = (BitNode*)malloc(sizeof(BitNode));
????????if (!*T)
????????{
????????????exit(OVERFLOW);
? ? ? ? }
????????(*T)->data = ch;//生成根節(jié)點
????????CreateBiTree(&(*T)->lchild);//構造左子樹
????????CreateBiTree(&(*T)->rchild);//構造右子樹
????}
}
//二叉樹的前序遍歷遞歸算法
void PreOrderTraverse(BitNode* T)
{
????if (T == NULL)
????{
????????return;
????}
????printf("%c ", T->data);
????PreOrderTraverse(T->lchild);
????PreOrderTraverse(T->rchild);
}
//二叉樹的中序遍歷遞歸算法
void InOrderTraverse(BitNode* T)
{
????if (T == NULL)
????????return;
????InOrderTraverse(T->lchild);
????printf("%c ", T->data);
????InOrderTraverse(T->rchild);
}
//二叉樹的后序遍歷遞歸算法
void postOrderTraverse(BitNode* T)
{
????if (T == NULL)
????????return;
????postOrderTraverse(T->lchild);
????postOrderTraverse(T->rchild);
????printf("%c ", T->data);
}
//生成一個其值等于字符串常量chars的串T,即在頭部添加長度
char* StrAssign(char* T, char* chars)
{
????T[0] = strlen(chars);
????for (int i = 1; i <= T[0]; i++)
????{
????????T[i] = *(chars + i - 1);
????}
????return T;
}
//初始條件:二叉樹存在
//操作結果:若T為空二叉樹,則返回TRUE,否則返回FALSE
bool BiTreeEmpty(BitNode* T)
{
????if (T)
????????return false;
????return true;
}
//初始條件:二叉樹T存在。操作結果:返回T的深度
int BiTreeDepth(BitNode* T)
{
????int i, j;
????if (!T)
????????return 0;
????if (T->lchild)
????????i = BiTreeDepth(T->lchild);
????else
????????i = 0;
????if (T->rchild)
????????j = BiTreeDepth(T->rchild);
????else
????????j = 0;
????return i > j ? i + 1 : j + 1;
}
//初始條件:二叉樹T存在。操作結果:返回T的根
char Root(BitNode* T)
{
????if (BiTreeEmpty(T))
????????return NULL;
????return T->data;
}
//初始條件:二叉樹T存在,p指向T某個節(jié)點
//操作結果:返回P所指向的節(jié)點的值
char Value(BitNode* p)
{
????return p->data;
}
//初始條件:二叉樹T存在
//給p所指節(jié)點賦值為value
void Assign(BitNode* p, char value)
{
????p->data = value;
}
線索二叉樹
我們會發(fā)現(xiàn)二叉樹中指針域并不是都充分的利用了,有空指針域的存在。在二叉鏈表上,我們只能知道每個節(jié)點指向其左右孩子節(jié)點的地址,而不知道某個節(jié)點的前驅的后繼。要想知道,必須遍歷一次。以后每次需要知道時,都必須先遍歷一次。所以可以在創(chuàng)建的時候就記住這些前驅和后繼。
把指向前驅和后繼的指針稱為線索,加上線索的二叉鏈表稱為線索鏈表,相應的二叉樹就稱為線索二叉樹。
二叉樹以某種次序遍歷使其變?yōu)榫€索二叉樹的過程稱作是線索化。
線索化的實質就是將二叉鏈表中的空指針改為指向前驅或后繼的線索。由于前驅和后繼的信息只有在遍歷二叉樹時才能得到,所以線索化的過程就是在遍歷的過程修改空指針的過程。
我們在決定lchid是指向左孩子還是前驅,rchild是指向右孩子還是后繼是需要一個區(qū)分標志的。因此,我們在每個節(jié)點再增設兩個標志域ltag和rtag,注意ltag和rtag只是存放0或1數(shù)字的布爾型變量,其占內存空間要小于像lchild和rchild的指針變量。
節(jié)點結構:
//二叉樹的二叉線索存儲結構定義
typedef enum MyEnum
{
????Link,//=0表示指向左右孩子指針
????Thread//=1表示指向前驅或者后繼的線索
} PointerTag;
struct BiThrNode
{
????char data;//節(jié)點數(shù)據(jù)
????BiThrNode* lchild;//左右孩子指針
????BiThrNode* rchild;
????PointerTag LTag;//左右標志
????PointerTag RTag;
};
線索二叉樹常用操作:
//按前序輸入二叉線索樹中節(jié)點的值,構造二叉樹T
//0(整型)/空格(字符型)表示空節(jié)點
int CreateBiThrTree(BiThrNode** T)
{
????char h;
????scanf("%c", &h);
????if (h == '#')
????????*T = NULL;
????else
????{
????????*T = (BiThrNode*)malloc(sizeof(BiThrNode));
????????if (!*T)
????????????exit(OVERFLOW);
????????(*T)->data = h;//生成根節(jié)點
????????CreateBiThrTree(&(*T)->lchild);//遞歸構造左子樹
????????if ((*T)->lchild)//有左孩子
????????????(*T)->LTag = Link;
????????CreateBiThrTree(&(*T)->rchild);
????????if ((*T)->rchild)
????????(*T)->RTag = Link;
????}
????return 0;
}
//中序遍歷進行中序線索化
void InThreading(BiThrNode* p)
{
????if (p)
????{
????????InThreading(p->lchild);//遞歸左子樹線索化
????????if (!p->lchild)//沒有左孩子
????????{
????????????p->LTag = Thread;//前驅線索
????????????p->lchild = pre;//左孩子指向前驅
????????}
????????if (!pre->rchild)
????????{
????????????pre->RTag = Thread;//后繼線索
????????????pre->rchild = p;//前驅右孩子指針指向后繼(當前節(jié)點p)
????????}
????????pre = p;//保持pre指向p的前驅
????????InThreading(p->rchild);//遞歸右子樹線索化
????}
}
有了線索二叉樹后,我們對它進行遍歷時發(fā)現(xiàn),其實就等于時操作一個雙向鏈表結構。和雙向鏈表一樣,在二叉樹線索鏈表上添加一個頭結點,如下圖所示,并令其lchild域的指針指向二叉樹的根節(jié)點,其rchild域的指針指向中序遍歷時訪問的最后一個節(jié)點。反之,令二叉樹的中序序列中的第一個節(jié)點中,lchild域指針和最后一個節(jié)點的rchild域指針均指向頭結點。這樣定義的好處是我們既可以從第一個節(jié)點起順后繼進行遍歷,也可以從最后一個節(jié)點起順前驅進行遍歷。
//中序遍歷二叉樹T,并將其中序線索化,Thrt指向頭結點
int InOrderThreading(BiThrNode** Thrt, BiThrNode* T)
{
????*Thrt = (BiThrNode*)malloc(sizeof(BiThrNode));
????if (!*Thrt)
????????exit(OVERFLOW);
????(*Thrt)->LTag = Link;//建頭結點
????(*Thrt)->RTag = Thread;
????(*Thrt)->rchild = (*Thrt);//右指針回指
? ? ?if (!T)//若二叉樹空,則左指針回指
????????(*Thrt)->lchild = *Thrt;
????else
????{
????????(*Thrt)->lchild = T;
????????pre = (*Thrt);
????????InThreading(T);//中序遍歷進行中序線索化
????????pre->rchild = *Thrt;
????????pre->RTag = Thread;//最后一個節(jié)點線索化
????????(*Thrt)->rchild = pre;
????}
????return 1;
}
int visit(char e)
{
printf("%c", e);
return 1;
}
//中序遍歷二叉線索樹T(頭結點)的非遞歸算法
int InOrderTraverse_Thr(BiThrNode* T)
{
????BiThrNode* p;
????p = T->lchild;//p指向根節(jié)點
????while (p != T)//空樹或遍歷結束時p==T
????{
????????while (p->LTag == Link)
????????p = p->lchild;
????????visit(p->data);
????????while (p->RTag == Thread&&p->rchild != T)
????????{
????????????p = p->rchild;
????????????visit(p->data);//訪問后繼節(jié)點
????????}
????????p = p->rchild;
????}
????return 1;
}
樹、森林與二叉樹的轉換:
樹的孩子兄弟表示法可以將一棵樹用二叉鏈表進行存儲,所以借助二叉鏈表,樹和二叉樹可以進行相互轉換。從物理結構來看,他們的二叉鏈表也是相同的,只是解釋不太一樣而已。因此,只要我們設定一定的規(guī)則,用二叉樹來表示樹,甚至表示森林都是可以的,森林與二叉樹也可以進行相互轉換。
1.樹轉換為二叉樹:
(1).加線。在所有兄弟之間加一條連線。
(2).去線。對樹中每個節(jié)點,只保留它與第一個孩子節(jié)點的連線刪除它與其它孩子節(jié)點之間的連線。
(3).層次調整。以樹的根節(jié)點為軸心,將整棵樹順時針旋轉一定的角度,使之結構層次分明。注意第一個孩子是二叉樹節(jié)點的孩子,兄弟轉換過來的孩子是節(jié)點的右孩子。
2.森林轉換為二叉樹:
森林是由若干棵樹組成的,所以完全可以理解為,森林中的每一棵樹都是兄弟,可以按照兄弟的處理辦法來操作。步驟如下:
(1).把每個樹轉換為二叉樹.
(2).第一棵樹不動,從第二棵二叉樹開始,依次把后一棵二叉樹的根節(jié)點作為前一棵二叉樹的根節(jié)點的右孩子,用線連接起來。當所有的二叉樹連接起來后就得到了由森林轉換來的二叉樹。
3.二叉樹轉換為樹:
二叉樹轉換為樹是樹轉換為二叉樹的逆過程。
(1).加線。若某節(jié)點的左孩子節(jié)點存在,則將這個左孩子的右孩子節(jié)點、右孩子的右孩子節(jié)點、右孩子的右孩子的右孩子節(jié)點……,即左孩子的n個右孩子節(jié)點都作為此節(jié)點的孩子。將該節(jié)點與這些右孩子節(jié)點用線連接起來。
(2).去線。刪除原二叉樹中所有節(jié)點與其右孩子節(jié)點的連線。
(3).層次調整。使之結構層次分明。
4.二叉樹轉換為森林:
判斷一棵二叉樹能夠轉換成一棵樹還是森林,只需要看這棵二叉樹的根節(jié)點有沒有右孩子,有就是森林,沒有就是一棵樹。
(1).從根節(jié)點開始,若右孩子存在,則把與右孩子節(jié)點的連線刪除,再查看分離后的二叉樹,若右孩子存在,則連線刪除……,直到所有孩子連線都刪除位置,所得的分離的二叉樹。
(2).再將每棵分離后的二叉樹轉換為樹即可。
5.樹與森林的遍歷:
樹的遍歷分為兩種方式:
(1).先根遍歷樹,即先訪問樹的根節(jié)點,然后依次先根遍歷根的每棵子樹。
(2).后根遍歷,即先依次后根遍歷每棵子樹,然后再訪問根節(jié)點。
森林的遍歷分為兩種方式:
(1).前序遍歷:先訪問森林中第一棵樹的根節(jié)點,然后再依次先根遍歷根的每棵子樹,再依次用同樣的方式遍歷除去第一棵樹的剩余樹構成的森林。
(2).后序遍歷:是先訪問森林中第一棵樹,后根遍歷的方式遍歷每棵子樹,然后再訪問根節(jié)點,再依次同樣方式遍歷除去第一棵樹的剩余樹構成的森林。
森林的前序遍歷和二叉樹的前序遍歷結果相同,森林的后序遍歷和二叉樹的中序遍歷結果相同。也就是說,當以二叉鏈表作樹的存儲結構時,樹的先根遍歷和后根遍歷完全可以借用二叉樹的前序遍歷和中序遍歷算法來實現(xiàn).也就是我們找到了對數(shù)和森林這種復雜問題的簡單解決辦法。
赫夫曼樹及其應用:
1.赫夫曼樹定義與原理:
下面兩個圖為葉子節(jié)點帶權的二叉樹(樹節(jié)點間的邊相關的數(shù)叫做權)。
從樹中一個節(jié)點到另一個節(jié)點之間的分支構成兩個節(jié)點之間的路徑,路徑上的分支數(shù)乘坐路徑長度。例如,二叉樹a中,根節(jié)點到節(jié)點D的路徑長度就為4,二叉樹b中根節(jié)點到節(jié)點D的路徑長度就為2.樹的路徑長度就是從樹根到每一節(jié)點的路徑長度之和。二叉樹a的樹路徑長度就為1+1+2+2+3+3+4+4=20;二叉樹b的樹路徑長度就為1+2+3+3+2+1+2+2=16。
如果考慮帶權的節(jié)點,節(jié)點的帶權的路徑長度就為從該節(jié)點到樹根之間的路徑長度與節(jié)點上權的乘積。樹的帶權路徑長度為樹中所有葉子節(jié)點的帶權路徑長度之和。假設有n個權值{,
,…,
},構造一棵有n個葉子節(jié)點的二叉樹,每個葉子帶權
,每個葉子的路徑長度為
,我們通常記作,則其中帶權路徑長度WPL最小的二叉樹稱作赫夫曼樹。也可稱為最優(yōu)二叉樹。
二叉樹a的WPL=51+15
2+40
3+30
4+10
4=315;
5是A節(jié)點的權,1是A、
節(jié)點的路徑長度,其他同理。
二叉樹b的WPL=220
赫夫曼樹的赫夫曼算法描述:
(1).根據(jù)給定的n個權值{,
,…,
}構成n棵二叉樹的集合F={
,
,…,
},其中每棵二叉樹
中只有一個帶權為
根節(jié)點,其左右子樹均為空;
(2).在F中選取兩棵根節(jié)點的權值最小的樹作為左右子樹構造一棵新的二叉樹,且置新的二叉樹的根節(jié)點的權值為其左右子樹上根節(jié)點的權值之和;
(3).在F中刪除這兩棵樹,同時將新得到的二叉樹加入F中;
(4).重復2和3步驟,知道F只含一棵樹為止。這棵樹便是赫夫曼樹。
2.赫夫曼編碼:
編碼中非0即1,長短不等的話其實是很容易混淆的,所以要設計長短不等的編碼,則必須是任一字符的編碼都不是另一個字符的編碼的前綴,這種編碼稱作前綴編碼。
一般地,設需要編碼的字符集為{,
,…,
},各個字符在電文中出現(xiàn)的次數(shù)或品綠集合為{
,
,…,
},以
,
,…,
作為葉子節(jié)點,以
,
,…,
作為相應葉子節(jié)點的權值來構造一棵赫夫曼樹。規(guī)定赫夫曼樹的左分支代表0,右分支代表1,則從根節(jié)點到葉子節(jié)點所經(jīng)過的路徑分支組成的0和1的序列便為該節(jié)點對應字符的編碼,這就是赫夫曼編碼。