树(Tree)是n(n≥0)个结点的有限集,它或为空树(n = 0);或为非空树,对于非空树T:
(1)有且仅有一个称之为根的结点;
(2)除根结点以外的其余结点可分为m(m>0)个互不相交的有限集T1, T2, …, Tm, 其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。
对于树,我们要明白一些最基本的术语:根节点(root),叶子(终端节点),森林(多颗不相交的数构成的集合),有序树(对于一个节点的子树,从左到右为其顺序,不可交换,比如二叉树),无序树,前驱结点,后继节点(孩子),兄弟,堂兄弟,组先(从根节点下来的所有分支节点的集合),子孙,节点的度,树的度(子树的个数,分支的个数),节点的层次,树的深度,分支节点。
而一般的树并不是我们研究的重点,我们研究的主要还是二叉树Binary Tree。为什么呢?普通树(多叉树)若不转化为二叉树,则运算很难实现。那二叉树有那些特征呢?
1-节点的度,一定小于等于2
2-他是有序树,左右子树不可颠倒。
二叉树可以实现很多功能,比如霍夫曼树实现数据压缩,二叉树来求表达式的值,
1-对于任何一颗二叉树,我们根据度数分为n0,n1,n2.那么一定有N=n0+n1+n2,并且如果二度的节点有n2个,叶子(0度节点)必定为n2+1.那么我们再看,n2+n0=2n2+1了,它必定是个奇数。
对于二叉树也有分类:
到此,我们可以出一道思考题了。
一棵完全二叉树有5000个结点,可以计算出其叶结点的个数是( )。
【Tip:完全二叉树,其中的度为1的结点个数最多是1个】
2-对完全二叉树,若从上至下、从左至右编号,则编号为i 的结点,其左孩子编号必为2i,其右孩子编号必为2i+1;其双亲的编号必为i/2。
3- 具有n个结点的完全二叉树的深度必为[log2n]+1,因为完全二叉树是有序的,而且前面都不缺省,用对数的思想,或者说,你可以数学归纳来想这个简单的问题。
4-任何树都可以转化为二叉树,根据左子指孩子,右子指本节点的亲兄弟的方法。
现在让我们言归正传,二叉树这个数据结构的存储的实现:
顺序存储:(非完全二叉树,缺省部分置一个特殊缺省值,比如0)
另外可以用链式存储结构:
比如二叉链表:
于是我们有可以出一道思考题:在n个结点的二叉链表中,有 _____个空指针域。
这种方式只能从上往下,于是我们可以引入一个父代节点指针,这样就美名为三叉链表了。
遍历的定义:指按某条搜索路线遍访每个结点并且不重复。
遍历的目的:对树结构进行 插入、删除、修改、查找、排序的前提,是二叉树运算的核心
遍历的规则:DLR先序遍历 LDR中序遍历 LRD后序遍历 RDL RLD,L代表左子树,R代表右子树,D代表节点。三种顺序遍历的过程都一样(Routine),只是访问各个结点的时机不同。
//In a recursive way:DLR
Status PreOrderTraverse(BiTree T)
{
if(T==NULL) return OK; //若为空,则执行空操作
else
{
printf("%d ",T->data); //访问节点
PreOrderTraverse(T->lchild); //递归遍历左子树
PreOrderTraverse(T->rchild);
}
}
层次图:
同理,中序遍历和后序遍历不过是内部递归调用和访问根节点的顺序不一样而已。
//二叉树的建立:
void CreateBiTree(BiTree &T){
scanf(“%c”,&ch);
if (ch==’#’) T=NULL; //递归结束,建空树
else{
T=new BiTNode; T->data=ch; //生成根结点
CreateBiTree(T->lchild); //递归创建左子树
CreateBiTree(T->rchild); //递归创建右子树
}
}
//二叉树节点的计数:
int NodeCount(BiTree T)
{
if(T == NULL ) return 0;
else return (NodeCount(T->lchild)+NodeCount(T->rchild)+1);
}
//count leafnode:
int LeadCount(BiTree T)
{
if(T==NULL) //如果是空树返回0
return 0;
if (T->lchild == NULL && T->rchild == NULL)
return 1; //如果是叶子结点返回1
else return LeafCount(T->lchild) + LeafCount(T->rchild);
}
遇到一个结点,就把它压栈,并去遍历它的左子树;
当左子树遍历结束后,从栈顶弹出这个结点并访问它;
然后按其右指针再去中序遍历该结点的右子树。
void InOrderTraversal( BiTree T )
{
BinTree T=BT;
Stack S = CreatStack( MAXMIZE );
/*创建并初始化堆栈S*/
while( T || !IsEmpty(S) )
{
while(T)
{
/*一直向左并将沿途结点压入堆栈*/
Push(S,T);
T = T->Left;
}
if(!IsEmpty(S))
{ T = Pop(S);
/*结点弹出堆栈*/
printf(“%5d”, T->Data);
/*(访问)打印结点*/
T = T->Right; /*转向右子树*/
}
}
}
从根节点到叶子结点一次经过的结点形成树的一条路径,最长路径的长度为树的深度。根节点的深度为1。
哈夫曼树:
在一般的数据结构的书中,树的那章后面,著者一般都会介绍一下哈夫曼(HUFFMAN)树和哈夫曼编码。哈夫曼编码是哈夫曼树的一个应用。哈夫曼编码应用广泛,如JPEG中就应用了哈夫曼编码。 首先介绍什么是哈夫曼树。哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树。所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的 路径长度(若根结点为0层,叶结点到根结点的路径长度为叶结点的层数)。树的带权路径长度记为WPL=(W1*L1+W2*L2+W3*L3+...+Wn*Ln),N个权值Wi(i=1,2,...n)构成一棵有N个叶结点的二叉树,相应的叶结点的路径长度为Li(i=1,2,...n)。可以证明哈夫曼树的WPL是最小的。
应用实例:哈夫曼的编码:
关键:要设计长度不等的编码,则必须使任一字符的编码都不是另一个字符的编码的前缀-前缀编码
哈夫曼树的构造过程:
根据给定的n个权值{w1,w2,……wn},构造n棵只有根结点的二叉树。 在森林中选取两棵根结点权值最小的树作左右子树,构造一棵新的二叉树,置新二叉树根结点权值为其左右子树根结点权值之和。 在森林中删除这两棵树,同时将新得到的二叉树加入森林中。 重复上述两步,直到只含一棵树为止,这棵树即哈夫曼树。
编码化:
一棵有n个叶子结点的Huffman树有 2n-1 个结点,采用顺序存储结构——一维结构数组。至于构造哈夫曼树:
1)初始化HT[1..2n-1]:lch=rch=parent=0
2)输入初始n个叶子结点:置HT[1..n]的weight值
3)进行以下n-1次合并,依次产生HT[i],i=n+1..2n-1:
3.1)在HT[1..i-1]中选两个未被选过的weight最小的两个结点HT[s1]和HT[s2] (从parent = 0 的结点中选)
3.2)修改HT[s1]和HT[s2]的parent值: parent=i
3.3)置HT[i]:weight=HT[s1].weight + HT[s2].weight , lch=s1, rch=s2
算法思路:
* 实现过程:着先通过 HuffmanTree() 函数构造哈夫曼树,然后在主函数 main()中 自底向上开始(也就是从数组序号为零的结点开始)向上层层判断,若在 父结点左侧,则置码为 0,若在右侧,则置码为 1。最后输出生成的编码。
借助最小堆的实现:(考完半期我再补一节关于堆、堆栈的水文吧,最近太忙)
typedef struct TreeNode *HuffmanTree;
struct TreeNode
{
int Weight;
HuffmanTree Left, Right;
}
HuffmanTree Huffman( MinHeap H )
{
/* 假设H->Size个权值已经存在H->Elements[]->Weight里 */
int i;
HuffmanTree T;
BuildMinHeap(H); /*将H->Elements[]按权值调整为最小堆*/
for (i = 1; i < H->Size; i++)
{
/*做H->Size-1次合并*/
T = malloc( sizeof( struct TreeNode) );
/*建立新结点*/
T->Left = DeleteMin(H);
/*从最小堆中删除一个结点,作为新T的左子结点*/
T->Right = DeleteMin(H);
/*从最小堆中删除一个结点,作为新T的右子结点*/
T->Weight = T->Left->Weight+T->Right->Weight;
/*计算新权值*/
Insert( H, T ); /*将新T插入最小堆*/
}
T = DeleteMin(H);
return T;
严蔚敏版教材:
typedef struct
{
int weight;//表征节点的权值
int parent,lchild,rchild;//节点的双亲,左子,右子的下标
}HTnode,*HuffmanTree;//动态分配数组内存存储哈夫曼树
//构造哈夫曼树
void CreateHuffmanTree(HuffmanTree &HT,int n)
{
if(n<=1) return;
int m=2*n-1; //n个有权的叶子节点,就一共有m=2n-1个节点
HT =new HTnode[m+1]; //动态分配内存,0号单元未引用
//Initialize
for(i=1;i<=m;++i)
{
HT[i].parent=0;HT[i].lchild=0;HT[i].rchild=0;
}
//Input the leafnode's weight in sequence
for(i=1;i<=n;i++)
cin>>HT[i].weight;
//Then we create a huffman tree
for(i=n+1;i<=m;++i)//通过n-1次的选择、删除、合并来生成一个哈夫曼树
{
Select(HT,i-1,s1,s2);
//在HT[k],1<=k<=i-1中选择一个双亲为0,权最小的节点,并且返回他们在HT中的序号s1和s2
}
}