二叉树与二叉搜索树

概述#

二叉树是一种特殊的树型结构,它由结点的有限集合构成。

二叉树是由唯一的起始结点引出的结点集合。这个起始节点称为根(root)。二叉树中的任何非根节点都有且仅有一个前去结点,称为该结点的父结点(parent)。根节点即为二叉树中唯一没有父结点的结点。二叉树中的任何结点最多可能有两个后继结点,分别称为左子结点(left child)和右子结点(right child)。具有相同父结点的结点之间互称兄弟结点(sibling)。二叉树中的一个结点的子数树木称为该结点的度(degree)。没有子结点的结点称为叶结点(leaf),叶结点的度为0。

二叉树的基本操作有:前序、中序、后续周游二叉树,删除给定二叉树,获得深度,获得叶子数,获得总结点数等。其中,前序、中序、后续周游尤为重要,他们运用了递归算法周游二叉树,可以大幅度简化非递归的周游算法,所以接下来,我们先给出递归算法的说明,再进一步说明如何运用递归算法实现二叉树的创建和基本操作。

递归思想#

递归算法就是一种直接或间接调用自己函数的算法,即函数里嵌套函数。

就像数学里常见的函数 f(x), 递归思想可以举例为: f ( f ( f ( x ) ) ),其中,总有一个函数可以求值,类似于递归函数中,总有一个递归有结束条件,这个递归成为递归出口。比如说一个简单的例子:

int add(int &a){
    char c='ABCD';
    if(c=='D') return 5;
    return add(a)+1;
}

《二叉树与二叉搜索树》 递归算法实现流程

递归可以大大减少算法的复杂度,但是伴随的缺点是,需要大量的内存空间来储存每个函数中的返回值和局部变量。
每开启一个递归,则会开辟一个新的栈空间,用来储存所需数据,结束递归后会清空栈里的内容,只留下返回值返回到上层递归,再进行一次类似运算。因此,如果递归次数较多,则需要大量空间,这时会造成栈溢出等问题。

我们来看下面一个二叉树前序周游递归实现。

void PreOrder(BiTree &root){    //先序遍历二叉树
    if( root!=NULL){
        cout<< root -> info<<" ";
        PreOrder(root->leftchild);
        PreOrder(root->rightchild);
    }
}

如果二叉树为:
a
b c
1.开始:根节点非空,输出a,进入到下一层,root指向leftchild的递归。
2.leftchild非空,则输出b,进入到下一层,root指向leftchild的递归。
3.leftchild为空,则不进行操作,返回到上一层(2)的递归函数继续进行操作。
4.b结点的右子树为空,则不进行操作。这时此层(2)递归函数已经执行完毕,则返回到上一层(1)root指向rightchild的递归。
5.rightchild为c,输出c,又进入一层root指向leftchild的递归。
6.leftchild为空,不进行操作,返回上一层(5)root指向rightchild的递归。
7.rightchild为空,不进行操作,(5)递归执行完毕,所有函数递归操作完毕,结束程序。

看上去很复杂,多理解,多实际操作就能懂了。

二叉树#

二叉树的创建与基本操作可以用递归方法实现,使得代码更简明。
以下给出了二叉树的递归创建、求先序中序后续周游、总结点数、叶子数、深度的算法。

  • 二叉树创建
    用先序方法创建二叉树。
    首先申请一个结点空间,root指针指向根结点。然后将输入的数据查入结点内容中。进入下一个点,即root指向leftchild,进行递归,直到叶子结点时,指针root为空,结束递归。
    操作如下:
    1.先插入根结点数据为A,root指向A的左子树。
    2.左子树插入B,root指向B的左子树。
    3.B的左子树插入#,表示为空,root指向B的右子树
    4.B的右子树插入#,表示为空,root指向A的右子树。
    5.A的右子树插入#,表示为空,root指向根节点,算法结束。
void creat(BiTree &root){   //先序创建二叉树
    char ch;
    cin>>ch;
    if (ch=='#'){
        root=NULL;}
    else{
        root = (BiTreeNode *)malloc(sizeof(BiTree));
        root -> info = ch;
        creat(root -> lchild);
        creat(root -> rchild);
    }
}
  • 二叉树周游
    这里运用递归思想实现二叉树周游算法。与创建二叉树类似,具体实现过程在递归思想中已经说明过,这里不再阐述了。
void PreOrder(BiTree &root){    //先序遍历二叉树
    if( root!=NULL){
        cout<< root -> info;
        PreOrder(root->lchild);
        PreOrder(root->rchild);
    }
}

void InOrder(BiTree &root){    //中序遍历二叉树
    if( root!=NULL){
        InOrder(root->lchild);
        cout<< root -> info;        
        InOrder(root->rchild);
    }
}

void PostOrder(BiTree &root){    //后序遍历二叉树
    if( root!=NULL){
        PostOrder(root->lchild);
        PostOrder(root->rchild);
        cout<< root -> info;
    }
}
  • 获取树深度
    基本实现思想是:
    先求左子树的深度,再求右子树的深度,求两者中较大的一方加一。采用递归的方法实现:
int GetDepth(BiTree &root)    //获取树的深度
{  
    int lDepth;
    int rDepth;
    if (root == NULL)  
    {  
        return 0;  
    }  
    lDepth= GetDepth(root->lchild);
    rDepth= GetDepth(root->rchild);
    
    if (lDepth> rDepth) return lDepth+1;
    else{ return rDepth +1;} 
} 
  • 结点数与叶子数
    求结点数时,可以参考周游算法,因为周游一遍二叉树也就是周游了一遍所有节点。可以每递归一次函数则总结点数加一。为了实现这一目标,我们可以先求左子树的结点数,再求右子树的结点数,再翻回他们的和外加一个根结点。
    求叶子数时,当指针非空且左孩子右孩子都为NULL时,则总叶子数加一;若指针为空,则返回0值;其他情况继续进行周游算法。具体代码如下:
int NodeNum(BiTree &root){   //结点数
    if( root==NULL){
        return 0;}
    else{
        int num1=NodeNum(root->lchild);
        int num2=NodeNum(root->rchild);
        return num1+num2+1;
    }
}
int LeafNum(BiTree &root){  //叶子树

    if(root!=NULL&&root->lchild==NULL && root -> rchild ==NULL){
        return 1;
    }
    else if(root==NULL){
        return 0;}
    else {
        return LeafNum(root->lchild)+LeafNum(root->rchild);
    }
}

总代码将在附录里给出,运行结果如下:

《二叉树与二叉搜索树》 Paste_Image.png

二叉搜索树#

  • 概述####

二叉搜索树是一类满足以下属性的特殊二叉树:二叉搜索树中的每个非空结点表示一个记录;若某结点左子树不为空,则左子树上的所有结点的值均小于该结点的关键码值;若其右子树不为空,则右子树上的所有节点的值均大于该结点的关键码值。二叉搜索树可以使一课空树,任何点的左右子树都是二叉搜索树。按照中序周游整个二叉树可得到一个由小到大的有序排列。

  • 应用

在实际应用中,经常会碰到这样的操作:在一组记录中检索一个记录,向其中插入一个记录或者删除一个记录等。
对于一个无序线性表,插入记录时只需要把该记录放在表的末端,但在表中查找一个特定记录的检索时间却相对较慢。对于有序线性表,如果用二分查找法检索特定的记录,检索效率较高,但是遇到动态增减变化的情况时(如插入或删除一个元素)需要移动大量的元素。
因此,二叉搜索树的性质可以达到这一要求,运用了二分法将小于结点数的数值放入左子树,将大于结点数的数值放入右子树,可以实现便捷查找。

  • 基本操作

二叉搜索树的基本操作包含:插入、删除和查找。这里我们给出插入、删除和查找最大最小值得算法。

插入结点
二叉搜索树的插入操作具体是这样:将待插入结点的关键码与根结点的关键码相比较,若待插入的关键码小于根结点的关键码,则进入左子树,否则进入右子树。按照同样的方式沿检索路径知道叶结点,确定插入位置,把待插入节点作为一个新叶节点插入到二叉搜索树中。

具体操作如下:

《二叉树与二叉搜索树》 二叉搜索树插入

1.待插入节点为18,先与根结点关键码50比较,小于根结点关键码,则root指向左孩子。
2.与根结点关键码19比较,小于关键码,则root指向左孩子。
3.与根结点关键码5比较,大于关键码,则root指向右孩子。
4.root为NULL,将关键码为18的newpoint插入上一指针的右孩子处。
具体算法实现如下:

void InsertNode( BiTree &root,BiTree &newpoint){   //插入结点
    BiTree point= NULL;
    if ( root == NULL){
        root= newpoint;
        cout<<"已插入"<<root->info<<endl;
        return;
    }
    else point =root;
    while ( point != NULL){
        if ( newpoint->info == point ->info){
            cout<<"已包含的结点"<<endl;
            return;}
        else if( newpoint->info < point ->info ){
            if(point ->leftchild == NULL){
                point->leftchild = newpoint;
                cout<<"已插入"<<point->info<<endl;
                return;
            }
            else point = point ->leftchild;
        }
        else{
            if(point -> rightchild == NULL){
                point -> rightchild = newpoint;
                cout<<"已插入"<<point->info<<endl;
                return;
            }
            else point= point -> rightchild;
        }
    }
}

删除结点
二叉树的删除较为复杂,可以分为四种情况。假设待删除的结点为p,则:

《二叉树与二叉搜索树》 删除结点的不同情况

1.p结点的左孩子与右孩子都为空,则删除p不对二叉树产生影响,可直接free(p)。
2.p结点的左孩子为空,则使p的父结点的右孩子改变为p的右孩子,free(p)。
3.p结点的右孩子为空,则使p的父结点的左孩子改变为p的左孩子,free(p)。
4.p结点的左右孩子都不为空,则比较复杂,应另外讨论。

由于二叉搜索树要求关键码之间满足一定地大小关系,这就使得从树中删除一个结点的算法比较复杂。从二叉搜索树中删除一个结点时,要保持二叉搜索树的性质,就不能再二叉搜索树中留下一个空位置,因此需要用另一个结点来填充这个位置并且保持性质。
设pointer,temppointer是指针变量,其中pointer为要删除的结点。首先,找到待删除的结点pointer,删除该节点的过程如下:

《二叉树与二叉搜索树》 当待删结点有左右孩子时

代码实现过程如下:


void DeleteNode(BiTree &root, int& data)  { 
BiTree parent;
BiTree pointer = root;  
BiTree temmpointer;
if (pointer == NULL) {  
    cout<<"未找到该结点"<<endl;  
    return;  
    }  
  
if (pointer->info == data) {  

    if (pointer->rightchild == NULL && pointer->leftchild == NULL) {   // 当没有左右孩子
        root = NULL;  
        free(pointer);  
    }  
    else if (pointer->rightchild == NULL) {     // 当没有右孩子
        root = pointer->leftchild;  
        free(pointer);  
    }   
    else if (pointer->leftchild == NULL) {     // 当没有左孩子
        root = pointer->rightchild;  
        free(pointer);  
    }   
    else {  
        temmpointer = pointer->rightchild;  

        if (temmpointer->leftchild==NULL)        //当临时结点为叶子结点,即待删结点的左子树只有一枚叶子结点          
              temmpointer->leftchild = pointer->leftchild;  

        else {    //当临时结点有左孩子,则寻找左孩子里关键码值最小的点,作为tempparent
              while (temmpointer->leftchild) {  
                    parent = temmpointer;              //记录tempparent的父结点信息
                    temmpointer = temmpointer->leftchild;  
                    }  
             parent->leftchild = temmpointer->rightchild;  
             temmpointer->leftchild = pointer->leftchild;  
             temmpointer->rightchild = pointer->rightchild;  
             }  
        root = temmpointer;  
        free(pointer);  
    }  
    }   
else if (data > pointer->info) {  
    DeleteNode((pointer->rightchild), data);  
    }  
else if (data < pointer->info) {  
    DeleteNode((pointer->leftchild), data);  
    }  
} 

查找最大值与最小值
最大值即右孩子的右孩子的右孩子……知道右孩子为NULL时,则返回该结点关键码值。
最小值即最左孩子。


void maxmix(BiTree &root){
    BiTree point;
    point= root;
    while(point -> leftchild){
        point=point -> leftchild;
    }
    cout<<"最小结点值为:"<<point->info<<endl;
    point = root;

    while(point -> rightchild){
        point=point -> rightchild;
        }
    cout<<"最大结点值为:"<<point->info<<endl;
}           

总结#

因为最近事情很多,再加上对递归算法理解不深,指针运用不熟练,做了这些可能挺简单的东西却用了很长时间。大量的时间运用在理解递归算法上,然后就是指针运用。
我理解的递归算法就是类似于函数套函数;一般我们考虑算法时都是从上往下设计,但是用到递归时就需要从下往上,因为递归算法出口才是返回数的来源,再根据来源逐步往上推。现在运用还是不太熟练,以后再看看。但是递归好像运用的不多,因为我感觉在一个大程序里,疯狂的占用内存,开辟栈,对计算机硬件的要求太变态了。
对于指针,一开始不知道是什么。不过老师给我讲了讲,有了大概思路,应该就是一个指向内存数据的地址。指针运用的比较多,因此可能需要更多的练习。

附录#

总程序贴在这里:


#include <iostream>
#include<stack>
#include "malloc.h"
using namespace std;

 
typedef struct BiTreeNode{
    int info;   
    struct BiTreeNode *leftchild;
    struct BiTreeNode *rightchild;
}BiTreeNode, *BiTree;

BiTree Parent(BiTree& root, BiTree &current){
    using std::stack;
    stack< BiTree> aStack;
    BiTree point;
    if( root != NULL && current != NULL){
        while( !aStack.empty()||point){
            if(point != NULL){
                if(current == point -> leftchild||current == point -> rightchild)
                    return point;
                aStack.push(point);
                point = point ->leftchild;
            }
            else{
                point = aStack.top();
                aStack.pop();
                point = point -> rightchild;
            }
        }
    }
}

void InsertNode( BiTree &root,BiTree &newpoint){
    BiTree point= NULL;
    if ( root == NULL){
        root= newpoint;
        cout<<"已插入"<<root->info<<endl;
        return;
    }
    else point =root;
    while ( point != NULL){
        if ( newpoint->info == point ->info){
            cout<<"已包含的结点"<<endl;
            return;}
        else if( newpoint->info < point ->info ){
            if(point ->leftchild == NULL){
                point->leftchild = newpoint;
                cout<<"已插入"<<point->info<<endl;
                return;
            }
            else point = point ->leftchild;
        }
        else{
            if(point -> rightchild == NULL){
                point -> rightchild = newpoint;
                cout<<"已插入"<<point->info<<endl;
                return;
            }
            else point= point -> rightchild;
        }
    }
}

void PreOrder(BiTree &root){    //先序遍历二叉树
    if( root!=NULL){
        cout<< root -> info<<" ";
        PreOrder(root->leftchild);
        PreOrder(root->rightchild);
    }
}

void InOrder(BiTree &root){    //中序遍历二叉树
    if( root!=NULL){
        InOrder(root->leftchild);
        cout<< root -> info<<" ";       
        InOrder(root->rightchild);
    }
}

void PostOrder(BiTree &root){    //后序遍历二叉树
    if( root!=NULL){
        PostOrder(root->leftchild);
        PostOrder(root->rightchild);
        cout<< root -> info;
    }
}

int NodeNum(BiTree &root){   //结点数
    if( root==NULL){
        return 0;}
    else{
        int num1=NodeNum(root->leftchild);
        int num2=NodeNum(root->rightchild);
        return num1+num2+1;
    }
}

int LeafNum(BiTree &root){
    if(root!=NULL && root->leftchild==NULL && root -> rightchild ==NULL){
        return 1;
    }
    else if(root==NULL){
        return 0;}
    else {
        return LeafNum(root->leftchild)+LeafNum(root->rightchild);
    }
}

int GetDepth(BiTree &root)    //获取树的深度
{  
    int lDepth;
    int rDepth;
    if (root == NULL)  
    {  
        return 0;  
    }  
    lDepth= GetDepth(root->leftchild);
    rDepth= GetDepth(root->rightchild);
    
    if (lDepth> rDepth) return lDepth+1;
    else{ return rDepth +1;} 
} 

void maxmix(BiTree &root){
    BiTree point;
    point= root;
    while(point -> leftchild){
        point=point -> leftchild;
    }
    cout<<"最小结点值为:"<<point->info<<endl;
    point = root;

    while(point -> rightchild){
        point=point -> rightchild;
        }
    cout<<"最大结点值为:"<<point->info<<endl;
}           

void DeleteNode(BiTree &root, int& data)  {    
BiTree parent;
BiTree pointer = root;  
BiTree temmpointer;
if (pointer == NULL) {  
    cout<<"未找到该结点"<<endl;  
    return;  
    }  

if (pointer->info == data) {  

    if (pointer->rightchild == NULL && pointer->leftchild == NULL) {   // 当没有左右孩子
        root = NULL;  
        free(pointer);  
    }  
    else if (pointer->rightchild == NULL) {     // 当没有右孩子
        root = pointer->leftchild;  
        free(pointer);  
    }   
    else if (pointer->leftchild == NULL) {     // 当没有左孩子
        root = pointer->rightchild;  
        free(pointer);  
    }   
    else {  
        temmpointer = pointer->rightchild;  

        if (temmpointer->leftchild==NULL)        //当临时结点为叶子结点,即待删结点的左子树只有一枚叶子结点          
              temmpointer->leftchild = pointer->leftchild;  

        else {    //当临时结点有左孩子,则寻找左孩子里关键码值最小的点,作为tempparent
              while (temmpointer->leftchild) {  
                    parent = temmpointer;              //记录tempparent的父结点信息
                    temmpointer = temmpointer->leftchild;  
                    }  
             parent->leftchild = temmpointer->rightchild;  
             temmpointer->leftchild = pointer->leftchild;  
             temmpointer->rightchild = pointer->rightchild;  
             }  
        root = temmpointer;  
        free(pointer);  
    }  
    }   
else if (data > pointer->info) {  
    DeleteNode((pointer->rightchild), data);  
    }  
else if (data < pointer->info) {  
    DeleteNode((pointer->leftchild), data);  
    }  
}

//创建一棵二叉查找树  
void create(BiTree& root, int *keyArray,int length)  
{  
    int i=0;  
    //逐个结点插入二叉树中  
    for(i=0;i<length;i++){ 
        BiTree newpoint=(BiTree)malloc(sizeof(BiTreeNode)); 
        newpoint ->info = keyArray[i];
        newpoint ->leftchild=NULL;
        newpoint ->rightchild=NULL;
        InsertNode(root,newpoint);  
}  
}
 
bool PrintTree(BiTree T, int nLayer){
    if(T == NULL)
        return false;
    PrintTree(T -> rightchild, nLayer+3);
    for (int i = 0; i < nLayer; i++){
        cout<<" ";
    }
    cout<<T ->info << endl;
    PrintTree( T-> leftchild, nLayer +3);
    return true;
}
        

int main(void)  
{   
    int nLayer=0;
    int choose=0;
    int value;
    BiTree root=NULL;  
    int nodeArray[11]={15,6,18,3,7,17,20,2,4,13,9};  
    create(root,nodeArray,11);  
    PreOrder(root);
    cout<<endl;
    InOrder(root);
    cout<<"打印树:"<<endl;
    PrintTree(root,nLayer);
    
    while(1){
        cout<<endl;
        cout<<"-------------------------------------------"<<endl;
        cout<<"请选择操作:\n1.插入结点.  2.删除结点.  3.查找最大最小结点.\n4.获得树深.  5.获得总结点数.  6.获得总叶子数.\n7.打印树"<<endl;
        cin>>choose;
        switch(choose){

        case(1):{
            cout<<"请输入插入数据"<<endl;
            cin>>value;
            BiTree newpoint=(BiTree)malloc(sizeof(BiTreeNode)); 
            newpoint ->info = value;
            newpoint ->leftchild=NULL;
            newpoint ->rightchild=NULL;
            InsertNode(root,newpoint);
            break;
            }
        case(2):{
            cin>>value;
            DeleteNode(root,value);
            break;
                }
        case(3):{
            maxmix(root);
            break;
                }
        case(4):{
            cout<<GetDepth(root)<<endl;
            break;}
        case(5):{
            cout<<NodeNum(root)<<endl;
            break;
                }
        case(6):{
            cout<<LeafNum(root)<<endl;
            break;}
        case(7):{
            PrintTree(root,nLayer);
            break;
                }

        }
    }
    system("pause");

    return 0;  
}

运行如下:

《二叉树与二叉搜索树》 运行结果

    原文作者:长胖的鱼
    原文地址: https://www.jianshu.com/p/814a19507731
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞