Chapter 5: 二叉树详解

       在探索计算机科学和编程世界的旅途中,数据结构是构成程序骨干的重要组成部分。它们不仅仅是存储数据的容器,更是提高算法效率、优化资源使用的关键。在众多的数据结构中,二叉树以其独特的结构和灵活性,成为了实现高效算法和解决复杂问题的重要工具。C语言,作为一种高效、接近硬件的编程语言,为数据结构的实现提供了简洁而强大的方法。本文旨在通过C语言的视角,详细介绍二叉树的基本概念、主要操作,以及如何在实际编程中有效地应用它。我们将从二叉树的基础定义出发,逐步深入到它的各种遍历方式,探讨二叉树在搜索、插入、删除等操作中的实现,以期使读者能够充分理解并掌握这一重要的数据结构。

文章目录

  • 树概念及结构
  • 二叉树概念及结构
  • 二叉树顺序结构及实现
  • 二叉树链式结构及实现
  • 总结

一、树的概念及结构

 1.1树的概念

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因 为它看起来像一棵倒挂的树,它是根朝上,而叶朝下的。

  • 有一个特殊的结点,称为根结点,根节点没有前驱结点
  • 除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<=i<=m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
  • 因此,树是递归定义的。

递归体现

注意:树形结构中,子树之间不能有交集,否则就不是树形结构

树种无环

1.2 树的相关概念

*节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6

*叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I...等节点为叶节点(没有孩子的节点)

非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G...等节点为分支节点

*双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点

*孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点

兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点

树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6(不一定是根的度)

节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;

(和数组比较一下)还有(比较1个节点的树和空树)

*树的高度或深度:树中节点的最大层次; 如上图:树的高度为4

堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点

*节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先

*子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙

森林:由m(m>0)棵互不相交的树的集合称为森林;

1.3 树的表示

树结构相对线性表复杂,存储表示起来比较麻烦,既要保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。这里简单的了解其中最常用的孩子兄弟表示法

typedef int DataType;
struct Node
{
   struct Node* _firstChild1; // 第一个孩子结点
   struct Node* _pNextBrother; // 指向其下一个兄弟结点
   DataType _data; // 结点中的数据域
};

双亲表示法表示的是双亲节点的下标(数组表示),根在哪个位置是不确定的

1.4 树在实际中的运用(表示文件系统的目录树结构)

形态:表示型和存储型(表示某种结构)

二、二叉树概念及结构

2.1概念

一棵二叉树是结点的一个有限集合,该集合:

(1)或者为空

(2)由一个根节点加上两棵分别称为左子树和右子树的二叉树组成

最多有两个孩子

浅谈搜索二叉树及相关拓展,如下:

从上图可以看出:

(1)二叉树不存在度大于2的结点

(2) 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

注意:对于任意的二叉树都是由以下几种情况复合而成的:

2.2现实中的二叉树:

2.3 特殊的二叉树:

(1) 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 2^k-1,则它就是满二叉树。(每一层都满)

(2)完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树引出来的。对于深度为K、有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。(n-1都满,最后一层不一定满,但要求从左到右必须连续

2.4 二叉树的性质

(1)若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 2^(i-1) 个结点

(2)若规定根节点的层数为1,则深度为h的二叉树的最大结点数是 2^h-1

(3)对任何一棵二叉树, 如果度为0的叶结点个数为 n0 , 度为2的分支结点个数为 n2, 则有 n0=n2+1

(4)若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=log2(n+1) (ps: 是log以2为底,n+1为对数)

(5)对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对 于序号为i的结点有:

  • 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
  • 若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
  • 若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子

2.5 选择题练习

解答:

8层肯定不够

故选B

第三题和第五题是一类:

故选B

2.5 二叉树的存储结构

二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。

(1) 顺序存储

顺序结构存储就是使用数组存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树

(2)链式存储

二叉树的链式存储结构是指用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是:链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前学习中一般都是二叉链,在高阶数据结构如红黑树等会用到三叉链。

typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
  struct BinTreeNode* _pLeft; // 指向当前节点左孩子
  struct BinTreeNode* _pRight; // 指向当前节点右孩子
  BTDataType _data; // 当前节点值域
}
 
// 三叉链
struct BinaryTreeNode
{
  struct BinTreeNode* _pParent; // 指向当前节点的双亲
  struct BinTreeNode* _pLeft; // 指向当前节点左孩子
  struct BinTreeNode* _pRight; // 指向当前节点右孩子
  BTDataType _data; // 当前节点值域
};

三、二叉树顺序结构实现

3.1 二叉树的顺序结构

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段

3.2 堆的概念及结构

如果有一个关键码的集合K = { k0,k1 ,k2 ,…,kn-1 },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki<=K2*i+1 且Ki<=K2*i+2 (Ki>=K2*i+1 且Ki>=K2*i+2)i=0,1,2...,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质:

  • 堆中某个节点的值总是不大于或不小于其父节点的值
  • 堆总是一棵完全二叉树。

对兄弟没有要求

3.3 选择题练习

3.3 堆的实现

3.2.1 堆向下调整算法

现在给出一个数组,逻辑上看作一棵完全二叉树。通过从根节点开始的向下调整算法可以把它调整 成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

int array[] = {27,15,19,18,28,34,65,49,25,37};

3.2.2堆的创建

下面给出一个数组,这个数组逻辑上可以看作一棵完全二叉树,但还不是一个堆,现在通过算 法,把它构建成一个堆。根节点左右子树不是堆,怎么调整呢?这里从倒数的第一个非叶子节点的 子树开始调整,一直调整到根节点的树,就可以调整成堆。

int a[] = {1,5,3,8,7,6}; 

3.2.3 建堆时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):

N和logN差别很大

因此,向上调整建堆的时间复杂度为O(N*logN)

向下调整算法从第h-1层开始向下调整

满二叉树最后基层就占了整棵树的绝大多数节点

因此,向下调整算法的时间复杂度为O(N)

比较两种算法可以得到向上调整算法节点多,调整次数多;向下调整算法是节点多,调整次数少,总体下来后者时间复杂度好很多

3.2.4 堆的插入

先插入一个10到数组的尾上,再进行向上调整算法,直到满足堆。

3.2.5 堆的删除

删除堆是删除堆顶的数据,将堆顶的数据和最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。

小堆向下调整时和小的子节点比较,大堆和大的子节点比较

3.2.6 堆的代码实现

Heap.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
#include<string.h>

typedef int HPDataType;

typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;  
}HP;

void HPInit(HP* php);

void HPInitArray(HP* php, HPDataType* a, int n);

void HPDestroy(HP* php);

void Swap(HPDataType* px, HPDataType* py);

void AdjustUp(HPDataType* a,int child);

void AdjustDown(HPDataType* a,int n,int parent);

void HPPush(HP* php, HPDataType x);//插入后保证数据是堆

HPDataType HPTop(HP* php);

void HPPop(HP* php);//删除堆顶的数据

bool HPEmpty(HP* php);

Heap.c

#include"Heap.h"

void HPInit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->size = 0;
	php->capacity = 0;
}


void HPInitArray(HP* php, HPDataType* a, int n)
{
	assert(php);

	php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (php->a == NULL)
	{
		perror("malloc fail");
		return;
	}
	memcpy(php->a, a, sizeof(HPDataType) * n);
	php->capacity = php->size = n;

	//建堆
	//向上调整建堆,O(N*logN)
	//for (int i = 1; i < php->size; i++)
	//{
	//	AdjustUp(php->a, i);//从孩子向上调整 
	//}

	//向下调整建堆
	for (int i = (php->size - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(php->a, php->size, i);
	}
}

void HPDestroy(HP* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->capacity = 0;
	php->size = 0;
}

void Swap(HPDataType* px, HPDataType* py)
{
	HPDataType tmp = *px;
	*px = *py;
	*py = tmp;
}

void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	//while (parent >= 0)
	while (child > 0)
	{
		if (a[child] > a[parent])//大堆
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (parent - 1) / 2;
		}
		else
		{
			break;
		}
	}
}




//时间复杂度:log*N
void HPPush(HP* php, HPDataType x)
{
	assert(php);

	if (php->size == php->capacity)
	{
		size_t newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = realloc(php->a, sizeof(HPDataType) * newCapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		php->a = tmp;
		php->capacity = newCapacity;
	}

	php->a[php->size] = x;
	php->size++;

	AdjustUp(php->a, php->size - 1);
}


HPDataType HPTop(HP* php)
{
	assert(php);

	return php->a[0];
}


void AdjustDown(HPDataType* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		//假设法,选出左右孩子中小的那个孩子
		if (child + 1 < n && a[child + 1] < a[child])//如果没有右孩子就不选了,不然就越界了
		{
			//if (child + 1 < n && a[child + 1] > a[child])//如果没有右孩子就不选了,不然就越界了
			//大堆

			++child;
		}
		if (a[child] < a[parent])
		{
			//if (a[child] > a[parent]),大堆

			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}

}

//时间复杂度:log*N
void HPPop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;

	AdjustDown(php->a, php->size, 0);
	//完全二叉树没有左孩子就没有右孩子,判断叶子节点算左孩子

}


bool HPEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}

3.4 堆的应用

3.4.1 堆排序

堆排序即利用堆的思想来进行排序,总共分为两个步骤:

(1) 建堆

  • 升序:建大堆
  • 降序:建小堆

升序不能用小堆,否则关系全乱

(2)利用堆删除思想来进行排序

建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

3.4.2 TOP-K问题

TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。

最佳的方式就是用堆来解决,

基本思路如下:

(1)用数据集合中前K个元素来建堆

  • 前k个最大的元素,则建小堆
  • 前k个最小的元素,则建大堆

(2)用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

找出前10个最大的用小堆,如果用大堆的话,只要出现一个很大的数,后面比它小的数都进不来了,因为大堆大的数在上面,小堆大的值在下面就不会影响。

该方法空间复杂度很低

创建随机数到数组进行测试:

void PrintTopK(int* a, int n, int k)
{
 // 1. 建堆--用a中前k个元素建堆
 
 // 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
}
 
void TestTopk()
{
   int n = 10000;
   int* a = (int*)malloc(sizeof(int)*n);
   srand(time(0));
   for (size_t i = 0; i < n; ++i)
   {
       a[i] = rand() % 1000000;
   }
   a[5] = 1000000 + 1;
   a[1231] = 1000000 + 2;
   a[531] = 1000000 + 3;
   a[5121] = 1000000 + 4;
   a[115] = 1000000 + 5;
   a[2335] = 1000000 + 6;
   a[9999] = 1000000 + 7;
   a[76] = 1000000 + 8;
   a[423] = 1000000 + 9;
   a[3144] = 1000000 + 10;
   PrintTopK(a, n, 10);
}

代码示例:

void CreateNDate()
{
	// 造数据
	int n = 100000;
	srand(time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}

	for (int i = 0; i < n; ++i)
	{
		int x = (rand() + i) % 1000000;
		fprintf(fin, "%d\n", x);
	}

	fclose(fin);
}

void topk()
{
	printf("请输入k:>");
	int k = 0;
	scanf("%d", &k);

	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error");
		return;
	}

	int val = 0;
	int* minheap = (int*)malloc(sizeof(int) * k);
	if (minheap == NULL)
	{
		perror("malloc error");
		return;
	}

	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &minheap[i]);
	}

	// 建k个数据的小堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(minheap, k, i);
	}

	int x = 0;
	while (fscanf(fout, "%d", &x) != EOF)
	{
		// 读取剩余数据,比堆顶的值大,就替换他进堆
		if (x > minheap[0])
		{
			minheap[0] = x;
			AdjustDown(minheap, k, 0);
		}
	}

	for (int i = 0; i < k; i++)
	{ 
		printf("%d ", minheap[i]);
	}

	fclose(fout);

}

//fprintf加换行表示中断,fscanf不加换行,发现换行能识别是多个数据分割

int main()
{
	//CreateNDate();
	topk();
	return 0;
}

在数值很大,数据很多的情况下,判断程序是否能正确的找出前k个最大值的方法

随机数最多只有三万多个

在文件生成的数据中,随机找几个数后面加几位数字,使得更容易通过肉眼观察出大小,再运行程序,很容易判断程序是否运行正确(手动造一些样本)

四、二叉树链式结构实现

二叉树的增删查改没有价值,普通二叉树结构复杂,不方便,重点在结构操作

4.1 前置说明

在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。

此处快速创建一棵简单的二叉树,进入二叉树操作学习,等二叉树结构了解的差不多时,反过头再来研究二叉树真正的创建方式。

typedef int BTDataType;
typedef struct BinaryTreeNode
{
   BTDataType _data;
   struct BinaryTreeNode* _left;
   struct BinaryTreeNode* _right;
}BTNode;
 
BTNode* CreatBinaryTree()
{
   BTNode* node1 = BuyNode(1);
   BTNode* node2 = BuyNode(2);
   BTNode* node3 = BuyNode(3);
   BTNode* node4 = BuyNode(4);
   BTNode* node5 = BuyNode(5);
   BTNode* node6 = BuyNode(6);
 
   node1->_left = node2;
   node1->_right = node4;
   node2->_left = node3;
   node4->_left = node5;
   node4->_right = node6;
   return node1;
}

注意:上述代码并不是创建二叉树的方式,真正创建二叉树方式后序详解重点讲解。

在看二叉树基本操作前,再回顾下二叉树的概念,二叉树是

(1)空树

(2)非空:根节点,根节点的左子树、根节点的右子树组成的

从概念中可以看出,二叉树定义是递归式的,因此后序操作中基本都是按照该概念实现的。

4.2 二叉树的遍历

4.2.1 前序、中序以及后序遍历

学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。

按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历

(1)前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。

(2)中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。

(3)后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。

由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为 根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。

// 二叉树前序遍历 
void PreOrder(BTNode* root);
// 二叉树中序遍历
void InOrder(BTNode* root);
// 二叉树后序遍历
void PostOrder(BTNode* root);

下面主要分析前序递归遍历,中序与后序图解类似。 前序遍历递归图解:

前序遍历结果:1 2 3 4 5 6

中序遍历结果:3 2 1 5 4 6

后序遍历结果:3 2 5 6 4 1

前序遍历详细分析:

右子树类似

空间可以复用,左边使用完,右边继续使用,空间复杂度看深度

4.2.2 层序遍历

层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。

用队列,出队的时候有孩子带一个孩子进来

// 层序遍历
void LevelOrder(BTNode* root);

练习:请写出下面的前序/中序/后序/层序遍历

4.3 选择题练习

4.4 节点个数以及高度等

// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);

 求树的节点个数错误示例:

凡是左右子树算完再算根的都是后序(求结果的),比如求树高、算节点个数

求高度:

不能用叶子节点判断法,以上面部分为例,访问右子树时root为空,出现空指针,程序就崩掉了

继续使用分治:

以上这种求树的高度的方法是正确的,但如果卡时间的话,程序是过不了的,因为时间复杂度太差

下面这段代码性能会好很多:

第K层节点个数:

查找x所在的节点,若使用遍历思想则用前序遍历,

但是以上这种写法是错误的,因为找到后向上返回没有值去接收,进而会返回一个随机值出现错误,VS上警告如下:

除非函数调用进来就是要找的节点

这里还是使用分治思想:

递归找3

找4举例,复杂一些:

4.5 二叉树基础oj练习

(1)单值二叉树  . - 力扣(LeetCode)

参考代码:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */
bool isUnivalTree(struct TreeNode* root) {
    if(root==NULL)
       return true;

    if(root->left  &&  root->left->val!=root->val)
    {
        return false;
    }

      if(root->right  &&  root->right->val!=root->val)
    {
        return false;
    }

    return isUnivalTree(root->left)
        && isUnivalTree(root->right);
}

(2)检查两颗树是否相同 . - 力扣(LeetCode)

参考代码:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */
bool isSameTree(struct TreeNode* p, struct TreeNode* q) {
    //根
    //左子树
    //右子树
    if(p==NULL&&q==NULL)
    return true;

    //其中一个为空
    if(p==NULL||q==NULL)
    return false;

    if(p->val!=q->val)
    return false;

    return isSameTree(p->left,q->left)
       &&  isSameTree(p->right,q->right);
}

(3)对称二叉树. - 力扣(LeetCode)

参考代码:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */

//子函数
bool _isSymmetric(struct TreeNode* root1,struct TreeNode* root2) {
    //根比较
    //左子树和右子树比较
    //右子树和左子树比较
    //都为空
    if(root1==NULL&&root2==NULL)
       return true;

    //一个为空,一个不为空
    if(root1==NULL||root2==NULL)
       return false;

    if(root1->val!=root2->val)
       return false;

    return _isSymmetric(root1->left,root2->right)
        && _isSymmetric(root1->right,root2->left);
}

bool isSymmetric(struct TreeNode* root) {
    return _isSymmetric(root->left,root->right);
}

(4)二叉树的前序遍历 . - 力扣(LeetCode)

需要把前序遍历的结果放在一个数组里

以上是错误解法,函数栈帧建立销毁后i也销毁了

正确代码:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */
/**
 * Note: The returned array must be malloced, assume caller calls free().
 */

int TreeSize(struct TreeNode* root)
{
    return root==NULL?0: TreeSize(root->left)+TreeSize(root->right)+1;
}

void preorder(struct TreeNode* root,int* a,int* pi)
{
    if(root==NULL)
      return;

    a[(*pi)++]=root->val;
    preorder(root->left,a,pi);
    preorder(root->right,a,pi);

}

 //统一模式,返回数组,就要返回大小
int* preorderTraversal(struct TreeNode* root, int* returnSize) {
    *returnSize=TreeSize(root);
    int* a=(int*)malloc(sizeof(int)*(*returnSize));
    int i=0;
    preorder(root,a,&i);


    return a;
}

(5)二叉树的中序遍历. - 力扣(LeetCode)

(6)二叉树的后序遍历. - 力扣(LeetCode)

(7)另一颗树的子树 . - 力扣(LeetCode)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */


//查找+数的比较

bool isSameTree(struct TreeNode* p, struct TreeNode* q) {
    //根
    //左子树
    //右子树
    if(p==NULL&&q==NULL)
    return true;

    //其中一个为空
    if(p==NULL||q==NULL)
    return false;

    if(p->val!=q->val)
    return false;

    return isSameTree(p->left,q->left)
       &&  isSameTree(p->right,q->right);
}

bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot){
    if(root==NULL)
       return false;//返回false是为了返回上一层时让它走右树

    if(root->val==subRoot->val && isSameTree(root,subRoot))
       return true;

    return isSubtree(root->left,subRoot)
        || isSubtree(root->right,subRoot);
}

4.6 二叉树的创建和销毁

二叉树的构建及遍历

二叉树遍历_牛客题霸_牛客网

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

typedef struct BinTreeNode
{
    struct BinTreeNode* left;
    struct BinTreeNode* right;
    char val;
}BTNode;

BTNode* CreateTree(char* a,int* pi)
{
    if(a[*pi]=='#')
    {
        (*pi)++;
        return NULL;
    }

    BTNode* root=(BTNode*)malloc(sizeof(BTNode));
    root->val=a[(*pi)++];
    root->left=CreateTree(a, pi);
    root->right=CreateTree(a, pi);

    return root;

}


void InOrder(BTNode* root)
{
    if(root==NULL)
    {
        return;
    }

    InOrder(root->left);
    printf("%c ",root->val);
    InOrder(root->right);
}


int main()
{
    char a[100];
    scanf("%s",a);
    int i=0;
    BTNode* root=CreateTree(a,&i);
    InOrder(root);

    return 0;
}
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root);

二叉树链式结构实现参考代码:

typedef struct BinTreeNode
{
	struct TreeNode* left;
	struct TreeNode* right;
	int val;
}BTNode;

#include"Queue.h"

BTNode* BuyBTNode(int val)
{
	BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->val = val;
	newnode->left = NULL;
	newnode->right = NULL;
	return newnode;
}

//手搓一棵树
BTNode* CreateTree()
{
	BTNode* n1 = BuyBTNode(1);
	BTNode* n2 = BuyBTNode(2);
	BTNode* n3 = BuyBTNode(3);
	BTNode* n4 = BuyBTNode(4);
	BTNode* n5 = BuyBTNode(5);
	BTNode* n6 = BuyBTNode(6);

	n1->left = n2;
	n1->right = n4;
	n2->left = n3;
	n4->left = n5;
	n4->right = n6;

	return n1;
}

//前序遍历
void PreOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}

	printf("%d ", root->val);
	PreOrder(root->left);
	PreOrder(root->right);
}


//中序遍历
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}

	InOrder(root->left);
	printf("%d", root->val);
	InOrder(root->right);
}

//后序遍历
void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}

	InOrder(root->left);
	InOrder(root->right);
	printf("%d", root->val);

}

//层序遍历
void TreeLevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root)
		QueuePush(&q,root);
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);//删除队列节点,树不受影响

		if (front)
		{
			printf("%d", front->val);
			//带入下一层
			QueuePush(&q, front->left);
            QueuePush(&q, front->right);

		}
		else
		{
			printf("N ");
		}//想要空打印N 


	//	printf("%d", front->val);

	//	//带入下一层
	//	if (front->left)
	//		QueuePush(&q, front->left);

	//	if (front->right)
	//		QueuePush(&q, front->right);
	}

	printf("\n");
	QueueDestroy(&q);
}



//int TreeSize(BTNode* root)
//{
//	static int size = 0;//静态变量存在静态区,不存在栈帧,但静态变量只会在第一次调用时被初始化
//	if (root == NULL)
//		return 0;
//	else
//		++size;
//	TreeSize(root->left);
//	TreeSize(root->right);
//	return size;
//
//}

int size = 0;//设置为全局变量或全局静态变量都可以,有线程安全的风险
//int TreeSize(BTNode* root)
//{
//	if (root == NULL)
//		return 0;
//	else
//		++size;
//	TreeSize(root->left);
//	TreeSize(root->right);
//	return size;
//
//}



//void TreeSize(BTNode* root,int* psize)
//{
//	if (root == NULL)
//		return 0;
//	else
//		++(*psize);
//	TreeSize(root->left,psize);
//	TreeSize(root->right,psize);
//
//}


//分治思想(管理思维)
int TreeSize(BTNode* root)
{
	return root == NULL ? 0 :
		TreeSize(root->left) + TreeSize(root->right) + 1;

}

//求树高
int TreeHeight(BTNode* root)
{
	if (root == NULL)
		return 0;

	int leftHeight = TreeHeight(root->left);
	int rightHeight = TreeHeight(root->right);

	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

//求第K层节点的个数
int TreeKLevel(BTNode* root, int k)
{
	assert(k > 0);

	if (NULL == root)
		return 0;

	if (k == 1)
		return 1;

	//不等于空,且k>1说明第k层的节点在子树里面,转换成子问题求解

	return TreeKLevel(root->left, k - 1);
	+TreeKLevel(root->right, k - 1);
}

//查找在不在
bool TreeFind(BTNode* root, int x)
{
	if (root == NULL)
		return NULL;

	if (root->val == x)
		return true;

	return TreeFind(root->left, x)
		|| TreeFind(root->right, x);
}


//查找x所在的节点
//此方法逻辑清晰
BTNode* TreeFind(BTNode* root, int x)
{
	if (root == NULL)
		return NULL;

	if (root->val == x)
		return root;

	BTNode* ret1 = TreeFind(root->left, x);
	if (ret1)
		return ret1;

	BTNode* ret2 = TreeFind(root->right, x);
	if (ret2)
		return ret2;

	return NULL;
}


//也可简化代码
//BTNode* TreeFind(BTNode* root, int x)
//{
//	if (root == NULL)
//		return NULL;
//
//	if (root->val == x)
//		return root;
//
//	BTNode* ret1 = TreeFind(root->left, x);
//	if (ret1)
//		return ret1;
//
//	return TreeFind(root->right, x);
//}


bool TreeIsComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root)
		QueuePush(&q, root);

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);

		//遇到空就跳出后序判断
		if (front == NULL)
			break;

		QueuePush(&q,front->left);
		QueuePush(&q, front->right);
	}

	//如果都是空,就是完全二叉树,存在非空就不是完全二叉树
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);

		if (front)
		{
			QueueDestroy(&q);
			return false;
		}
	}
		
		
		 QueueDestroy(&q);
		 return true;
}

//销毁
void TreeDestroy(BTNode* root)
{
	if (root == NULL)
		return;

	TreeDestroy(root->left);
	TreeDestroy(root->right);
	free(root);
	//root == NULL;//在里面置空要传二级指针
}


int main()
{
	BTNode* root = CreateTree();
	PreOrder(root);
	printf("\n");

	InOrder(root);
	printf("\n");

	printf("%d\n", TreeSize(root));
	size = 0;//每次调用之前手动初始化一下
	printf("%d\n", TreeSize(root));
	size = 0;
	printf("%d\n", TreeSize(root));

	printf("TreeSize:%d\n", TreeSize(root));
	printf("TreeKLevel:%d\n", TreeKLevel(root, 3));

	BTNode* ret = TreeFind(root, 3);
	printf("TreeFind:%p\n", ret);
	ret->val++;
	PreOrder(root);
	printf("\n");

	printf("TreeIsComplete:%d\n", TreeIsComplete(root));

	TreeLevelOrder(root);

	TreeDestroy(root);
	root = NULL;

	return 0;
}


总结

       经过本文的学习和讨论,我们不仅了解了二叉树的基本定义和特性,还深入学习了如何在C语言中具体实现二叉树的各种操作,包括节点的添加、删除、查找,以及不同的遍历方法。通过实际的代码示例和操作分析,可以发现二叉树在数据存储和快速检索方面的优势,特别是在处理大量数据时更显其重要性。二叉树的应用不仅限于理论知识,它在现实世界的许多问题解决中也发挥着巨大作用,如优先级队列、索引结构等。通过本篇文章,希望读者能够对二叉树的结构有更深刻的认识,并在今后的编程实践中有效运用这一工具,以提高程序的效率和性能。学习二叉树及其操作是一个值得投入时间的过程,它能够极大地提升我们解决问题的能力和编程技巧。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

智能编程,一触即发:使用AIGC优化CSS——提升前端开发效率与质量

文章目录 一、AIGC在CSS优化中的应用场景智能代码生成自动布局调整性能优化建议样式和色彩建议 二、使用AIGC优化CSS的具体步骤明确需求选择AIGC工具输入描述或设计稿审查和调整集成和测试 三、AIGC优化CSS的优势与挑战优势&#xff1a;挑战&#xff1a; 《CSS创意项目实践&…

按图搜索新体验:阿里巴巴拍立淘API返回值详解

阿里巴巴拍立淘API是一项基于图片搜索的商品搜索服务&#xff0c;它允许用户通过上传商品图片&#xff0c;系统自动识别图片中的商品信息&#xff0c;并返回与之相关的搜索结果。以下是对阿里巴巴拍立淘API返回值的详细解析&#xff1a; 一、主要返回值内容 商品信息 商品列表…

<PLC><HMI><汇川>在汇川HMI画面中,如何为UI设置全局样式?

前言 汇川的HMI软件是使用了Qt来编写的,因此在汇川的HMI程序编写过程,是支持使用qt的样式来自定义部件样式的,即qss格式。 概述 汇川的软件本身提供三个系统的style样式,我们可以直接使用,但是,如果系统提供的样式不符合你的需求,那么你可以对其进行修改,或者自己新建…

Docker无法拉取镜像!如何解决?

问题现象 继去年Docker Hub被xxx后&#xff0c;各大NAS的注册表均出现问题&#xff0c;例如群晖的Docker套件注册表无法连接&#xff08;更新至DSM7.2版本后恢复&#xff09;。而在今年2024年6月初&#xff08;约2024.06.06&#xff09;&#xff0c;NAS中最重要的工具Docker又…

RV1126 Linux 系统,接外设,时好时坏(二)排查问题的常用命令

在 RV1126 Linux 系统中,排查外设连接问题时,可以使用多种命令来诊断和调试。以下是一些常用的命令和工具: 1. 查看系统日志 dmesg: 显示内核环形缓冲区的消息,通常包含设备初始化、驱动加载和错误等信息。 dmesg | grep <设备名或相关关键字>journalctl: 查看系统…

内网横向:PTHPTKPTT

1.PHT横向 2.PTK横向 3.PTT横向 1.PHT横向&#xff1a; 条件&#xff1a;有管理员的NTLM Hash 并且目标机器开 放445端口 在工作组环境中&#xff1a; Windows Vista 之前的机器&#xff0c;可以使用本地管理员组内用户进行攻击。 WindowsVista 之后的机器&#xff0c;只能是…

怎么将图片转为pdf?教你5种图片转pdf小技巧

在日常的学习办公中&#xff0c;图片转PDF的需求日益增多&#xff0c;无论是整理旅行照片、工作报告还是学习资料&#xff0c;将图片转换为PDF格式都能让文件更加规范、易于分享和保存。下面给大家分享5种能够将图片转为PDF格式的方法&#xff0c;让你的文档处理变得轻松又高效…

HTTP 缓存

缓存 web缓存是可以自动保存常见的文档副本的HTTP设备&#xff0c;当web请求抵达缓存时&#xff0c;如果本地有已经缓存的副本&#xff0c;就可以从本地存储设备而不是从原始服务器中提取这个文档。使用缓存有如下的优先。 缓存减少了冗余的数据传输缓存环节了网络瓶颈的问题…

UI界面卡顿检测工具--UIHaltDetector

引言&#xff1a; 在日常工作当中&#xff0c;我们经常会遇到软件的界面出现卡顿的问题&#xff0c;而为了确定卡顿原因&#xff0c;我特地写了一个UI界面卡顿的小工具&#xff1a;UIHaltDetector&#xff1b;该工具可以在检测到目标窗口出现卡顿的时候直接打印堆栈日志和输出…

Android 15 适配整理——实践版

背景 谷歌发布Android 15后&#xff0c;国内的手机厂商迅速行动&#xff0c;开始了新系统的适配工作。小米、OPPO、vivo和联想等金标联盟成员联合发布了适配公告&#xff0c;督促APP开发者在2024年8月31日前完成适配工作&#xff0c;否则将面临搜索标签提示、应用降级、分机型…

邮件安全篇:邮件反垃圾系统运作机制简介

1. 什么是邮件反垃圾系统&#xff1f; 邮件反垃圾系统是一种专门设计用于检测、过滤和阻止垃圾邮件的技术解决方案。用于保护用户的邮箱免受未经请求的商业广告、诈骗信息、恶意软件、钓鱼攻击和其他非用户意愿接收的电子邮件的侵扰。 反垃圾系统的常见部署形式 2. 邮件反垃圾…

win11 安装 Gradle

一、win11 安装Gradle(7.5.1)&#xff1a; 1.1、下载二进制包 Gradle下载页面 1.2、配置环境变量 变量名&#xff1a;GRADLE_HOME 变量值&#xff08;二进制包解压路径&#xff09;&#xff1a;D:\develop-tool\gradle-7.5.1 变量名&#xff1a;GRADLE_USER_HOME 变量值&a…

6 C 语言指针的奥秘:理论与实践详解

目录 1 变量访问机制 1.1 内存地址 1.2 变量的直接访问 1.3 变量的间接访问 2 指针变量及其内存大小 2.1 指针与指针变量 2.2 指针变量的定义格式 2.3 指针变量的内存大小 3 取地址操作符与取值操作符 3.1 取地址操作符 & 3.2 取值操作符 * 3.3 解引用与数据类…

Android APP CameraX应用(02)预览流程

说明&#xff1a;camera子系统 系列文章针对Android12.0系统&#xff0c;主要针对 camerax API框架进行解读。 1 CameraX简介 1.1 CameraX 预览流程简要解读 CameraX 是 Android 上的一个 Jetpack 支持库&#xff0c;它提供了一套统一的 API 来处理相机功能&#xff0c;无论 …

基于微信小程序的高校排课系统 /基于微信小程序的排课管理系统/课程管理系统

摘 要 随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#xff0c;各行各业相继进入信息管理时代&a…

PE文件(十二)导入表

导入表 导入表的引入 当一个PE文件&#xff08;如.dll/.exe等&#xff09;需要使用别的模块的函数&#xff0c;也叫做依赖某模块&#xff0c;就需要一个清单来记录使用的模块&#xff08;一般为.dll文件&#xff0c;为方便理解&#xff0c;以后我们将模块都认为是.dll文件&am…

electron安装及快速创建

electron安装及快速创建 electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 详细内容见官网&#xff1a;https://www.electronjs.org/zh/docs/latest/。 今天来记录下练习中的安装过程和hello world的创建。 创建项目文件夹&#xff0c;并执行npm 初始化命…

将iPad 作为Windows电脑副屏的几种方法(二)

将iPad 作为Windows电脑副屏的几种方法&#xff08;二&#xff09; 1. 前言2. EV 扩展屏2.1 概述2.2 下载、安装、连接教程2.3 遇到的问题和解决方法2.3.1 平板连接不上电脑 3. Twomon SE3.1 概述3.2 下载安装教程 4. 多屏中心&#xff08;GlideX&#xff09;4.1 概述4.2 下载安…

东莞网站建设前期需要做好哪些工作

在进行东莞网站建设前&#xff0c;需要做好一系列的前期工作。这些工作的实施将为网站建设奠定坚实的基础&#xff0c;确保项目能够按计划顺利进行&#xff0c;并最终达到预期的目标。 首先&#xff0c;需要明确网站的定位和目标。东莞是一个经济发达的城市&#xff0c;因此&am…

Oracle系统表空间的加解密

实验环境 数据库选择的是orclpdb1&#xff0c;当前系统表空间未加密&#xff1a; SQL> show con_nameCON_NAME ------------------------------ ORCLPDB1SQL> select TABLESPACE_NAME, STATUS, ENCRYPTED from dba_tablespaces;TABLESPACE_NAME STATUS …