树
层次结构的数据在实现世界中大量存在.例如,一个国家有若干个省,一个省有若干个市等
线性数据结构一般不适合用来表示层次数据.为了组织层次数据,需要对树型数据的结构进行研究.
(1) 树的定义
描述树状结构采用的是倒置树,倒置树的顶端是根,根有几个分枝,称为子树,
每棵子树再分成几个小分枝,小分枝再分为更小的分枝,每个分枝也都是树,一个结点也是树.
树(tree)是包括n各结点的有限非空集合T,
其中,一个特定的结点r称为根(root),区域结点T-{r}划分成m(m>=0)个互不相交的子集T1,T2,T3..Tm,
其中,每个子集都是树,称为树根r的子树.
根据树的定义,一个结点的树是仅有根结点的树,这时m=0,这棵树没有子树.
(2) 基本术语
树中元素称为结点(node)
如果某结点到另一结点之间存在着连线,则称此线为一条边(edge)
如果从某个结点沿着树中的边可到达另一个结点,则称这两个结点间存在一条路径(path)
如果一个结点有子树,则称该结点为子树的双亲(parent)
子树的根称该结点的孩子(child)
具有相同双亲的结点称为兄弟(sibling)
结点拥有的子树数称为结点的度(degree)
度为0的结点称为叶子(leaf)
规定根结点的层次为1,树中结点的最大层次称为树的高度(hight)
如果树中结点的各子树是从左至右有次序的,则称该树为有序树,否则称为无序树.
(3)二叉树
二叉树(binary tree)是由有限个结点组成的集合,
它或者为空集合,或者仅含一个根结点,或者由一个根结点和两棵互不相交的左,右子树组成.
树定义与二叉树定义稍有区别.
首先树不能为空树,却可以有空二叉树.
一般树的子树不分次序的,二叉树的子树却分左右子树.
一般树中结点的度可以大于2,二叉树中结点的度均不超过2.
(4)二叉树的性质
若二叉树的高为h,树的结点总数为2^h-1,称此二叉树为满二叉树.
对于高度为h的二叉树,
如果第1-(h-1)层构成满二叉树,而第h层的叶子结点严格按照从左到右依次排列,称此二叉树为完全二叉树.
性质1: 高度为h的二叉树的结点总数 n <= 2^h-1.
性质2: 对于含n(n>=1)个结点的完全二叉树,其高度h=[log2(n+1)].([]表示取整)
性质3: 对于一棵非空二叉树,如果度为0的结点数为n0,度为2的结点数为n2,则n0=n2+1;
性质4: 对于含n个结点的完全二叉树,按从上到下,从左到右的顺序,从0到n-1编号.对于树中编号为i(0<=i<=n-1)的结点,有:
(1) 如果i=0;则该结点为二叉树的根结点.
(2) 如果2i+1<n,则其左孩子为2i+1,否则不存在左孩子;
如果2i+2<n,则其左孩子为2i+2,否则不存在右孩子.
(3) 结点i的双亲为[(n-1)/2]
(5)二叉树的ADT
ADT Bintree
数据:
0个或有限个结点组成的集合.
运算:
Create() 创建一棵空二叉树或二叉树
IsEmpty() 若二叉树为空,则返回 1,否则返回 0;
PreOrder() 先序遍历
InOrder() 中序遍历
PostOrder() 后序遍历
Display() 输出二叉树
(6)二叉树的数组表示
用一维数组来存储二叉树,首先将二叉树想象成一棵完全二叉树,
对于没有左孩子或右孩子的结点,用特殊字符代替,
再依次从上至下,从左至右的次序,依次将结点值存放到一维数组之中.
#include<iostream>
#include<cmath>
using namespace std;
#define MaxSize 64 //结点数不超过63,即树高不超过6
//创建二叉树
void Create(char Node[],int &n){
cout<<"按完全二叉树输入结点字符,空字符用*代替"<<endl;
cin>>Node;
//计算所含的字符数
n=0;
while(Node[n]!='\0')
n++;
}
//输出二叉树
void Display(char Node[],int n){
int hight,layer,num,i,j,k; //hight 树高 layer树层 num结点编号
hight=ceil(log(n+1)/log(2));
num=0;
for(layer=1;layer<=hight;layer++){ //输出 1层 到hight层
//每层前面空格的输出控制
for(i=0;i<pow(2,hight-layer);i++)
cout<<" ";
//每层结点的输出控制
for(j=0;j<pow(2,layer-1);j++,num++){
if(Node[num]!='*'&&Node[num]!='\0')
cout<<Node[num];
else if(Node[num]=='*')
cout<<" ";
else
break;
//每层结点间空格的输出控制
for(k=0;k<pow(2,hight-layer+1)-1;k++)
cout<<" ";
}
cout<<endl;
}
}
int main(){
int n;
char Node[MaxSize];
Create(Node,n);
cout<<"二叉树为:"<<endl;
Display(Node,n);
}
(7) 二叉树的链表表示
链式二叉树结点的逻辑结构:[*lChild | data | *rChild]
#include<iostream>
using namespace std;
//二叉树结点定义
struct Node{
char data;
Node *lChild,*rChild;
};
//二叉树的定义
struct BinTree{
Node *root;
};
//创建二叉树
void Create(BinTree &T,char x,Node *left,Node *right){
Node *NewNode;
NewNode = new Node;
NewNode->data=x;
NewNode->lChild=left;
NewNode->rChild=right;
T.root=NewNode;
}
//前序遍历
void PreOrder(Node *p){
if(p!=NULL){
cout<<p->data;
PreOrder(p->lChild);
PreOrder(p->rChild);
}
}
//中序遍历
void InOrder(Node *p){
if(p!=NULL){
InOrder(p->lChild);
cout<<p->data;
InOrder(p->rChild);
}
}
//后序遍历
void PostOrder(Node *p){
if(p!=NULL){
PostOrder(p->lChild);
PostOrder(p->rChild);
cout<<p->data;
}
}
int main(){
BinTree a,b,c,e,k;
Create(k,'k',NULL,NULL);
Create(e,'e',NULL,k.root);
Create(b,'b',NULL,e.root);
Create(c,'c',NULL,NULL);
Create(a,'a',b.root,c.root);
cout<<"前序遍历:";PreOrder(a.root);cout<<endl;
cout<<"中序遍历:";InOrder(a.root);cout<<endl;
cout<<"后序遍历:";PostOrder(a.root);cout<<endl;
}
定理: 任意n(n>=0)个不同结点的二叉树,都可由其前序遍历序列和中序遍历序列唯一确定.
(8) 常用二叉树之排序二叉树
排序二叉树定义:
设二叉树的所有结点值互异,满足一下条件:
(1)若左子树不空,则左子树上所有结点的值均小于根结点的值
(2)若右子树不空,则左子树上所有结点的值均大于根结点的值
(3)左右子树也分别为排序二叉树
#include<iostream>
using namespace std;
//二叉树结点定义
struct Node{
int data;
Node *lChild,*rChild;
};
//二叉树的定义
struct BinTree{
Node *root;
};
//创建空二叉树
void Create0(BinTree &T){
T.root=NULL;
}
//判空
int IsEmpty(BinTree &T){
if(T.root==NULL) return 1;
else return 0;
}
//创建排序二叉树
void Create(BinTree &T,int x){
Node *NewNode,*p;
NewNode = new Node;
NewNode->data=x;
NewNode->lChild=NULL;
NewNode->rChild=NULL;
int flag=0; //标识:是否入树 0表示为入树
if(IsEmpty(T)) T.root=NewNode;
else{
p=T.root;
while(!flag){
if(x<p->data){ //进入左子树
if(p->lChild == NULL){
p->lChild=NewNode;
flag=1;
}else{
p=p->lChild;
}
}else{ //进入右子树
if(p->rChild == NULL){
p->rChild=NewNode;
flag=1;
}else{
p=p->rChild;
}
}
}
}
}
// 创建二叉树
void Create1(BinTree &T,char x,Node *left,Node *right){
Node *NewNode;
NewNode = new Node;
NewNode->data=x;
NewNode->lChild=left;
NewNode->rChild=right;
T.root=NewNode;
}
//前序遍历
void PreOrder(Node *p){
if(p!=NULL){
cout<<p->data<<" ";
PreOrder(p->lChild);
PreOrder(p->rChild);
}
}
//中序遍历
void InOrder(Node *p){
if(p!=NULL){
InOrder(p->lChild);
cout<<p->data<<" ";
InOrder(p->rChild);
}
}
//后序遍历
void PostOrder(Node *p){
if(p!=NULL){
PostOrder(p->lChild);
PostOrder(p->rChild);
cout<<p->data<<" ";
}
}
int main(){
BinTree T;
Create0(T);
int i=1,x;
cout<<"请输入二叉树结点的值,输入-1结束"<<endl;
while(1){
cout<<"请输入第"<<i<<"个结点的值:";
cin>>x;
if(x == -1) break;
else Create(T,x);
i++;
}
cout<<"前序遍历:";PreOrder(T.root);cout<<endl;
cout<<"中序遍历:";InOrder(T.root);cout<<endl;
cout<<"后序遍历:";PostOrder(T.root);cout<<endl;
}
排序二叉树的优点:建树方便,中序遍历为升序序列.
(9) 常用二叉树之堆
设k0,k1,k2,...kn-1为互异的结点值,且按完全二叉树方式存放在一维数组k(0...n-1)之中
如果 k[i]>=k[2i+1]且k[i]>=k[2i=2](或k[i]<=k[2i+1]且k[i]<=k[2i=2]) i=0,1...(n-1)/2
则称该集合为最大堆(max heap)(或最小堆(min heap))
最大堆的意义是:双亲结点值大于等于其左右孩子结点值.
最小堆的意义是:双亲结点值小于等于其左右孩子结点值.
用下滑法,可以将完全二叉树调整为最大堆.
#include<iostream>
#include<iomanip>
using namespace std;
//输出数组
void Display(int *k,int n){
for(int i=0;i<n;i++)
cout<<setw(3)<<k[i];
cout<<endl;
}
//下滑法调最大堆
void SiftDown1(int *k,int n){
int i,j,temp,parent;
int m=0;
for(parent=(n-2)/2;parent>=0;parent--){
i=parent;
temp=k[i];
j=2*i+1;
while(j<n){
if(j+1<n)
j=k[j]<k[j+1]? j+1:j;
if(temp>k[j])
break;
else{
k[i]=k[j];
i=j;
j=2*i+1;
}
}
k[i]=temp;
cout<<++m<<"趟调堆:";Display(k,n);
}
}
//下滑法调最小堆
void SiftDown2(int *k,int n){
int i,j,temp,parent;
int m=0;
for(parent=(n-2)/2;parent>=0;parent--){
i=parent;
temp=k[i];
j=2*i+1;
while(j<n){
if(j+1<n)
j=k[j]>k[j+1]? j+1:j;
if(temp<k[j])
break;
else{
k[i]=k[j];
i=j;
j=2*i+1;
}
}
k[i]=temp;
cout<<++m<<"趟调堆:";Display(k,n);
}
}
int main(){
int k[9]={32,17,16,24,35,87,65,4,12};
cout<<"初始数据:";Display(k,9);
cout<<"最大堆"<<endl;
SiftDown1(k,9);
cout<<"最小堆"<<endl;
SiftDown2(k,9);
}
最大堆具有以下性质:
(1)堆顶结点的值最大
(2)任一结点的值均大于其左右孩子的值
(3)任一结点的值按完全二叉树存储在一维数组中
(10) 常用二叉树之哈夫曼树
路径长度(path length):路径上的分支数目
树的路径长度(tree path length):根结点到每个叶子结点的路径长度之和树的带权路径长度(weighted path length):
WPL =sum(w(i)l(i)) i=1...n
w(i)是第i个叶子结点的权值,l(i)为从根结点到第i个叶子结点的路径长度,
n为叶子结点的总数.
最优二叉树:WPL最小的二叉树,最优二叉树也称为哈夫曼树(Huffman tree)
哈夫曼树的性质:
(1) 哈夫曼树不存在度为 1 的结点.
(2) 哈夫曼树的带权路径长度等于所有度为 2 的结点值之和.
哈夫曼编码:
以不同字符出现的频树为权值,创建一棵哈夫曼树,
将根结点到叶子结点的路径编号,左分支路取 0,右分支路径取 1,
于是,任一叶子结点到根结点的路径序列就是它的哈夫曼二进制编码.
#include<iostream>
#include<iomanip>
using namespace std;
#define N 8 //叶子结点个数
//哈夫曼结点定义
struct hNode{
int suffix; //结点下标
int weight; //权值
int parent,lChild,rChild;//双亲,左右孩子下标
};
//哈夫曼编码结点定义
struct cNode{
int suffix; //结点下标
int weight; //权值
int length; //编码长度
char *code; //编码字符串
};
//哈夫曼结点数组初始化
void Initial(hNode h[],int w[]){
for(int i=0;i<N;i++){ //c初始化哈夫曼数组前半部分
h[i].suffix=i; //叶子结点的标号从0到N-1
h[i].weight=w[i];
h[i].parent=-1;
h[i].lChild=-1;
h[i].rChild=-1;
}
for(int i=N;i<2*N-1;i++){ //c初始化哈夫曼数组后半部分
h[i].suffix=i; //非叶子结点的标号从N到2*N-1
h[i].weight=0;
h[i].parent=-1;
h[i].lChild=-1;
h[i].rChild=-1;
}
}
//对哈夫曼结点h[m],....h[n]进行排序
//action=0 或 1 分别表示依权值或下标做升序排序
void Sort(hNode h[],int m,int n,int action){
int i,j,pos,min,flag;
hNode temp;
if(action == 0){
for(i=m;i<n-1;i++){
min=h[i].weight;
pos=i;
flag=0;
for(j=i+1;j<=n;j++)
if(h[j].weight<min){
min=h[j].weight;
pos=j;
flag=1;
}
if(flag==1){
temp=h[i];
h[i]=h[pos];
h[pos]=temp;
}
}
}else{
for(i=m;i<n-1;i++){
min=h[i].suffix;
pos=i;
flag=0;
for(j=i+1;j<=n;j++)
if(h[j].suffix<min){
min=h[j].suffix;
pos=j;
flag=1;
}
if(flag==1){
temp=h[i];
h[i]=h[pos];
h[pos]=temp;
}
}
}
}
//输出哈夫曼结点数组
void OutputNode(hNode h[]){
cout<<setw(5)<<"下标"<<setw(5)<<"权值"<<setw(5)<<"双亲"<<setw(7)<<"左孩子"<<setw(7)<<"右孩子"<<endl;
for(int i=0;i<N*2-1;i++)
cout<<setw(5)<<h[i].suffix<<setw(5)<<h[i].weight<<setw(5)<<h[i].parent<<setw(7)<<h[i].lChild<<setw(7)<<h[i].rChild<<endl;
}
//输出哈夫曼编码数组
void OutputCode(cNode c[]){
cout<<setw(5)<<"下标"<<setw(5)<<"权值"<<setw(5)<<"码长"<<setw(7)<<"编码"<<endl;
for(int i=0;i<N;i++)
cout<<setw(5)<<c[i].suffix<<setw(5)<<c[i].weight<<setw(5)<<c[i].length<<setw(7)<<c[i].code<<endl;
}
//将含N个叶子结点的哈夫曼初始数组,合并成哈夫曼树数组
void Merge(hNode h[]){
int i,j;
for(i=0;i<N-1;i++){ //对于N个叶子结点,做N-1次合并,构建哈夫曼树
j=i*2; //确定排序的起始位置
Sort(h,j,N+i-1,0);
//将N+i个结点的权值等于两个较小权值结点之和
h[N+i].weight=h[j].weight+h[j+1].weight;
h[N+i].lChild=h[j].suffix; //左孩子为h[j]的下标
h[N+i].rChild=h[j+1].suffix; //右孩子为h[j+1]的下标
h[j].parent=h[j+1].parent=N+i; //h[j],h[j+1]的双亲下标为N+i
}
}
//对哈夫曼树进行编码
void Code(hNode h[],cNode c[]){
int i,j,k,len;
char temp[N]; //定义临时存放叶子结点编码的字符数组
for(i=0;i<N;i++){ //对h[]中的前N个叶子结点依次编码
len=0; //编码长度初始为0
//用回溯法,对叶子结点i进行编码,并将编码暂存temp中
for(j=i,k=h[i].parent;k!=-1;j=k,k=h[k].parent){
if(h[k].lChild==j) //如果叶子结点i是它双亲的左孩子,则编码取 '0' 否则取 '1'
temp[len]='0';
else
temp[len]='1';
len++;
}
c[i].suffix=h[i].suffix;
c[i].weight=h[i].weight;
c[i].length=len;
//将存放在temp中的叶子结点i的编码,反转导入c[i].code
c[i].code=new char[len+1];
c[i].code[len]='\0';
len--;
j=0;
while(len>=0)
c[i].code[j++]=temp[len--];
}
}
int main(){
int w[N]={3,4,5,8,11,20,21,28};
hNode hArray[2*N-1];
Initial(hArray,w);
Merge(hArray); //合并成哈夫曼树
cout<<"哈夫曼树为:"<<endl;
OutputNode(hArray);
cNode cArray[N];
//依下标对哈夫曼树做升序排序,使前N个结点是叶子结点.
Sort(hArray,0,2*N-2,1);
Code(hArray,cArray); //编码
cout<<"哈夫曼编码为:"<<endl;
OutputCode(cArray);
}