二叉树

本文详细介绍了二叉树的基础概念,包括节点、父节点、子节点和兄弟节点,以及树的深度和节点的度。接着阐述了二叉树的特点,如左子节点<父节点<右子节点,并讨论了不同类型的二叉树形状,如斜树、满二叉树和完全二叉树。此外,还探讨了二叉树的常见操作,如建立、遍历(前序、中序、后序和层序遍历)、搜索和删除节点,以及平衡二叉树的重要性。

二叉树是一种特殊的树形结构,这篇博文主要介绍二叉树的特点和二叉树的操作。

介绍二叉树

基础概念

节点

节点是一个结构体,在二叉树中节点专指树节点,树节点在C中的一般定义形式为

struct Node{
    struct Node *left;
	int value;
	struct Node *right;
	};

根据代码,我们可以看见一个节点主要由三部分组成:左分支序号右分支。除这些外,可以自由加入储存信息的内容,但这三个部分是必要的。形象地说,节点就像是一个伸出两个分支的树枝,多个这样的节点一起构成一个树。

父节点、子节点和兄弟节点

节点间的关系
父节点与子节点对应,直白地说,相互连接的两个节点,在上面的节点称为在下面的节点的父节点,在下面的节点称为在上面的节点的子节点。

兄弟节点,直白地说就是有同一个父节点的多个节点。

以上图为例,C的子节点有E和F,D的子节点有G、H和I,B和C为兄弟节点。

  • 一个父节点可以有多个子节点
  • 一个子节点只能有一个父节点
  • 一个节点可以既为父节点,又为子节点

树的深度和节点的度

树是一种由上至下分层,每层的节点数呈指数增加的结构,一个树的层数就是树的深度。

以下图为例,我们可以说树的深度为4。
树的深度

一个节点有几个子节点成为这个节点的度。以上图为例,A的度为2,E的度为1,D的度为3。对于二叉树而言,节点的度只能为0、1、2。

认识二叉树

二叉树的特点

  • 对于一个二叉树父节点的子节点,左侧的子节点称为左子节点,右侧的子节点称为右子节点。
  • 左子节点<父节点<右子节点。
  • 即使一个父节点只有一个子节点或没有子节点,子节点依然有左右之分。
  • 以左子节点为根节点的子树为左子树,以右子节点为根节点的子树为右子树。

二叉树的形状

二叉树1
这是最普通的二叉树,符合二叉树的所有定义,但没有特殊的性质。
二叉树2
二叉树3
上面的两个树称为斜树,对于二叉树搜索,斜树是最费时的结构,因此需要对斜树进行平衡,这在后面的博文中会有介绍。
二叉树4
这是满二叉树,是最理想的二叉树结构。在满二叉树中,叶节点(无子节点的节点)只出现在最下面一层并且最下面一层全是叶节点。每个父节点的度都为2。
二叉树5
这是完全二叉树,如果对一棵树的节点按层、自左至右进行编号i(1<=i<=n),每一个节点都与同样深度的满二叉树对应,则称这个树为完全二叉树。完全二叉树有许多性质。

  • 叶节点只出现在最下层和次下层
  • 最下层叶节点集中在左侧
  • 次下层叶节点集中在右侧
  • 左子树与右子树的深度差<=1
  • 如果节点度为1,则节点只有左子节点,没有右子节点

二叉树的操作

建立二叉树

对于节点的序号,我们规定在二叉树中的所有节点满足左子节点<父节点<右子节点,因此,若我们想要插入一个新的节点,就需要根据这个特点找到新的节点应该在的位置,并连接新插入的节点和它的父节点。

struct Node *planttree()
{
    int v;
    struct Node *pre,*current,*p,*root;
    printf("we will define the root,please give me a number\n");
    scanf("%d",&v);
    root=(struct Node *)malloc(sizeof(struct Node)); /*建立根节点*/
    (*root).left=NULL;
    (*root).value=v;
    (*root).right=NULL;
    printf("please add a number\n");
    scanf("%d",&v);
    while(v)                                                              /*只要v不为负数,就继续插入节点,结束插入的条件可以更改*/
    {
        p=(struct Node *)malloc(sizeof(struct Node)); /*建立新插入的节点*/
        (*p).left=NULL;
        (*p).value=v;
        (*p).right=NULL;
        pre=root;                                                         /*开始寻找新插入的节点应该在的位置*/
        current=pre;
        while(current)                                                  /*只要current还未指向NULL*/
        {
            pre=current;                                                /*pre移动到current的位置*/
            if(v>(*pre).value)                                         /*若v比当前节点的序号大,current则移动到当前节点的右子节点*/
            {
                current=(*pre).right;
            }
            if(v<(*pre).value)                                          /*若v比当前节点的序号小,current则移动到当前节点的左子节点*/
            {
                current=(*pre).left;
            }
            if(v==(*pre).value)                                        /*若v与当前节点的序号一致,说明序号已存在,终止循环,停止寻找*/
            {
                break;
            }
        }
        if((*pre).value<v)                                              /*连接新插入的节点和它的父节点*/
        {
            (*pre).right=p;
        }
        if((*pre).value>v)
        {
            (*pre).left=p;
        }
        if(v==(*pre).value)
        {
            printf("this value has existed\n");
        }
        printf("please add a number\n");
        scanf("%d",&v);
    }
    return root;
}

二叉树的插入操作并不难,与链表的插入区别不大,只是链表寻找插入的位置是逐个寻找,二叉树寻找插入的位置会有左右的选择。
这样插入新的节点,新的节点一定是叶节点。当然在保证二叉树的节点的序号正确的前提下,可以把节点插入二叉树两个节点之间,只是这样会比较麻烦。

遍历二叉树

遍历二叉树两种方法,其中一种为层序遍历,即自上而下对二叉树逐层遍历;另一种方法有三种表现形式,分别为前序、中序和后序。
遍历二叉树

前序遍历
A-B-D-H-I-E-J-C-F-G

void print(struct Node *root)
{
    if(root==NULL)
    {
        return;
    }
    printf("%d\n",(*root).value);
    preprint((*root).left);
    preprint((*root).right);
}

前序遍历使用迭代的方法遍历二叉树所有的节点,顺序为父节点–>左子树–>右子树。

中序遍历
H-D-I-B-J-E-A-F-C-G

void print(struct Node *root)
{
    if(root==NULL)
    {
        return;
    }
    preprint((*root).left);
	printf("%d\n",(*root).value);
    preprint((*root).right);
}

中序遍历与前序遍历方法一样,只是遍历的顺序不同。中序遍历的顺序为左子树–>父节点–>右子树。

后序遍历
H-I-D-J-E-B-F-G-C-A

void print(struct Node *root)
{
    if(root==NULL)
    {
        return;
    }
    preprint((*root).left);
    preprint((*root).right);
	printf("%d\n",(*root).value);
}

中序遍历与前序遍历方法一样,只是遍历的顺序不同。后序遍历的顺序为左子树–>右子树–>父节点

层序遍历
A-B-C-D-E-F-G-H-I-J
层序遍历逐层、从左至右遍历二叉树。

搜索节点

二叉树的搜索操作在建立二叉树时已经做过示范,即根据二叉树的节点的序号左子节点<父节点<右子节点的特点进行节点的搜索。

void search(struct Node *root,int v)
{
    struct Node *pre,*current;
    pre=root;
    current=root;
    while(current)
    {
        pre=current;
        if((*current).value>v)
        {
            current=(*pre).left;
        }
        else if((*current).value<v)
        {
            current=(*pre).right;
        }
        else
        {
            printf("have found, v=%d",(*current).value);
            break;
        }
    }
    if(current==NULL)
    {
        printf("not existed\n");
    }
}

删除节点

删除节点是二叉树中最复杂的操作,删除的节点类型有四种:叶节点、只有右子树、只有左子树、左右子树都有,同时还要小心删除的节点是根节点,接下来我们将展示删除节点的操作。

在进行删除节点的操作前要搜索这个节点

struct Node *pre,*current;
int lr=0;                                  /*设置变量lr来指示pre与current的位置关系,lr=0,current在pre左侧;lr=1,current在pre右侧。*/
pre=root;
current=root;
while(current)
{
    if((*current).value==v)
    {
        break;
    }
    pre=current;
    if((*current).value>v)
    {
        current=(*pre).left;
        lr=0;
    }
    else
    {
        current=(*pre).right;
        lr=1;
    }
}
if(current==NULL)                      /*如果最后current指向NULL,则要删除的节点不存在*/
{
    printf("not existed\n");
    return(root);
}

在搜索结束后,如果要删除的节点存在,则current指向要删除的节点,pre指向它的父节点。

(1) 删除叶节点
删除叶节点是二叉树删除节点操作里最简单的一个,只需要把叶节点和它的父节点断开连接即可。

if((*current).left==NULL&&(*current).right==NULL)        /*判断当前节点为叶节点*/
{
    if(current==root) return(NULL);                                  /*若要删除的点为根节点,说明二叉树只有一个根节点,这时只需要将root指向NULL即可*/
    else
    {
        if(lr==0) (*pre).left=NULL;                                       /*根据current与pre的位置关系断开两节点的连接*/
        else (*pre).right=NULL;
        return(root);
    }
}

(2)节点只有右子树
删除只有右子树的节点也不难,可以看做节点的右子节点代替了自己二叉树的位置。

else if((*current).left==NULL)                                           /*判断当前的节点只有右子树*/
{
    if(current==root) return((*root).right);                           /*若当前节点为根节点,只需返回根节点的右子节点即可*/
    else
    {
        if(lr==0) (*pre).left=(*current).right;                           /*根据current与pre的位置关系,连接pre和删除节点的右子节点*/
        else (*pre).right=(*current).right;
    }
}                                                                                         /*在整个函数的结尾return(root)*/

(3)节点只有左子树
删除只有左子树的节点与删除只有右子树的节点一样,可以看做节点的左子节点代替了自己二叉树的位置。

else if((*current).right==NULL)                                           /*判断当前的节点只有左子树*/
{
    if(current==root) return((*root).left);                           /*若当前节点为根节点,只需返回根节点的左子节点即可*/
    else
    {
        if(lr==0) (*pre).left=(*current).left;                           /*根据current与pre的位置关系,连接pre和删除节点的左子节点*/
        else (*pre).right=(*current).left;
    }
}                                                                                         /*在整个函数的结尾return(root)*/

(4)节点既有左子树又有右子树
我们发现删除操作的核心有两点:确定是哪个节点代替被删除的节点、替代后重新连接节点。对于既有左子树又有右子树的节点,与它的编号最接近的有两个节点:它的左子树上编号最大的节点(比它编号小的节点里编号最大的节点)、它的右子树上编号最小的节点(比它编号大的节点里编号最小的节点)。我们以用它的右子树上编号最小的节点代替被删除节点为例,展示删除操作。

else
{
    struct Node *presuccessor,*successor;    /*successor指向删除节点的替代节点,presuccessor指向替代节点的父节点*/
    presuccessor=current;
    successor=(*current).right;
    int isLeft=0;    /*设置变量isLeft来指示presuccessor与successor的位置关系,isLeft=0,successor在presuccessor右侧;isLeft=1,successor在presuccessor左侧*/
    while((*successor).left)    /*遍历右子树,successor不停指向左子节点,直到找到替代节点*/
    {
        if((*successor).left==NULL)
        {
            break;
        }
        presuccessor=successor;
        successor=(*successor).left;
        isLeft=1;
    }
	/*以下的连接过程较复杂,如果想不清楚可以画一个二叉树出来试一下*/
    if(current==root)    /*如果删除节点是根节点*/
    {
        if(isLeft)                                            
        {
            (*presuccessor).left=(*successor).right;    /*替代节点successor的右子节点代替successor在二叉树的位置,与presuccessor连接,这样保证了替代节点successor的右子树不会丢失并且位置正确*/
            (*successor).left=(*root).left;    /*替代节点successor代替根节点root,连接根节点root的左子节点与右子节点*/
            (*successor).right=(*root).right;
        }
        else
        {
            (*successor).left=(*root).left;    /*直接代替即可*/
        }
        return(successor);
    }
    if(isLeft==0)    /*如果删除节点不是根节点root并且isLeft=0,直接代替即可*/
    {
        if(lr==0)
        {
            (*pre).left=(*current).right;
        }
        else
        {
            (*pre).right=(*current).right;
        }
    }
    else    /*如果isLeft=1,删除操作就比较复杂,但这才是删除有左子树与右子树的节点的一般操作*/
    {
        if(lr==0)
        {
            (*pre).left=successor;    /*将替代节点的父节点变为pre指向的节点*/
            (*presuccessor).left=(*successor).right;    /*替代节点successor的右子节点代替successor在二叉树的位置,与presuccessor连接,这样保证了替代节点successor的右子树不会丢失并且位置正确*/
            (*successor).left=(*current).left;    /*替代节点successor代替删除节点current,连接删除节点current的左子节点与右子节点*/
            (*successor).right=(*current).right;
        }
        else    /*与lr=0一样,只是pre与current的位置关系有区别*/
        {
            (*pre).right=successor;                                   
            (*presuccessor).left=(*successor).right;
            (*successor).left=(*current).left;
            (*successor).right=(*current).right;
        }
    }
   return(root);
}

完整的删除代码如下

struct Node *del(struct Node *root,int v)
{
   struct Node *pre,*current;
   int lr=0;
   pre=root;
   current=root;
   while(current)
   {
       if((*current).value==v)
       {
           break;
       }
       pre=current;
       if((*current).value>v)
       {
           current=(*pre).left;
           lr=0;
       }
       else
       {
           current=(*pre).right;
           lr=1;
       }
   }
   if(current==NULL)
   {
       printf("not existed\n");
       return(root);
   }
   else
   {
       if((*current).left==NULL&&(*current).right==NULL)
       {
           if(current==root) return(NULL);
           else
           {
               if(lr==0) (*pre).left=NULL;
               else (*pre).right=NULL;
               return(root);
           }
       }
       else if((*current).left==NULL)
       {
           if(current==root) return((*root).right);
           else
           {
               if(lr==0) (*pre).left=(*current).right;
               else (*pre).right=(*current).right;
           }
       }
       else if((*current).right==NULL)
       {
           if(current==root) return((*root).left);
           else
           {
               if(lr==0) (*pre).left=(*current).left;
               else (*pre).right=(*current).left;
           }
       }
       else
       {
           struct Node *presuccessor,*successor;
           presuccessor=current;
           successor=(*current).right;
           int isLeft=0;
           while((*successor).left)
           {
               if((*successor).left==NULL)
               {
                   break;
               }
               presuccessor=successor;
               successor=(*successor).left;
               isLeft=1;
           }
           if(current==root)
           {
               if(isLeft)
               {
                   (*presuccessor).left=(*successor).right;
                   (*successor).left=(*root).left;
                   (*successor).right=(*root).right;
               }
               else
               {
                   (*successor).left=(*root).left;
               }
               return(successor);
           }
           if(isLeft==0)
           {
               if(lr==0)
               {
                   (*pre).left=(*current).right;
               }
               else
               {
                   (*pre).right=(*current).right;
               }
           }
           else
           {
               if(lr==0)
               {
                   (*pre).left=successor;
                   (*presuccessor).left=(*successor).right;
                   (*successor).left=(*current).left;
                   (*successor).right=(*current).right;
               }
               else
               {
                   (*pre).right=successor;
                   (*presuccessor).left=(*successor).right;
                   (*successor).left=(*current).left;
                   (*successor).right=(*current).right;
               }
           }
           return(root);
       }
   }
}

平衡二叉树

当我们进行二叉树搜索操作时,我们希望二叉树的形态为完全二叉树,这样的搜索效率是最高的。但现实是当我们对二叉树进行操作时可能会较大地改变二叉树的形状,使二叉树逐渐偏离完全二叉树,这样搜索效率就变低了。因此我们要引入平衡二叉树的操作。

节点的高度
在介绍平衡二叉树的操作之前,我们要先了解如何求节点的高度。节点的高度即以这个节点为根节点的树的深度。因为只有知道了节点的深度,我们才能判断二叉树是否失衡。

我采用迭代的方法求节点的深度。

int depth(struct Node *root)
{
    int leftdepth,rightdepth;
    if(root==NULL) return 0;    /*NULL深度为0*/
    leftdepth=depth((*root).left);    /*左子树的深度为leftdepth*/
    rightdepth=depth((*root).right);    /*右子树的深度为rightdepth*/
    return(1+(leftdepth>rightdepth?leftdepth:rightdepth));    /*返回左右子树深度的较大值加一,即节点root的高度*/
}

平衡二叉树的操作
判断一个二叉树是否平衡,我们只需要判断根节点的左子节点的深度和右子节点的深度相差是否小于等于1,我们可以根据这一点一步步地完成二叉树的平衡操作。

我们可以想象,假设当右子树过高时,根节点root的位置会不停地向右子节点移动,这样右子树高度减一,左子树高度加一,直到二叉树平衡。

这个过程用代码描述就是

while(depth((*root).right)-depth((*root).left)>1)
{
    struct Node *rightnode,*root;
	rightnode=(*root).right;    /*使rightnode指向根节点root的右子节点*/
    (*root).right=(*rightnode).left;    /*将rightnode的左子节点与根节点root连接,变成root的右子节点*/
    (*rightnode).left=root;    /*rightnode的左子节点变为根节点root*/
    root=rightnode;    /*root指向rightnode指向的节点,这个节点变为新的根节点root*/
}

变换过程想不明白的朋友可以自己画图一步步地变换。左子树的深度比右子树大时,与这个变换过程没有区别,只是变换的方向相反。

while(depth((*root).left)-depth((*root).right)>1)
{
    struct Node *leftnode,*root;
	leftnode=(*root).left;
    (*root).left=(*leftnode).right;
    (*leftnode).right=root;
    root=leftnode;
}

到这里对二叉树操作的展示就结束了,二叉树并不复杂,作为一种储存结构,它与链表并没有本质性的区别,只是有左右的区分,这样可以减少搜索的时间。唯一要注意的是二叉树较复杂的节点间的连接,但是,只要了解每个操作中对节点连接的改变的原因,并能熟悉地完成节点间的重新连接,二叉树只是另一种形态的链表罢了

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值