树是n(n >= 0)个结点的有限集。当n = 0时,称为空树。在任意一颗非空树上应该满足:
双亲表示法:用一组连续空间来存储每个结点,同时为每个结点中增设一个伪指针(表示该结点的双亲结点的位置)
#define MAX_TREE_SIZE 100 // 树中最多结点数
typedef struct {// 树中结点定义
ElemType data;// 数据元素
int parent;// 双亲结点位置
}PTNode;
typedef struct{// 树的类型定义
PTNode nodes[MAX_TREE_SIZE];
int n;// 结点数
}PTree;
该存储结构的特点是比较容易得到结点的双亲结点,但是想要得到结点的孩子结点则需要遍历整个数组
孩子表示法:每个结点的孩子结点用单链表链接在一起形成一个线性结构,而每个结点都由连续的空间存储
代码如下:
struct CTNode {// 链式村粗的孩子结点
int child;// 孩子结点在数组的位置
struct CTNode *next;// 下一个结点
}
typedef struct {// 顺序存储的每个结点
ElemType data;// 数据元素
struct CTNode *firstChild;// 指向第一个孩子结点
}CTBox;
typedef struct {// 表示树
CTBox nodes[MAX_TREE_SIZE];
int n,r;// 结点数和根的位置
}CTree;
孩子兄弟表示法:以二叉链表作为树的存储结构。每个结点包含三部分的内容:结点值、指向第一个孩子结点的指针、指向结点下一个兄弟结点的指针
代码:
typedef struct CSNode {
ElemType data;
struct CSNode *firdtchild,*nextsibling;// 第一个孩子以及该孩子的第一个右兄弟指针
}CSNode,*CSTree;
二叉树就是每个结点至多有两颗子树,且左右子树有左右之分,不可以颠倒次序
其定义如下:
二叉树是n(n>=0)个结点的有限集合
满二叉树:二叉树的每一层都是满的,也就是说一个高度为h的满二叉树,其结点有2的h次方-1个;如果按照层序编号,对于一个编号为i的结点,其左孩子的编号为2i,右结点的编号为2i+1,其双亲结点的编号为i/2的向下取整
完全二叉树 :在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则该层包含 1~ 2^(h-1) 个节点。
二叉排序树:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉排序树
平衡二叉树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。它在满足二叉排序树的条件下,还要求任意节点的左右子树的高度差不超过1。也就是说,平衡二叉树是一棵自平衡的二叉排序树。
1.顺式存储的方式如图:
2.如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2
3.定义代码如下:
#define MaxSize 100
struct TreeNode {
ElemType value;// 结点中的数据元素
bool isEmpty;// 结点是否为空
}
TreeNode t[MaxSize];
typedef struct BiTNode {
ElemType data;
struct BitNode *lchild,*rchild;
}BitNode,*BiTree;
void preOrder(BiTree T){
if(T) {
visit(T);// 访问根结点
preOrder(T->lchild);// 以此方式访问左子树
preOrder(T->rchild);// 以此方式访问右子树
}
}
void InOrder(BiTree T){
if(T) {
InOrder(T->lchild);// 以此方式访问左子树
visit(T);// 访问根结点
InOrder(T->rchild);// 以此方式访问右子树
}
}
void PostOrder(BiTree T){
if(T) {
PostOrder(T->lchild);// 以此方式访问左子树
PostOrder(T->rchild);// 以此方式访问右子树
visit(T);// 访问根结点
}
}
代码如下:
void LevelOrder(BiTree T) {
LinkQueue Q;
InitQueue(Q); // 初始化辅助队列
BiTree p;
EnQueue(Q,T);// 根节点入队
while(!IsEmpty(Q)) {
DeQueue(Q,p);// 队头结点出队
visit(p);// 访问出队结点
if(p->lchild != NULL)
EnQueue(Q,p->lchild);// 左孩子入队
if(p->rchild != NULL)
EnQueue(Q,p->rchild);// 右孩子入队
}
}
以下代码是普通二叉树访问指定结点的双亲结点
// 递归中序遍历
void InOrder(BiTree T) {
if(T!=NULL) {
InOrder(T->left);// 左
visit(T);// 中
InOrder(T->right);// 右
}
}
// 访问结点q
void visit(BiTNode *q) {
if(q == p) // 当前访问的结点等于指定结点p时
final = pre;// 找到p的前驱
else
pre = q;// pre指向当前访问的结点
}
BiTNode *p;// 目标结点
BiTNode *pre = NULL;// 当前访问结点的前驱
BiTNode *final = NULL:// 当前结点
typedef struct ThreadNode{
ElemType data;// 数据元素
struct ThreadNode *lchild,*rchild;// 左右孩子指针
int ltag,rtag;// 左右线索标志
}
二叉树的线索化就是将二叉链表中的空指针改为指向前驱或后继的线索。而前驱和后继的信息只有在遍历的时候才可以得到。实质上还是遍历一下二叉树。
void InThread(ThreadTree &p,ThreadTree &pre) {
if(p!=NULL) {
InThread(p->lchild,pre);// 递归线索化左子树
// 本层逻辑
if(p->lchild == NULL) {// 该结点左孩子为空,则将其指向其父节点、线索标志位置为1
p->lchild = pre;
p->ltag = 1;
}
if(pre != NULL&&pre->rchild == NULL) {// 若该结点的前驱结点不为空且前驱节点的右孩子为空,则将其右孩子指针指向该结点、线索标志位置为1
pre->rchild = p;
pre->rtag = 1;
}
pre = p;
InThread(p->rchild,pre);// 递归线索化右子树
}
}
void CreateInThread(ThreadTree T) {
ThreadTree pre = NULL;
if(T != NULL) {
InThread(T,pre);// 线索化
pre->rchild = NULL;// 处理最后一个结点
pre->rtag = 1;
}
}
线索二叉树的结点分为两种情况,一个是线索化的结点,一个是没有线索化的结点
// 传统方法迭代找到该子树的最左下结点
ThreadNode *Firstnode(ThreadNode *p) {
while(p->ltag == 0) p = p->lchild;// 最左下结点、不一定是叶子结点
return p;
}
// 寻找后继
ThreadNode *Nextnode(ThreadNode *p) {
if(p->rtag == 0) return Firstnode(p->rchild);// 若该结点的右指针没有线索化则通过传统方法迭代找到该子树的最左下结点为其后继
else return p->rchild;// 如果线索化的,则直接返回其右指针即可
}
// 中序遍历
void Inorder(ThreadNode *T) {
for(TreadNode *p = Firstnode(T); p != NULL; p = Nextnode(p)) {
visit(p);
}
}
(1)结点的权:有某种现实含义的数值
(2)结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积
(3)树的带权路径长度:树中所有叶子结点的带权路径长度之和
哈夫曼树的定义
在含有n个带权叶子结点的二叉树中,其中带权路径长度最小的二叉树称为哈夫曼树,也称最优二叉树
以上4棵树,只有中间的两颗是哈夫曼树
哈夫曼树的构造:给定n个权值分别为w1,w2,w3,…,wn的结点,构造哈夫曼树的算法描述如下:
(1)将这n个结点分别作为n课仅仅含一个结点的二叉树,构成森林F
(2)构造一个新结点,从F中选取两颗根节点权值最小的二叉树作为新节点的左右子树,并且将新节点的权值置为左右子树上的根节点的权值之和
(3)从F中删除刚才选出的两棵树、同时将新加到的树加入F中
(4)重复步骤2和3,直到F中只剩下一棵树为止
哈夫曼树的特点:
(1)每个初始结点最终都会成为叶子结点,且权值越小的结点到根节点的路径长度越大
(2)哈夫曼树的结点总数为2n-1
(3)哈夫曼树没有度为1的结点
(4)哈夫曼树不唯一,但是WPL一定相同且为最优
(2)可变长度编码:允许对不同字符用不同长度的二进制位表示
(3)为什么A不可以用1个二进制位去编码呢?
答:这样就会混淆信息了。这里引申出一个概念:前缀编码(指没有一个编码是另一个编码的前缀),这样的编码方式不会出现歧义