二叉树是n(n≥0)个节点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根节点和两棵互不相交的、分别称为根节点的左子树和右子树的二叉树组成。
每个节点最多有两棵子树;
二叉树是有序的,其次序不能任意颠倒。
- 所有节点都只有左子树的二叉树称为左斜树;
- 所有节点都只有右子树的二叉树称为右斜树;
- 左斜树和右斜树统称为斜树。
- 在斜树中,每一层只有一个节点;
- 斜树的节点个数与其深度相同。
在一棵二叉树中,如果所有分支节点都存在左子树和右子树,并且所有叶子都在同一层上。
对一棵具有n个节点的二叉树按层序编号,如果编号为i(1≤i≤n)的节点与同样深度的满二叉树中编号为i的节点在二叉树中的位置完全相同。
简单来讲就是k层的完全二叉树前k-1层和k-1层的满二叉树一样,在第k层自左端数起,叶子节点都是连续的。
或者说在满二叉树中,从最后一个节点开始,连续去掉任意个节点,即是一棵完全二叉树。
叶子节点只能出现在最下两层,且最下层的叶子节点都集中在二叉树的左部。
完全二叉树中如果有度为1的节点,只可能有一个,且该节点只有左孩子。
深度为k的完全二叉树在k-1层上一定是满二叉树。
二叉树的第 i 层上最多有2i-1个节点(i≥1)。
一棵深度为k的二叉树中,最多有2k-1个节点,最少有k个节点。
深度为k且具有2k-1个节点的二叉树一定是满二叉树,
深度为k且具有k个节点的二叉树不一定是斜树。(可能左右交错)
在一棵二叉树中,如果叶子节点数为n0,度为2的节点数为n2,则有: n0=n2+1。 (简单证明如下:
二叉树总节点数 == 度为0的节点数+度为1的节点数+度为2的节点数
总节点数-1 == 度为1的节点数+2*度为2的节点数
)具有n个节点的完全二叉树的深度为[log2n]+1 ([ ]表示向下取整)
若将n个节点的完全二叉树从1开始层序标号,则有如下性质:
根节点标号为1.
节点 i(i>1) 的双亲节点是i/2,根节点无双亲节点
节点 i 的左儿子标号为 2*i,右儿子为 2*i+1 ,若左(右)儿子标号超过n,则无左(右)儿子
二叉树的顺序存储结构就是用一维数组存储二叉树中的节点,并且节点的存储位置(下标)应能体现节点之间的逻辑关系——父子关系。
普通二叉树需按照完全二叉树的编号方式编号,然后以完全二叉树的形式存储到一维数组中,造成很多浪费,故二叉树的顺序存储结构一般仅存储完全二叉树.
令二叉树的每个节点对应一个链表节点,链表节点除了存放与二叉树节点有关的数据信息外,还要设置指示左右孩子的指针。
typedef struct BiTNode{ // 二叉链表
TElemType data; // 数据域
struct BiTNode *lchild, *rchild; //左右孩子指针
} BiTNode, *BiTree;
空间浪费: 具有n个节点的二叉链表中,有n+1个空指针
在二叉链表的节点上多加一个双亲指针.便于找到此节点的双亲.
typedef struct TriTNode { // 三叉链表
TElemType data; // 数据
struct TriTNode *lchild, *rchild; // 左右孩子指针
struct TriTNode *parent; //双亲指针
} TriTNode, *TriTree;
这个书中并没有详细介绍,经由查询,这种存储方式便于寻找某节点的双亲节点,不便于进行寻找儿子或兄弟的操作,仅仅适用于少数场景,是一种静态链表(也可写作动态)
数组下标 | 数据 | 双亲位置 | 左/右标记 |
---|---|---|---|
0 | B | 2 | L |
1 | C | 0 | R |
2 | A | -1(根节点无双亲节点) | |
3 | D | 2 | R |
4 | E | 3 | R |
5 | F | 4 | L |
typedef struct BPTNode{//节点结构
TElemType data;
int parent;//双亲的下标
char LRTag;//左,右孩子的标志域
}BPTNode;
typedef struct BPTree{//树结构
BPTNode *nodes; // 后续需申请空间
int num_node;//节点数目
int root;//根节点的位置
}BPTree;
这个后续线索二叉树会讲到,此处按下不表
具体遍历方法已在博客中书写过,不再重复讲述
传送门: 二叉树遍历
根据前序序列的第一个元素建立根节点;
在中序序列中找到该元素,确定根节点的左右子树的中序序列;
在前序序列中确定左右子树的前序序列;
由左子树的前序序列和中序序列建立左子树;
由右子树的前序序列和中序序列建立右子树。
(4,5条基本就是递归求解的意思)
二叉树递归应用
0 | 1 | |
---|---|---|
LTag | lchild指向该结点的左孩子 | lchild指向该结点的前驱结点 |
RTag | rchild指向该结点的右孩子 | rchild指向该结点的后继结点 |
typedef enum { Link, Thread } PointerThr; // 左右孩子指针(0) or 线索(1)
typedef struct BiThrNod {
TElemType data; // 数据域
struct BiThrNode *lchild, *rchild; // 左右指针
PointerThr LTag, RTag; // 左右标志
} BiThrNode, *BiThrTree;
线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。由于前驱和后继信息只有在遍历该二叉树时才能得到,所以,线索化的过程就是在遍历的过程中修改空指针的过程。
这里指的是更普遍意义上的树,不仅仅局限于二叉树
基本思想:用一维数组来存储树的各个节点(一般按层序存储),数组中的一个元素对应树中的一个节点,包括节点的数据信息以及该节点的双亲在数组中的下标。
与前文表示的双亲链表没区别,还把那个"左右标志"去掉了.
查找双亲复杂度O(1),查找孩子和兄弟的复杂度都是O(n),n是元素个数.
此方法思路主要分为两种,第一种是让每个节点都有多个指针域,
故换个思路:
基本思想:将每个节点的孩子节点都用线性表(链表)存储起来,然后n个节点就得到了n条链表,再将这个n条链表的头指针组成一个线性表(数组)
struct CTNode{ // 孩子节点
int child; // 孩子下标
CTNode *next; // 下一个孩子节点
};
struct CBNode{ // 表头节点
TElemType data; // 数值域
CTNode *firstchild; // 第一个孩子
};
struct CTree{ // 数组结构
CBNode * nodes; // 数组头指针
int n; // 节点数量
int r; // 树根位置
}
但这种方法查找孩子简单,查找双亲就变得困难.故可以考虑其与双亲表示法结合一下.
// 双亲孩子表示法
struct CBNode{ // 表头节点
TElemType data; // 数值域
int parent; //双亲的下标
CTNode *firstchild; // 第一个孩子
};
又称二叉树表示或者二叉链表表示法,即以二叉链表作树的存储结构.链表中节点的两个链域分别指向该节点的第一个孩子节点和下一个兄弟节点
struct TNode{ // 孩子兄弟表示法
TElemType data; // 数据域
TNode *firstchild, *rightsib; //第一个孩子和下一个兄弟
};
既可方便查找孩子,又可方便查找兄弟,若增设双亲域,也便于查找其双亲(书上写的,感觉不方便啊?)
不过通过这个我们也发现,对于任意一棵树,都能找到唯一的二叉树与之对应
普通树转化成的二叉树,其根节点都没有右孩子,即普通树对应的二叉树肯定没有右子树。
这句话指的是对整个树而言,是一定没有右子树的.
根据上文发现的,普通树转化的二叉树都没有右孩子,所以把森林里所有树都转化成二叉树,然后第一棵树的根节点作为最后大二叉树的根节点,依次把后一棵二叉树的根节点作为前一棵二叉树根节点的右孩子,最后得到的就是森林转化的二叉树。
注: 树显然不存在“中序”这种方式,因为不一定是二叉,根节点的遍历顺序会出现问题。
// 哈夫曼树结点结构
// 由于哈夫曼树的构建是从叶子结点开始,不断地构建新的父结点,直至树根,所以结点中应包含指向父结点的指针。
typedef struct {
int weight;//结点权重
int parent, left, right;//父结点、左孩子、右孩子在数组中的位置下标
}HTNode, *HuffmanTree;
注:哈夫曼树最终需要2*n-1个节点,n为元素个数,一般采用一维数组的方式实现。
// n 为全局变量,指n个元素
void Select(element huffTree[ ],int *s1 ,int *s2){ // 选择为进入树的两个最小元素
int min1, min2;
int i = 0;
//先找到2个还没构建树的结点
while(HT[i].parent != 0 && i <2*n-1){
i++;
}
min1 = HT[i].weight;
*s1 = i;
i++;
while(HT[i].parent != 0 && i <= 2*n-1){
i++;
}
//对找到的两个结点比较大小,min2为大的,min1为小的
if(HT[i].weight < min1){
min2 = min1;
*s2 = *s1;
min1 = HT[i].weight;
*s1 = i;
}else{
min2 = HT[i].weight;
*s2 = i;
}
//两个结点和后续的所有未构建成树的结点做比较
for(int j=i+1; j <2*n-1; j++){
//如果有父结点,直接跳过,进行下一个
if(HT[j].parent != 0){
continue;
}
//如果比最小的还小,将min2=min1,min1赋值新的结点的下标
if(HT[j].weight < min1){
min2 = min1;
min1 = HT[j].weight;
*s2 = *s1;
*s1 = j;
}
//如果介于两者之间,min2赋值为新的结点的位置下标
else if(HT[j].weight >= min1 && HT[j].weight < min2){
min2 = HT[j].weight;
*s2 = j;
}
}
}
void HuffmanTree(element huffTree[ ], int w[ ], int n ) { // 构建哈夫曼树
for (i=0; i<2*n-1; i++) { // 初始化数组
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++) { // 开始构建树
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;
}
}
//HT为哈夫曼树,HC为存储结点哈夫曼编码的二维动态数组,n为结点的个数
void HuffmanCoding(element HT[ ], HuffmanCode *HC,int n){
*HC = (HuffmanCode) malloc((n+1) * sizeof(char *));
char *cd = (char *)malloc(n*sizeof(char)); // 暂时存放结点哈夫曼编码的字符串数组
cd[n-1] = '\0'; // 字符串结束符(因为字符串最长长度是n-1)
for(int i=0; i<n; i++){ // 从叶子结点出发,得到的哈夫曼编码是逆序的,需要在字符串数组中逆序存放
int start = n-1; // 当前字符哈夫曼编码数组中已完成的位置
int c = i; // 当前结点的父结点在数组中的位置
int j = HT[i].parent;
while(j != 0){ // 一直寻找到根结点
if(HT[j].left == c) // 若是左子树,编码为0
cd[--start] = '0';
else
cd[--start] = '1';
// 以父结点为孩子结点,继续朝树根的方向遍历
c = j;
j = HT[j].parent;
}
// 跳出循环后,cd数组中从下标 start 开始,存放的就是该结点的哈夫曼编码
(*HC)[i] = (char *)malloc((n-start)*sizeof(char));
strcpy((*HC)[i], &cd[start]);
}
// 使用malloc申请的cd动态数组需要手动释放
free(cd);
}
前缀编码:一组编码中任一编码都不是其它任何一个编码的前缀 。其保证了解码时不会有多种可能。