森林、樹、二叉樹的性質與關系
這篇博客寫的太累了。。。
本文中對於這部分的講解沒有提到的部分:
對於二叉樹的遍歷:重點講了非遞歸遍歷的實現方式和代碼(遞歸方法使用的相對較多,請直接參考博客代碼)
對於哈夫曼編碼和線索二叉樹的代碼實現沒有列出。
樹
我們對於樹和二叉樹這一部分的內容主要研究樹的邏輯結構和存儲結構,由於計算機的特殊性存儲結構及二叉樹的簡單性,我們更主要討論二叉樹的邏輯結構和存儲結構並對其進行實現(其中包含二叉樹的一些重要性質),另外我們在研究這一類問題時,首先要考慮到樹與森林之間的轉換,以及樹與二叉樹之間的轉換。從而簡化為最簡單的二叉樹問題。
知識體系結構圖:
樹的定義:(采用遞歸方法去定義樹)
樹:n(n≥0)個結點的有限集合。
當n=0時,稱為空樹;
任意一棵非空樹滿足以下條件:
(1)有且僅有一個特定的稱為根的結點;
(2)當n>1時,除根結點之外的其余結點被分成m(m>0)個互不相交的有限集合T1,T2,… ,Tm,其中每個集合又是一棵樹,並稱為這個根結點的子樹。(用圖的定義法去描述樹:連通而不含回路的無向圖稱為無向樹,簡稱樹,常用T表示樹)
樹的基本術語:
結點的度:結點所擁有的子樹的個數。
樹的度:樹中各結點度的最大值。
葉子結點:度為0的結點,也稱為終端結點。
分支結點:度不為0的結點,也稱為非終端結點。
孩子、雙親:樹中某結點子樹的根結點稱為這個結點的孩子結點,這個結點稱為它孩子結點的雙親結點;
兄弟:具有同一個雙親的孩子結點互稱為兄弟。
祖先、子孫:在樹中,如果有一條路徑從結點x到結點y,那么x就稱為y的祖先,而y稱為x的子孫。
路徑:如果樹的結點序列n1, n2, …, nk有如下關系:結點ni是ni+1的雙親(1<=i<k),則把n1, n2, …, nk稱為一條由n1至nk的路徑;路徑上經過的邊的個數稱為路徑長度。
結點所在層數:根結點的層數為1;對其余任何結點,若某結點在第k層,則其孩子結點在第k+1層。
樹的深度:樹中所有結點的最大層數,也稱高度。
層序編號:將樹中結點按照從上層到下層、同層從左到右的次序依次給他們編以從1開始的連續自然數。
有序樹、無序樹:如果一棵樹中結點的各子樹從左到右是有次序的,稱這棵樹為有序樹;反之,稱為無序樹。(數據結構中討論的一般都是有序樹)
森林:m (m≥0)棵互不相交的樹的集合。
同構:對兩棵樹,若通過對結點適當地重命名,就可以使這兩棵樹完全相等(結點對應相等,結點對應關系也相等),則稱這兩棵樹同構。
例:
樹的抽象數據類型定義:(ADT Tree)
Operation:(功能)
InitTree(Create):構造一棵樹。
DestroyTree:銷毀一棵樹。(釋放該樹占用的存儲空間)
Root:求樹的根節點信息。
Parent:求樹的雙親信息。(輸入結點x輸出x雙親的信息)
Depth:求樹的深度。
PreOrder:前序遍歷。
InOrder:中序遍歷。(錯誤),在樹(或者森林)中是不能中序遍歷的(存在多個子樹的情況)(二叉樹是存在中序遍歷的:先左子樹后父節點后右子樹)
PostOrder:后序遍歷。
LevelOrder:層序遍歷。
樹的存儲結構:
一、順序存儲:本質上是靜態指針
1.雙親表示法
2.雙親、孩子表示法
3.雙親、兄弟表示法
二、鏈式存儲:
1.孩子表示法–多重鏈表示法(出現空間浪費或結點結構不一致的缺點)
2.孩子表示法–孩子鏈表表示法
3.孩子兄弟表示法
一、順序存儲:本質上是靜態指針
1.雙親表示法:
template
struct PNode{
T data; //數據域
int parent; //指針域,雙親在數組中的下標
} ;
2.雙親、孩子表示法:
3.雙親、兄弟表示法:
二、鏈式存儲:
1.孩子表示法–多重鏈表示法(出現空間浪費或結點結構不一致的缺點)
方案一:指針域的個數等於樹的度(空間浪費)
方案二: 指針域的個數等於該結點的度(結點結構不一致)
2.孩子表示法-孩子鏈表表示法(每個節點創建一個單鏈表)(n個結點共有 n 個孩子鏈表)
這 n 個單鏈表共有 n 個頭指針,這 n 個頭指針又組成了一個線性表,為了便於進行查找采用順序存儲存儲每個鏈表的頭指針。
存儲結構:
孩子結點:
struct CTNode
{
int child;
CTNode *next;
};
表頭結點:
template
struct CBNode
{
T data;
CTNode *firstchild;
};
3.孩子兄弟表示法:設置兩個分別指向該結點的第一個孩子和右兄弟的指針。
template
struct TNode{
T data;
TNode *firstchild, *rightsib;
};
二叉樹:
下面我們來進入二叉樹的講解:
我們學習使用二叉樹的原因:二叉樹結構簡單,適合計算機處理。我們可以將樹轉化為二叉樹,從而利用二叉樹解決樹的有關問題。
二叉樹的定義 :
二叉樹是n(n≥0)個結點的有限集合,該集合或者為空集(稱為空二叉樹),或者由一個根結點和兩棵互不相交的、分別稱為根結點的左子樹和右子樹的二叉樹組成。
二叉樹的特點:
⑴ 每個結點最多有兩棵子樹;
⑵ 二叉樹是有序的,其次序不能任意顛倒。
特殊的二叉樹:
斜樹:(從結構上將,我們可以將其看成是單鏈表)
1 .所有結點都只有左子樹的二叉樹稱為左斜樹;
2 .所有結點都只有右子樹的二叉樹稱為右斜樹;
3.左斜樹和右斜樹統稱為斜樹。
例:
特點:
- 在斜樹中,每一層只有一個結點;
2.斜樹的結點個數與其深度相同。
滿二叉樹:
在一棵二叉樹中,如果所有分支結點都存在左子樹和右子樹,並且所有葉子都在同一層上。
例:
滿二叉樹:
非滿二叉樹:
特點:
1.葉子只能出現在最下一層;
2.只有度為0和度為2的結點。
3.滿二叉樹在同樣深度的二叉樹中結點個數最多
4.滿二叉樹在同樣深度的二叉樹中葉子結點個數最多
完全二叉樹:
對一棵具有n個結點的二叉樹按層序編號,如果編號為i(1≤i≤n)的結點與同樣深度的滿二叉樹中編號為i的結點在二叉樹中的位置完全相同則稱該樹為完全二叉樹。(可以理解為完全二叉樹降低了滿二叉樹的標准:只要求位置,不要求是否全部填充滿。)
例:(上面為滿二叉樹,下面的則為完全二叉樹)
特點:
- 葉子結點只能出現在最下兩層,且最下層的葉子結點都集中在二叉樹的左部;(由完全二叉樹的性質和層序編號的方式所決定)
- 完全二叉樹中如果有度為1的結點,只可能有一個,且該結點只有左孩子。
- 深度為k的完全二叉樹在k-1層上一定是滿二叉樹。(層序編號性質決定)
二叉樹的基本性質:
1.二叉樹的第i層上最多有2的(i-1)次個結點(i≥1)
2.一棵深度為k的二叉樹中,最多有2的k次-1個結點,最少有k個結點。
3.在一棵二叉樹中,如果葉子結點數為n0,度為2的結點數為n2,則有: n0=n2+1。
3.證明:
證明一:(利用二叉樹結點總數等於度為0,1,2的和與二叉樹結點總數等於根節點+度為1的節點數+2*度為2的節點數(利用了分枝))設n為二叉樹的結點總數,n1為二叉樹中度為1的結點數,則有:
n=n0+n1+n2
在二叉樹中,除了根結點外,其余結點都有唯一的一個分枝進入,由於這些分枝是由度為1和度為2的結點射出的,一個度為1的結點射出一個分枝,一個度為2的結點射出兩個分枝,所以有:
n=n1+2n2+1
因此可以得到:n0=n2+1 。
證明二:(可能更好理解一些):假設我們要求的二叉樹都是滿二叉樹,那么此時葉子結點數n0=n2+1(此時所有的點度都為2,但是最后一層上的節點數等於上面層數的節點數和+1:2的k次等於2的0次+2的1次+…+2的k-1次+1,即此時滿足n0=n2+1) 我們在該滿二叉樹的基礎上減去實際並不存在的葉子結點,那么葉子結點數n0-1,但同時度為2的結點數n2-1,等式依然成立。如果我們對度為2的結點操作,減去其上的兩個葉子結點,那么n0-2,但是該點變為葉子結點,n0+1,最后為n0-1,同時n2-1,等式依然成立。
二叉樹的遍歷操作(通過給定的遍歷序列模擬建樹過程):
對於二叉樹的遍歷,給定其前序序列和后序序列不能確定該二叉樹:
例:
已知前序遍歷序列為ABC,后序遍歷序列為CBA:
滿足條件的二叉樹:
已知一棵二叉樹的前序序列和中序序列,可以唯一確定這棵二叉樹:
例:
前序:A B C D E F G H I中序:B C A E D G H F I
由根節點:
前序:B C
中序:B C
依次循環查找:
前序: D E F G H I
中序: E D G H F I
直到最后求出解。
void create(char preorder[],char inorder[],int start_p, int end_p,int start_i,int end_i, char data[],int root){
if(start_p>end_p)
return ;
else{
int k;
for(int i=start_i;i<=end_i;i++){
if(inorder[i]==preorder[start_p]){
k=i;
break;
}
}
data[root]=preorder[start_p];
create(preorder,inorder,start_p+1,start_p+k-start_i,start_i,k-1,data, 2*root);//左子樹遍歷填充
create(preorder,inorder,start_p+k-start_i+1,end_p,k+1,end_i,data,2*root+1); //右子樹遍歷填充
}
return ;
}
int main(){
char * data;
int total=1;
char preorder[100],inorder[100];
cin>>preorder>>inorder;
int length=0;
while(preorder[length]!='\0')
length++; //求出樹的深度(結點個數)
data=new char[pow(2,length+1)]; //利用結點個數模擬樹的結構(對應深度為length時,樹的length層最多結點個數為2的length次,那么總的結點個數最多為2的(length+1)次-1,這里直接寫2的(length+1)次)
memset(data,'\0',pow(2,length+1)); //首先將樹的模型搭建好,然后設該樹為一棵空樹,填充相應數據即可得到所求樹
create(preorder,inorder,0,length-1,0,length-1,data,1); //創建該樹(在樹的模型上填充數據建立該樹)
order(1,data);
return 0;
}
我們通過上面的程序可以看出,上面的思想是模擬建樹的過程,模擬建立了一棵二叉樹的結構放在那里用於填充數據,但是當我們數據量較小時(比如斜樹),我們就會造成不必要的空間浪費(存在很多空節點)。因此二叉樹的順序存儲結構一般僅存儲完全二叉樹。
下面我們
采用二叉鏈表的基本思想去實現樹的建立:
具有n個結點的二叉鏈表中,有n+1個空指針。
二叉鏈表的儲存聲明:
template <class T>
class BiTree
{
public:
BiTree();
~BiTree( );
void PreOrder(){PreOrder(root);}
void InOrder() {InOrder(root);}
void PostOrder() {PostOrder(root);}
void LevelOrder(){LeverOrder(root)};
private:
BiNode<T> *root;
BiNode<T> * Creat( );
void Release(BiNode<T> *root);
void PreOrder(BiNode<T> *root);
void InOrder(BiNode<T> *root);
void PostOrder(BiNode<T> *root);
void LevelOrder(BiNode<T> *root);
};
但是很明顯二叉鏈表只適合於求樹上某結點的孩子,卻不適合找某結點的雙親,我們可以在二叉鏈表的基礎上添加結點的雙親,使之成為三叉鏈表,以便我們對某結點雙親的求取。
三叉鏈表:
結點結構:
代碼實現:(跟二叉鏈表實際是一樣的,只不過傳參時把父節點傳給子節點,子節點記錄下來繼續遞歸傳遞)
template <class T>
BiNode<T> * BiTree<T>::Creat(BiNode<T> * &root ,BiNode<T> *parent){
T ch;
cout<<"請輸入創建一棵二叉樹的結點數據"<<endl;
cin>>ch;
if (ch=="#") root = NULL;
else{
root = new BiNode<T>; //生成一個結點
root->data=ch;
root->parent=parent;
Creat(root->lchild,root ); //遞歸建立左子樹
Creat(root->rchild,root); //遞歸建立右子樹
}
return root;
}
template<class T>
BiTree<T>::BiTree( int i)
{
number=0;
Creat(root,NULL); //根節點父節點為空(根節點不存在父節點)
}
二叉樹遍歷分為遞歸調用與非遞歸調用兩種:
在前中后序遍歷中,遞歸調用的使用方式都是類似的,只是調用的順序有所不同。
對於非遞歸調用:(遵循從左至右的遍歷原則,體現深度優先搜索的思想)
在前序遍歷過某結點的整個左子樹后,如何找到該結點的右子樹的根指針是非遞歸調用的關鍵。
我們可以利用棧的存儲結構性質來實現非遞歸調用:棧是實現遞歸的最常用的結構。
前序遍歷非遞歸算法:
思想:
遇到一個結點,就訪問該結點,並把此結點推入棧中,然后遍歷它的左子樹;
遍歷完它的左子樹后,從棧頂托出這個結點,並按照它的右鏈接指示的地址再去遍歷該結點的右子樹結構。
代碼實現:
template <class T>
void BiTree::PreOrder(BiNode<T> *root) {
SeqStack<BiNode<T> *> s;
while (root!=NULL | | !s.empty()) { //結點指向為空(結點處不存在子樹)且棧空(不存在后繼結點)則樹空(遍歷完畢)
while (root!= NULL) {
cout<<root->data;
s.push=root; //訪問節點后壓棧
root=root->lchild; //然后遍歷其左子樹
//左子樹遍歷完畢后指針指向為空,跳出內部while進入if語句訪問父節點的右子樹
}
if (!s.empty()) {
root=s.pop(); //刪除該節點(空節點),訪問其父節點的右子樹
root=root->rchild;
}
}
}
中序遍歷非遞歸算法:
當我們遇到樹的結點時,我們不能立刻去訪問它,而是先去訪問該節點的左子樹,那么我們在遇到該節點時不訪問(不輸出),訪問過程放到遍歷左子樹完畢后(再輸出),即每次遇到后先將其壓棧,然后遍歷左子樹直至為空,然后訪問輸出節點后繼續訪問右子樹。(遇見節點不等於訪問輸出節點)
后續遍歷非遞歸算法1:
與前序和中序遍歷有所不同的是后續遍歷先訪問其左子樹后訪問其右子樹,最后訪問該節點,但是我們遇到該節點一共兩次(第一次是從其父節點遍歷過來,另一次是從其左子樹遍歷完成后返回到該節點,最后一次遇見(右子樹遍歷完畢后返回)便成了訪問輸出),對於棧來說可以設置一個flag標志:
flag=1代表遍歷完左子樹,該點不能訪問,flag=2代表遍歷完右子樹,該節點可以訪問。
具體過程:
設根指針為bt:
若bt不等於NULL,則bt及標志flag(置為1)入棧,遍歷其左子樹。
若bt等於NULL,此時若棧空,則整個遍歷結束;若棧不空,則表明棧頂結點的左子樹或右子樹已遍歷完畢(簡單的說就是棧不空表示該節點還沒有被訪問),若棧頂的標志flag=1,則表明棧頂節點的左子樹已遍歷完畢,將flag修改為2,並遍歷棧頂點的右子樹;若棧頂點的標志flag=2,則表明棧頂點的右子樹也遍歷完畢,輸出棧頂結點並壓出即可。
代碼實現:
template<class DataType>
void BiTree<DataType>::PostORder(BiNode<DataType>*bt){
top=-1; //采用順序棧,並假定棧不會發生上溢
while(bt!=NULL||top!=-1){
while(bt!=NULL){
top++;
s[top].ptr=bt;
s[top].flag=1;
bt=bt->lchild;
}
while(top!=-1&&s[top].flag==2){
bt=s[top--].ptr; //相當於出棧操作
cout<<bt->data;
}
if(top!=-1){ //與上述while循環不可顛倒(不是遞歸操作,無法直接調用,如果if寫在前面便不會訪問右子樹,而是直接輸出該節點返回該節點的父節點了)
s[top].flag=2;
bt=s[top].ptr->rchild;
}
}
}
后續遍歷非遞歸算法2:
需要用棧實現,根據后續遍歷的要求及棧操作的特點(FILO),依次將
根結點、根結點的右兒子、根節點的左兒子入棧(不訪問),當結點出棧時再進行訪問。
當棧頂出現葉子結點時,直接進行出棧操作,當剛出棧元素和棧頂元素之間關系是“兒子-雙親”關系時,進行出棧操作。
代碼實現:
void tree::T_print(bnode *bt){
stack<bnode*> s;
bnode *cur, *pre=NULL;
if (root==NULL) return;
s.push(bt);
while (!s.empty()) {
cur=s.top();
if ((cur->Lchild==NULL&&cur->Rchild==NULL) ||(pre!=NULL&&(pre==cur->Lchild||pre==cur->Rchild)))
{
cout<<cur->data; s.pop(); pre=cur; //每次pre都會指向剛剛出棧的結點(然后如果剛出棧元素和棧頂元素之間關系是“兒子-雙親”關系時,直接再進行出棧操作)
}
else
{
if (cur->Rchild!=NULL) s.push(cur->Rchild);
if (cur->Lchild!=NULL) s.push(cur->Lchild);
}
}
}
二叉樹的層序遍歷:(利用隊列來進行遍歷)
1.隊列Q初始化;
2. 如果二叉樹非空,將根指針入隊;
3. 循環直到隊列Q為空
3.1 q=隊列Q的隊頭元素出隊;
3.2 訪問結點q的數據域;
3.3 若結點q存在左孩子,則將左孩子指針入隊;
3.4 若結點q存在右孩子,則將右孩子指針入隊;
代碼實現:
#include <queue>
using namespace std;
template<class T>
void BiTree<T>::LevelOrder(BinaryTreeNode<T>* root){
queue<BiTreeNode<T>*> aQueue;
if(root)
aQueue.push(root);
while(!aQueue.empty())
{
root=aQueue.front(); //取隊列首結點
aQueue.pop();
cout<<pointer->data;//訪問當前結點
if(root->lchild) //左子樹進隊列
aQueue.push(root->lchild);
if(root->rchild) //右子樹進隊列
aQueue.push(root->rchild);
}//end while
}
上面對二叉樹的遍歷操作完成后:
我們通過遍歷序列來建立一棵二叉樹:
為了建立一棵二叉樹,將二叉樹中每個結點的空指針引出一個虛結點,其值為一特定值如“#”,以標識其為空,把這樣處理后的二叉樹稱為原二叉樹的
擴展二叉樹:
擴展二叉樹的前序遍歷序列:A B # D # # C # #
代碼實現:
template <class T>
void BiTree<T>::Creat(BiNode<T> * &root )
{
T ch;
cout<<"請輸入創建一棵二叉樹的結點數據"<<endl;
cin>>ch;
if (ch=="#") root = NULL;
else{
root = new BiNode<T>; //生成一個結點
root->data=ch;
Creat(root->lchild ); //遞歸建立左子樹
Creat(root->rchild); //遞歸建立右子樹
}
}
對於一棵樹來說,在我們進行操作完成之后,我們要對其進行析構銷毀,避免造成空間的浪費:
二叉樹的析構:
template<class T>
void BiTree<T>::Release(BiNode<T>* root){
if (root != NULL){
Release(root->lchild); //釋放左子樹
Release(root->rchild); //釋放右子樹
delete root;
}
}
template<class T>
BiTree<T>::~BiTree(void)
{
Release(root);
}
樹、森林與二叉樹的轉換:
我們一開始的思路是簡化分析樹,森林,研究最簡單的二叉樹。那么我們在研究完二叉樹的基本構造與析構和不同的遍歷操作之后,又該把問題回到相對復雜的樹與森林上,下面我們通過對樹和森林與二叉樹之間的轉換,將樹與森林問題轉換為相對簡單的二叉樹問題進行求解。
樹與二叉樹之間的轉換:
樹:兄弟關系 <–> 二叉樹:雙親和右孩子
樹:雙親和長子 <–> 二叉樹:雙親和左孩子
樹轉化為二叉樹具體實現過程:
1.兄弟加線.
2.保留雙親與第一孩子連線,刪去與其他孩子的連線.
3.順時針轉動,使之層次分明.
例:
性質:左兒子右兄弟:
樹的前序遍歷等價於二叉樹的前序遍歷:
只是兄弟之間加線,兄弟變成父子關系(長子為父,其余為孩子),而且父節點與長子之間的連線是確定的,這樣的構造並沒有影響前序遍歷的順序,所以結果是不變的。
樹的后序遍歷等價於二叉樹的中序遍歷:
例:
森林轉換為二叉樹 :
⑴ 將森林中的每棵樹轉換成二叉樹;
⑵ 從第二棵二叉樹開始,依次把后一棵二叉樹的根結點作為前一棵二叉樹根結點的右孩子,當所有二叉樹連起來后,此時所得到的二叉樹就是由森林轉換得到的二叉樹。
例:
二叉樹轉化為樹或森林:
⑴ 加線:若某結點x是其雙親y的左孩子,則把結點x的右孩子、右孩子的右孩子、……,都與結點y用線連起來;
⑵ 去線:刪去原二叉樹中所有的雙親結點與右孩子結點的連線;
⑶ 層次調整:整理由⑴、⑵兩步所得到的樹或森林,使之層次分明。
例:
最優二叉樹-哈夫曼樹及哈夫曼編碼:
葉子結點的權值:對葉子結點賦予的一個有意義的數值量。
二叉樹的帶權路徑長度(weighted path length(WPL)):
設二叉樹具有n個帶權值的葉子結點,從根結點到各個葉子結點的路徑長度與相應葉子結點權值的乘積之和。
哈夫曼樹:
給定一組具有確定權值的葉子結點,帶權路徑長度最小的二叉樹。(不一定唯一)
哈夫曼樹的特點:
- 權值越大的葉子結點越靠近根結點,而權值越小的葉子結點越遠離根結點。
- 只有度為0(葉子結點)和度為2(分支結點)的結點,不存在度為1的結點。(由哈夫曼樹的構造決定的)
哈夫曼算法基本思想:
⑴ 初始化:由給定的n個權值{w1,w2,…,wn}構造n棵只有一個根結點的二叉樹,從而得到一個二叉樹集合F={T1,T2,…,Tn};
⑵ 選取與合並:在F中選取根結點的權值最小的兩棵二叉樹分別作為左、右子樹構造一棵新的二叉樹,這棵新二叉樹的根結點的權值為其左、右子樹根結點的權值之和;
⑶ 刪除與加入:在F中刪除作為左、右子樹的兩棵二叉樹,並將新建立的二叉樹加入到F中;
⑷ 重復⑵、⑶兩步,當集合F中只剩下一棵二叉樹時,這棵二叉樹便是哈夫曼樹。
根據二叉樹的性質可知,具有n個葉子結點的哈夫曼樹共有2n-1個結點,其中有n-1個是非葉子結點,它們是在n-1次合並中生成的。
那我們就可以設置一個數組huffTree[2n-1]保存哈夫曼樹中各點的信息:
其中:
weight:權值域,保存該結點的權值;
lchild:指針域,結點的左孩子結點在數組中的下標;
rchild:指針域,結點的右孩子結點在數組中的下標;
parent:指針域,該結點的雙親結點在數組中的下標。
struct element
{ int weight;
int lchild, rchild, parent;
};
例:
代碼實現:
void HuffmanTree(element huffTree[ ], int w[ ], int n ) {
for (i=0; i<2*n-1; i++) { //初始化構造:雙親結點與孩子結點都設為-1(其中有n個葉子結點,需要2*n-1個結點位置)
huffTree [i].parent= -1;
huffTree [i].lchild= -1;
huffTree [i].rchild= -1;
}
for (i=0; i<n; i++) //初始化構造:前n個結點初始化構造為帶權的葉子結點
huffTree [i].weight=w[i];
for (k=n; k<2*n-1; k++) { //對n到2*n-1這n-1個結點進行構造,每次尋找權值最小的兩個結點構成一棵新樹
Select(huffTree, &i1, &i2);
huffTree[k].weight=huffTree[i1].weight+huffTree[i2].weight;
huffTree[i1].parent=k;
huffTree[i2].parent=k;
huffTree[k].lchild=i1;
huffTree[k].rchild=i2;
}
}
哈夫曼樹應用——哈夫曼編碼:
編碼:給每一個對象標記一個二進制位串來表示一組對象。
例:ASCII,指令系統
等長編碼:表示一組對象的二進制位串的長度相等。
不等長編碼:表示一組對象的二進制位串的長度不相等。
前綴編碼:
一組編碼中任一編碼都不是其它任何一個編碼的前綴 。
前綴編碼保證了在解碼時不會有多種可能。
例:
一組字符{A, B, C, D, E, F, G}出現的頻率分別是{9, 11, 5, 7, 8, 2, 3},設計最經濟的編碼方案。
線索二叉樹:
(合理利用二叉鏈表的空鏈域, 將遍歷過程中結點的前驅、 后繼信息保存下來,也便於查找結點的前驅和后繼)
我們知道,在有n個結點的二叉鏈表中共有2n個鏈域,但只有n-1個有用的非空鏈域,其余n+1個鏈域是空的。
我們可以利用剩下的n+1個空鏈域來存放遍歷過程中結點的前驅和后繼信息。
線索:將二叉鏈表中的空指針域指向前驅結點和后繼結點的指針被稱為線索;
線索化:使二叉鏈表中結點的空鏈域存放其前驅或后繼信息的過程稱為線索化;
線索二叉樹:加上線索的二叉樹稱為線索二叉樹。
例:
結點結構:
基本思想:
在遍歷的過程中完成線索化
可以采用前序、中序、后序遍歷建立前序線索二叉樹、中序線索二叉樹和后序線索二叉樹。