【数据结构】二叉树的功能实现


关于二叉树的创建

在笔者的上一篇文章中堆进行了一个详细介绍,而二叉树是以堆为基础进行创建,它与堆的显著不同是

堆像是一个线性结构,堆的结构往往是一个数组,通过对父子索引的查找进行大多数功能的实现

而二叉树是一个逻辑结构,通过结构体实现二叉树的每一个节点,然后再通过指针将各个节点给联系起来

这里放一下两者的结构体对比,更加明显些

typedef int QDataType;
typedef struct QueueNode
{
	struct QueueNode* next;
	QDataType val;
}QNode;

typedef struct Queue
{
	QNode* phead;
	QNode* ptail;
	int size;
}Queue;
  • 二叉树
typedef char BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

BTNode* BuyNode(int x);

如何创建二叉树

如果需要创建一个二叉树,我们往往需要一个能够提供二叉树元素根前序逻辑的数组,比如这个

char a[17] = { ‘A’,‘B’,‘D’,‘#’,‘#’,‘E’,‘#’,‘H’,‘#’,‘#’,‘C’,‘F’,‘#’,‘#’,‘G’,‘#’,‘#’ };

这里补充一下前序、中序、后序的概念

  1. 前序遍历(Preorder Traversal 亦称先序遍历)–访问根结点的操作发生在遍历其左右子树之前。
  2. 中序遍历(Inorder Traversal)–访问根结点的操作发生在遍历其左右子树之中(间)
  3. 后序遍历(Postorder Traversal)–访问根结点的操作发生在遍历其左右子树之后。

比如说前序:即根-左孩子-右孩子的顺序呈现二叉树的逻辑
在这里插入图片描述


既然能理解前序的概念我们就可以发现如果暗战数组元素顺序,那么第一个进来的就是根,通过递归本函数,我们可以实现先将根创建完后再创建左子树,最后创建右子树

一旦遇到 # 我们就退出递归,回到上一级

还需要注意的是,我们用来创建二叉树的往往是一个堆逻辑的数组,所以为了获取下一个元素,我们需要一个能够在递归时确定当前元素下标的变量,因此我们可以这样子做

	int b = 0;
	int* pi = &b;

由此一来pi对应是b的指针,即使在递归途中,我们不用改变指针pi直接通过指针改变b的值,即可以实现定位元素下标了

实现代码如下

BTNode* BuyNode(int x)
{
	BTNode* node = (BTNode*)malloc(sizeof(BTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		return;
	}
	node->data = x;
	node->left = NULL;
	node->right = NULL;

	return node;
}

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{
	//a为外界传进的数组
	//n为最大长度
	//pi为我们遍历数组的指针
	//使用‘#’表示NULL
	//(*pi)++ 意味着pi所指向的那个数加一,所以pi作为指针,它所指向的数的地址不会发生变化,但它所指向的那个数会加一
	if (a[*pi] == '#' || *pi >= n)
	{
		printf("N ");
		(*pi)++;
		//因为是二叉树,所以遇到 '#' 意味着后面很有可能还有,所以pi所指向的那个数,即要查看现在查看的数组元素的下一个元素
		return NULL;
	}

	//若不为#就要创建一个新的节点
	BTNode* dst = BuyNode(a[(*pi)]);
	printf("%c ", dst->data);

	//递归数组的下一个元素
	(*pi)++;

	//赋值左右节点元素给当前节点
	dst->left = BinaryTreeCreate(a, n, pi);
	dst->right = BinaryTreeCreate(a, n, pi);

	return dst;
}

另外加一嘴,因为我们创建的二叉树是一个一个节点创建的,所以我们为了避免内存泄漏,最后也是需要通过递归一个一个释放,这里我们可以通过函数递归一直找到叶子节点,往上一个一个释放,即

// 二叉树销毁
void BinaryTreeDestory(BTNode** root)
{
	//利用二叉树节点的特点,递归到最底层的结点,并释放
	//再一层层返回调用,自下而上逐渐销毁
	if (*root == NULL)
	{
		return;
	}
	BinaryTreeDestory(&((*root)->left));
	BinaryTreeDestory(&((*root)->right));

	free(*root);
	root = NULL;
	return;
}

实现二叉树的前、中、后序遍历

刚刚我们提到了前、中、后序的概念,所以当我们需要通过这三种形式提取二叉树中的元素,通过递归左右节点来获取根节点,就可以通过改变三者的输出顺序即可实现,还是比较简单的

// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	printf("%c ", root->data);
	BinaryTreePrevOrder(root->left);
	BinaryTreePrevOrder(root->right);
}

// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	BinaryTreeInOrder(root->left);
	printf("%c ", root->data);
	BinaryTreeInOrder(root->right);
}


// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	BinaryTreePostOrder(root->left);
	BinaryTreePostOrder(root->right);
	printf("%c ", root->data);
}

层序遍历

层序遍历就与之前的遍历不同了,因为父子节点中往往可以通过指针直接获取对应的额位置和值,但是同一层中的节点却无法通过这种方法实现。

因此,我们需要用到之前学的一个数组结构 - 队列,即先进先出的数据结构

通过这种数据结构,我们将每次提取出来的节点放到队列的末尾,这样最后输出的队列,从头往后就是二叉树的层序遍历。

需要注意的是,如果大家在看别的博客的时候可能会遇到,他们直接使用队列的尾插功能,但其实这病不行,因为队列我们在创建时它的尾插功能的对象往往是队列的结构体,如果直接将其用来放入二叉树的层序遍历功能中,会出现bug

因此我们在二叉树中新建一个队列尾插功能,并将其的形参设为二叉树的结构体

// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
	if (root == NULL) {
		return;
	}

	// 使用队列实现层序遍历
	int front = 0, rear = 0;
	BTNode** queue = (BTNode**)malloc(sizeof(BTNode*) * 1000); // 假设节点数不超过1000
	queue[rear++] = root;

	while (front < rear) {
		BTNode* current = queue[front++]; // 取出队列前端节点
		printf("%c ", current->data);

		if (current->left != NULL) {
			queue[rear++] = current->left; // 左子节点入队
		}
		if (current->right != NULL) {
			queue[rear++] = current->right; // 右子节点入队
		}
	}

	free(queue); // 释放队列内存
}

不仅如此,最后为了防止内存泄漏,我们需要把这个新建立的队列给释放,注意不能全部释放二叉树,要不然后面就没得用了


  • 52
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 16
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值