复习
这个就比较easy了,我们知道二叉树的存储结构里面就是有一个结点,然后还有结点的两个孩子,一家三口和和睦睦~当然,树也可以这么干,存一个结点,把他所有的孩子都存起来(但是树的孩子可能有很多个,这就对如何存孩子提出了一个要求,我们可以创开辟一个数组,里面存孩子结点,也可以创建一个链表,存孩子结点),这就是传说中的孩子表示法。
二叉树里面,孩子分为左结点(哥哥)和右结点(弟弟)。咱这里当然也可以这么分,我们可以分为左结点(*firstchild,孩子),右结点(*nextsibling,兄弟们)。这就是孩子兄弟表示法。下面我们看下他的结构体:
typedef struct CSNode{
int data;
struct CSNode *firstchild,*nextsibling;
}*CSTree;
我们也可以创建一个结点,然后不存他孩子,存他爸~。这个树的结构也就确立下来了(那么为什么二叉树里面不整这一套?),这就是双亲表示法(这里双亲其实就只的父亲,中文翻译成双亲其实不太准确,因为他只有一个父节点)。
这三种种都有缺陷,孩子表示法和孩子兄弟表示法可以方便找孩子,但是很难找爸(那么找爸的复杂度是多少)。双亲表示法可以方便找爸,但是找孩子就很难(找孩子复杂度是多少)。
2.1 树和二叉树的转换(这是考试里面经常爱考的东西,一般是选择题。)
我们看下刚才树的孩子兄弟表示法,有没有发现很神奇的地方,一棵树可以用二叉树的表示法进行表示。只不过树里面的左孩子和二叉树里面的左孩子定义是一样的,只不过树里面的右孩子存的是左孩子的剩下那些兄弟。也就是“左孩子,右兄弟”。
既然可以互相表示,那么二叉树和森林就可以互相转换。
a)树转二叉树:树转化为二叉树后,二叉树中左孩子就是原来树中左孩子,右孩子就是原来树中左孩子剩下的那帮兄弟一个连着一个。我们从下到上,从左到右处理每一个非叶子结点:
1.把他所有孩子,先断绝父子关系,
因为结点的右结点是用来存兄弟的,不是存他孩子的,他的孩子都得放在左结点处,所以他的孩子都得先松开
2.把老大连在当前结点的左结点处
左孩子是用来存孩子的,先把老大加进去
3.把剩下的兄弟,按照顺序依次连到老大的右结点处(为什么不随便连)。
这帮孩子松开以后按照顺序连到他的左结点处,但是孩子之间的连接方式也得定义,因为右结点连得是兄弟,所以下一个孩子是当前孩子的右结点。
举个栗子
第一个非叶子节点应该是第三层的F,把F的孩子断开,然后连在其左结点处:
第三层没有其他非叶子节点了,我们看第二层,从左到右,先看B,把他孩子都断开,然后连在左孩子处:
注意,这里是先把D,F断开,然后把D连在B的左结点处,把F连在D的右结点处,G的位置相对不变。
同理我们处理C:
然后处理A:
这样就转成一个二叉树了~
2.2 森林转二叉树
这个东西其实就是树转二叉树的高阶版本,因为森林是由一个个树组成的,所以:
1.把每个树转化成二叉树。
2.把第一个树的根节点当做整个转化后二叉树的根节点,第一个树的右结点肯定是空的(想想为什么),我们依次把剩下几棵转化为二叉树的树当成它的兄弟,连在右结点处就好了。
2.3 二叉树转树
永远记住左孩子右兄弟,做的时候就比较容易:
我们可以看到,这个二叉树根节点A没有右子树,说明A没有兄弟,也说明我们转化以后因该是一个树,而不是森林(如果是森林的话,右结点肯定有东西)。
转成树时候,从上到下,从左到右.把当前结点左孩子,以及左孩子右结点,左孩子右结点的右结点,左孩子右结点的右结点的右结点...都断开,依次连到当前结点上。就OK了
我们先看A:按照这个规矩处理完是这个样子:
然后看B,处理完以后:
然后看C,处理完以后:
C就一个孩子H,把H放左边、放下边、放右边都是一样的(因为森林里面,直说孩子,不说左孩子右孩子,就算说也说得是第一个孩子和右兄弟)。
然后处理F,处理F的方式和C一样,结果也就是上图~
2.3 二叉树转森林
和二叉树转树是一样的操作方式,只不过根节点连右子树的话,转出来是森林,森林中树的个数取决于从从根节点开始,一直访问右结点,能访问几次,不连的话出来是树
先划分子树:
然后处理每一个子树:
二叉排序树或者是一棵空树,或者是一颗具有下列特征的非空二叉树:
1)若左子树为空,则左子树上所有结点关键字均小于根节点的关键字值
2)若右子树为空,则右子树上所有结点关键字值均大于根结点的关键字值
3)左、右子树本身也分别是一颗二叉排序树
说白了,对于任意结点,都有左<当前结点<右,也就是L
这里,我们学校考研里面对这个东西的代码不做要求,如果想看的话,可以去网上搜,这里我也提供我当初写的代码:
(个人小建议,本科学数据结构和算法时候,老师上面讲一个内容,下面一定要会利用博客,搜搜这个东西别人是怎么写的,然后自己实现一遍)。
#include
#include
typedef struct Node
{
int key;
struct Node *parent,*l_child,*r_child;
}*BiTree;
void BiTreeInsert(BiTree &T,int key)
{
BiTree p;
p = (BiTree)malloc(sizeof(struct Node));
p->key = key;
p->l_child = NULL;
p->r_child = NULL;
p->parent = NULL;
if(T->key == -1) //头结点为空
T = p;
else if(p->keykey&&T->l_child == NULL)
{
T->l_child = p;
p->parent = T;
}
else if(p->key>T->key&&T->r_child == NULL)
{
T->r_child = p;
p->parent = T;
}
else if (p->keykey&&T->l_child != NULL)
BiTreeInsert(T->l_child,key);
else if(p->key>T->key&&T->r_child != NULL)
BiTreeInsert(T->r_child,key);
}
void CreatBiTree(BiTree &T,int sum)
{
int key;
T = (BiTree)malloc(sizeof(struct Node));
T->key = -1;
T->parent = NULL;
T->l_child = NULL;
T->r_child = NULL;
while(sum--)
{
scanf("%d",&key);
BiTreeInsert(T,key);
}
}
void Visit(BiTree T)
{
printf("%d ",T->key);
}
void BiTreePrint(BiTree T)
{
if(T)
{
BiTreePrint(T->l_child);
Visit(T);
BiTreePrint(T->r_child);
}
}
int main()
{
BiTree T;
CreatBiTree(T);
BiTreePrint(T);
return 0;
}
我们主要说一下,二叉排序树在做题时候,一般会怎么考:
1.二叉排序树的插入
排序二叉树插入结点的过程是:若原二叉排序树为空,则直接插入节点,否则,若关键字k小于根结点关键字,则插入到左子树中,若关键字k大于根结点关键字,则插入到右子树中
举个栗子
1.插入的新结点一定是叶结点
2.待插入的序列顺序不一样,最后二叉树的结果也不一样(如果待插入序列是32,47,48,49,51,54的结果是什么?)。
2.二叉排序树的删除
如果要把二叉排序树中的某个结点删除了,我们肯定得找个结点补在他的位置,不然就是两个二叉排序树了,而补完以后,一定不能破坏二叉排序树(L
1.删除结点是叶子结点,就直接删除。
2.只有一颗左子树或者右子树,把左/右子树放在原来位置就好
3.有左右两棵子树:
a.我们可以找比它小一点点的那个结点,把这个结点放在原位置
小一点点的意思是,如果你要删除49,小一点点的就是48(47小太多了),那么如何找到小一点点的那个结点?我们知道其左子树都是比它小的,我们需要找到左子树里面最大的,而左子树里面,我们只要一直找右结点,找到最后一个就是他最大的(也就是他的前驱结点)!
b.我们可以找比它大一点点的那个结点,把这个结点放在原来位置
同a一样的思路,我们找右子树里面最左面的结点(也就是他的后继结点)。
c.为什么要找小一点点的(大一点点的)而不能小(大)太多?
因为,如果小很多的话,比如我们找了47,那么48就在47的左子树里面了,这是不允许的。
3.二叉排序树的查找效率的分析
查找效率也就是我们要查找的一个结点,成功的查找长度是多少,失败的查找是多少?
a.成功的查找长度
成功意思就是待查找的数字肯定在二叉排序树里面,如果是54的话,我们需要查找1次,如果是32--2次,47-2次,49-3次,48-4次 ,51-4次
所以平均查找长度就是:
而我们每查找一次,就会往下走一层,所以结点查找的长度就是结点的深度,或者是所属层数,所以公式也可以是:
就是结点
所属的层数,说白了,把每个结点的层数一加然后除以结点个数。
那么如果待插入序列是32,47,48,49,51,54的结果是什么?
b.失败的查找长度
失败就是待查找的结点不在二叉排序树里面,而我们的排序二叉树,只有查找到要找的结点或者查找NULL结点才会罢休,查找到结点这个想法是不可能了,因为我们求得是失败,所以我们只能查找到空结点。
这里面,32有两个空结点,47有一个空结点,所以一共有3个层为3的空结点,但是我们前面乘以了2,不是3,2的意思就是我们在二叉排序树里面找了几次结点(有的书上说应该乘以3,有的是乘以2,这里我们以乘以2为主)。48和51各有两个空结点,所以是4*4.
哈夫曼树的提出主要是用在密码学,压缩存储,数据传输方面,英文有26个字母,每个字母使用的频率是不一样的,比如s,a这些字母使用频率就很高,而x这种字母频率就很低。如果我们给每个字母编码,每个字母编码长度都是一样的,这样的编码其实不是最高效的。我们一般采用变长编码,如果我们能给s编码为0,a编码为01, x编码为000101。这样使用频率越高,编码越短,会增加传输效率。比如我们有001010101---我们就可以翻译为sax,至于为什么可以,后面再说。
定义书上有,这里说下怎么构建:假设我们现在有一个文本,文本里面字母出现频率(也就是权重)如下
a ---30, s---31, b--20, c---10, x ----3, q ----7, d----6
首先排个序:
WPL = (30 + 31 +20)*2 + 10*3 + 7*4 + (3+6)*5
编码:a:00 s:01 b:10 c:110 q:1110 x:11110 d:11111
为什么没有一个码是其他码的前缀?
1.x和d的顺序可以交换,我习惯小的放左面~所以哈夫曼编码的树结构可以不一样
2.哈夫曼的总结点数为多少?
3.每一个原始结点在树中都是什么结点
4.哈夫曼中有没有度为1的结点?