【数据结构】二叉树

 树的作用

静态查找之顺序查找

结构中存着一个数组以及一个记录数组大小的变量

技巧:哨兵

在设计程序的过程中,循环终止条件为找到对应的值或者达到数组边界,把数组中的最后一位 设置成与目标数值一样,就能如上上图所示,少写一行判断代码,返回下标,如果是数组边界,那就没有找到目标数值,否则反之。

静态查找之二分查找

前提条件

假设有10000个数,二分查找次数:log2的10000

 

其中函数形参第一个是指向一个结构的指针,指针中包含一个数组以及数组长度,第二个参数是要寻找的目标数。由于数组的起始点0在上面用来存放了哨兵数值,所以这里是从1开始计算。

动态查找之判定树

 ASL指的是每一层有多少个节点的个数乘以每个节点成功查找次数的深度,要按照一定的顺序排列之后,原理类似于二分查找,复杂度就是log2的n。

树的定义

每个子树也有他自己的根,比如B节点,C节点等,子树应该是互不相交的有限集,所以不能够相交。

树的基本术语

 

树的表示方法

表示这个树,可以使用结构加链表的方式,但是每一个节点的指针域不同,不方便处理,所以可以直接让每个节点的指针域相同,这里就是设置成3(因为最大就是3个,按照最大的来),然后这里有3n个指针域,但是有n-1根线,有效的指针域是n-1,所以2n+1个指针域空的,造成空间浪费。 所以使用下面这种:

也就是一个指针指向儿子节点,一个指针指向兄弟节点 ,所以一共就有2n个指针域,有效指针域是n-1(就是线的数目),无效指针是n+1,大大改善了。度为2的树叫做二叉树。

二叉树性质

一般的度为2的树子树是没有左右之分的。

 缺右边的一部分可以,中间缺了不行。

二叉树重要性质 

 

其中叶节点指的是度为0的节点,度为2的非叶节点,意思是B这种有两个子树的这种。

二叉树的抽象数据类型定义

 二叉树的存储结构之顺序存储

数组表示----顺序存储

(完全二叉树比较方便,不会造成空间浪费)

给每一个节点按照从上到下,从左到右的顺序编号,按照数组的索引排列好。【】是取整的意思,由于数组是顺序存储的,很难直接看出某节点的父节点,但是可以找到规律。如下:

 

一般二叉树的话,需要 把空缺的节点补全成为一个完全二叉树,才能够用顺序存储的办法来表示二叉树,但是会造成大量的空间浪费。

 二叉树的存储结构之链表存储

 left跟right都是指向结构体的指针,里面存储着数据以及另外两个指向结构体的指针。

二叉树的遍历(链式存储为主)

BT是指向根的指针

 注意看下图,按照入口以及出口的路线走,哪一种符号先出来,就是该序的打印路线,跟着线走,碰到的符号就是打印的顺序,对于某些节点来说,第一次碰到就是前序,第二次就是中序,第三次就是后序,有一些是连着碰到的。

 二叉树中序非递归遍历

也就是用堆栈实现

 

根据路线,然后根据中序遍历的规则,初始节点开始,然后左子树到右子树,遇到一个节点就把一个节点push入栈中,走到尽头需要回头就把栈最上面的节点数据pop出去,popD之后又popB,然后访问B右子树,一直到E,然后popE,没有右子树就popF,又没有右指数,就把堆栈最后的一个节点POP出去。

T是一个指向结构的指针。 大循环的条件是二叉树非空,或者堆栈非空。if那一段的意思是,如果栈非空,节点弹出,打印数据,然后指针指向该节点的右子树,注意D点是0,然后又弹出一盒节点,指针指向该节点,打印,然后转向右子树,如此。

无论是先序后序还是中序,它的路线是一样的,区别只是在哪一个节点进行操作而已,这里进行的是打印操作,所以只要改变操作位置就行,因为前序的顺序是根节点,左子树,右子树,所以按照这个顺序,把打印操作移动到压入堆栈后面。push是第一次碰到节点,pop是第二次碰到节点,先序,就要在第一碰到的时候进行操作。 

至于后序,个人觉得是改变if的条件,改为当>一个节点的时候进行,因为根节点是第一个push进去的,最后左右子树搞定之后,再pop根节点。

下面是非递归后序遍历:

后序就是先左子树,然后右子树,最后再根节点。

下面代码的核心思路是:向左压入堆栈,之后回退但是不pop,检查有没有右子树,如果有,就则率先执行,然后再压入执行这个右子树的左子树如果有的话。如果没有右子树,就可以pop出去了并打印,然后指针归零,防止再次把节点压入堆栈,因为while(T)存在。进入了一个新的右子树节点,才再次进行左子树查找。

void PostOrderTraversal(BinTree BT)
{
    BinTree T=BT;
    BinTree Temp=NULL;//记录访问过的结点
    Stack S=CreatStack(MaxSize);//创建并初始化堆栈S
    while(T||!IsEmpty(S))
    {
        while(T)
        {
            //一直向左并将沿途结点压入堆栈
            Push(S,T);
            T=T->Left;
        }
        if(!IsEmpty(S))
        {
            T=GetTop(S);//读取栈顶结点,非出栈
            if(T->Right&&T->Right!=Temp)
            {
                //若右子树存在,且未被访问过
                T=T->Right;//转向右子树
            }
            else
            {
                //否则弹出结点并访问
                T=Pop(S);
                printf("%5d",T->Data);//(访问)打印结点
                Temp=T;//记录最近访问过的结点
                T=NULL;//结点访问完后,重置T指针
            }
        }
    }
}

二叉树遍历的核心问题

就是二维结构的线性化,二维结构也就是二叉树,线性化指的是之前用过的先序中序后序遍历这些,以一定的顺序来访问二叉树的所有节点。

在访问过程中,假设没有用堆栈或者队列来存储节点,因为一个节点指向两个节点,左孩子以及右孩子,在访问了父节点之后,可以访问其中一个,假设访问了左孩子,若右孩子被遗忘,就永远也找不到它了,更有甚者父节点与右孩子一起被遗忘,不保存的话永远也找不到。

所以需要堆栈或者队列来保存,堆栈保存的是父节点,队列保存的是未被访问的节点。

层序遍历

 

例子 

 

 根据根左右,左右根这个顺序确定分别是什么。

先序序列的初始是根节点,在中序中找到相同的根节点位置,那么在他左边就是左子树,右边就是右子树,然后回过来找到先序序列的左子树,也就是包含中序序列左子树的节点名称的一群数,然后先序序列左子树的第一个就是左子树自身的根节点,然后又根据这个根节点在中序序列的左子树中找到左子树以及右子树,再回到中序序列中找到子树的根节点,如此循环往复。 

 判断二叉树是否同构

 

 根据输入得出二叉树并判断是否同构

 后面两个数分别是节点的两个孩子节点。而且编号也不一定从根节点开始,可以从任何节点开始。难点之一就是找到根节点,方法就是用后面跟着的孩子节点的编号减去所有的编号,剩余的那个就是根节点。

 

1.二叉树表示

二叉树表示方法:可以用数组也可以用链表

 下面是数组的表示方法,要把一般二叉树补全为完全二叉树,然后进行编号。

 静态链表下图所示,他有链表的灵活性,像链表一样除了物理上数据还会有一个数据存储左孩子以及右孩子在哪里。但是它的存储又在数组上,这个成为静态链表。

空指针为什么用-1表示。在stdio.h中定义了NULL是0,在数组中0是下标,为了区分两者,所以不能用NULL(0),只能用自己定义的Null(-1)。

上图定义了两个数组,每个元素都是像上面一样的结构。 

同一个树在静态链表上面的表示可能有所不同,ABCD在数组的位置可以随便换。

 寻找根的话,就可以找到存在left与right的编号,跟自己设置的编号做减法,剩余的编号就是根。

其中01234是数组的下标。

2.二叉树的建立

 右上角的就是这个函数的输入,先输入的是节点的个数,后面就是各个节点的信息。每轮循环读一行数据,为了处理方便,读进来的时候都以字符的形式读进来,Element直接放到结构里面,而结构里面的left以及right需要经过读入的字符与‘0’相减得到证书之后再赋值。返回值就是树根,写一部就是确定树根位置:可以根据把所有的节点从头到尾扫描一遍,检查有没有节点没有其他节点指向它,那它就是根节点。下文的实现就是,用一个数组,把所有的节点都设为0,然后检查没一个节点是都有左孩子以及右孩子,如果有,把它所在的位置设置成1,上面的红字01234567就代表的是他们的索引,最后再遍历一遍,看哪个i不等于1,该i,就是1根节点所在的数组索引。

判别方法就是是不是‘-’。

 3.判别两树是否同构的代码实现

 

T1是一个数组,T1[R1]代表数组的一个元素,每一个元素就是一个结构,每一个结构就是一个节点。R1R2接手的是刚刚建立好的两棵树的返回值Root,也就是i,根的索引。

两个都是空子树;

一个空一个不空;

两个根不一样;

两个树的左子树都是空的,那就接着判别右子树,以这样的方法。

下图是接着上面的:

 左边同时不空并且element根一样,就判断左边是否相等,右边是否相等。

否则就判断是否一个的左边跟另一个右边相等,右边跟左边相等。

二叉搜索树之查找

静态查找就是没有节点的插入删除,静态查找一般用二分查找,实现把它有序化,形成了一种判定树的结构,复杂度降到了log2N,查找效率就是树的高度。得到启发,能不能不放在数组上面实现这种查找,而是放在树上,这样动态性更加强,插入删除比在线性做的要方便。 

 在程序要返回的时候才出现的递归叫做尾递归,一般尾递归都可以用循环来代替,提高执行的效率。

 

 二叉搜索树之插入

 

35与30比较,大了往右走,再与41比,小了左走,再与33比,大了右走,是空的,就开辟一个新的空间,把数据以及指针读进去。如下图的if(!BST部分)而else后面的部分就是一步一步走到最后这个位置的过程,BST一开始指在30的,小就进入左递归,大则右,每次递归都要返回给上一节点下一节点的地址进入递归的时候,传给函数的BST是上一BST指向的结构体里面的某个指针,一次实现指针的递推。

当BST指向33的时候,进入递归,把33中的右指针(也就是空指针)传给函数,开拓空间,然后赋值,然后返回BST,BST此时指向35,把BST返回给上一节点的某一个指针域,一直返回,让新建立的节点与上一节点建立联系(前面还没建立联系,只是创建了这个东西,其余已经联系在一起的,再存一次也无所谓)

月份按照单词字母的顺序用二叉树的顺序排列。 每一次输入都要从jan开始比较走到它应该去的位置,先与跟比较然后一步步。

二叉搜索树的删除

前面还要经过一个查找的过程,从30开始一直到35,注意。

  

 直接把爷爷节点的指针指向孙子,不要父节点了,即可。

 右子树的最小值在右子树的最左边以及左子树的最大值一定在左子树最右边,不可能存在两个儿子,就相当于把删除有两个儿子的节点转换成了一个儿子的节点甚至没有儿子的节点。

平衡二叉树 

 按照(a)(b)(c)后面的顺序输入,但是匹配位置的时候,就是按照字典顺序比较大小

 ASL侧面反映了查找的效率,公式是深度乘以每一层数量再除以总数。相比之下中间的查找效率更高,特点是左右子树比较平衡,衡量的指标是左右的节点数差不多或者说是左右子树差不多不超过1。

 

高度指的是路径中最长路径边的个数。 

 

平衡二叉树的调整 

平衡二叉树也可以进行插入删除什么的,但是会打破平衡,怎么办。

 旋转之后,需要重新调整B上挂着的东西,按照左边小右边大来重新排列!所以BL移动到了A的右边。发现者指的是子树平衡因子超过或者等于-2的节点,麻烦节点指的是引起这个变化的节点。

整块扔到右边,替换掉第一种情况那部分。

虽然有两个都到达2了,但是只看下面那部分,下面平衡了上面自然就平衡了。

把发现者的左边节点转变为父亲节点,被破坏者作为右儿子。 BR满足左边小右边大,插在

A处。

只看左上角那红圈那三个,要转成右下角这种三足鼎立的形式,根据左边小右边大的原理进行。

-------------------

例题

插在左子树的右子树上,要进行左右旋转,连着的MAR,DEC,以及Jan三个节点转化,变成下图:(oct后续插入)

右右旋转 ,变成下图:(sept后续插入),插入后没破坏平衡,结构不用变,但是平衡因子要改变

是否同一棵二叉搜索树

 

 

 

 首先会有两个整数,如例子就是4跟2

前一个4代表要比较的序列构成的树有多少个节点,2表示有多少组序列需要比较,这里是3142跟后面的3412和3241比较。

不建树:就是找到根节点,然后把比根节点小的按顺序放在左边,根节点大的按顺序放在右边,再以此方法比较子树即可。

 

 

 

 

 循环就是遍历每一行,judge读入每一行的N个数,然后与原本构建好了的树进行比较。

在比较过程中会用到flag表示有没有被使用过,reset就是把flag重置

然后释放掉树,进行下一个比较(是下一大组的比较),scanf读入下一组的N数据,进行构建下一组的比较。

以上是主函数的思路,现在看具体函数的实现。

思路是:节点数传给maketree函数,再读入每一个序列的数值,也就是3 4 1 2这样一个个读进去,用newnode函数创建新的根节点,并且在newnode函数把数据与指针这些放进去,然后利用循环简历新的节点并且读进去,然后利用insert递归把节点连接起来。

判别序列跟树是否一致

 

标志flag作为标志

 

在check这个函数中,tree树种查找V这个整数

如果flag等于1,表示查找过了,就到这个节点的左边或者右边查找

如果相等,说明序列中的一个整数出现了两次以上,就是重复的出现了,也认为不一致的,return 0

至于else那部分,说的就是flag等于0,也就是没有被查找过的节点,就比较V以及节点的数据,如果一样,就把flag置1,返回1,否则返回0.

 不读完进入下一个序列的时候,会把剩下的整数误读

改进,

增加一个flag,不过这个flag是红色那个flag,判断的是整个序列跟树一不一样,而T->flag表示的是已经被查找过了的节点。后面的if语句,如果红色的flag已经为1了,就是整个序列已经不符合二叉树了,就不用正常的进行check,直接跟着循环把数读完就行了。最后判断是否一致然后返回。

查找完成之后,就进行清除标记以及释放树的空间

 

 即便没有return,执行完下一个递归之后,还是会返回上一个递归。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值