一、树的基本概念
分支结点(非终端结点);叶子节点(终端节点)
树是一种递归定义的数据结构。
树的每棵子树之间互不相交。
eg电脑里的文件系统 使用到树。
祖先节点 从该结点到根节点路径上的 多个结点
子孙结点
双亲结点 孩子结点 兄弟结点
有序树(例如族谱树:同一行要按出生顺序从左往右写);无序树;
树的边都是有向边,只能从上往下。
两个结点之间的路径:只能从上往下。
路径长度:经过几条边。
森林
★常考,易错!
树的度:树中结点的度的最大值。(树的度=孩子最多的那个结点的度)
常考题型:度为4的树,给出度分别为4、3、2、1的结点个数,求叶子结点个数。
原理:
1)度为4的树,其结点的度的最大值为4:只有度为4、3、2、1、0的结点。
2)树的结点数量=所有结点的度之和+1(除根结点以外的结点数量+1个根结点)
步骤:
1)求出所有结点的度之和=20×4+10×3+1×2+10×1=122
2)求出树的结点数量=所有结点的度之和+1=123个
3)已知除叶子结点以外,有20+10+1+10=41个非叶子结点,所以叶子结点个数=123-41=82个
树的路径长度是指:树根到每个结点的路径长的总和。
树的常考性质
二、二叉树的概念
二叉树可以是 1)空二叉树 2)根结点和两个互不相交的左子树和右子树。左子树和右子树又分别是一棵二叉树。
二叉树中每个结点至多有两棵子树。
★注意:左右子树不能颠倒。二叉树是有序树。
★区别二叉树和度为2的树:
度为2的树——至少有一个结点有两棵子树。
二叉树——每个结点至多有两棵子树。
特殊的二叉树
1)满二叉树
根结点编号1,按曾依次往下编号,层数越高,结点的编号越大。
2)完全二叉树
每个结点的编号都与满二叉树对应。
3)二叉排序树
4)平衡二叉树
二叉树的性质:
●二叉树n0=n2+1
●二叉树第i层至多有几个结点?
●高度为h的二叉树至多有几个结点?
完全二叉树的性质
●具有n个结点的完全二叉树,高度h为多少?
●完全二叉树最多只有一个度为1的结点,n1=0 /n1=1。
对于完全二叉树,可以由结点总数n推出度为0、1、2的结点个数。
小结:
二叉树的存储结构
顺序存储:定长数组,从上至下,从左至右依次存储各个结点。
二叉树的顺序存储结构只适合存储完全二叉树。实际应用中很少用顺序存储方式存储一棵二叉树。
如果不是完全二叉树,将不能进行顺序存储——无法从结点编号反映出结点间的逻辑关系。
如何找结点i 的左、右孩子、父结点的编号?
如何判断结点i是否为叶子结点?
顺序存储的最坏情况:浪费许多空间
链式存储——二叉链表。二叉树的每一个结点BiTNode有一个data域和两个指针:*lchild、*rchild分别指向它的左孩子、右孩子。
按理说一棵二叉树共有2n个指针域。
除了根结点以外,剩下n-1个结点 每个结点头上都连着一个指针。即:n个结点的二叉树有n-1个指针域非空。
★n个结点的二叉链表共有2n-(n-1)= n+1个空指针域。→可以利用起来构造线索二叉树。
二叉树的链式存储(二叉链表)构建过程:
1、定义一棵空树:
2、用malloc函数申请一个根结点,在根结点中存入数字1,根结点的左右指针此时指向NULL
3、用同样方式malloc申请一个新的结点,在结点中存入数字2,结点的左右指针此时指向NULL。
然后将根结点的左孩子指针lchild指向当前结点P。
4、再用同样的方法插入其他的结点。
二叉树的链式存储(二叉链表):
找到指定结点P的左/右孩子:很简单——看指针指向哪
找到指定结点P的父节点:麻烦,只能从根结点root开始遍历寻找——看哪个结点的左/右孩子指针指向P结点
如果应用场景中经常需要逆向找到父结点,可以在结构体中再定义一个指针:父结点指针*parent,指向它的父节点(每个结点总共有三个指针,称为“三叉链表”)
三、二叉树的遍历、线索二叉树
按理说一棵二叉树共有2n个指针域。
★除了根结点以外,剩下n-1个结点 每个结点头上都连着一个指针。
即:n个结点的二叉树有n-1个指针域非空。
★n个结点的二叉链表共有2n-(n-1)= n+1个空指针域。→可以利用起来构造线索二叉树。
线索二叉树分为三种类型:中序线索二叉树、先序线索二叉树、后续线索二叉树。
线索二叉树的结点定义和二叉树的结点有什么区别?数据结构不同,线索二叉树的结点多了两个标志域ltag和rtag,当tag=0指向孩子,当tag=1表示线索指向前驱/后继。
注意:初始化ltag=rtag=0(表示此时指针指向的都是孩子,并没有线索化)。第一个遍历的结点没有(中序/先序/后序)前驱,令lchild指针域为前驱线索指向NULL;最后一个遍历的结点没有(中序/先序/后序)后继,令rchild指针域为后继线索指向NULL。
考题:手算将二叉树画成线索二叉树。
步骤1、写出相应遍历序列(中序/先序/后序) ,给二叉树的各个结点标上它们被访问的次序(序号从1开始)
2、确定各个结点前驱和后继的关系,将n+1个空链域连上前驱和后继。(另外n-1个非空指针域不用管,它们已经连上结点的左/右孩子)
将二叉树的某一结点线索化:就是把结点的空链域(左孩子指针or右孩子指针)连上它的前驱or后继。(记得将ltag/rtag设为1表示线索)
建立线索的目的:为了更方便的从一个结点开始找到它的前驱/后继结点(根据中序/先序/后序序列)。
机算代码
eg中序线索化(机算)
上面代码都跑完以后,最后指针pre和指针q会都指向最后一个被遍历的结点C。此时C的右rtag=0还未线索化。还要加上特别处理 if(pre->rchild==NULL){pre->rtag=1;}
eg先序线索化(机算)
注意避免爱的魔力转圈圈问题——在visit完以后,通过增加判断条件ltag是否等于0,来决定是否需要遍历该结点的左子树
eg后序线索化(机算)
小结
在线索二叉树中怎么找前驱/后继
优化:利用线索,非递归的实现二叉树中序遍历,空间复杂度O(1)
for循环
1初始化,给循环变量赋初值:找到根结点为p的子树的最左下结点(作为中序遍历的开始结点)
2循环条件,条件成立时执行中间循环体:结点!=NULL
这里的中间循环体是:访问该结点 visit(p)
(执行的中间循环体可以为一个语句,也可以为多个语句,当中间循环体只有一个语句时,其大括号{}可以省略,执行完中间循环体后接着执行末尾循环体。)
3末尾循环体,每执行完一次中间循环体以后执行:找到线索二叉树中p结点的后继
(执行末尾循环体后将再次进行条件判断,若条件还成立,则继续重复上述循环,当条件不成立时则跳出当下for循环。)