【数据结构】8. 二叉树

文章目录

一、树的概念及结构

1、树的概念

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

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

  • 注意: 在树形结构中,子树间是不相交的,并且除根结点外每个结点有且只有一个父结点,否则就不是树形结构。

2、树的相关概念

★结点的度: 结点的子树个数。 如上图:A的度为6。

★叶结点或终端结点: 度为0的结点。如上图:B、C、H、I等结点为叶结点。
非终端结点或分支结点: 度不为0的结点。 如上图:D、E、F、G等结点为分支结点

★双亲结点或父结点: 含有子结点的结点,这个结点称为其子结点的父结点。 如上图:A是B的父结点。
★孩子结点或子结点: 结点的子树的根结点,称为该结点的子节点。 如上图:B是A的孩子结点。
兄弟结点: 具有相同父结点的结点互称为兄弟结点; 如上图:B、C是兄弟结点

★树的度: 树中所有结点度的最大值。

★树的高度或深度: 树中结点的最大层次。如上图:树的高度为4。

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

★结点的祖先: 从根到该结点所经分支上的所有结点。如上图:A->D->H这条分支上,A、D都是H的祖先。
子孙: 以某结点为根的子树中任一结点都称为该结点的子孙。如上图:所有结点都是A的子孙。
森林: 由m棵互不相交的树的集合称为森林。

3、树的表示

树的结构体中既要保存值域,也要保存结点和结点之间的关系。实际中树有很多种表示方式:双亲表示法,孩子表示法,孩子双亲表示法以及孩子兄弟表示法等。

下面是最常用的孩子兄弟表示法

c 复制代码
typedef int DataType;
struct TreeNode
{
	struct TreeNode* leftchild;//左孩子
	struct TreeNode* rightBrother;//右兄弟
	DataType val;//值
};

4、树的实际运用

树在实际中的运用可以用来表示文件系统的目录的树结构。

二、二叉树的概念及结构

1、二叉树的概念

一颗二叉树是由n个结点构成的集合,该集合有两种情况:为空或者由一个根结点加上两棵被称为左子树和右子树的二叉树组成。

从上图可以看出:

①:二叉树不存在度大于2的结点。

②:二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。

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

2、特殊的二叉树

  • 满二叉树:二叉树每一层的结点数都达到最大值,这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为k,且结点总数是2k-1 ,则它就是满二叉树。
  • 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为k,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
    (可以理解为前k-1层都是满的,最后一层不满,最后一层从左到右是连续的)

3、二叉树的性质

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

②:若规定根结点的层数为1,则深度为 h 的二叉树的最大结点数 是2h-1。

③★:对任何一棵二叉树,如果度为0的叶结点个数为 n 0 n_0 n0,度为2的分支结点个数为 n 2 n_2 n2,则有 n 0 n_0 n0= n 2 n_2 n2+1。注意:总结点N = n 0 n_0 n0+ n 1 n_1 n1+ n 2 n_2 n2。(当N为偶数时 n 1 n_1 n1=1,否则为0)

④★:若规定根结点的层数为1,具有n个结点的满二叉树的深度 :h= log ⁡ 2 ( n + 1 ) \log_2 (n+1) log2(n+1)。

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

  • 若i>0,i 位置结点的双亲下标:(i-1)/2
  • 若2i+1<n,左孩子下标:2i+1
  • 若2i+2<n,右孩子下标:2i+2

练习题

c 复制代码
1. 某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为(B)  
A. 不存在这样的二叉树  
B. 200  
C. 198  
D. 199  

解析:根据性质3,叶子结点等于度为2的分支节点+1,也就是200。

2. 下列数据结构中,不适合采用顺序存储结构的是(A)  
A. 非完全二叉树  
B. 堆  
C. 队列  
D. 栈  

解析:非完全二叉树使用顺序存储会浪费大量的空间。

3. 在具有 2n 个结点的完全二叉树中,叶子结点个数为(A)  
A. n  
B. n+1  
C. n-1  
D. n/2  

解析:根据性质3,总结点为偶数,n1=1,N=n0+n1+n2,n0=n2+1,所以N=2n0,叶子结点为n。

4. 一棵完全二叉树的结点数位为 531 个,那么这棵树的高度为( B)  
A. 11  
B. 10  
C. 8  
D. 12  

解析:根据性质2,当n=9时,满二叉树最大有512个节点,多出的节点是下一层的,因此n=10

4、二叉树的存储结构

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

①:顺序存储

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

如图所示:

下面这里使用数组来存储非完全二叉树,可以看到在数组中许多空间都没有使用到,在数组中其实都是浪费了的。

②:链式存储

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

c 复制代码
typedef int BTDataType;

// 二叉链
struct BinaryTreeNode4
{
    struct BinTreeNode* left;//左孩子
    struct BinTreeNode* right;//右孩子
    BTDataType data;//值
};

// 三叉链
struct BinaryTreeNode12
{
    struct BinTreeNode* parent;//双亲
    struct BinTreeNode* left;//左孩子
    struct BinTreeNode* right;//右孩子
    BTDataType data;//值
};

三、二叉树的顺序结构及实现

1、二叉树的顺序结构

一般针对完全二叉树 会使用顺序结构的数组 存储。

堆在物理层面上是数组,而在逻辑层面上是完全二叉树,因此可以使用数组来存储堆。

2、堆的概念及结构

如果有一个关键码的集合K = { k 0 k_0 k0, k 1 k_1 k1 , k 2 k_2 k2 , ..., k n − 1 k_{n-1} kn−1 },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足: K i K_i Ki <= K 2 ∗ i + 1 K_{2*i+1} K2∗i+1且 K i K_i Ki<= K 2 ∗ i + 2 K_{2*i+2} K2∗i+2( K i K_i Ki >= K 2 ∗ i + 1 K_{2*i+1} K2∗i+1 且 K i K_i Ki >= K 2 ∗ i + 2 K_{2*i+2} K2∗i+2) 。 i = 0,1,2...,则称为小堆(或大堆)。

将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。

堆的性质:

①堆中某个结点的值总是大于等于或小于等于其父结点的值。

②堆是一棵完全二叉树。

如图所示:

堆的练习题:

c 复制代码
1. 下列关键字序列为堆的是:(A)
A. 100,60,70,50,32,65
B. 60,70,65,50,32,100  
C. 65,100,70,32,50,60 
D. 70,65,100,32,50,60 
E. 32,50,100,70,65,60
F. 50,100,70,65,60,32  

选项A如图:

c 复制代码
2. 已知小根堆为8,15,10,21,34,16,12,删除关键字8之后需重建堆,在此过程中,关键字之间的比较次数是(B)。  
A. 1  
B. 2  
C. 3  
D. 4  

如图所示:

c 复制代码
3. 最小堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后,其结果是()  
A. [3,2,5,7,4,6,8]  
B. [2,3,5,7,4,6,8]  
C. [2,3,4,5,7,8,6]  
D. [2,3,4,5,6,7,8]

如图所示:

3、堆的实现

0)准备工作

实现堆之前我们先创建三个文件:

Heap.h:库函数头文件的声明,堆结构体的定义,方法的声明。

Heap.c:方法的实现。

test.c:方法的测试。

先来实现Heap.h文件:

c 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

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

//算法的声明
//交换数据
void Swap(HPDataType* p1, HPDataType* p2);

//向上调整算法
void AdjustUP(HPDataType* a, int child);

//向下调整算法
void AdjustDown(HPDataType* a, int n, int parent);

//方法的声明
//堆的初始化与销毁
void HPInit(HP* php);
void HPDestroy(HP* php);

//堆的插入
void HPPush(HP* php, HPDataType x);

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

//取堆顶元素
HPDataType HPTop(HP* php);

//堆的判空
bool HPEmpty(HP* php);

//堆的排序
void HPSort(HPDataType* a, int n);

1)算法

1.a)数据交换
c 复制代码
void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
1.b)向上调整算法
c 复制代码
void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] < a[parent])//小堆
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

下面这条代码是创建小堆的判断条件:只要父亲大于孩子,就交换顺序。

c 复制代码
if (a[child] < a[parent])

只要改变判断条件:只要父亲小于孩子,就交换顺序。就是创建大堆。

c 复制代码
if (a[child] > a[parent])
1.c)向下调整算法
c 复制代码
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])
		{
			++child;
		}
		
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}

如果想要建大堆,那么上述代码要修改的有两点:

①:先找到较大的孩子。

②:改变判断条件。

1.e)向上/向下调整算法时间复杂度分析



由上述公式推导得出

  • 向下调整算法的时间复杂度为:O(N)
  • 向上调整算法的时间复杂度为:0(N*logN)

2)初始化和销毁

c 复制代码
void HPInit(HP* php)
{
	assert(php);

	php->a = NULL;
	php->size = php->capacity = 0;
}

void HPDestroy(HP* php)
{
	assert(php);

	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;
}

再在test.c中进行测试:

3)堆的插入

在数据插入之后再调用向上调整算法。

c 复制代码
void HPPush(HP* php, HPDataType x)
{
	assert(php);

	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
		HPDataType* tmp = (HPDataType*)realloc(php->a, newcapacity * sizeof(HPDataType));
		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);
}

再进行测试:

c 复制代码
void test()
{
	int a[] = { 4,2,8,1,5,6,9,7,3,2 };
	int len = sizeof(a) / sizeof(a[0]);
	HP hp;
	HPInit(&hp);

	//依次将数组元素插入到堆上
	for (int i = 0; i < len; i++)
	{
		HPPush(&hp, a[i]);
	}
}

int main()
{
	test();

	return 0;
}

调试结果:

逻辑结构如图所示:

4)堆的删除

删除堆是删除堆顶的数据,将堆顶的数据跟最后的数据先交换,然后删除数组最后一个数据(原来的堆顶),再进行向下调整算法。

c 复制代码
void HPPop(HP* php)
{
	assert(php);
	assert(php->size);

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

	AdjustDown(php->a, php->size, 0);
}

再进行测试:

c 复制代码
void test()
{
	int a[] = { 4,2,8,1,5,6,9,7,3,2 };
	int len = sizeof(a) / sizeof(a[0]);
	HP hp;
	HPInit(&hp);

	//依次将数组元素插入到堆上
	for (int i = 0; i < len; i++)
	{
		HPPush(&hp, a[i]);
	}

	HPPop(&hp);
	HPPop(&hp);
	
	HPDestroy(&hp);
}

int main()
{
	test();

	return 0;
}

调试结果:

5)取堆顶元素

直接返回堆顶元素即可。

c 复制代码
HPDataType* HPTop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	return php->a[0];
}

再进行测试;

c 复制代码
void test()
{
	int a[] = { 4,2,8,1,5,6,9,7,3 };
	int len = sizeof(a) / sizeof(a[0]);
	HP hp;
	HPInit(&hp);

	//依次将数组元素插入到堆上
	for (int i = 0; i < len; i++)
	{
		HPPush(&hp, a[i]);
	}
	
	printf("%d\n", HPTop(&hp));
	
	HPPop(&hp);
	printf("%d\n", HPTop(&hp));

    HPDestroy(&hp);
}

int main()
{
	test();

	return 0;
}

运行结果:

6)堆的判空

直接判断size是否等于0即可。

c 复制代码
bool HPEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}

再进行测试依次取出所有堆顶的元素:

c 复制代码
void test()
{
	int a[] = { 4,2,8,1,5,6,9,7,3 };
	int len = sizeof(a) / sizeof(a[0]);
	HP hp;
	HPInit(&hp);

	//依次将数组元素插入到堆上
	for (int i = 0; i < len; i++)
	{
		HPPush(&hp, a[i]);
	}

	while (!HPEmpty(&hp))
	{
		printf("%d ", HPTop(&hp));
		HPPop(&hp);
	}
}

int main()
{
	test();

	return 0;
}

运行结果:

4、堆的应用

1)堆排序

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

①建堆

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

建堆使用向上或者向下调整算法都可以实现,向上调整算法其实套用的就是建堆的方法,如果使用向下调整算法建堆,时间复杂度更小,效率会更高。

②利用堆删除思想来进行排序。

在堆中被删除的数据,实际上在数组中还存在,并且已经处于有序状态。

c 复制代码
void HPSort(HPDataType* a, int n)
{
	//建堆   
	//向上调整
	//for (int i = 1; i < n; i++)
	//{
	//	AdjustUp(a, i);
	//}

	//向下调整   O(N)
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}

	//堆删除   O(N*logN)
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

再进行测试:

c 复制代码
int main()
{
	int arr[] = { 4,2,8,1,5,6,9,7,3 };
	int len = sizeof(arr) / sizeof(arr[0]);
	HPSort(arr, len);

	for (int i = 0; i < len; i++)
	{
		printf("%d ", arr[i]);
	}
	
	return 0;
}

运行结果:

堆排序的时间复杂度分析:

如果建堆时采用向上调整算法,那么堆排序的时间复杂度为:O(N* l o g N log^N logN)。相比于冒泡排序的时间复杂度O(N2)已经大大减少了。

如果建堆时采用向下调整算法,时间复杂度将锐减为O(N),效率大大提升。

2) TOP-K问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

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

①用数据集合中前K个元素来建堆

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

②用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素。将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

c 复制代码
void testHeap2()
{
	int k;
	printf("请输入k:");
	scanf("%d", &k);

	//开辟k个大小的数组
	int* kminheap = (int*)malloc(sizeof(int) * k);
	if (kminheap == NULL)
	{
		perror("malloc fail");
		return;
	}

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

	//读取文件前k个数
	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &kminheap[i]);
	}

	//建k个数的小堆  (向下调整建堆) O(N)
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(kminheap, k, i);
	}

	//读取剩下的N-K个数,并依次与堆顶元素比较
	int x = 0;
	while (fscanf(fout, "%d", &x) > 0)
	{
		if (x > kminheap[0])
		{
			kminheap[0] = x;
			AdjustDown(kminheap, k, 0);
		}
	}

	//打印
	printf("最大的前%d个数:", k);
	for (int i = 0; i < k; i++)
	{
		printf("%d ", kminheap[i]);
	}
	printf("\n");
}

接着再进行测试:

首先创建一个100000个随机整数的文件data.txt

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

	for (int i = 0; i < n; i++)
	{
		//生成10000000以内的数
		int x = (rand() + i) % 10000000;
		fprintf(fin, "%d\n", x);
	}

	fclose(fin);
}

为了方便我们查看是否查找正确,我们可以在data.txt中将部分数据改得突出一点。

现在就可以测试了。

c 复制代码
int main()
{
	//CreateNDate();
	testHeap2();
	return 0;
}

运行结果:

四、二叉树链式结构的实现

1、二叉树的链式结构

0)准备工作

我们在这里创建3个文件:
BT.h:二叉树的定义,方法的声明
BT.c:方法的实现
test.c:测试

先在BT.h中实现二叉树的定义以及方法的声明:

c 复制代码
#include<stdio.h.>
#include<stdlib.h>

typedef int BTDataType;

typedef struct BinaryTreeNode
{
    BTDataType data;
    struct BinaryTreeNode* left;
    struct BinaryTreeNode* right;
} BTNode;

//手动创建二叉树
BTNode* CreateBinaryTree();

//前序
void PrevOrder(BTNode* root);

//中序
void InOrder(BTNode* root);

//后序
void PostOrder(BTNode* root);

1)二叉树的手动创建

在手动创建二叉树之前,需要先编写创建二叉树单独节点的函数。

c 复制代码
BTNode* BuyNode(int x)
{
    BTNode* node = (BTNode*)malloc(sizeof(BTNode));
    if (node == NULL)
    {
        perror("malloc fail");
        return NULL;
    }

    node->data = x;
    node->left = NULL;
    node->right = NULL;

    return node;
}

接着再进行创建:

c 复制代码
BTNode* CreateBinaryTree()
{
    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;
}

创建的二叉树的逻辑结构如下所示:

2)二叉树的遍历(前序、中序、后序)

三种遍历如下所示:

c 复制代码
//前序
void PrevOrder(BTNode* root)
{
    //访问到空节点
    if (root == NULL)
    {
        printf("N ");
        return;
    }

    printf("%d ", root->data);
    PrevOrder(root->left);
    PrevOrder(root->right);
}

//中序
void InOrder(BTNode* root)
{
    //访问到空节点
    if (root == NULL)
    {
        printf("N ");
        return;
    }

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

//后序
void PostOrder(BTNode* root)
{
    //访问到空节点
    if (root == NULL)
    {
        printf("N ");
        return;
    }

    PostOrder(root->left);
    PostOrder(root->right);
    printf("%d ", root->data);
}

再来进行测试:

c 复制代码
int main()
{
	//创建一颗二叉树
	BTNode* root = CreateBinaryTree();

	//前序
	printf("前序遍历:");
	PrevOrder(root);
	printf("\n");

	//中序
	printf("中序遍历:");
	InOrder(root);
	printf("\n");

	//后序
	printf("后序遍历:");
	PostOrder(root);
	printf("\n");
	return 0;
}

运行结果:

因此遍历节点的先后顺序如下所示:

c 复制代码
前序遍历结果:1 2 3 4 5 6
中序遍历结果:3 2 1 5 4 6
后序遍历结果:3 2 5 6 4 1

3)节点个数

c 复制代码
//节点个数
int TreeSize(BTNode* root)
{
    return root == NULL ? 0 :
        TreeSize(root->left) + TreeSize(root->right) + 1;
}

再进行测试;

c 复制代码
int main()
{
	BTNode* root = CreateBinaryTree();
	printf("节点个数:%d\n", TreeSize(root));
	return 0;
}

运行结果:

4)叶子节点个数

c 复制代码
//叶子节点个数
int TreeLeafSize(BTNode* root)
{
    if (root == NULL)
        return 0;

    if (root->left == NULL && root->right == NULL)
        return 1;

    return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}

再进行测试:

c 复制代码
int main()
{
	BTNode* root = CreateBinaryTree();
	printf("叶子节点个数:%d\n", TreeLeafSize(root));
	return 0;
}

运行结果:

5)高度

c 复制代码
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;
}

再进行测试:

c 复制代码
int main()
{
	BTNode* root = CreateBinaryTree();
	printf("二叉树的高度为:%d\n", TreeHeight(root));
	return 0;
}

运行结果:

这里测试的二叉树依旧是之前的创建的二叉树,高度为3,上面的图片仅作方法的演示。

6)第k层节点数

c 复制代码
int TreeLevelKSize(BTNode* root, int k)
{
    if (root == NULL)
        return 0;

    if (k == 1)
        return 1;

    //子问题
    return TreeLevelKSize(root->left, k - 1) + 
        TreeLevelKSize(root->right, k - 1);
}

进行测试:

c 复制代码
int main()
{
	BTNode* root = CreateBinaryTree();
	printf("二叉树第3层的节点个数为:%d\n", TreeLevelKSize(root,3));
	return 0;
}

运行结果:

7)查找值为x的节点

c 复制代码
BTNode* TreeFind(BTNode* root,int x)
{
    if (root == NULL)
        return NULL;

    if (root->data == 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;
}

再进行测试:

c 复制代码
int main()
{
	BTNode* root = CreateBinaryTree();
	int k = 0;
	printf("请输入你要查找的值:");
	scanf("%d", &k);
	int find = TreeFind(root, k);
	if (find)
		printf("找到了!\n");

	else
		printf("没有找到...\n");
	return 0;
}

运行结果:

2、二叉树的创建和销毁

1)前序遍历数组构建二叉树

例如:通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树。

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

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

     root->left = CreateTree(a, pi);
     root->right = CreateTree(a, pi);

     return root;
 }

我们可以通过前序遍历这个二叉树并打印,观察是否成功创建:

c 复制代码
 void PrevPrint(BTNode* root)
 {
     if (root == NULL)
     {
         printf("# ");
         return;
     }

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

 int main()
 {
     char a[30] = "ABD##E#H##CF##G## ";
     int i = 0;
     BTNode* root = CreateTree(a, &i);
     PrevPrint(root);
     return 0;
 }

运行结果:

可以看到已经成功创建二叉树。

2)二叉树的销毁

c 复制代码
 void TreeDestroy(BTNode* root)
 {
     if (root == NULL)
         return;
    
     TreeDestroy(root->left);
     TreeDestroy(root->right);
     free(root);
 }

3)判断二叉树是否是完全二叉树

c 复制代码
 bool TreeComplete(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);

         //遇到非空->不是完全二叉树  直接销毁返回false
         if (front)
         {
             QueueDestroy(&q);
             return false;
         }
     }

     //是完全二叉树
     QueueDestroy(&q);
     return true;
 }
相关推荐
体系结构论文研讨会34 分钟前
多项式环及Rq的含义
算法
智驱力人工智能42 分钟前
极端高温下的智慧出行:危险检测与救援
人工智能·算法·安全·行为识别·智能巡航·高温预警·高温监测
森焱森1 小时前
60 美元玩转 Li-Fi —— 开源 OpenVLC 平台入门(附 BeagleBone Black 驱动简单解析)
c语言·单片机·算法·架构·开源
千帐灯无此声1 小时前
Linux 测开:日志分析 + 定位 Bug
linux·c语言·c++·bug
课堂剪切板2 小时前
ch07 题解
算法·深度优先
R_AirMan2 小时前
深入浅出Redis:一文掌握Redis底层数据结构与实现原理
java·数据结构·数据库·redis
ChuHsiang2 小时前
【C语言学习】实现游戏——三子棋
c语言
科大饭桶3 小时前
数据结构自学Day5--链表知识总结
数据结构·算法·leetcode·链表·c
小高Baby@3 小时前
map数据结构在Golang中是无序的,并且键值对的查找效率较高的原因
数据结构
北风toto3 小时前
python学习DataFrame数据结构
数据结构·python·学习