二叉树的二叉链表表示与基本操作

二叉树的二叉链表表示与基本操作(伪代码详细注释版,含源码)

  • 一、二叉链表表示法
      • 1. 二叉树
      • 2. 二叉链表
  • 二、二叉树的遍历(输出结点)
      • 1. 层次遍历
      • 2. 先序、中序、后序遍历(递归算法)
      • 3. 先序、中序、后序遍历(非递归算法)
  • 三、创建一棵二叉树(依次输入结点信息)
  • 四、二叉树的深度/高度
  • 五、二叉树的拷贝
  • 六、二叉树的判等
  • 七、二叉树同构的判定
  • 八、完全二叉树与满二叉树的判定
      • 1. 概念
      • 2. 联系
      • 3. 算法
  • 九、二叉排序树 / 二叉搜索树
      • 1. 概念
      • 2. 查找结点
      • 3. 建立一棵二叉排序树(结点插入)
      • 4. 二叉排序树结点删除
  • 十、总结


PS:请在浏览本篇文章之前,先看下上面的大标题有没有自己想要的内容,如果没有,赶紧去搜索别人的吧,别浪费了时间。

PS:本篇只讲二叉树的二叉链表表示,其他表示法这里不说,而且里面没有写动态插入、删除结点的操作,建立一棵二叉树后,这棵二叉树就不动了,后续都是基于这个已经确定了的二叉树而进行的操作,没有增删改的部分,有这部分需求的,也请去搜索别的内容。


一、二叉链表表示法

1. 二叉树

所谓二叉树,就是指树中每个结点最多含有两个孩子结点的树,对以任意一个结点,它要么没有孩子、要么有一个或两个,但不能超过两个。注意,一棵空树也满足二叉树的定义。

2. 二叉链表

对于二叉树来讲,二叉链表表示法是我认为最生动形象,最容易理解,以及在操作上最简洁的表示方法。它基于链表,同样有数据域Data,但是有两个指针,一个指向他的左孩子Left,另一个指向它的右孩子Right.

对于一个二叉链表,其每个结点由三部分组成
① Data域,存放结点信息;
② Left指针(有些教材用lchild,一样),指向其左孩子结点,其指针类型即是结点类型;
③ Right指针,指向其右孩子结点,其指针类型即是结点类型;
如下图所示:

Left Data Right

那么,一棵树用二叉链表可以表示为:
二叉树的二叉链表表示与基本操作_第1张图片

下面,我们写出二叉链表的数据结构定义(C/C++)

typedef char ElementType;  //这里定义树的结点值为字符型,方便赋值 A、B、C……可以根据需要改成int或其他
typedef struct TNode  //定义结构体
{
    ElementType Data;  //数据域
    struct TNode *left, *right;  //左右子树的链域,分别指向一个TNode类型结点
}TNode, *BiTree;  //定义根节点类型TNode和二叉树类型BiTree,便于区分和理解

这里,我们为结构体类型进行了重命名,TNode表示一个二叉树结点BiTree代表整棵二叉树,它可以被理解为是一个指向二叉树根结点的指针,BiTree等价于LNode *,只是我们为了方便区分树和结点这样定义。

二叉链表特殊的地方在于,它跟单链表一样,都是只能从根结点往下进行操作,而无法从下到上,因此它是单方向的。而它的优势在于,其左右指针的地位相同,因此对其左右子树的操作不用单独讨论。

二、二叉树的遍历(输出结点)

为什么这里先讲遍历而不是创建呢?是因为创建二叉树的典型办法都是以遍历的思想为前提而一个个对结点赋值的过程。

1. 层次遍历

顾名思义,层次遍历的思想就是按我们写字的方向,从左到右、从上到下逐行输出结点信息,如下图:
二叉树的二叉链表表示与基本操作_第2张图片

因此,对上图的树,层次遍历结果就是 A B C D E F G H I J K L.

那我们在实现的时候,应该怎么考虑?当然是借助队列实现!

  1. 我们首先把根结点入队,作为起始状态。
  2. 只要队列不空,就出队一个结点,每出队就访问。同时,按照“先左孩子、后右孩子”的顺序分别将其左孩子、右孩子入队(如果有,没有的话直接忽略,继续出队)。
    ps:因为层次遍历要求从左到右,故先入队左孩子,再右孩子,这样保证出队访问的顺序也一定是先左后右,别忘了队列可是FIFO特性。
  3. 执行以上操作,直到队列空。

有了以上思路,我们以上图中的树进行模拟:

① 入队A,此时队列为 | A |;
② 出队A,并访问,同时入队B、C,此时队列中为 | B | C |;
③ 出队B,并访问,同时入队D、E,此时队列中为 | C | D | E |;
④ 出队C,并访问,同时入队F、G,此时队列中为 | D | E | F | G |;
⑤ 出队D,并访问,同时入队H、I,此时队列中为 | E | F | G | H | I |;
⑥ 出队E,并访问,此时队列中为 | F | G | H | I |;
⑦ 出队F,并访问,同时入队J、K,此时队列中为 | G | H | I | J | K |;
⑧ 出队G,并访问,同时入队L,此时队列中为 | H | I | J | K | L|;
⑨ 依次出队H、I、J、K、L,队列空,结束。

得到的出队序列 A B C D E F G H I J K L 即遍历结果。

下面给出先序遍历算法的「伪代码」,详细「源代码」请参见文末链接。其中为了简便,使用了顺序队列,但入队出队操作做了优化,这里建议使用循环队列

/* 层次遍历算法 */
void Level_Traversal(BiTree T)
{
    if(!T)return;  //空树不遍历,直接return
    SqQueue Q;  //定义一个队列
    TNode *temp;  //临时操作结点
    EnQueue(Q, T);  //先入队根结点
    while(!EmptyQueue(Q))  //只要队列不空,就一直操作
    {
        DeQueue(Q, temp);  //出队后接着访问
		visit(temp);
        if(temp->left)  //入队左孩子(若有)
            EnQueue(Q, temp->left);
        if(temp->right)  //入队右孩子(若有)
            EnQueue(Q, temp->right);
    }
}

2. 先序、中序、后序遍历(递归算法)

  1. 先序遍历是按照“根结点、左子树、右子树”的顺序,对于每个子树,也依然按照“根结点、左子树、右子树”的顺序遍历;
  2. 中序遍历是按照“左子树、根结点、右子树”的顺序,对于每个子树,也依然按照“左子树、根结点、右子树”的顺序遍历;
  3. 后序遍历是按照“左子树、右子树、根结点”的顺序,对于每个子树,也依然按照“左子树、右子树、根结点”的顺序遍历。

注意,不要把左右子树理解成左右孩子,子树≠孩子!只有当子树只剩下一个结点的时候,这时才可以把子树当成一个孩子结点看待,访问该结点即可。
二叉树的二叉链表表示与基本操作_第3张图片
我们依然以上图做参照,以中序遍历为例,详细说一下遍历过程:

① 以A为根结点,先遍历其左子树;
② 左子树以B为根结点,那么也要先遍历B的左子树;
③ B的左子树以D为根节点,先遍历D的左子树;
④ D的左子树只剩下一个结点H,访问H,H是我们第一个访问的结点;
⑤ 之后访问根节点D,D是我们访问的第二个根节点;
⑥ 按照“左、根、右”的顺序,现在应该遍历D的右子树,即只有一个结点I,访问I;
⑦ 这个时候,对于结点B,其左子树已经遍历完毕,现在访问根节点,即B自己;
⑧ 然后遍历B的右子树,只剩一个节点E,故访问E;
⑨ 此时,对于结点A,其左子树已经遍历完毕,现在访问根节点,即A自己;
⑩ 以此类推,分别按照 J、F、K、C、G、L的顺序访问,整棵树遍历完毕。

得到访问序列 H D I B E A J F K C G L 即遍历结果。

我们用箭头画一下整个中序遍历的访问过程,如下:
二叉树的二叉链表表示与基本操作_第4张图片

对于,先序遍历和后序遍历,与中序遍历的思想一致,这里就不再详细说明,下面给出中序和后序遍历序列以供参考:
先序:A B D H I E C F J K G L;
后序:H I D E B J K F L G C A.

下面给出三种遍历方法的伪代码(递归实现):

/* 先序遍历的递归算法 */
void Preorder_Traversal(BiTree T)
{
    if(!T)return;
    visit(T);  //先访问根节点,然后分别遍历左右子树。void visit(TNode *n)函数这里自己实现,一般都是cout <Data;
    Preorder_Traversal(T->left); //这里无须再判断T->Left是否等于NULL,因为这是递归算法,递归算法的出口就是当前结点的指针为NULL时自动return
    Preorder_Traversal(T->right);
}

/* 中序遍历的递归算法 */
void Inorder_Traversal(BiTree T)
{
    if(!T)return;
    Inorder_Traversal(T->left);
    visit(T);
    Inorder_Traversal(T->right);
}

/* 后序遍历的递归算法 */
void Postorder_Traversal(BiTree T)
{
    if(!T)return;
    Postorder_Traversal(T->left);
    Postorder_Traversal(T->right);
    visit(T);
}

3. 先序、中序、后序遍历(非递归算法)

同样,使用非递归算法也能实现这三种遍历,这里我们就要借助栈来实现,其跟我们之前说的层次遍历很相似,层次遍历使用队列,而这里我们借助顺序栈。

  1. 我们首先把根结点入栈,作为起始状态。
  2. 只要栈不空,就出栈一个结点。如果是先序遍历,我们便先访问该结点,然后依次入栈其右、左孩子;如果是中序,就先入栈右孩子、再访问该结点,最后入栈左孩子;后序遍历就先入栈右孩子、再入栈左孩子,最后访问该结点。
    ps:因为栈是FILO特性,因此先入栈的后访问,故要求先入栈右孩子再入栈左孩子。
  3. 执行以上操作,直到栈空。

下面给出三种遍历的伪代码:

/* 先序遍历的非递归算法 */
void Preorder_Traversal_Non_Recursion(BiTree T)  
{
    if(!T)return;  //空树则无需遍历
    
    SqStack S = CreateStack();  //建立一个新的栈
    TNode *n = new TNode;  //出栈保存结点用
    
    Push(S, T);  //根结点入栈
    while(T && !EmptyStack(S))
    {
        Pop(S, n);  //出栈即访问
        visit(n);
        if(n->right)Push(S, n->right);  //分别入栈右、左子树
        if(n->left)Push(S, n->left);
    }
}

/* 中序遍历的非递归算法 */
void Inorder_Traversal_Non_Recursion(BiTree T)
{
    if(!T)return;
    
    SqStack S = CreateStack();  //建立一个新的栈
    TNode *n = new TNode;  //出栈保存结点用
    
    Push(S, T);  //根结点入栈
    while(T && !EmptyStack(S))
    {
        Pop(S, n);
        if(n->right)Push(S, n->right);
        visit(n);
        if(n->left)Push(S, n->left);
    }
}

/* 后序遍历的非递归算法 */
void Postorder_Traversal_Non_Recursion(BiTree T)  
{
    if(!T)return;
    
    SqStack S = CreateStack();  //建立一个新的栈
    TNode *n = new TNode;  //出栈保存结点用
    
    Push(S, T);  //根结点入栈
    while(T && !EmptyStack(S))
    {
        Pop(S, n);
        if(n->right)Push(S, n->right);
        if(n->left)Push(S, n->left);
        visit(n);
    }
}

三、创建一棵二叉树(依次输入结点信息)

创建二叉树的方法有很多种,无非就是一个结点一个结点的输入,只是输入的顺序不一样罢了。这里,我们以采用类似先序遍历的方法建立二叉树为例,当然你可以按照中序、后续的方法。

这里需要注意的一点就是,你要让程序知道,哪些结点是作为根结点来处理的,要不然你会没完没了的往下赋值。这里方法很多,本文章中介绍的方法是当检测到输入“#”号的时候,表明当前结点为空结点,如果左右孩子结点均为"#",那自然这个结点就是根结点了。
二叉树的二叉链表表示与基本操作_第5张图片

我们假设要构建一棵上图那样的二叉树,就要先“还原出”根结点——其左右孩子均为“#”,如下:
二叉树的二叉链表表示与基本操作_第6张图片
我们按照先序遍历走一遍,结果为:A、B、D、#、#、#、C、E、#、G、F、#、#
那这个遍历顺序就是我们要输入的结点信息顺序,有了这个思想,我们就可以写出伪代码了:

/* 创建一棵二叉树,并通过依次输入结点值来为之赋值 */
void Assign_BiTree(BiTree &T)  //先序顺序赋值,记住这里要加引用符&
{
    char ch;
    cin >>ch;
    switch(ch)
    {
        case '#':  //'#'结束分支深入
        {
            T = NULL;
            break;
        }
        default:
        {
            T = new TNode;  //因为是构造树,完全可以把当前的T直接开辟新空间
            T->Data = ch;
            Assign_BiTree(T->left);
            Assign_BiTree(T->right);
        }
    }
}

程序执行时,我们只需输入

A B D # # # C E # G F # #

即可。

四、二叉树的深度/高度

什么是二叉树的深度/高度,这个不再多说,很好理解,就是层数。
如下图,该树的深度/高度就是4.
二叉树的二叉链表表示与基本操作_第7张图片

整个算法的思想也很简单,就是递归。空树的深度为0,把这个设为递归函数的出口,对每个结点,分别求其左右子树的深度,并取最大值,最后要加1,因为该结点自己也算一层。

以上图为例,模拟算法如下:

① 从A开始,分别求A的左右子树深度最大值;
② 对于A的左子树,以B为根节点,分别求B的左右子树深度的最大值;
③ 对于B的左子树,只有一个结点D,故深度为1;其右子树为空,深度记为0,取最大值1,并加1 = 2,作为以B为根结点的子树深度;
④ 对于A的右子树,以C为根结点,分别求C的左右子树深度的最大值;
⑤ 对于C的左子树,以E为根结点,分别求E的左右子树深度的最大值;
⑥ 对于E的左子树,为空,记深度为0,右子树只有一个结点,记深度为1,取最大值1,并加1 = 2,作为以E为根结点的子树深度,即C的左子树深度为2;
⑦ 对于C的右子树,分别求以F的左右子树深度的最大值;
⑧ F的左右子树均为空,最大值当然为0,加1 = 1,记深度为1,即C的右子树深度为1
⑨ 取C的左右子树深度最大值2,加1 = 3,作为以C为根结点的子树的深度;
⑩ 取A的左右子树深度最大值3,加1 = 4,作为以A为根结点的子树的深度,到此,整棵树深度计算完成,为4.

下面给出伪代码

/* 返回二叉树的深度 */
int Depth_BiTree(BiTree T)
{
    if(!T)return 0;
    int ld = Depth_BiTree(T->left);
    int rd = Depth_BiTree(T->right);
    return ld > rd ? ld+1 : rd+1;  //返回ld、rd两者最大值并+1
}

五、二叉树的拷贝

由于二叉树这里是用结构体定义的,因此我们很难用“=”去复制一棵相同的二叉树,这里我们就需要写个复制二叉树的函数,之后可以利用操作符重载实现“=”的二叉树复制。

这里我们也采用递归。其实我们也可以按照二叉树构造那样,根据先序遍历(或中后序)一一为结点赋值,但看起来比较繁琐。这里采用递归的思想是对于树的每一个结点都有左子树和右子树,而且操作一模一样,因此我们只要对每个结点进行重复的操作就可以了。

具体过程就是从叶子结点开始,构造成一个小树,然后这个小树又作为上层根结点的子树,与另一个同级子树和根结点构成更大的树,以此类推,直到构成完整的二叉树,下面给出伪代码:

/* 复制一棵一样的二叉树,并返回该树 */
BiTree Copy_BiTree(BiTree T)
{
    if(!T)return NULL;
    TNode *left_branch = Copy_BiTree(T->left); //左子树(分支)
    TNode *right_branch = Copy_BiTree(T->right); //右子树(分支)
    
    BiTree T_Copy = new TNode;  
    T_Copy->Data = T->Data;  //根结点直接赋值
    T_Copy->left = left_branch;  //分别设置左右子树
    T_Copy->right = right_branch;
    
    return T_Copy;
}

六、二叉树的判等

很明显,只有当两棵二叉树结构一模一样的时候,才能判定这两棵树相等,即各级左右子树均一模一样。

具体思路是这样的:从两棵树的根结点开始,先判断他们是否相同,如果这都不相同,下面直接不用看了,肯定不相等;若根结点相等,则判断他们的孩子结点是否一样,包括位置和Data域。然后,若相等,分别将左右孩子再次按照以上的方法判定,直到左右孩子到了叶子结点为止。

实现的时候,我们这里借助两个栈,同时进行入栈和出栈操作,若最后发现两个栈不是都空了,则说明肯定有一棵树和另一棵树不一样。

下面给出伪代码:

/* 判断两棵树是否完全相同 */
bool Is_Equal(BiTree T1, BiTree T2) 
{
    SqStack S1 = CreateSqStack();  //建一个空栈
    SqStack S2 = CreateSqStack();
    TNode *n1 = T1, *n2 = T2;  //n1,n2起始指向两棵树的根结点
    Push(S1, n1);  //根结点先入栈
    Push(S2, n2);
    while(!EmptyStack(S1) && !EmptyStack(S2))
    {
        Pop(S1, n1);
        Pop(S2, n2);
        
        if(n1->Data != n2->Data)return false;  //根结点值不等则树不等
        
        if(n1->left!=NULL && n2->left!=NULL) //左子树均非空
        {
            Push(S1, n1->left);  //分别将两个左子树入栈
            Push(S2, n2->left);
        }
        else if(n1->left || n2->left)return false;  //n1、n2左子树情况不一致,则树不等,if条件写全就是((n1->left && !n2->left) || (n2->left && !n1->left)),实际等价
		//这里省略了左子树均空的情况,即到了叶子结点,不进行操作
        
        if(n1->right!=NULL && n2->right!=NULL)  //右子树均非空
        {
            Push(S1, n1->right);  //分别将两个右子树入栈
            Push(S2, n2->right);
        }
        else if(n1->right || n2->right)return false;  //n1、n2右子树情况不一致,则树不等,if条件写全就是((n1->right && !n2->right) || (n2->right && !n1->right)),实际等价
		//这里省略了右树均空的情况,即到了叶子结点,不进行操作
    }
    return(EmptyStack(S1) && EmptyStack(S2));  //出了while循环,表明某个栈空了,即有一棵树已经完全遍历完了,且两棵树目前已经遍历过的部分均一致。如果此时有某个栈非空,说明对应的树与遍历完的那棵树不相等,要多出一部分;若两个栈同时空,则两棵树才算一模一样。
}

七、二叉树同构的判定

二叉树的同构,意思就是指两棵二叉树结构相同。什么叫结构相同?就是如果一棵二叉树通过交换各级左右子树能够与另一颗二叉树完全相同,那么他们就是同构的。
举例子来说,对于下面两颗二叉树:
二叉树的二叉链表表示与基本操作_第8张图片

不妨令左边的树叫做T1,右边的叫做T2,若按照以下步骤,交换T1的各级子树:
① 交换结点E的左右子树;
② 交换结点C的左右子树,即T1中C的右子树左边空的位置,成为左子树;
③ 交换根结点A的左右子树。
这样,T1就转换为T2,由此证明,T1和T2同构。

特殊地,如果两棵树完全相同,则同构;如果两棵树左右对称,则同构;如果两棵树分别于第三棵树同构,则该两棵树也同构。这表明同构满足离散数学中典型的等价关系。

整个算法的思想也根判等差不多,利用两个栈。只不过,判断规则要多了一些,如果发现左右孩子不等时(包括左/右子树为空的情况),这时候要交换左右子树,再次判断是否相等,再往下进行。

下面给出伪代码,代码略长但理解起来很容易,就是分类讨论:

/* 判断两棵树是否同构 */
bool isomorphism(BiTree T1, BiTree T2)
{
    SqStack S1 = CreateSqStack();  //建一个空栈
    SqStack S2 = CreateSqStack();
    TNode *n1 = Copy_BiTree(T1), *n2 = Copy_BiTree(T2);
    Push(S1, n1);
    Push(S2, n2);
    
    while(!EmptyStack(S1) && !EmptyStack(S2))
    {
        Pop(S1, n1);
        Pop(S2, n2);

        if(n1->Data != n2->Data)return false;  //两根结点值不等,非同构
        
        else //两根结点值相等,则比较左右子树
        {
            if(is_leaf(n1) && is_leaf(n2))  //均没有左右子树,即叶子结点,不操作
                 continue;
            
            if(only_have_right(n1) && only_have_right(n2)) //均只有右子树,则比较右孩子结点
            {
                if(n1->right->Data == n2->right->Data)  //若相同
                {
                    Push(S1, n1->right);  //右子树入栈,准备下一轮循环
                    Push(S2, n2->right);
                }
                else return false;  //不相同,则非同构
            }
            
            else if(only_have_left(n1) && only_have_left(n2))  //均只有左子树,则比较左孩子结点,同上
            {
                if(n1->left->Data == n2->left->Data)
                {
                    Push(S1, n1->left);
                    Push(S2, n2->left);
                }
                else return false;
            }
            
            else if(only_have_left(n1) && only_have_right(n2))  //n1只有左子树而n2只有右子树
            {
                Exchange_Sub(n1);  //交换n1左右子树
                Push(S1, n1->right);  //统一入栈右子树
                Push(S2, n2->right);
            }
            
            else if(only_have_right(n1) && only_have_left(n2))  //n1只有右子树而n2只有左子树
            {
                Exchange_Sub(n1);  //交换n1左右子树
                Push(S1, n1->left);  //统一入栈左子树
                Push(S2, n2->left);
            }
            
            else if(n1->left && n1->right && n2->left && n2->right)  //若左右子树均有
            {
                if((n1->left->Data == n2->left->Data) && (n1->right->Data == n2->right->Data))  //在左右结点值均相等的情况下
                {
                    Push(S1, n1->left);  //入栈,准备下一轮循环
                    Push(S2, n2->left);
                    Push(S1, n1->right);
                    Push(S2, n2->right);
                }
                
                else if((n1->left->Data == n2->right->Data) && (n1->right->Data == n2->left->Data))  //左右结点值对称的情况下
                {
                    Exchange_Sub(n1);  //交换n1左右子树(或n2左右子树,一样的)
                    Push(S1, n1->left);  //再入栈,准备下一轮循环
                    Push(S2, n2->left);
                    Push(S1, n1->right);
                    Push(S2, n2->right);
                }
            }
            else return false;  //不满足以上情况的,均不满足同构条件
        }
    }
    return(EmptyStack(S1)&&EmptyStack(S2));
}

八、完全二叉树与满二叉树的判定

1. 概念

(1) 满二叉树
在讲完全二叉树时,要先知道什么是满二叉树满二叉树就是指除了叶子结点,其他每个结点都必有两个孩子。如下图所示,T1是满二叉树,而T2不是满二叉树,因为第三层的结点D只有一个孩子,且F也只有一个孩子。
二叉树的二叉链表表示与基本操作_第9张图片
若按照每层结点数相加来算,完全二叉树的总结点数 = 1+2+4+8+……+2层数-1,是个等差数列求和,其结果是2层数-1.

(2) 完全二叉树
完全二叉树的要求相对于满二叉树来说,少了一点:完全二叉树要求除最后一层外其他所有层构成的树必须均满足满二叉树,并且最后一层的每个节点(不要求满)严格按照满二叉树那样由左到右分布,之间不允许有间隔。举个例子,下图中,

T1是满二叉树,而T2不是满二叉树,因为第四层I与J之间还隔了一个结点。
二叉树的二叉链表表示与基本操作_第10张图片

2. 联系

满二叉树是在完全二叉树的基础上增加条件而形成的,即
完全二叉树 ≦ 满二叉树
因此。如果一棵树是满二叉树,那么它必是完全二叉树。反过来,如果它是完全二叉树,且总结点数满足是2的某次方-1,则它必是满二叉树,由此我们可以简化算法,只需要在完全二叉树的基础上再判断一下结点总数即可。

3. 算法

对于完全二叉树,我们考虑使用队列,用层次遍历的方法遍历整棵树。但与传统的层次遍历不同的是,我们要考虑“空结点”,即若一个结点没有左子树,我们视为该结点左孩子的位置是空结点,空结点也要入队列,右子树与之类似。由于我们依旧按照先左孩子后右孩子的顺序出入队列,因此如果在访问某结点时,发现前面已经有一个空结点了,那不就说明,中间“隔了一个”吗,所以可以判断它不是完全二叉树。如果遍历到最后都没有发现这种情况,则判断它是完全二叉树。

下面我们就某棵非完全二叉树,走一遍算法:
二叉树的二叉链表表示与基本操作_第11张图片
如上图,我们按照层次遍历(不知道的请看上面二、1. 部分)的方式进行:

首先令记号flag == 0,A入队;
① A出队,分别入队其左右孩子B、C;
② B出队,分别入队其左右孩子D、E;
③ C出队,分别入队其左右孩子F、G;
④ D出队,分别入队其左右孩子H、I;
⑤ E出队,分别入队其左右孩子**NULL**、J;
⑥ F出队,分别入队其左右孩子K、**NULL**;
⑦ G出队,分别入队其左右孩子**NULL**、**NULL**;
⑧ H出队,分别入队其左右孩子**NULL**、**NULL**;
⑨ I出队,分别入队其左右孩子**NULL**、**NULL**;
⑩ “NULL”出队,即绿色圈圈部位,并标记flag == 1;
这个时候J出队,发现flag == 1,即它的前面已经出现了“空位”,程序结束,判断为非完全二叉树。

对于满二叉树,整个过程根完全二叉树一致,只是多了个结点数的累加过程,最简单的就是设置一个计数器num=0,每出队列一次就+1. 在判定是完全二叉树后,要看看总结点数是不是2的某次方-1.

下面给出伪代码:

/* 判断完全二叉树 */
bool Is_Complete(BiTree T) 
{
    if(!T)return true;  //空树是完全二叉树
    
    SqQueue Q = CreateSqQueue();
    TNode *n = new TNode;  //用于出队列暂存结点的
    int flag = 0;
    
    EnQueue(Q, T);
    while(!EmptyQueue(Q))
    {
        DeQueue(Q, n);
        if(!n)flag = 1;  //标记空结点
        else if(n && flag == 1)  //表明当前节点之前就已经出现了空结点
            return false;
        else
        {
            EnQueue(Q, n->left);
            EnQueue(Q, n->right);
        }
    }
    return true;
}
/* 判断满二叉树 */
bool Is_Full(BiTree T) 
{
    if(!T)return true;  //空树是满二叉树
    
    SqQueue Q = CreateSqQueue();
    TNode *n = new TNode;
    int flag = 0;
    int num = 0; //结点数
    
    EnQueue(Q, T);
    while(T && !EmptyQueue(Q))
    {
        DeQueue(Q, n);
        if(!n)flag = 1;  //标记空结点
        else if(n && flag == 1)  //表明当前节点之前就已经出现了空结点
            return false;  //不是完全二叉树,更不是满二叉树
        else
        {
            num++;  //出队列后记录一个结点数
            EnQueue(Q, n->left);
            EnQueue(Q, n->right);
        }
    }
    return ((!num) == num+1) ? false : true;  //如果结点数+1为2的次方数,则是满二叉树
}

注:这里在判断num是否等于2的某次方-1时使用了网上别人的方法,具体是哪位大神的记不清了,总之这里还是要说明一下。即num二进制取逻辑非后与num+1作比较,如果相等,则num是2的某次方-1,下图已经很清楚了:
二叉树的二叉链表表示与基本操作_第12张图片

九、二叉排序树 / 二叉搜索树

1. 概念

二叉搜索树,又名二叉排序树,它是用于查找操作的一种二叉树。一般来说,二叉排序树保证对于每个结点,其左孩子的(权)值要小于该结点,右孩子的(权)值要大于该结点,这样就保证了二叉排序树中的任意一个结点的子树都是一棵二叉排序树。注意,这里我们设定二叉排序树中不能有相同(权)值的结点。

如下所示,T1是一棵二叉排序树,T2则不是二叉排序树:
二叉树的二叉链表表示与基本操作_第13张图片
这里可以发现T1既不是完全二叉树也不是满二叉树,因此二叉排序树不需要满足完全二叉树或者满二叉树的要求。

2. 查找结点

那就有人想了,既然二叉排序树也是二叉树,那我们能不能按照层次遍历或者先序遍历那种方式查找到这个结点呢?当然可以,不过你这样的话,二叉排序树的局部有序特性不就白白浪费了吗?我们当然要根据左小右大的特性来查找这个结点啊!

具体的思路是这样的:首先与树根结点比较,如果小,就往左分支(即左子树)继续查找,否则去右分支查找。如果找到最后发现已经空了,即叶子结点也都检查过了不是所要的,那么说明这个结点一定不存在了。

举个例子,如下图中的二叉排序树,我们打算找12这个结点的位置。
二叉树的二叉链表表示与基本操作_第14张图片

① 首先与根结点16比较,12<16,往16的左子树方向继续比较;
② 与10比较,12>10,往10的右分支比较;
③ 与13比较,2<13,往13的左分支比较;
④ 与12比较,相等,找到,结束。

补充:如果是找11的话,要与12比较,发现11<12,往12的左分支,发现空,查找失败。

查找函数非常简单,使用递归实现,设置递归出口为找到空结点或者找到所要找的节点,如果要找的结点值小于当前结点的值,则传入当前结点的左子树继续递归,大于,则去右子树递归,下面给出伪代码:

/* 从树BST找到值为X的结点,并返回该结点,没找到返回空 */
TNode* Find(BiTree BST, ElementType X) 
{
    if(!BST || X == BST->Data)
        return BST;
    else if(X < BST->Data)
        return Find(BST->Left, X);
    else
        return Find(BST->Right, X);
}

这个函数我们可以这样用:

LNode *root = Find(BST, 15);
if(root)
	cout <<"查找" <<root->Data <<"成功!" <<endl;
else cout <<"查找失败。" <<endl;

3. 建立一棵二叉排序树(结点插入)

假设我们有一个序列,那如何通过这个序列建立一个合法的二叉排序树呢?这里关键的一步就是,每当新加入一个结点的时候,这个结点只能插入到叶子的部位,而不能插入到树的中间层。而且插入时,也要按照上线讲的查找一样,遵循二叉排序树的“左小右大”原则,依次与根结点比较,如果小,就应当插入到其左子树的位置,否则插入到右子树的位置。

下面,我们举个实例,将序列
16 10 22 8 13 19 27 9 12 15 构建成一棵二叉排序树:

① 第一个元素16作为根结点;
② 元素10,比当前根结点小,插入到16的左子树位置,而16目前左子树为空,正好10可以直接插入到其左孩子位置;
③ 元素22,比当前根结点16大,同上,插入到其右孩子位置;
④ 元素8,比16小,往左子树走,比10小,再往左子树走,10目前左子树为空,故8插入到10的左孩子位置;
⑤ 元素13,比16小,往左,比10大,往右,插入到10的右孩子位置;
⑥ 元素19,比16大,往右,比22小,往左,插入到22的左孩子位置;
⑦ ……以此类推,构造出的二叉排序树即上图中的T1.

在代码实现时,我们主要用递归的思想,只要当前结点不空,就一直与根结点比较,看往哪个分支走,一旦当前节点为空,就把该结点插入到这个位置作为叶子结点,下面给出插入结点的伪代码:

/* 把值为元素X的结点插入到二叉排序树BST的合法位置,并返回这棵树 */
BiTree Insert(BiTree BST, ElementType X )
{
    if(!BST)  //找到叶子结点下的一个空位置,则可以插入
    {
        BiTree BST = new TNode;
        BST->Data = X;
        return BST;
    }
    else if(X < BST->Data)BST->left = Insert(BST->left, X);  //如果不是空位置,继续向下找
    else if(X > BST->Data)BST->right = Insert(BST->right, X);
    return BST;
}

这个函数可以用于从无到有建立一棵二叉树,也可以单独用作某个新结点的插入。我们在使用时,可以按照下面的格式:

BiTree BST = new TNode;
Elementype data[10] = {16, 10, 22, 8, 13, 19, 27, 9, 12, 15};
for(int i = 0; i < 10; i++)
	BST = Insert(BST, data[i]);

4. 二叉排序树结点删除

结点的删除过程主要分两步:

第一步主要是“”,即要“找结点的位置”,就是上面讲的查找过程,一模一样,要通过比较大小找到该结点在哪个分支的哪个地方,然后定位到这个结点。

第二步才是“”,只有找到才能删除!这里跟结点插入的不同在于,结点插入只能在叶子的位置,而删除操作可以在任意位置,可以在叶子处删除(这也是最简单的方式),还能在树的中间删除,甚至可能会删除树的根结点。那这个时候,就必须考虑结点之间的移动了,因为在树的内部删除结点,其他结点必然要挪位置以保证仍是一颗完整的二叉排序树。

那删除结点之后,其他结点如何“挪位置”呢?下面是要遵循的法则:

  1. 如果待删结点是根结点,直接删掉即可,也就是置为NULL,其他所有结点都不用动;
  2. 如果待删结点只有左子树,直接用左子树替换这个结点即可;
  3. 如果待删结点只有右子树,直接用右子树替换这个结点即可;
  4. 如果待删结点既有左子树也有右子树,则应该用左子树中的最大结点替换当前结点,再执行从左子树删除这个“最大结点”,而右子树保持不动。
    或者用右子树的最小结点替换当前结点,再执行从右子树删除这个“最大结点”,而左子树保持不动。两者哪一种都可以。

下面举个实例,看图:
二叉树的二叉链表表示与基本操作_第15张图片

原二叉排序树
模拟删除结点12的过程:
找到结点12(步骤看上面查找过程),置空,结束。

如下:
二叉树的二叉链表表示与基本操作_第16张图片

删除结点12后的二叉排序树
(接上图)模拟删除结点8的过程:
找到结点8,用右子树替换,8的位置现在是9,结束。

如下:
二叉树的二叉链表表示与基本操作_第17张图片

删除结点8后的二叉排序树
(接上图)模拟删除结点22的过程:
找到结点2,找右子树最大值(其实也就一个27,替换22,并删除「原来27」这个结点,左子树不动,结束。
(或者找到左子树最大值(其实也就一个19),替换22,并删除「原来19」这个结点,右子树不动,结束)

二叉树的二叉链表表示与基本操作_第18张图片

删除结点22后的二叉排序树
(接上图)模拟删除结点16的过程:
① 找到结点16,找到左子树最大值15,替换16(15不要删除),右子树不动,结束;

步骤①如下所示:
二叉树的二叉链表表示与基本操作_第19张图片

② 把原结点16的子树(也就是10-9-13-15这棵子树)进行删除结点15操作,由于是叶子结点,直接置空即可。

步骤②如下所示:
二叉树的二叉链表表示与基本操作_第20张图片
至此,结点16删除结束。

设计算法的时候,也要考虑递归,递归出口应该是依照以下两种:要么没找到(包含空树和确实没找到两种情况),要么找到了。

如果找到该结点,要判断该结点所处的位置,
下面给出伪代码:

/* 从二叉排序树树BST中删除值为X的结点,并返回该树 */
BiTree Delete(BiTree BST, ElementType X)
{
    if(!BST)  //空树,找不到该结点
    {
        cout <<"找不到待删值 " <<X <<" .\n";
        return BST;
    }
    
    else if(X < BST->Data)  //这块代码思路根查找一模一样
        BST->Left = Delete(BST->Left, X);
    else if(X > BST->Data)
        BST->Right = Delete(BST->Right, X);
 
    if(X == BST->Data)  //如果找到该结点
    {
        if(BST->Left && !BST->Right)  //仅有左子树
            BST = BST->Left;  //直接用左子树替换当前结点
        else if(!BST->Left && BST->Right)  //仅有右子树
            BST = BST->Right;  //直接用右子树替换当前结点
        else if(!BST->Left && !BST->Right)  //为根节点
            BST = NULL;  //直接置空
        else  //左右子树都有,可以选择用左子树最大结点替换当前结点,也可以用右子树最小结点替换当前结点
        {
            struct TNode *temp = FindMax(BST->Left);
            BST->Data = temp->Data;  //先替换结点
            BST->Left = Delete(BST->Left, temp->Data);  //再从子树删除这个结点
             
            /* 这是用右子树最小结点替换当前结点的方法
            struct TNode *temp = FindMin(BST->Right);
            BST->Data = temp->Data;
            BST->Right = Delete(BST->Right, BST->Data);
            */
        }
    }
    return BST;  //别忘了要返回删除成功后的树
}

注意,里面的函数LNode* FindMax(BiTree BST)是指从二叉排序树BST中返回最大值的结点,FindMin同理,原理很简单,一直往左/右分支走即可,最后一个结点必是最值结点,实现如下:

/* 从二叉排序树BST中返回最小值的结点 */
TNode* FindMin(BiTree BST)
{
    if(!BST->left)return BST;
    return FindMin(BST->left);
    /* 下面是非递归算法
    if(!BST) return NULL;
    while(BST->left)
    	BST = BST->left;
    return BST;
    */
}
/* 从二叉排序树BST中返回最大值的结点 */
TNode* FindMax(BiTree BST)
{
    if(!BST->right)return BST;
    return FindMax(BST->right);
    /* 下面是非递归算法
    if(!BST) return NULL;
    while(BST->right)
    	BST = BST->right;
    return BST;
    */
}

十、总结

以上便是关于本篇二叉树二叉链表表示法的全部内容,不一定完全涵盖所有基本知识点,但一定是非常详尽的,相信大家看了也能够基本明白所涉及的算法。如果有补充,会更新此文。
ps:里面涉及的代码均是伪代码,不要拿来直接用,会有很多语法编译错误会逼死你的,本文附带的源代码才是可以直接跑的!!!里面关于栈和队列的数据结构定义和函数,也均在源代码里。

源代码下载,需要3个积分/C币,写这些代码不容易,请多多支持!
https://download.csdn.net/download/weixin_42708321/85001376

你可能感兴趣的:(数据结构学习笔记,数据结构,二叉树,二叉排序树,中序遍历,层次遍历)