初级数据结构(七)——二叉树

news2025/2/3 5:44:17

     文中代码源文件已上传:数据结构源码

<-上一篇 初级数据结构(六)——堆        |        NULL 下一篇->

1、写在前面

        二叉树的基本概念在《初级数据结构(五)——树和二叉树的概念》中已经介绍得足够详细了。上一篇也演示了利用顺序表模拟二叉树。但链表形式的二叉树在逻辑上相对于顺序表尤其复杂,当然也比顺序表更为灵活。

        链表形式的二叉树任何操作,本质都是有条件地遍历各个节点。而熟练掌握递归算法对遍历链表形式二叉树尤为重要。如果你对递归还犯迷糊可先翻阅《轻松搞懂递归算法》一文,其中对递归有较为详细的介绍。

2、建立

        链表形式的二叉树的创建操作已经属于遍历操作了,本部分将通过边创建边说明的方式演示如何遍历二叉树。

2.1、前期工作

        老样子,先建文件。

        binaryTree.h :用于创建项目的结构体类型以及声明函数;

        binaryTree.c :用于创建二叉树各种操作功能的函数;

        main.c :仅创建 main 函数,用作测试。

        这次演示是通过字符串创建二叉树,空节点以“ ? ”表示,所以在 binaryTree.h 中先写下如下代码:

#include <stdio.h>
#include <stdlib.h>

typedef char DATATYPE;

#define NULL_SYMBOL '?'
#define DATA_END '\0'
#define DATAPRT "%c"

//创建二叉树节点
typedef struct Node
{
	DATATYPE data;
	struct Node* left;
	struct Node* right;
}Node;

//函数声明-------------------------------------
//创建二叉树
extern Node* BinaryTreeCreate(DATATYPE*);
//销毁二叉树
extern void BinaryTreeDestroy(Node*);

         然后在 binaryTree.c 中 include 一下:

#include "binaryTree.h"

        在 main.c 中创建个 main 函数的空客:

#include "binaryTree.h"

int main()
{
	return 0;
}

2.2、常规遍历

        二叉树有三种常用遍历顺序,称为前序、中序和后序。前中后序指的是访问节点中数据的次序。

        前序:先访问根节点,之后问左树,最后访问右树。

        中序:先访问左树,之后问根节点,最后访问右树。

        后序:先访问左树,之后问右树,最后访问根节点。

        先看图:

        用前序访问上图第一棵树顺序是 A→B→C ,中序是 B→A→C ,后序则是 B→C→A 。而这是相对于子树而言的。如果访问上图第二棵树需要将树根据当前访问的节点拆分为子树。如用前序访问,先访问 D ,之后定位到 D 的左节子点 E , 但此时是先将 E 节点当作子树,访问的是该子树的根节点。 之后访问 G 也是如此。用前序访问的顺序是 D→E→G→F→H→I 。而实际访问顺序如下图:

        DEGNULLNULLNULLFHNULLNULLINULLNULL

        用前序来说明可能不太明显。如果用中序,先定位到 D 节点,此时先不访问 D 的数据,而是访问 D 的左子节点 E 。而 E 作为子树,它还存在自己的左子节点,因此也不访问 E 的数据,而是它的子节点 G 。此时以 G 为根节点的子树不存在左子节点,因此访问 G 的数据,然后访问 G 的右子节点。但 G 不存在右子节点,所以访问完 G 的数据也就是访问完以 G 为根节点的子树,相当于 E 的左树访问完毕,此时才访问 E 的数据。下一步访问 E 的右子节点,但 E 不存在右子节点,所以 以 E 为根的子树访问完成,相当于 D 的左子树访问完毕,所以访问 D 的数据,然后访问 D 的右子树 F ……因此,以中序访问这棵树顺序是 G→E→D→H→F→I 。实际访问顺序:

        NULLGNULLENULLDHNULLNULLFINULLNULL 

        后序的逻辑可以类比中序,访问顺序是 G→E→H→I→F→D 。实际访问顺序:

        NULLNULLGNULLENULLNULLHNULLNULLIF

2.3、操作函数

2.3.1、创建二叉树

        创建二叉树的过程也是在遍历二叉树。而创建过程中,必须先有根节点,才能创建子树,所以建立二叉树是以前序边创建边访问建立的。

        需要解决的问题是在创建节点的结构体时,并没有创建指向父节点的指针成员变量。当创建完左树之后,要如何回到根节点。这里先往回思考,在前中后序的说明中不难看出,这就是一种递归。树拆成子树,子树又拆成子树的子树……而不论拆分成哪一级子树,访问方式都是统一的顺序。而递归是具有回溯属性的,也就是说,用递归的方式创建二叉树再合适不过了。函数的代码便呼之欲出:

//创建二叉树
Node* BinaryTreeCreate(DATATYPE** ptr2_data)
{
	//参数有效性判定
	if (!ptr2_data || !*ptr2_data)
	{
		fprintf(stderr, "Data Address NULL\n");
		return NULL;
	}
	//数据为空节点符号或末位尾符号则返回
	if (**ptr2_data == NULL_SYMBOL || **ptr2_data == DATA_END)
	{
		return NULL;
	}
	//创建节点
	Node* node = NULL;
	while (!node)
	{
		node = (Node*)malloc(sizeof(Node));
	}

	//前序递归
	node->data = **ptr2_data;
	*ptr2_data += !(**ptr2_data == DATA_END);
	node->left = BinaryTreeCreate(ptr2_data);
	*ptr2_data += !(**ptr2_data == DATA_END);
	node->right = BinaryTreeCreate(ptr2_data);

	return node;
}

        在 main 函数中用以下代码进行测试:

DATATYPE data[] = "abc??d??f?g?h";
DATATYPE* ptr_data = data;
Node* root = BinaryTreeCreate(&ptr_data);

         树并不像线性表那么直观,检查测试结果时最好自己先画图,然后在监视窗口中检查。对于以上测试结果应当如下图:

        调试起来,将调试窗口中逐层展开,对其中的信息对比上图。或者另外画图, 将两张图作对比进行检查。

        结果正确。顺带写出前中后三种顺序访问二叉树的函数。这里为了方便观察遍历顺序,多加一个参数用于控制是否显示空节点。先在 binaryTree.h 中加个枚举类型以完善传参时代码的可读性:

enum NULL_VISIBLE
{
	HIDE_NULL,    //空节点不可见 = 0
	SHOW_NULL     //空节点可见 = 1
};

        然后加声明:

//前序访问二叉树
extern void PreOrderTraversal(Node*, int);
//中序访问二叉树
extern void InOrderTraversal(Node*, int);
//后序访问二叉树
extern void PostOrderTraversal(Node*, int);

        函数具体内容: 

//前序访问二叉树
void PreOrderTraversal(Node* root, int NULLvisible)
{
	//空树返回
	if (!root)
	{
		if (NULLvisible == SHOW_NULL)
		{
			printf("NULL ");
		}
		return;
	}

	printf(DATAPRT" ", root->data);
	PreOrderTraversal(root->left, NULLvisible);
	PreOrderTraversal(root->right, NULLvisible);
}

//中序访问二叉树
void InOrderTraversal(Node* root, int NULLvisible)
{
	//空树返回
	if (!root)
	{
		if (NULLvisible == SHOW_NULL)
		{
			printf("NULL ");
		}
		return;
	}

	InOrderTraversal(root->left, NULLvisible);
	printf(DATAPRT" ", root->data);
	InOrderTraversal(root->right, NULLvisible);
}

//后序访问二叉树
void PostOrderTraversal(Node* root, int NULLvisible)
{
	//空树返回
	if (!root)
	{
		if (NULLvisible == SHOW_NULL)
		{
			printf("NULL ");
		}
		return;
	}

	PostOrderTraversal(root->left, NULLvisible);
	PostOrderTraversal(root->right, NULLvisible);
	printf(DATAPRT" ", root->data);
}

        这里可以观察到,所谓前中后序不过是调整了一下访问 root->data 语句的位置,其余完全一样。 

        然后开始测试。

int main()
{
	DATATYPE data[] = "abc??d??f?g?h";
	DATATYPE* ptr_data = data;
	Node* root = BinaryTreeCreate(&ptr_data);

	//前序:
	printf("前序 --------------------\n");
	PreOrderTraversal(root, SHOW_NULL);
	printf("\n");
	PreOrderTraversal(root, HIDE_NULL);
	printf("\n");

	//中序
	printf("中序 --------------------\n");
	InOrderTraversal(root, SHOW_NULL);
	printf("\n");
	InOrderTraversal(root, HIDE_NULL);
	printf("\n");

	//后序
	printf("后序 --------------------\n");
	PostOrderTraversal(root, SHOW_NULL);
	printf("\n");
	PostOrderTraversal(root, HIDE_NULL);
	printf("\n");

	return 0;
}

        对比上面的图,说明代码正确。 

2.3.2、销毁二叉树

        销毁跟创建是同样的逻辑,必须从底层开始销毁。当然也可以从根部销毁,但如果不先销毁子节点,一旦销毁根节点之后便无法再找到子节点的地址,因此还得对子节点地址进行记录后再销毁,显得过于麻烦。因此采用后序遍历销毁最为简便。

//销毁二叉树
void BinaryTreeDestroy(Node* root)
{
	//空树直接返回
	if (!root) return;
	//后序递归销毁节点
	BinaryTreeDestroy(root->left);
	BinaryTreeDestroy(root->right);
	free(root);
}

        这个函数测试过程需要一步一步调试观察。 实际上跟之前后序访问的函数是一个道理,这里也没必要再多作测试。但使用完该函数记得把根节点指针置空。

2.3.3、搜索

        搜索也是通过遍历比对节点中的数据,再返回节点地址。

        必不可少的声明:

//二叉树搜索
extern Node* BinaryTreeSearch(Node*, DATATYPE);

        代码:

//二叉树搜索
Node* BinaryTreeSearch(Node* root, DATATYPE data)
{
	//空树直接返回
	if (!root) return NULL;

	//创建节点地址指针
	Node* node = NULL;
	//前序搜索
	node = (root->data == data ? root : NULL);
	node = (node ? node : BinaryTreeSearch(root->left, data));
	node = (node ? node : BinaryTreeSearch(root->right, data));

	return node;
}

        在刚才创建的二叉树基础上测试:

	Node* node = NULL;
	node = BinaryTreeSearch(root, 'f');
	if (node)
		printf("Found Data '"DATAPRT"' In 0x%p\n", node->data, node);
	else
		printf("Not Found\n");

	node = BinaryTreeSearch(root, 'j');
	if (node)
		printf("Found Data '"DATAPRT"' In %p\n", node->data, node);
	else
		printf("Not Found\n");

         测试通过。此外,搜索既然实现了,修改就不必说了,这里不再演示。 

3、层序

        除了之前提到的前中后序遍历二叉树以外,还有层序遍历。顾名思义,就是逐层遍历二叉树中每个节点。层序遍历是最复杂的一种遍历方式。由于二叉树节点中并不包含兄弟节点和堂兄弟节点的指针,因此层序遍历需要其他变量来记录各层节点的左右子节点,并按照一定顺序排序。

3.1、队列

        这里可以利用队列的特性,访问完根节点后,对左右子节点地址进行入队,并将根节点出队,从而实现遍历。因此,这里先在 binaryTree.h 中创建个队列。

//队列类型
typedef struct Queue
{
	int top;
	int bottom;
	size_t capacity;
	Node* data[];
}Queue;

        之后是在 binaryTree.c 中创建操作队列的各个函数。

//创建队列并初始化
static Queue* QueueCreate()
{
	Queue* queue = NULL;
	//创建队列
	while (!queue)
	{
		queue = (Queue*)malloc(sizeof(Queue) + sizeof(Node*) * 4);
	}
	queue->top = -1;
	queue->bottom = -1;
	queue->capacity = 4;
	//返回队列
	return queue;
}

//数据入队
static void QueuePush(Queue** queue, Node* node)
{
	//若队列空间不足,扩容
	if ((*queue)->top + 1 >= (*queue)->capacity)
	{
		Queue* tempQueue = NULL;
		while (!tempQueue)
		{
			tempQueue = (Queue*)realloc(*queue, sizeof(Queue) + sizeof(Node*) * (*queue)->capacity * 2);
		}
		(*queue) = tempQueue;
		(*queue)->capacity *= 2;
	}

	//节点入队
	(*queue)->bottom = ((*queue)->bottom == -1 ? 0 : (*queue)->bottom);
	(*queue)->top++;
	(*queue)->data[(*queue)->top] = node;
}

//数据出队
static void QueuePop(Queue* queue)
{
	//空队列返回
	if (queue->top == -1)return;

	//出队
	queue->bottom++;

	//出队后若为空队列
	if (queue->bottom > queue->top)
	{
		queue->bottom = -1;
		queue->top = -1;
	}
}

3.2、层序访问

        由于二叉树是有序树,每一层节点从左到右必然是有序的。仍以这棵树作演示:

        首先将根节点 D 入队,访问完根节点后,将左右子节点 E、F 依次入队,排在 D 之后,然后弹出 D 。之后访问 E,再将 E 的左右子节点 G 和 NULL 入队,弹出 E 。继续访问 F ,H、I 入队后再弹出 F 。如果当前根节点为 NULL ,则不再将子节点入队,仅仅弹出 NULL 。最后当队列为空时,树也遍历完毕。

        根据以上描述,可以知道层序访问顺序为 D→E→F→G→H→I,实际访问顺序:

        DEFGNULLHINULLNULLNULLNULLNULLNULL 

3.3、代码部分

        思路已经有了,代码也就顺理成章了。

//层序打印
static void LevelOrderPrint(Queue** queue)
{
	//空队列返回
	if ((*queue)->top == -1) return;

	//非空节点的左右子节点入队
	if ((*queue)->data[(*queue)->bottom])
	{
		QueuePush(queue, ((*queue)->data[(*queue)->bottom])->left);
		QueuePush(queue, ((*queue)->data[(*queue)->bottom])->right);
	}

	//打印非空节点
	if ((*queue)->data[(*queue)->bottom] != NULL)
	{
		printf(DATAPRT" ", ((*queue)->data[(*queue)->bottom])->data);
	}

	//根节点出队
	QueuePop(*queue);
	LevelOrderPrint(queue);
}

//层序遍历二叉树
void LevelOrderTraversal(Node* root)
{
	//创建队列
	Queue* queue = QueueCreate();

	//根节点入队
	QueuePush(&queue, root);

	//层序打印
	LevelOrderPrint(&queue);

	//销毁队列
	free(queue);
}

        仍沿用开头的测试案例,然后在 main 函数最后加入以下语句进行测试:

    //层序
	printf("层序 --------------------\n");
	LevelOrderTraversal(root);
	printf("\n");

        至此完成。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1328852.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

听GPT 讲Rust源代码--src/tools(21)

File: rust/src/tools/miri/src/shims/x86/mod.rs 在Rust的源代码中&#xff0c;rust/src/tools/miri/src/shims/x86/mod.rs文件的作用是为对x86平台的处理提供支持。它包含一些用于模拟硬件操作的shim函数和相关的类型定义。 具体来说&#xff0c;该文件中的函数是通过使用一组…

Python如何生成随机图形验证码

python生成随机图形验证码 使用python生成随机图片验证码,需要使用pillow模块 1.安装pillow模块 pip install pillow 2.pillow模块的基本使用 1.创建图片 from PIL import Image #定义使用Image类实例化一个长为400px,宽为400px,基于RGB的(255,255,255)颜色的图片 img1Ima…

Halcon 检测焊点短路

Halcon 检测焊点短路 read_image (Image1, D:/image/bilibili/photo/检测焊接短路 (4).bmp) dev_close_window () dev_open_window (0, 0, 512, 512, black, WindowHandle) dev_display (Image1) set_display_font (WindowHandle, 16, mono, true, false) threshold (Image1, …

modbus_tcp的实现 through python.

0.引言 当前科技似乎处于加速发展期&#xff0c;各个模块都在快速迭代&#xff0c;迭代的速度会让既有的一些经验产生问题&#xff0c;在用python实现modbus_tcp协议时&#xff0c;网上流传的一些代码中import语句会出现问题。导致pymodbus模块用起来很不好用。 这个原因出在…

集合工具类Collections

概述 java.utils.Collections:是集合工具类 作用&#xff1a;Collections并不属于集合&#xff0c;是用来操作集合的工具类。 Collections常用的API Collections排序相关API 使用范围&#xff1a;只能对于List集合的排序。 排序方式1&#xff1a; 注意&#xff1a;本方式…

23 在HST场景中,考虑物理层相关技术

文章目录 实验参数A&#xff1a;解调参考信号(DMRS)1 DMRS分配图2 实验结果图figur3figur4 3. 实验结论 B 映射资源元素。1 映射图2 实验结果图figure 6figure 7 3. 实验结论figure8 3 补充结论 C μ1 实验结果图&#xff1a;figure 9figure 10 2 结论 实验参数 注释&#xff1…

数据结构和算法-红黑树(定义 性质 查找 插入 删除)

文章目录 红黑树的定义和性质为什么要发明红黑树&#xff1f;红黑树怎么考总览红黑树的定义实例&#xff1a;一颗红黑树练习&#xff1a;是否符合红黑树的要求一种可能的出题思路补充概念&#xff1a;节点黑高 红黑树的性质 红黑树的查找红黑树的插入实例小结与黑高相关的理论 …

web前端游戏项目-堆木头游戏【附源码】

web前端游戏项目-堆木头游戏 《堆木头》游戏玩法简单&#xff0c;通过鼠标点击放木头的按钮&#xff0c;叠加在一起&#xff0c;构建出各种结构。游戏适合所有年龄段的孩子&#xff0c;可以锻炼孩子的动手能力和手眼协调能力&#xff0c;激发孩子的创造力和想象力 运行效果 …

使用GitZip下载GitHub指定文件

目录 一、GitZip二、安装GitZip三、链接GitHub四、检验是否安装成功五、总结 一、GitZip GitZip是一个非常实用的浏览器插件&#xff0c;它主要有以下几个优点&#xff1a; 下载指定文件&#xff1a;在我们浏览Github时&#xff0c;如果只想下载某个子目录的内容&#xff0c;…

15-高并发-如何扩容

对于一个发展初期的系统来说&#xff0c;不太确定商业模型到底行不行&#xff0c;最好的办法是按照最小可行产品方法进行产品验证&#xff0c;因此&#xff0c;刚开始的功能会比较少&#xff0c;是一个大的单体应用&#xff0c;一般按照三层架构进行设计开发&#xff0c;使用单…

服务器IBM x3650 m2 管理口访问故障处理

服务器的内存告警后&#xff0c;连接管理口查看信息&#xff0c;管理口状态灯显示正常&#xff0c;但是无法ping通和访问。 处理过程如下&#xff1a; 1、在centos 6.6中安装ipmitool&#xff0c;替换为阿里云的yum源&#xff0c;然后安装。 # wget -O /etc/yum.repos.d/Cen…

【String、StringBuilder 和 StringBuffer 的 区别】

✅ String、StringBuilder 和 StringBuffer 的 区别 ✅典型解析✅扩展知识仓✅String 的不可变性✅ 为什么JDK 9 中把String 的char[ ] 改成了 byte[ ] ? ✅为什么String设计成不可变的✅缓存✅安全性✅线程安全✅hashcode缓存✅ 性能 ✅String 的 " " 是如何实现的…

Ubuntu 常用命令之 du 命令用法介绍

&#x1f4d1;Linux/Ubuntu 常用命令归类整理 Ubuntu系统下的du命令是一个用来估计和显示文件和目录所占用的磁盘空间的命令。du是“disk usage”的缩写&#xff0c;这个命令可以帮助用户了解磁盘被哪些文件和目录使用。 du命令的常见参数有 -a&#xff1a;列出所有文件和目…

STM32的以太网外设+PHY(LAN8720)使用详解(3):PHY寄存器详解

0 工具准备 1.野火 stm32f407霸天虎开发板 2.LAN8720数据手册 3.STM32F4xx中文参考手册1 PHY寄存器 前面介绍到&#xff0c;站管理接口&#xff08;SMI&#xff09;允许应用程序通过2线时钟和数据线访问任意PHY寄存器&#xff0c;同时该接口支持访问最多32个PHY&#xff0c;也…

一个基于多接口的业务自动化测试框架!

这是一个成熟的框架&#xff0c;不是要让别人当小白鼠&#xff0c;它已经先后在两家公司的5条业务线进行了推广应用&#xff0c;用例条数到了几千条以上&#xff0c;并且从2018年开始每天都在CI/CD流程中被调用执行。 已有那么多接口测试框架&#xff0c;为什么重复造轮子&…

在芯片设计端,从事DFT岗位是什么体验?

从1975年PHILIPS公司实验中心首次提出“可测性设计”概念至今&#xff0c;DFT已经伴随着芯片走过了半个世纪。 但DFT在数字IC设计岗位中还是显得很神秘很低调。 你说他重要吧&#xff0c;并不是所有芯片设计公司都有这个岗位&#xff0c;你说他不重要吧&#xff0c;但凡芯片产…

数据结构 | 北京大学期末试卷查漏补缺

目录 顺序存储 优点 缺点 适用于&#xff1a; 链式存储 优点 缺点 适用于&#xff1a; 折半查找为什么要使用顺序存储结构 树的存储结构​编辑 对于一个数据结构&#xff0c;一般包括 DFS&BFS 什么是递归程序 C语言不带头结点的单链表逆置 检测字符…

【CMake保姆级教程】制作动静态链接库、指定动静态库输出路径

文章目录 前言一、动静态链接库的介绍1.1 动态链接库 (DLL)1.2 静态链接库 (LIB) 二、制作静态库三、制作动态库四、指定动静态库输出路径4.1 方式1 - 适用于动态库4.2 方式2 - 都适用 总结 前言 在软件开发中&#xff0c;我们经常听到动态链接库&#xff08;Dynamic Link Lib…

c语言易错题之数据类型变换

1.题目 #include<stdio.h> int main() {int arr[]{1,2,3,4,5};short*p (short*)arr;int i 0;for(i0;i<4;i){*(pi)0;}for(i0;i<5;i){printf("%d ",arr[i];}return 0; }2.解析 这道题主要容易错在&#xff0c;大家会以为通过指针赋值的时候&#xff0c;…

基于springboot的日记本系统源码+数据库+安装使用说明

之前写的SpringBoot日记本系统备受好评&#xff0c;考虑到还是有很多小伙伴不会部署&#xff0c;所以这一篇文章就单独来讲一下部署步骤吧。 需要资源 idea&#xff08;破不破解都行&#xff09; MySQL&#xff08;最好5.7以上版本&#xff0c;最好8.0&#xff09; Navicat…