本节手打5000字,对你有帮助的话希望可以点个赞。
1 所有树的基本概念
根、子树、有序树、森林、父亲、儿子、度、次数、叶子结点、树的深度、树的高度
2 二叉树定义
从p100到p102的引理5.1,5.2,5.3,5.4,5.5,定义5.3,5.4都需要看一遍,不需要刻意背诵。其中重点是完全二叉树和满二叉树定义,还有引理5.4便于理解接下来的顺序存储。
3 顺序存储
下标为1为根结点。对于每个结点的下标,*2是他的左儿子下标,*2+1是他的右儿子下标。即使在代码中用数组表示也不要每个下标向前移一位,数组0下标空间不使用即可。
4 链接存储
链接存储这里可以提到可以在每个里加一个父亲指针,便于向根节点前进。这让我想到了,二叉树的很多小改动可以增加很多其他功能,我可能会在文章的后续提到这些功能的实现以及在我编写代码时的一些思路。
5 二叉树的建立
贴一个我当年写代码的习惯,我记得当时很多人学书上一样通过返回指针来建立二叉树,我忘记那种详细的书写步骤了,但是这种书写步骤不好进行细节的调整,也很难理解,会出问题。
//输入为一组用空格间隔的整数,表示带空指针信息的二叉树先根序列。其中空指针信息用0表示。
typedef struct Node{
int num;
struct Node* left;
struct Node* right;
}node;
node* root; //全局变量防止未来某些函数中需要用到根
void build(node* i){
int x;
scanf("%d",&x);
if(x){
node *p=new node;
p->num=x;
i->left=p;
build(i->left);
}
else i->left=NULL;
scanf("%d",&x);
if(x){
node *p=new node;
p->num=x;
i->right=p;
build(i->right);
}
else i->right=NULL;
}
int main(){
root=new node;
scanf("%d",&root->num);
build(root);
}
6 二叉树的遍历
6.1 递归遍历
二叉树的遍历主要是要自己理解想通即可,代码书写反而比较简单。一定记住顺序里左右是指,遍历完整个左子树和右子树。
你能理解这个例子就差不多了:
先根(根左右):FCADBEHGM
中根(左根右):ACBDFHEMG
后根(左右根):ABDCHMGEF
代码:
void output(node* i){
if(i->left) output(i->left); //左
if(i->right) output(i->right); //右
printf("%d ",i->num); //根
}
不管是哪种遍历,把这三句话调顺序就好了,在主函数中调用output(root)即可。
值得注意的一点是,在打代码的过程中,要将每一步的指针=null也写上,不要觉得默认就一定是null,某些编译器不会提供默认值。这点上我吃过非常多的亏。
6.2 非递归的中根遍历
先将根入栈,循环:【有新结点刚入栈,就一直入栈左儿子直到没有左儿子可以入栈。出栈一个元素,并且将其右儿子入栈(如果有右儿子)】。这里的实现是将p <- Right ( p ) (p) (p),如果无右儿子就直接p=null、跳出while、继续下一次出栈、并找右儿子即可。
学习一下数里入栈出栈的adl写法,和创造和删除结点比较相似,并且学号GOTO这种adl耍赖类型。
6.3 非递归的后根遍历
用标记记录每个结点遍历情况,0代表未遍历,1代表遍历完左子树,2代表遍历完右子树。
先将(根结点,0)入栈。
每次弹栈:
- 如果i=0,压入(p,1),如果左指针不为空,压入(Left ( p ) (p) (p),0)
- 如果i=1,压入(p,2),如果右指针不为空,压入(Right ( p ) (p) (p),0)
- 如果i=2,访问结点p
我刚开始还以为书上顺序写反了,然后才想起来堆栈是后进出,那Left ( p ) (p) (p)先弹栈然后压栈,经过一系列操作左子树遍历完,才会弹出(p,1)
6.4 利用队列层次遍历
压入根,每次弹出结点输出,并压入其结点的所有子节点。
不详谈,后续还会提到,在广度优先和深度优先的时候。
7 二叉树的其他操作
我不想说书上这些,我想说的是代码部分。
就像在如果经常有需求查找父节点的时候,可以在结构体里加一个* father指针,在已有构建代码上进行修改就可以。比如我马上想得到的,在主函数里要写root->father=null;在构建的时候要在递归前,node *p=new node;后,写上p->fatter=i; 这些都是需要预想到的调整。
再比如,如果我需要知道树的高度,那么我可以在每次传参的时候加一个int h储存当前的高度,在主函数里调用build(root, 0),在递归时使用build(p, h+1),并且使用一个全局变量max_height储存最大高度,即在每层进行max_height=max(max_height, h)操作。
如果更精细的,我对每个结点的高度有需求,我可以在结构体里开一个int height的空间储存当年高度,只要在每层p创建之后补一句p->height=h+1;即可。
这些都是可能产生的小变化,而题中会有更多可能用到树的这种小变化去存一些内容,需要写题者自行把握。
8 表达式树
8.1 中缀转后缀
将算术表达式转换成后缀表达式,书上不讲但是考试考。
规则:从左到右遍历中缀表达式的每个数字和符号,是数字就输出;是符号的话,新的符号如果不比栈顶的符号优先级更高就一直弹栈到栈空或者不满足这种情况,然后将新符号压栈。优先级为:乘 = 除 > 加 = 减,只要遇到右括号就一直弹栈到左括号。
举例: a + (b - c) * (d + e) / f
a 栈中:
a 栈中:+
a 栈中:+ (
a b 栈中:+ (
a b 栈中:+ ( -
a b c 栈中:+ ( -
a b c 栈中:+ ( -
a b c - 栈中:+ 注:)出现,弹栈到(
a b c - 栈中:+ *
a b c - 栈中:+ * (
a b c - d 栈中:+ * (
a b c - d 栈中:+ * ( +
a b c - d e 栈中:+ * ( +
a b c - d e + 栈中:+ * 注:)出现,弹栈到(
a b c - d e + * 栈中:+ / 注:/出现,弹栈*,+优先级低于/不弹
a b c - d e + * f 栈中:+ / 注:最终结束,全部弹栈
后缀表达式为:a b c - d e + * f / +
8.2 后缀建树
把每个字母当作一个结点压进栈中,每当遇到一个运算符号就弹出两个栈顶,将其变为新运算符号的右儿子和左儿子(此左右有先后顺序),然后将新的运算符号的结点压入栈中。
8.3 如何遍历表达式树
我觉得书上那种用刚才2.5.3非递归的后根遍历过于复杂了,我认为我这种更简单,因为不会存在运算符的左右儿子为空,或者数字有左右儿子的情况存在。
double output(node* i){
if(这个结点是数字) return 存的数字;
else return output(i->left)运算符output(i->right);
}
8.4 有关全程的代码
如何用代码来表示这全部的过程:
用两个栈,一个是符号栈,用这个栈从中序表达式变为后续表达式;一个是后缀栈,用这个栈从后缀表达式变为表达式树。
符号栈读取中序表达式,如果是数字就压入后缀栈,如果是字符就压入字符栈,当有字符从字符栈弹出的时候,就从后缀弹出两个结点作为左右进行建树,然后将其根节点压入后缀栈中。
9 线索二叉树
我上网上查只看见csdn上有一个帖子说了一嘴线索二叉树可能的作用
当路由器使用CIDR,选择下一跳的时候,或者转发分组的时候,通常会用最长前缀匹配(最佳匹配)来得到路由表的一行数据,为了更加有效的查找最长前缀匹配,使用了一种层次的数据结构中,通常使用的数据结构为二叉线索。
虽然我不理解,但是除此之外我也没体会到任何线索二叉树的作用,先学着玩吧。
幸运的是,线索二叉树考察的难度有限。
9.1 线索二叉树的概念
每个结点里多一个LThread和RThread,只占一个二进制位,每个里存储这个结点是否有左儿子/右儿子,如果有为0,没有为1,没有的时候对应左/右指针指向它的前驱结点/后继结点,它的前驱结点和后继结点是什么一般是根据它是如何遍历的线索二叉树决定的,比如书上喜欢举的例子就是中根遍历(中序)线索二叉树。
9.2 线索二叉树的操作
9.2.1 查找中根序的第一个和最后一个结点
第一个结点就是从根开始左儿子直到没有左儿子,最后一个结点就是从根开始右儿子直到没有右儿子。我请问,这和线索二叉树有什么关系。
9.2.2 查找中序后继结点和中序前驱结点
后继:如果RThread=1,则Right§指向p后继;如果RThread=0,则找它右儿子的无限左儿子直到没有左儿子。
前驱:如果LThread=1,则Left§只想p前驱;如果LThread=0,则找他的右父亲直到没有右父亲,那个结点的左父亲就是前驱(可能是null)。这里右父亲,是指其为其父亲的左儿子,理解即可。
9.2.3 说明
别学了,这都是我现学现写的,当年都没学过这么多,为了多写点笔记写没用的没必要。
10 压缩与哈夫曼树
10.1 哈夫曼算法
哈夫曼是为了压缩储存内存,给已有字符编码,需要做到表示每个字符的编码不同,且没有任何一个字符的编码是另一个字符的编码的前缀,在这个条件下要找到Σ i = 1 n c i l i ^n_{i=1}c_il_i i=1ncili最小的,其中 c c c为出现次数, l l l为编码长度。
10.2 定义
扩充二叉树、外通路长度、内通路长度、最优二叉树
都是看看就可以,感觉重要性一般。
10.3 哈夫曼树
每次都选择两个值最小的结点,作为一个新结点的左儿子和右儿子,新结点的值就是左儿子的值+右儿子的值,如此重复直到最后变为一棵树。
哈夫曼树的左儿子为一位0,右儿子为一位1,最后一条路上所有的位数连起来就成为该外结点的编码值。
10.4 有关代码
这就体现出adl的强大之处,一句“把新组合结点t插入数组H中,使得Weight(H[i+1])≤Weight(H[m])”,我当年复现了两天,不过应该还是结构设计的复杂了,建议多去csdn搜搜有关代码教程,自己硬扒一遍,上机爱考。
11 树的存储和操作
树和二叉树转换:二叉树时左原儿子右原兄弟,简称左儿子右兄弟。
树和森林转换:根之间当成兄弟。
12 并查集
并查集主要的目的时,判断两个元素是否在同一集合中,那我把一个集合构造成一棵树,然后找两个元素的根是否为同一个结点,如果是的话那就在同一个集合中。值得注意的是,每个树的根的父亲是它自己。
祭出我的并查集模板:
int find(int x){
//只有根节点的a[x]==x,这个递归在根节点结束,return 根节点的下标
//不结束的时候a[x]=find(a[x])是将路径上的每个点都更新为a[x]=根节点的下标
if(x!=a[x]) a[x]=find(a[x]);
return a[x];
}
教材里的应该没有更新这一步,代码应该为:
int find(int x){
if(x!=a[x]) return find(a[x]); //如果不是根节点,就返回其find到的根
return a[x]; //是根节点就返回其下标,这里加不加else没区别
}
我这里是使用数组a储存父亲结点的下标是多少,结点也是按数组存储的而不是链表,如果不能理解,那就看如下代码:
node* find(node* x){
if(x!=x->father) return find(x->father);
return x;
}
合并两个集合,就是将一个树的根结点认另一个树的根节点做父亲即可。
void Union(node *x, node *y){
find(x)->father=find(y);
}
13 决策树
这个我一直到学机器学习才学到这个,好像不必在这学。