数据结构教程—二叉树相关操作C++实现

介绍:

代码使用如下二叉树作为样例,其括号表示法表示为:A(B(D(,G)),C(E,F)).
前序遍历结果:A B D G C E F
中序遍历结果:D G B A E C F
后序遍历结果:G D B E F C A
层次遍历结果:A B C D E F G
在这里插入图片描述

头文件及类定义:

//创建一个辅助的node类以及BinaryTree类中多数函数使用BTNode *p的参数是为了实现递归,而同时简化结构
#include<iostream>
#include<stack>
constexpr auto MaxSize = 30;

using namespace std;

typedef char Element;

typedef class node {
public:
	Element data;
	node* lchild;//指向左孩子节点
	node* rchild;//指向右孩子节点
}BTNode;

class BinaryTree {
private:
	BTNode* root;//根节点指针

	void DelTree(BTNode *p);//私有函数,只被析构函数调用

public:
	BinaryTree();//无参构造
	BinaryTree(char *str);//有参构造
	~BinaryTree();//析构函数不能通过调用自身来释放节点空间,所以采用调用DelTree的方式
	BTNode* GetRoot();//返回根节点
	BTNode* FindBTNode(BTNode *p,Element x);//查找值为x的节点,返回其指针
	BTNode* LchildBTNode(BTNode* p);//返回*p节点的左孩子节点指针
	BTNode* RchildBTNode(BTNode* p);//返回*p节点的右孩子节点指针
	int Depth_BinaryTree(BTNode *p);//返回树高
	void Disp_BinaryTree(BTNode *p);//以括号表示法输出树

	//递归遍历算法
	void PreOrder(BTNode* p);//先序遍历
	void InOrder(BTNode* p);//中序遍历
	void PostOrder(BTNode* p);//后序遍历

	//非递归遍历算法
	void PreOrder2(BTNode* b);
	void InOrder2(BTNode* b);
	void PostOrder2(BTNode* b);

	void LevelOrder(BTNode* b);//层次遍历算法
	
};

各函数的实现:

两个构造函数:

有参构造函数使用括号表示法表示的合法二叉树为参数,并使用栈作为辅助来构造二叉树。二叉树的结构特点使得其回到某一节点的前驱节点比较困难,因而使用栈保存当前操作节点的前驱节点,使得对某一节点进行操作后能直接(通过出栈)获得其前驱节点指针。后面使用到栈的关于二叉树的算法也是为了实现类似功能。

//无参构造
BinaryTree::BinaryTree() {
	root = new BTNode;
}
//有参构造
BinaryTree::BinaryTree(char *str) {//传入一个采用括号表示法表示的合法二叉树
	BTNode* p = NULL;
	root = NULL;
	BTNode* St[MaxSize];//以此栈辅助创建二叉树
	int top = -1;
	int j = 0;//用于遍历str
	int k=0;//用于标识当前节点是栈顶节点的左孩子节点(值为1)或者右孩子节点(值为2),此处赋初值为了减少警告,并无实际效果
	char ch=str[j];

	while (ch != '\0') 
	{//遍历str中所有元素
		switch (ch)
		{
		case '(':
			top++; St[top] = p; k = 1;
			break;
		case ')': 
			top--;
			break;
		case ',':
			k = 2;//k值置为2,标识后面处理的节点为右节点
			break;
		default:	
			p = new BTNode();
			p->data = ch; p->lchild = p->rchild = NULL;
			if (root == NULL)//switch语句的第一次进入直接执行此处
				root = p;
			else 
			{
				switch (k)
				{
				case 1:St[top]->lchild = p; break;
				case 2:St[top]->rchild = p; break;
				default:
					break;
				}
			}
			break;
		}
		ch = str[++j];
	}
}
析构函数:

析构函数通过调用DelTree函数实现释放空间的功能。这样做是由于释放空间过程用到了递归,我们将递归过程放到另一个函数中,否则功能将很难实现。

//私有函数,被析构函数调用
void BinaryTree::DelTree(BTNode*p) {
	if (p != NULL) {
		DelTree(p->lchild);
		DelTree(p->rchild);
		free(p);
	}
}

//析构函数,调用DelTree函数,DelTree函数将树中节点递归删除
BinaryTree::~BinaryTree() {
	if (root == NULL)
		DelTree(root);
}
返回根节点函数:

添加该函数的目的是为了能在其他函数中(此例中是在main函数中)访问根节点。我们使用两个类(一个BTNode类定义二叉树节点类型,一个BinaryTree类定义对二叉树的各种操作和维持一个root指针)来描述二叉树,在各个重要函数的实现中(所有使用递归的函数)更多的是对BTNode类节点的操作,而这些操作定义在了BinaryTree类中,所以对操作过程的调用必定要用传参方式,这个过程就需要在调用位置获取root指针。当然由此可以想到另一个做法是将函数都放到BTNode类中,取消使用BinaryTree类,但由此会产生一些新的问题,比如root节点怎么维持、怎么为成员开辟空间等。

//返回根节点
BTNode* BinaryTree::GetRoot() {
	return root;
}
FindBTNode函数:
BTNode* BinaryTree::FindBTNode(BTNode* p, Element x) {
	BTNode* q;
	if (p == NULL)
		return NULL;
	else if (p->data == x)
		return p;
	else {
		q = FindBTNode(p->lchild,x);//递归调用,遍历左子树
		if (q != NULL)//在左子树中查找到值为x的节点时返回p
			return q;
		else
			return FindBTNode(p->rchild,x);//递归调用,遍历右子树,此时即便未查找到也返回p
	}
}
返回左孩子节点和返回右孩子节点函数:

此处是两个简单例子,它们使用BinaryTree中的函数但操作对象却是BTNode类型。

//返回*p节点的左孩子节点指针
BTNode* BinaryTree::LchildBTNode(BTNode* p) {
	return p->lchild;
}
//返回*p节点的右孩子节点指针
BTNode* BinaryTree::RchildBTNode(BTNode* p) {
	return p->rchild;
}
Depth_BinaryTree函数:
//返回树高
int BinaryTree::Depth_BinaryTree(BTNode*p) {
	int rchilddep,lchilddep;
	if (p== NULL)
		return 0;
	else {
		lchilddep = Depth_BinaryTree(p->lchild);//递归求左子树树高
		rchilddep = Depth_BinaryTree(p->rchild);//递归求右子树树高
		return (lchilddep > rchilddep) ? (lchilddep + 1) : (rchilddep + 1);
	}

}
Disp_BinaryTree函数:
//输出树
void BinaryTree::Disp_BinaryTree(BTNode* p) {
	if (p != NULL) {
		cout << p->data;
		if (p->lchild!= NULL || p->rchild != NULL) {//当左节点和右节点有一个不为空即执行下面语句
			cout << "(";
			Disp_BinaryTree(p->lchild);	//处理左子树
			if (p->rchild != NULL)	//当左子树不为空才输出“,”
				cout << ",";
			Disp_BinaryTree(p->rchild);	//处理右子树
			cout << ")";	//左右子树处理完输出")"
		}
	}
}
三种遍历树的递归函数:
//先序遍历
void BinaryTree::PreOrder(BTNode* p) {
	if (p != NULL) {
		cout << p->data;
		PreOrder(p->lchild);//递归遍历左子树
		PreOrder(p->rchild);//递归遍历右子树
	}
}
//中序遍历
void BinaryTree::InOrder(BTNode* p) {
	if (p != NULL) {
		InOrder(p->lchild);
		cout << p->data;
		InOrder(p->rchild);
	}
}
//后序遍历
void BinaryTree::PostOrder(BTNode* p) {
	if (p != NULL) {
		PostOrder(p->lchild);
		PostOrder(p->rchild);
		cout << p->data;
	}
}
前序遍历(非递归)函数:

与构造函数类似,使用栈获得直接退回某一节点的前驱节点的功能。
执行过程:先将根节点进栈,在栈不为空是循环:出栈p,访问p节点,若其右孩子节点不为空将右孩子节点进栈,若其左孩子节点不为空再将其左孩子节点进栈。

void BinaryTree::PreOrder2(BTNode* b) {
	BTNode* St[MaxSize], * p;
	int top = -1;

	if (b != NULL){
		top++;
		St[top] = b;//节点入栈
		while (top > -1) {
			p = St[top]; top--;//出栈并访问栈顶节点
			cout << p->data<<" ";//多输出一个空格以区分递归和非递归
			if (p->rchild != NULL) {//右孩子进栈,先进栈右孩子以便左孩子先出栈
				top++;
				St[top] = p->rchild;
			}
			if (p->lchild != NULL) {//左孩子进栈
				top++;
				St[top] = p->lchild;
			}
		}
		cout << endl;
	}
}
中序遍历(非递归)函数:

执行过程:用指针指向当前要处理的节点。先扫描(并非访问)根节点的所有左节点并将它们一一进栈,当无左节点时表示栈顶元素无左节点,然后出栈这个节点, 并访问它,将p指向刚出栈节点的右孩子,对右子树做同样的处理。需要注意的是,当节点*p的所有左下节点进栈后,这时的栈顶节点要么没有左子树要么其左子树已访问过,就可以访问这个栈顶节点。如此重复操作,直到栈空为止。

void BinaryTree::InOrder2(BTNode* b) 
{
	BTNode* St[MaxSize], * p;
	int top = -1;
		p = b;
	if (p != NULL) 
	{
		while (top > -1 || p != NULL) 
		{
			while (p!=NULL)
			{//扫描p的所有左子节点并将其依次入栈
			top++;
			St[top] = p;
			p = p->lchild;
			}
		//执行到此处时,栈顶元素没有左孩子节点或左孩子节点均已访问过
			if (top > -1) 
			{
				p = St[top];
				top--;
				cout << p->data << " ";//访问出栈的元素 
				p = p->rchild;//转向处理右孩子节点
			}
		}
		cout << endl;
	}
}
后序遍历(非递归)函数:

采用一个栈保存需要返回的节点指针,先扫描根节点的所有左孩子节点并一一进栈,出栈一个节点 b作为当前节点,然后扫描右子树。当一个节点的左右子树均访问后再访问该节点,如此重复操作,直到栈空为止。
其中难点是如何判断一个节点
b的右子树已访问过(实际上右孩子节点已访问过的话,则其右子树就已访问过),为此用p指针保存刚刚访问过的节点(初值为NULL),若b->rchild == p成立(在后序遍历中,b的右孩子节点一定刚好在访问b之前访问),说明b的左右子树均已访问,现在应访问b.

void BinaryTree::PostOrder2(BTNode* b) {
	BTNode* St[MaxSize],*p;
	int flag, top = -1;

	if (b != NULL) {
		do {
			while (b != NULL) 
			{//将*b的所有左节点进栈
				top++;
				St[top] = b;
				b = b->lchild;
			}
			//执行到此处时,栈顶元素没有左孩子节点或左孩子节点均已访问过
			p = NULL;
			flag = 1;
			while (top != -1 && flag) 
			{
				b = St[top];
				if (b->rchild == p) 
				{
					cout << b->data << " ";
					top--;
					p = b;
				}
				else 
				{
					b = b->rchild;
					flag = 0;
				}
			}

		} while (top != -1);
		cout << endl;
	}
}
层次遍历函数:

在进行层次遍历时,对一层节点访问完后,在按照对它们的访问次序对各个节点的左右孩子顺序访问。这样一层一层进行,先访问的节点其左右孩子也要先访问,与队列的操作原则比较吻合,因此用环形队列来辅助实现。
执行过程:先将根节点进队,在队不为空时循环:从队中出列一个节点*p,访问它;若它有左孩子节点将左孩子节点进队,若它有右孩子节点将右孩子节点进队。如此直到队列为空。

//层次遍历算法
void BinaryTree::LevelOrder(BTNode* b) {
	BTNode* p;
	BTNode* qu[MaxSize];
	int front, rear;
	front = rear = -1;
	rear++;
	qu[rear] = b;

	while (front != rear) {//队列不为空时循环
		front = (front + 1) % MaxSize;
		p = qu[front];//队列头出队
		cout << p->data;
		if (p->lchild != NULL) {//有左孩子时将其进队
			rear = (rear + 1) % MaxSize;
			qu[rear] = p->lchild;
		}
		if (p->rchild != NULL) {//有右孩子时将其进队
			rear = (rear + 1) % MaxSize;
			qu[rear] = p->rchild;
		}

	}
	cout << endl;
}

主函数测试代码及运行结果:

int main() {
	char str[] = "A(B(D(,G)),C(E,F))";//合法树的括号表达式
	BTNode* p,*q;
	BinaryTree BT(str);//调用有参构造函数构造树

	BT.Disp_BinaryTree(BT.GetRoot()); cout << endl;//调用输出函数输出栈

	p=BT.FindBTNode(BT.GetRoot(), 'B');//调用查找指定值
	if (q=BT.LchildBTNode(p))//调用返回左孩子节点的函数
		cout << q->data << endl;
	else
		cout << "NULL\n";
	if (q=BT.RchildBTNode(p))//调用返回右孩子节点的函数
		cout << q->data << endl;
	else
		cout << "NULL\n";

	int i = BT.Depth_BinaryTree(BT.GetRoot());//调用求树高函数
	cout << i << endl;

	//测试三种遍历
	BT.PreOrder(BT.GetRoot()); cout << endl;
	BT.InOrder(BT.GetRoot()); cout << endl;
	BT.PostOrder(BT.GetRoot()); cout << endl;

	//测试三种非递归遍历
	BT.PreOrder2(BT.GetRoot()); 
	BT.InOrder2(BT.GetRoot()); 
	BT.PostOrder2(BT.GetRoot()); 
	
	BT.LevelOrder(BT.GetRoot());//层次遍历

	return 0;
}

在这里插入图片描述

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
二叉树是一种重要的数据结构,由于其特殊的结构和性质,需要具备一些基本的操作来对二叉树进行处理。 首先是创建二叉树。可以通过读取用户输入或者其他方式来构建一个二叉树创建二叉树的过程可以使用递归的方式,通过不断地输入节点的值和连接关系来构造二叉树。 其次是遍历二叉树。常见的遍历方式有前序遍历、中序遍历和后序遍历。前序遍历先访问根节点,然后遍历左子树和右子树;中序遍历按照左子树、根节点和右子树的顺序遍历;后序遍历先遍历左子树和右子树,最后访问根节点。通过递归的方式,可以实现这三种遍历方式。 另外一个常用的操作是查找二叉树中的节点。可以通过比较节点的值,逐层搜索二叉树,找到目标节点。如果目标节点不存在,可以返回一个特定的值来表示找不到。 还有一个重要的操作是插入节点。可以通过比较节点的值,找到插入的位置。如果待插入的节点小于当前节点,就插入到左子树中;如果待插入的节点大于当前节点,就插入到右子树中。插入节点后,需要调整二叉树的结构,保持二叉树的性质。 最后,删除节点也是一个常见的操作。删除节点时,需要考虑节点的左右子树。可以通过将节点的左子树的最大节点或者右子树的最小节点上移来替代被删除的节点。删除节点后,同样需要调整二叉树的结构,保持二叉树的性质。 这些是二叉树的基本操作,它们在实际应用中有广泛的应用,比如在搜索、排序和图等领域。掌握这些操作,可以更好地理解和应用二叉树

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值