C数据结构--二叉树的基本操作

C数据结构–二叉树的基本操作

本蒟蒻的第一篇博客~,如果有错误请各位大佬指正!!!!!!感激不尽。
好啦好啦,回归正题。话不多说,直接从数据结构的演示系统来分析二叉树的各种操作是如何实现的吧!
(主要代码是头哥平台数据结构实验的作业,有需要的uu可以参考)

一些约定和声明

//常量定义
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2


//结构体声明
typedef int status;
typedef int KeyType; 
typedef struct {
    KeyType  key;
    char others[20];
} TElemType; //二叉树结点类型定义
typedef struct BiTNode{  //二叉链表结点的定义
	TElemType  data;
	struct BiTNode *lchild,*rchild;
} BiTNode, *BiTree;
struct Node{
    string name;
    BiTree T;
    //int len;
}forest[100];

操作1.二叉树的创建

二叉树的创建方式有很多种,因为提供的创建二叉树的序列多种多样。先来看一种以带空结点的先序遍历串来创建二叉树。

递归法!

//i是全局变量
void create(BiTree &T,TElemType def[]){
    if(def[i].key==0){
        T=NULL;
        i++;
        return ;
    }
    else if(def[i].key==-1)
        return ;
    else{
        T=(BiTree)malloc(sizeof(BiTNode));
        T->data.key=def[i].key;
        strcpy(T->data.others,def[i++].others);
        T->lchild=NULL;
        T->rchild=NULL;
    }
    create(T->lchild,def);
    create(T->rchild,def);

}


非递归!(有一点点费脑子)

第二种是采用非递归算法,借助了c++的STL中的stack辅助实现。如果是C语言,可以考虑用一个结构体数组来模拟栈,然后入栈出栈操作通过自己构造函数实现。

//TElemType definition[]//结构体数组,含有一个int和一个字符串
for(int i=0;definition[i].key!=-1;i++){
		vis[definition[i].key]++;
		if(vis[definition[i].key]>=2&&definition[i].key!=0)
			return ERROR;
	}//遍历所有数据,记录关键字,判重

在某些题目要求中,会有要求二叉树每个节点的关键字不能重复,所以会先进行遍历原始序列,通过vis数组判定关键字是否重复。考虑到关键字可能存在负数,vis声明如下

map<int,int>vis;

通过STL的map映射来判重(当然也可以用set)。
接下来就开始任重而道远的非递归创建了!
首先创建根节点,常规的申请空间然后赋值。
创建完成以后入栈。

	BiTree root=(BiTree)malloc(sizeof(BiTNode)),cur;
	root->data.key=definition[0].key;
	strcpy(root->data.others,definition[0].others);
	q.push(root);

再来考虑先序遍历的特点:一直向左深入,直到最左子节点再返回。所以对于先序遍历的序列,我们首先要不断的向左建立子树。

			if(flag==0&&definition[i].key!=0){
				BiTree p=(BiTree)malloc(sizeof(BiTNode));
				p->data.key=definition[i].key;
				strcpy(p->data.others,definition[i].others);
				cur->lchild=p;
				i++;
				q.push(cur->lchild);
			}

不断的重复类似建立根节点的操作,直到左子树没有了(defination.key==0表示空结点)
然后再开始建立右子树。那怎么告诉我们的电脑我们现在要建立右子树了呢?

int flag=0;

这时候一就需要一个标志来帮助我们了!

				if(flag==0){
					if(cur!=NULL)
						cur->lchild=NULL,flag=1;
					i++;
				}

defination.key0以后,触发建立空结点的模式。flag0 说明左边这个是空结点,赋值NULL以后,flag改成1,告诉电脑:接下来要建立右边的节点了!请注意!

			else if(flag&&definition[i].key!=0){
				BiTree p=(BiTree)malloc(sizeof(BiTNode));
				p->data.key=definition[i].key;
				strcpy(p->data.others,definition[i].others);
				cur->rchild=p;
				i++;
				q.pop();
				q.push(cur->rchild);
				flag=0;//每次右子树建立完成即可出栈,flag作为标志判断左右
				
			}

建立过程与建立左节点相似。但是!!!!右节点的建立,标志这个节点的左右都建立好了!!!于是就不需要留在栈里面了,于是调用pop()出栈(不然会造成死循环哟!)。

T=root;

最后把根节点传出去就可以美美完成二叉树的非递归建立辣!
完整代码如下。

status CreateBiTree(BiTree &T,TElemType definition[])
/*根据带空枝的二叉树先根遍历序列definition构造一棵二叉树,将根节点指针赋值给T并返回OK,
如果有相同的关键字,返回ERROR。此题允许通过增加其它函数辅助实现本关任务*/
{
	//int a[5000]={0},vis[5000]={0};
	for(int i=0;definition[i].key!=-1;i++){
		vis[definition[i].key]++;
		if(vis[definition[i].key]>=2&&definition[i].key!=0)
			return ERROR;
	}//遍历所有数据,记录关键字,判重
    if(definition[0].key==0)
        return OK;
	stack<BiTree>q;//用一个栈,实现非递归建立二叉树
	BiTree root=(BiTree)malloc(sizeof(BiTNode)),cur;
	root->data.key=definition[0].key;
	strcpy(root->data.others,definition[0].others);
	q.push(root);
	int i=1;
	int flag=0;
	while(q.size()){
		BiTree cur=q.top();
		if(definition[i].key!=-1){
			if(flag==0&&definition[i].key!=0){
				BiTree p=(BiTree)malloc(sizeof(BiTNode));
				p->data.key=definition[i].key;
				strcpy(p->data.others,definition[i].others);
				cur->lchild=p;
				i++;
				q.push(cur->lchild);
			}
			else if(flag&&definition[i].key!=0){
				BiTree p=(BiTree)malloc(sizeof(BiTNode));
				p->data.key=definition[i].key;
				strcpy(p->data.others,definition[i].others);
				cur->rchild=p;
				i++;
				q.pop();
				q.push(cur->rchild);
				flag=0;//每次右子树建立完成即可出栈,flag作为标志判断左右
				
			}
			else if(definition[i].key==0){
				if(flag==0){
					if(cur!=NULL)
						cur->lchild=NULL,flag=1;
					i++;
				}
				else{
					if(cur!=NULL)
						cur->rchild=NULL;
					i++;
					q.pop();
				}	
			}
		}
		else{
			break;
		}
	}
	T=root;
	return OK;
}

操作2.二叉树清空

首先从根节点开始,依次释放每个节点的空间,释放完记得指向NULL,否则容易因为野指针发生错误

status ClearBiTree(BiTree &T)
//将二叉树设置成空,并删除所有结点,释放结点空间
{
    if(!T)
        return INFEASIBLE;
    if(T){
        ClearBiTree(T->lchild);
        ClearBiTree(T->rchild);
        free(T);
        T=NULL;
    }
    return OK;
}

操作3.二叉树深度

这个就比较有意思了,需要对递归的理解比较深入。可以试着脑补一下整个递归过程。在叶子节点处,左孩子右孩子都不存在,返回给叶子节点的ansl,ansr都是0,于是叶子节点ans也是0,但是叶子节点这里有一层,所以返回给上一层是ans+1.同理,不断重复,ans代表的是这个节点的左右子树深度更大的一个,加一意思是加上自己这一层。

int BiTreeDepth(BiTree T)
//求二叉树T的深度
{
    int ansl,ansr,ans;
    if(T){
        ansl=BiTreeDepth(T->lchild);
        ansr=BiTreeDepth(T->rchild);
        ans=ansl>ansr?ansl:ansr;
        return ans+1;
    }
    return 0;
}

操作4.查找指定关键字的节点

根据关键字查找的思路跟在数组中查找相似,都是需要遍历二叉树,找到对应的节点。具体实现如下。

BiTNode* LocateNode(BiTree T,KeyType e)
//查找结点
{
    if(!T)
        return NULL;
    if(T->data.key==e)
        return T;
    BiTNode* ans;
    if(T){
        ans=LocateNode(T->lchild,e);
        if(ans)
            return ans;
        ans=LocateNode(T->rchild,e);
        if(ans)
            return ans;
    }
    return ans;
}

操作5.节点赋值

为了防止关键字重复,首先遍历二叉树,把所有关键字统计,防止赋值导致关键字重复。

map<int,int>vis;//标记数组,统计关键字判重,用map是为了防止负数关键字出现
void traverse(BiTree T){
    if(T){
        vis[T->data.key]++;
        traverse(T->lchild);
        traverse(T->rchild);
    }
    return ;
}
status Assign(BiTree &T,KeyType e,TElemType value)
//实现结点赋值。此题允许通过增加其它函数辅助实现本关任务
{
    BiTNode* target=LocateNode(T,e);
    if(target==NULL)
        return ERROR;
    //memset(vis,0,sizeof(vis));
    vis.clear();
    traverse(T);
    if(vis[value.key]&&value.key!=target->data.key)
        return ERROR;
    target->data.key=value.key;
    strcpy(target->data.others,value.others);
    return OK;
}

操作6.获得兄弟节点

获得兄弟节点思路与查找节点相似,只不过需要先定位到指定节点的父节点,即

T->lchild&&T->lchild->data.key==e
T->rchild&&T->rchild->data.key==e
//首先保证这个节点存在,否则访问空指针会导致错误

然后在根据节点的位置返回另一个节点就可以了,具体实现如下。

BiTNode* GetSibling(BiTree T,KeyType e)
//实现获得兄弟结点
{
    if(!T||T->lchild==NULL&&T->rchild==NULL)
        return NULL;
    if ((!T->lchild && T->rchild && T->rchild->data.key == e) || 
        (T->lchild && !T->rchild && T->lchild->data.key == e)) // 当前结点只有一个孩子且该孩子的关键字等于e,则返回另一个孩子
        return T->lchild ? T->rchild : T->lchild;
    if(T->lchild&&T->lchild->data.key==e)
        return T->rchild;
    if(T->rchild&&T->rchild->data.key==e)//首先保证左右孩子的存在再讨论其data
        return T->lchild;
    BiTNode* ans=NULL;
    if(T){
        ans=GetSibling(T->lchild,e);
        if(ans)
            return ans;
        ans=GetSibling(T->rchild,e);
        if(ans)
            return ans;
    }
    return ans;
}

操作7.插入节点

插入有很多种,这里根据具体题目描述如下:

任务描述
本关任务:编写一个函数实现结点插入
函数原型:status InsertNode(BiTree &T,KeyType e,int LR,TElemType c);
功能说明:e是和T中结点关键字类型相同的给定值,LR为0或1,c是待插入结点;根据LR为0或者1,插入结点c到T中,作为关键字为e的结点的左或右孩子结点,结点e的原有左子树或右子树则为结点c的右子树,返回OK。如果插入失败,返回ERROR。
特别地,当LR为-1时,作为根结点插入,原根结点作为c的右子树。

根据题意,插入操作不难实现,只需要修改指针指向就可以了。

//有些之前写过的函数这里就直接调用了
status InsertNode(BiTree &T,KeyType e,int LR,TElemType c)
//插入结点。此题允许通过增加其它函数辅助实现本关任务
{
    BiTree cur=LocateNode(T,e);
    if(cur==NULL)
        return ERROR;
    //memset(vis,0,sizeof(vis));
    vis.clear();
    traverse(T);
    if(vis[c.key])
        return ERROR;
    if(LR==0){
        //if(cur->lchild!=NULL)
            //return ERROR;
        BiTree p=(BiTree)malloc(sizeof(BiTNode));
        p->data.key=c.key;
        strcpy(p->data.others,c.others);//这里有点麻烦了,结构体是可以直接赋值的
        p->rchild=cur->lchild;
        p->lchild=NULL;
        cur->lchild=p;
    }
    else if(LR==1){
        //if(cur->rchild!=NULL)
            //return ERROR;
        BiTree p=(BiTree)malloc(sizeof(BiTNode));
        p->data.key=c.key;
        strcpy(p->data.others,c.others);
        p->rchild=cur->rchild;
        p->lchild=NULL;
        cur->rchild=p;
    }
    else if(LR==-1){
        BiTree p=(BiTree)malloc(sizeof(BiTNode));
        p->data.key=c.key;
        strcpy(p->data.others,c.others);
        p->rchild=T;
        p->lchild=NULL;//不用的节点一定要放NULL,不然申请了空间会乱码
        T=p;
    }
    return OK;
}

操作8.删除节点

具体题目要求如下:

本关任务:编写一个函数实现结点删除
函数原型:status DeleteNode(BiTree &T,KeyType e);
功能说明:e是和T中结点关键字类型相同的给定值。删除T中关键字为e的结点;同时,根据该被删结点的度进行讨论:
如关键字为e的结点度为0,删除即可;
如关键字为e的结点度为1,用关键字为e的结点孩子代替被删除的e位置;
如关键字为e的结点度为2,用e的左子树(简称为LC)代替被删除的e位置,将e的右子树(简称为RC)作为LC中最右节点的右子树。
成功删除结点后返回OK,否则返回ERROR。

代码有点长,而且有一个查找父节点的辅助函数。
首先考察待删除节点的几种情况:

  1. 没有子树
  2. 有左子树但是没有右子树
  3. 有右子树但是没有左子树
  4. 两颗子树都有

第一种情况最简单,直接free节点就完事

		if(l->lchild==NULL&&l->rchild==NULL){
            free(l);
            T=NULL;
        }

第二三两种情况相同,在free之前用存在的那颗子树的根节点顶替删除节点的位置,然后释放空间

		else if(l->lchild==NULL&&l->rchild!=NULL){
            T=l->rchild;
            free(l);
        }
        else if(l->rchild==NULL&&l->lchild!=NULL){
            T=l->lchild;
            free(l);
        }

第四种比较复杂
需要借助父节点来进行辅助,根据题目要求用特定的节点顶替删除节点

		if(l->lchild&&l->rchild){
            T=l->lchild;
            cur=l->lchild;
            while(cur->rchild!=NULL){
                cur=cur->rchild;
            }
            cur->rchild=l->rchild;
            free(l);
        }

*注意!!如果是根节点删除需要特殊处理!具体参见代码。

BiTNode* LocateNoder(BiTree T,KeyType e)
//查找fa结点
{
    if(!T)
        return NULL;
    if(T->lchild!=NULL&&T->lchild->data.key==e||T->rchild!=NULL&&T->rchild->data.key==e)
        return T;
    BiTNode* ans;
    if(T){
        ans=LocateNoder(T->lchild,e);
        if(ans)
            return ans;
        ans=LocateNoder(T->rchild,e);
        if(ans)
            return ans;
    }
    return ans;
}
status DeleteNode(BiTree &T,KeyType e)
//删除结点。此题允许通过增加其它函数辅助实现本关任务
{
    BiTree cur=LocateNoder(T,e);//找到这个节点的父节点方便进行删除操作
    BiTree root=LocateNode(T,e);//根节点特判
    if(cur==NULL&&root==NULL)
        return ERROR;//待删除节点不存在
    BiTree l;
    if(cur==NULL){
        l=root;
        if(l->lchild==NULL&&l->rchild==NULL){
            free(l);
            T=NULL;
        }
        else if(l->lchild==NULL&&l->rchild!=NULL){
            T=l->rchild;
            free(l);
        }
        else if(l->rchild==NULL&&l->lchild!=NULL){
            T=l->lchild;
            free(l);
        }
        else if(l->lchild&&l->rchild){
            T=l->lchild;
            cur=l->lchild;
            while(cur->rchild!=NULL){
                cur=cur->rchild;
            }
            cur->rchild=l->rchild;
            free(l);
        }
        return OK;
    }
    else{
        l=cur->lchild;
        if(l!=NULL&&l->data.key==e){
            if(l->lchild==NULL&&l->rchild==NULL){
                free(l);
                cur->lchild=NULL;
            }
            else if(l->lchild==NULL&&l->rchild!=NULL){
                cur->lchild=l->rchild;
                free(l);
            }
            else if(l->rchild==NULL&&l->lchild!=NULL){
                cur->lchild=l->lchild;
                free(l);
            }
            else if(l->lchild&&l->rchild){
                cur->lchild=l->lchild;
                cur=cur->lchild;
                while(cur->rchild!=NULL){
                    cur=cur->rchild;
                }
                cur->rchild=l->rchild;
                free(l);
            }
            return OK;
        }   
        l=cur->rchild;
        if(l!=NULL&&l->data.key==e){
            if(l->lchild==NULL&&l->rchild==NULL){
                free(l);//free后变成野指针,要注意!!!
                cur->rchild=NULL;
            }
            else if(l->lchild==NULL&&l->rchild!=NULL){
                cur->rchild=l->rchild;
                free(l);
            }
            else if(l->rchild==NULL&&l->lchild!=NULL){
                cur->rchild=l->lchild;
                free(l);
            }
            else if(l->lchild!=NULL&&l->rchild!=NULL){
                cur->rchild=l->lchild;
                cur=cur->rchild;
                while(cur->rchild!=NULL){
                    cur=cur->rchild;
                }
                cur->rchild=l->rchild;
                free(l);
            }
            return OK;
        }
        return ERROR;
    }
}

操作10.二叉树的四种遍历算法

递归还是比较容易实现的,前序中序后序遍历的区别就在于打印语句的位置不同

		PreOrderTraverse(T->lchild,visit);
        visit(T);
        PreOrderTraverse(T->rchild,visit);

  		InOrderTraverse(T->lchild,visit);
        visit(T);
        InOrderTraverse(T->rchild,visit);

		PostOrderTraverse(T->lchild,visit);
        PostOrderTraverse(T->rchild,visit);
        visit(T);//visit是打印结构体成员的函数

下面浅谈一下非递归实现前序遍历(因为前序非递归最简单hhhhhhh)
首先,三种顺序的非递归借助的数据结构都是栈。根据前序遍历是不断向左进行深入,而栈的特点是后进先出,所以!前序遍历时先把当前节点的右孩子入栈!!!!再入栈左孩子,这样就能保证下一个栈顶取出的时左孩子辣!!然后不断重复就okl。

再讲讲层序遍历,这个就比较容易了,借助的数据结构是队列。为什么呢?因为如果是栈,每次取栈顶,会造成不断的深入的现象。(可以脑补一下)因为是不断把孩子入栈,然后栈顶也是孩子,于是就不断往下。而队列呢,再孩子入队以后,队头还是父母,这样就可以实现把这一层节点的孩子全部入队,而不会造成层层深入。

status PreOrderTraverse(BiTree T,void (*visit)(BiTree))
//先序遍历二叉树T
{
    stack<BiTree>s;
    s.push(T);
    if(T!=NULL){
        while(s.size()){
            BiTree p=s.top();
            s.pop();
            printf(" %d,%s",p->data.key,p->data.others);
            if(p->rchild!=NULL){
                s.push(p->rchild);
            }
            if(p->lchild!=NULL){
                s.push(p->lchild);
            }
        }
    }
    return OK;
}



status InOrderTraverse(BiTree T,void (*visit)(BiTree))
//中序遍历二叉树T
{
    if(!T)
        return 0;
    if(T){

        InOrderTraverse(T->lchild,visit);
        visit(T);
        InOrderTraverse(T->rchild,visit);
    }
    return OK;
}



status PostOrderTraverse(BiTree T,void (*visit)(BiTree))
//后序遍历二叉树T
{
    if(!T)
        return 0;
    if(T){

        PostOrderTraverse(T->lchild,visit);
        //visit(T);
        PostOrderTraverse(T->rchild,visit);
        visit(T);
    }
    return OK;
}


status LevelOrderTraverse(BiTree T,void (*visit)(BiTree))
//按层遍历二叉树T
{
    if(!T)
        return 0;
    queue<BiTree>q;
    q.push(T);
    while(q.size()){
        BiTree cur=q.front();
        q.pop();
        visit(cur);
        if(cur->lchild!=NULL)
            q.push(cur->lchild);
        if(cur->rchild!=NULL)
            q.push(cur->rchild);
    }
    return OK;
}


好啦,就讲到这里辣!第一篇博客撰写不易!希望大家多多支持呀!

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值