树与二叉树 第三节
1.树与森林
1.1 树的存储结构
我们在第一节已经学习过树的逻辑结构,而且对于二叉树的存储结构和抽象数据类型已经很熟悉了,下面考虑如何存储一棵普通的树。(三种存储结构,重点是孩子兄弟表示法)
1.1.1 双亲表示法(顺序存储)
存储方式:每个节点包括数据域data和指针域parent,指针域指向该节点的双亲索引,parent是int型,用数组来存储这些节点,根节点的parent=-1。
区别于二叉树的顺序存储,二叉树顺序存储的数组索引能表达出节点之间的逻辑关系,而树的双亲表示法索引不能表达逻辑关系,各个节点存在哪里不是很重要。
typedef struct {
ElemType data;
int parent;
}PTNode;
typedef struct {
PTNode nodes[MaxSize];
int n;//节点数量
}PTree;
找双亲节点:parent指向的即是双亲节点。
找孩子节点:只能遍历树。
删除节点:将最后一个节点移动到该节点处覆盖该节点值。(保证遍历树的时候不会遍历到空值浪费时间)。
添加节点:在索引n添加节点设置parent即可。
1.1.2 孩子表示法(顺序+链式存储)
存储方式:普通节点包括数据域data和指针域firstChild,firstChild指向一个索引节点,该索引节点的数据域是普通节点的第一个孩子节点的索引,索引节点的指针域next指向其兄弟节点。
存储代码:
#define MaxSize 100
#define ElemType char
typedef struct CTNode{
int child;
CTNode* next;
}CTNode;
typedef struct {
ElemType data;
CTNode* firstChild;
}CTBox;
typedef struct {
CTBox nodes[MaxSize];
int n,r;//节点数和根的位置
}CTree;
只有找孩子简单,其他操作都十分复杂。
1.1.3 孩子兄弟表示法(链式存储)
我们对用二叉链表存储二叉树已经十分熟悉了,这里将用二叉链表存储普通的树,它的代码是这样的。
#define ElemType char
typedef struct CSNode {
ElemType data;
CSNode* firstChild, * nextSibling;//分别指向第一和孩子节点和下一个兄弟节点
}CSNode,CSTree;
节点的两个指针域分别指向第一个孩子和下一个兄弟,使用孩子兄弟表示法,上面的树可以变成如下的样子:
一棵普通的树使用孩子兄弟表示法它的物理结构变成了二叉链表(二叉树),而对于二叉链表(二叉树)的各种操作我们已经很熟悉了。
1.2 森林的存储结构
多棵树就构成了森林,如图所示:
我们将每棵树的根节点看作兄弟,既可以用孩子兄弟表示法将森林作为一棵树来存储,变成如下形式:
如此森林使用孩子兄弟表示法它的物理结构变成了二叉链表(二叉树),而对于二叉链表(二叉树)的各种操作我们已经很熟悉了。
1.3 树与森林的抽象数据类型
我们将树和森林都是用孩子兄弟表示法变成"二叉树"的样子,不倒霉不至于遇到树和森林的算法题,若遇到了将其用孩子兄弟表示法,变成“二叉树”后就是我们熟悉的算法了,只要稍微修改即可。
树的先根遍历:就是二叉树的先序遍历。
树的后根遍历:就是二叉树的后序遍历。
森林的先序遍历:先序遍历每一棵树,或者用孩子兄弟表示森林时先序遍历二叉树。
森林的中序遍历:后序遍历每棵树,或者用孩子兄弟表示法时中序遍历二叉树。(有点绕,试一下就相信了)
2.树的应用
一、二叉排序树(BST)
2.1 应用场景
设想,我们按顺序存储一段数据,那我们该选择哪种数据结构能方便查找某一数据呢?线性表和普通链表都需要遍历数据可能遍历完。如果用树呢?看下面这棵二叉树,这颗二叉树规定左子树的节点的值都小于根节点,根节点都小于右子树节点的值:
如此,假如我们要找28,28小于50向左,大于25向右,下雨35向左,等于28找到。如此只需要比较4次即可。上图就是一棵二叉排序树
2.2. 二叉排序树的定义
1)二叉排序树是一棵空二叉树或者是一棵二叉树;
2)二叉排序树的根节点关键字大于左子树所有节点关键字小于右子树所有节点关键字;
3)左子树和右子树也是二叉排序树;
2.3 二叉排序树算法
#define OK 1
#define ERROR 0
#define ElemType int
#define Status int
typedef struct BSTNode {
int data;
BSTNode* lchild, * rchild;
}BSTNode, * BSTree;
2.3.1 二排序叉树的查找算法
BSTNode* BST_Search(BSTree T, ElemType key) {
while (T != NULL && T->data != key) {
if (T->data > key)
T = T->lchild;
else T = T->rchild;
}
return T;
}
BSTNode* BST_Search(BSTree T, ElemType key) {//二叉排序树的递归查找
if (T != NULL) {
if (T->data = key)
return T;
else if (key < T->data)
BST_Search(T->lchild,key);
else BST_Search(T->rchild,key);
}
return NULL;
}
2.3.2 二叉排序树的插入
Status BST_insert(BSTree &T, ElemType key) {
if (T == NULL) {//空树
T = (BSTNode*)malloc(sizeof(BSTNode));
if (!T) {
cout << "申请内存错误!" << endl;
exit(1);
}
T->data = key;
T->lchild = T->rchild = NULL;
}
else {
if (T->data > key)
BST_insert(T->lchild, key);
else if (T->data < key)
BST_insert(T->rchild, key);
else {//不允许插入相同值的关键字
return ERROR;
}
}
}
2.3.3 二叉排序树的构造
Status CreatBST(BSTree &T,ElemType key[],int n) {
T = NULL;
int i = 0;
while (i < n) {
BST_insert(T, key[i]);
i++;
}
return OK;
}
小实验:
int main() {
BSTree T;
ElemType key[10] = { 15,28,9,16,3,24,55,28,17,10 };
CreatBST(T, key, 10);
cout << "在二叉排序树查找3:" << endl;
BSTNode* s = BST_Search(T, 3);
cout << s->data << endl;
cout << "在二叉排序树插入12后查找12:" << endl;
BST_insert(T, 12);
s = BST_Search(T, 12);
cout << s->data << endl;
}
2.3.4 二叉排序树的删除
1)若是叶子节点则直接删除;
2)若删除节点只有一棵左子树或右子树,让其孩子代替该节点的位置成为该节点父亲节点的孩子;
3)若删除节点有两棵树,首先令该节点的中序直接后继(其右子树最左的节点是后继,该后继是叶子节点或者只有右子树)代替该节点成为其父亲节点的孩子节点以及其孩子节点的父节点,然后该后继若为叶子节点则删除完成,若有右子树令其右子树称为其原来父节点的左子树。
删除的算法不难,但是为了方便找到父节点我们需要用三叉链表来存储,大家可以自己试一下。
二、平衡二叉树
三、哈夫曼树与哈夫曼编码
3.1 应用场景
假设有一场考试,100道选择题的答案为50个A,30个B,10个C,10个D。我们如何用计算机传递出答案,是我们传递的数据量最少呢?
如果用ASCII码传递,一个字符8bit,我们需要800bit。
了解计算机组成原理我们可以使用2bit对字符进行编码,A为00,B为01,C为10,D为11。这样我们需要200bit就足够了。
如果A为0,B为10,C为111,D为110,这样我们需要50 * 1 + 30 * 2 +10 * 3 + 10 * 3 = 170bit就足够了,这就是哈夫曼编码。
那么存在一个疑问,如果将B改为1,只占一个bit不是可以更好吗?答案是不行的,假如我们传输ABBBCD,编码会是0111111110,出现歧义了,我们可以翻译为ACCD也可以翻译为ABBBBBBD等。但是如果用哈夫曼编码就不会出现这种情况,可以尽情尝试。
因为哈夫曼编码是一种前缀编码,即每一个字符的编码都不是另一个字符的前缀。
3.2 哈夫曼树的逻辑结构
节点的权:树的节点常常被赋予一个表示某种意义的值,称为节点的权。
带权路径长度(WPL):从根结点出发到任意节点的边数乘以该节点的权称为该节点的带权路径长度。树中所有叶节点的带权路径之和称为树的带权路径长度。
如上图所示,该树的带权路径长度=5 * 1 + 4 * 2 + 3 * 3 + 1 * 3 = 25;
哈夫曼树:在带有n个叶节点的二叉树中,WPL最小的二叉树称为哈夫曼树,也称最优二叉树。
(哈夫曼树关注的是叶节点,叶节点与前缀编码的关系密不可分,哈夫曼树并不唯一!)
3.3 哈夫曼树的构造
1)将叶子节点全部加入待构造集合,在待构造集合中将权值最小的两个节点作为新建节点的左右孩子(那个左哪个右都可以),新建节点加入待构造集合,这两个节点从待构造集合中删除,将新建节点的权值设为这两个节点的权值之和。
2)将重复上述过程直到待构造集合仅剩一个节点,即哈夫曼树的根节点。
举例:
如上述例子50个A,30个B,10个C,10个D,我们将A,B,C,D都设为叶子节点并加入待构造集合:
如图我们构造完成了一棵哈夫曼树,那每个字符编码是什么呢?我们可以规定,从根节点出发道某一结点,向左一次为0,向右一次为1,则A0,B10,C110,D111。这就是哈夫曼树的编码。(哈夫曼树的算法在普通考试中不会让手写算法的,暂时略过,到此树一章就完事了,还有一篇习题)