【数据结构】—— 树和二叉树

news2024/12/24 3:26:46

  • 1、树的概念
  • 2、树的相关术语
  • 3、树的常见表示方法
  • 4、树的实际应用
  • 5、二叉树的相关概念和性质
  • 6、二叉树的顺序存储(堆)
    • 6.1 堆的概念
    • 6.2 堆的结构和接口
    • 6.3 堆的初始化和销毁
    • 6.4 堆的插入
    • 6.5 堆的删除
    • 6.5 取堆顶数据
    • 6.6 获取有效节点个数
    • 6.7 判空
    • 6.8 源代码
  • 7、堆的应用
    • 7.1 堆排序
    • 7.2 TopK
  • 8、二叉树的链式结构
    • 8.1 基本概念
    • 8.2 结构和接口
    • 8.3 二叉树的创建
    • 8.4 二叉树的遍历
      • 前序遍历
      • 中序遍历
      • 后序遍历
    • 8.5 二叉树节点个数
    • 8.6 二叉树的深度/高度
    • 8.7 二叉树叶节点个数
    • 8.8 二叉树第k层节点个数
    • 8.9 二叉树查找值为x的节点
    • 8.10 层序遍历
    • 8.11 判断是否为完全二叉树
    • 8.12 销毁
    • 8.13 源代码
  • 总结和思考

1、树的概念

在这里插入图片描述

树是一种非线性的数据结构,它是由n(n>=0) 个有限结点组成一个具有层次关系的集合,当n=0时被称为空树。
树的特点

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

2、树的相关术语

在这里插入图片描述

  • 节点的度:一个节点含有的子树的个数称为该节点的度
  • 叶节点或终端节点:度为0的节点称为叶节点,如上图:B、C、H、I、P…等节点
  • 非终端节点或分支节点:度不为0的节点,如上图:D、E、F、G…等节点
  • 双亲节点或父节点:若一个节点含有子节点,则这个节点被称为其子节点的父节点,如F是K和L的父节点
  • 孩子节点或子节点:一个节点含有子树的根节点称为该节点的子节点,如J是E的子节点
  • 兄弟节点:具有相同父节点的节点互称为兄弟节点,如D和E,I和J等等
  • 树的度:一棵树中,最大的节点的度称为树的度,如上图度为6
  • 节点的层次:从根开始定义,根为第1层,根的子节点为第2层,以此类推
  • 树的深度或高度:树中节点的最大层次,如上图:树的高度为4
  • 堂兄弟节点:双亲在同一层的互为堂兄弟;如上图:H和I,他们的父亲在同一层
  • 节点的祖先:从根到该节点所经分支上所有节点的祖先。如上图:A是图中所有节点的祖先
  • 子孙:以某节点为根的所有子树的节点称为该节点的子孙。如上图:所有节点都是A的子孙
  • 森林:由m棵互不相交的树的集合称为森林

3、树的常见表示方法

(1)链式结构(左孩子右兄弟)

typedef struct TreeNode {
    int data;
    struct TreeNode* left;//左孩子
    struct TreeNode* right;//右兄弟
}TNode;//链式结构是最常见的表示结构

优点

  • 无需浪费空间:链表动态分配内存,这样在构建树时不需要预先知道树的大小,可以有效利用内存。

  • 插入删除效率高 :在链式结构中,插入或删除节点不需要移动大量数据,只需调整指针,操作时间复杂度为 O(1),使得这些操作相对高效。因此在树的结构变化频繁的场景下,链式结构表示更为合适

缺点

  • 不适合需要频繁执行全树遍历或计算树的深度、宽度等操作的场景。
  • 节点除了存储数据外,还需要存储指向子节点的指针,增加了额外的内存开销

在这里插入图片描述

(2)动态顺序结构

typedef struct Heap
{
	HPDataType* a;//相当于数组存储
	int size;
	int capacity;//开辟的空间,用于判断是否需要扩容
}HP;

优点

  • 简单高效的访问:可以通过索引快速访问任何节点,通常时间复杂度为 O(1)
  • 节省空间:无需额外申请节点

缺点:插入、删除操作及频繁调整树结构时需要移动大量元素,特别是数组的头部和中间位置,时间复杂度为O(n)

(3)静态顺序结构

#define MAX_SIZE 100

int tree[MAX_SIZE];  // 假设树的节点存储在数组中

适合完全二叉树满二叉树

(4)邻接表表示法

typedef struct Node {
    int data;                   // 节点数据
    struct Node** children;     // 指向子节点的指针数组
    int childCount;             // 子节点数量
} Nd;

邻接表表示法在处理树结构时非常高效,尤其是在动态结构空间利用方面表现突出
(5)链表数组表示法

#define MAX_LEVELS 10

typedef struct TreeNode {
    int data;
    struct TreeNode* next;
} TreeNode;

TreeNode* levelArray[MAX_LEVELS];

链表数组表示法结合了数组和链表的优点,适用于需要高效存储树结构的场景,特别是当树的结构较为稀疏时(和普通链式结构二叉树原理类似)

4、树的实际应用

  • 文件系统
    操作系统中的文件系统通常使用树结构来组织文件和目录。根目录是树的根节点,子目录和文件作为树的子节点,形成层级关系。
    在这里插入图片描述

  • 数据库索引
    数据库管理系统(DBMS)使用树结构(如B树或B+树)来索引数据表。索引结构帮助快速定位表中的记录。
    在这里插入图片描述

5、二叉树的相关概念和性质

在这里插入图片描述

二叉树是一种特殊的树形结构
二叉树需要满足两个条件:

  • 树形结构
  • 所有节点的度<=2

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

在这里插入图片描述
满二叉树:每一层节点均为最大节点数的二叉树。
完全二叉树:前h-1层为满二叉树,第h层自左向右是连续的二叉树。

在这里插入图片描述
二叉树的节点数和深度计算

  • 二叉树第i层的结点个数:若规定根结点的层数为 1 ,则一棵非空二叉树的第i层上最多有 2i-1 个结点
  • 二叉树的总结点个数:若规定根结点的层数为 1 ,则深度为 h 的二叉树的最大结点数是 2h-1
  • 满二叉树的深度:若规定根结点的层数为 1 ,则具有 n 个结点的满二叉树的深度为h = log2 (n + 1)
  • 二叉树的叶节点个数和度为2节点个数的关系h0=h2+1

叶节点个数代表二叉树的分支个数
在这里插入图片描述

6、二叉树的顺序存储(堆)

(实现小根堆)

6.1 堆的概念

使用数组存储二叉树即为堆

堆可以分为两种主要类型
最大堆:每个节点的关键码都大于或等于其子节点的关键码,即根节点具有最大的关键码
最小堆:每个节点的关键码都小于于或等于其子节点的关键码,即根节点具有最小的关键码

最大堆(大根堆)
在这里插入图片描述

最小堆(小根堆)
在这里插入图片描述

6.2 堆的结构和接口

typedef int HPDataType;

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

//初始化
void HeapInit(HP* php);
//销毁
void HeapDestroy(HP* php);
//插入
void HeapPush(HP* php, HPDataType x);
// 规定删除堆顶(根节点)
void HeapPop(HP* php);
//获取堆顶元素
HPDataType HeapTop(HP* php);
//获取堆有效元素个数
size_t HeapSize(HP* php);
//判空
bool HeapEmpty(HP* php);
//用于交换节点
void Swap(HPDataType* p1, HPDataType* p2);
//向上调整
void AdjustUp(HPDataType* a, int child);
//向下调整
void AdjustDown(HPDataType* a, int size, int parent);

6.3 堆的初始化和销毁

和顺序表类似

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

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

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

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

6.4 堆的插入

用向上调整算法遍历整个数组,交换节点位置,使新节点插入到合适的位置

void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

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;
		}
	}
}


// O(logN)
void HeapPush(HP* php, HPDataType x)
{
	assert(php);
	if (php->size == php->capacity)
	{
		int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, newCapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}

		php->a = tmp;
		php->capacity = newCapacity;
	}

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

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

向上调整算法分析:
如何获取父节点:parent=(child-1/)2;
将新节点插入到堆底,随后和父节点比较大小,如果小于父节点则交换两个节点,更新父节点和孩子节点位置,再继续向上调整,直到堆中所有父节点都小于孩子节点。否则直接退出循环
循环条件:while(chilid>0);一直调整到根节点
在这里插入图片描述

6.5 堆的删除

堆的删除时删除堆顶数据。将堆底元素覆盖堆顶,对堆顶元素使用向下调整算法即可

void AdjustDown(HPDataType* a, int size, int parent)
{
	int child = parent * 2 + 1;

	while (child < size)
	{
		// 假设左孩子小,如果解设错了,更新一下

		if (child + 1 < size && a[child + 1] < a[child])
		{
			++child;
		}

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

void HeapPop(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);
}

向下调整算法分析
已知一个父节点parent算出左右孩子节点:

  • 左孩子:child = 2 * parent + 1
  • 右孩子:child = 2 * parent + 2
    在这里插入图片描述

向下调整算法:如果建的是小堆,首先从根节点作为父结点开始与其左右孩子进行比较,如果父节点大于左孩子节点或者大于右孩子节点(如果都大于则与最小的孩子节点进行交换),然后更新父节点和孩子节点,继续向下调整,如果父节点小于左右孩子节点则停止,使得所有的父节点都小于或等于孩子节点。如果建的是大堆,则反之。

注意:当向下调整到最后一层的时候就停止,因为最后一层如果作为父节点是没有孩子节点的,所以不用继续向下调整了,所以循环条件为child<n

6.5 取堆顶数据

返回堆顶即可

HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	return php->a[0];
}

6.6 获取有效节点个数

返回size即可

size_t HeapSize(HP* php)
{
	assert(php);

	return php->size;
}

6.7 判空

判断size的大小即可

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

	return php->size == 0;
}

6.8 源代码

.h头文件

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

typedef int HPDataType;

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

void HeapInit(HP* php);
void HeapDestroy(HP* php);
void HeapPush(HP* php, HPDataType x);
// 规定删除堆顶(根节点)
void HeapPop(HP* php);
HPDataType HeapTop(HP* php);
size_t HeapSize(HP* php);
bool HeapEmpty(HP* php);

void Swap(HPDataType* p1, HPDataType* p2);
void AdjustUp(HPDataType* a, int child);
void AdjustDown(HPDataType* a, int size, int parent);

void TopK1(int* a, int n, int k);
void TopK2(int* a, int n, int k);

.c源文件

#include"Heap.h"

// 小堆
void HeapInit(HP* php)
{
	assert(php);

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

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

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

void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//20:19jixu 
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 = (child - 1) / 2;
			//child = (child - 1) / 2;
			//parent = (parent - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

// O(logN)
void HeapPush(HP* php, HPDataType x)
{
	assert(php);
	if (php->size == php->capacity)
	{
		int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, newCapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}

		php->a = tmp;
		php->capacity = newCapacity;
	}

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

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

// 21:15继续

void AdjustDown(HPDataType* a, int size, int parent)
{
	int child = parent * 2 + 1;

	while (child < size)
	{
		// 假设左孩子小,如果解设错了,更新一下

		if (child + 1 < size && a[child + 1] > a[child])
		{
			++child;
		}
		//大堆
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapPop(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);
}

HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	return php->a[0];
}

size_t HeapSize(HP* php)
{
	assert(php);

	return php->size;
}

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

	return php->size == 0;
}

void TopK1(int* a, int n, int k)
{
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, k, i);
	}
	printf("排序后:");
	//堆排序
	int end = n - 1;
	while (k > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		printf("%d ", a[end]);
		--end;
		k--;
	}
}


void TopK2(int* a, int n, int k)
{
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, k, i);
	}
	printf("排序后:");
	//topk
	for (int i = k; i < n; i++)
	{
		if (a[i] < a[0])
		{
			Swap(&a[i], &a[0]);
			AdjustDown(a, k, 0);
		}
	}
}

测试文件

#include"Heap.h"

//int main()
//{
//	int a[] = { 4,6,2,1,5,8,2,9};
//	HP hp;
//	HeapInit(&hp);
//	for (int i = 0; i < sizeof(a) / sizeof(int); ++i)
//	{
//		HeapPush(&hp, a[i]);
//	}
//
//	/*int k = 3;
//	while (k--)
//	{
//		printf("%d\n", HeapTop(&hp));
//		HeapPop(&hp);
//	}*/
//
//	while (!HeapEmpty(&hp))
//	{
//		printf("%d ", HeapTop(&hp));
//		HeapPop(&hp);
//	}
//	printf("\n");
//
//	return 0;
//}

// 升序
//void HeapSort(int* a, int n)
//{
//	// 建大堆
//
//	// O(N*logN)
//	/*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);
//	}
//	//堆排序
//	int end = n - 1;
//	while (end > 0)
//	{
//		Swap(&a[0], &a[end]);
//		AdjustDown(a, end, 0);
//		--end;
//	}
//}
//
//int main()
//{
//	int a[] = { 4, 6, 2, 1, 5, 8, 2, 9 };
//	printf("排序前:");
//	for (int i = 0; i < sizeof(a) / sizeof(int); i++)
//	{
//		printf("%d ", a[i]);
//	}
//	printf("\n");
//	HeapSort(a, sizeof(a)/sizeof(int));
//	printf("排序后:");
//	for (int i = 0; i < sizeof(a)/sizeof(int); i++)
//	{
//		printf("%d ", a[i]);
//	}
//	printf("\n");
//
//	return 0;
//}



void HeapSort(int* a, int n)
{
	// 建大堆

	// O(N*logN)
	/*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);
	}
	//堆排序
	int end = n-1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

//int main()
//{
//	int a[] = { 4, 6, 2, 1, 5, 8, 2, 9 };
//	printf("排序前:");
//	for (int i = 0; i < sizeof(a) / sizeof(int); i++)
//	{
//		printf("%d ", a[i]);
//	}
//	printf("\n");
//	HeapSort(a, sizeof(a) / sizeof(int));
//	printf("排序后:");
//	for (int i = 0; i < sizeof(a) / sizeof(int); i++)
//	{
//		printf("%d ", a[i]);
//	}
//	printf("\n");
//
//	return 0;
//}



int main()
{
	int a[] = { 4, 6, 10, 1, 5, 8, 2, 9 };
	printf("排序前:");
	for (int i = 0; i < sizeof(a) / sizeof(int); i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
	TopK2(a, sizeof(a) / sizeof(int), 3);
	for (int i = 0; i < 3; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");

	return 0;
}

7、堆的应用

1)堆排序
2)TopK

7.1 堆排序

在原数组排序

(1)将原数组调整为堆
(2)交换堆顶元素和堆尾元素
(3)堆顶进行向下调整操作
(4)调整元素个数end--;,使调整过的元素在正确位置,不再改变(此时堆底的i个元素已经在正确位置,因此不当作在堆里面)
(5)重复2~4步骤直到end=1,排序结束

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
#include<time.h>

typedef int HPDataType;

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

void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void AdjustDown(HPDataType* a, int size, int parent)
{
	int child = parent * 2 + 1;

	while (child < size)
	{
		// 假设左孩子小,如果解设错了,更新一下

		if (child + 1 < size && a[child + 1] < a[child])
		{
			++child;
		}

		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
//升序建大堆
void HeapSort(int* a, int n)
{
	// 建大堆

	// O(N*logN)
	/*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);
	}
	//堆排序
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

int main()
{
	int a[] = { 4, 6, 2, 1, 5, 8, 2, 9 };
	printf("排序前:");
	for (int i = 0; i < sizeof(a) / sizeof(int); i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
	HeapSort(a, sizeof(a)/sizeof(int));
	printf("排序后:");
	for (int i = 0; i < sizeof(a)/sizeof(int); i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");

	return 0;
}

7.2 TopK

最小/大的k个节点

当数据量过大的时候,数据只能存储在内存中,而内存中不能建堆,只能使用第二种方法

方法一对原数组进行操作

  • 时间复杂度:O(nlog2n)
  • 原理和堆排序相同:堆排序操作n-1次完成排序,TopK操作k次找出最大/小的k个节点
    找最大的k个节点用大堆

1)使用向下调整算法建堆
在这里插入图片描述

// 建堆 O(N)
//保证所有父节点大于等于子节点
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
	AdjustDown(a, n, i);
}

2)取堆顶元素,与堆尾元素交换

Swap(&a[0],&a[end-1]);

3)从堆顶向下调整,选出堆中的最值,堆元素个数减1

AdjustDown(a, end, 0);
end--;

4)重复 2 3 步骤k次

以选出最大的3个节点分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

即选出最大的三个节点 99 85 66

完整代码

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
#include<time.h>

typedef int HPDataType;

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

void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void AdjustDown(HPDataType* a, int size, int parent)
{
	int child = parent * 2 + 1;

	while (child < size)
	{
		// 假设左孩子小,如果解设错了,更新一下

		if (child + 1 < size && a[child + 1] > a[child])
		{
			++child;
		}

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


void TopK(int* a, int n, int k)
{
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}
	printf("排序后:");
	//堆排序
	int end = n - 1;
	while (k > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		printf("%d ", a[end]);
		--end;
		k--;
	}
}

方法二:用前k个数据建小堆,后续数据和堆顶数据比较,如果比堆顶数据大,就替代堆顶,向下调整。遍历完数组结束(取小建大堆,取大建小)
具体步骤

((取最大k个数)建小堆 /(取最小k个数)建大堆 )

1) 取前k个数建堆
在这里插入图片描述
2)

  • 将剩余的n-k个节点和堆顶比较大小
  • 如果小于堆顶则入堆(替换堆顶),从堆顶进行一次向下调整(Adjustdown),否则不入堆
    在这里插入图片描述

得出最大/小的k个数(topk)
在这里插入图片描述

void TopK2(int* a, int n, int k)
{
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, k, i);
	}
	printf("排序后:");
	//topk
	for (int i = k; i < n; i++)
	{
		if (a[i] < a[0])
		{
			Swap(&a[i], &a[0]);
			AdjustDown(a, k, 0);
		}
	}
}

需要注意的取得的前k个小的数不一定是有序的
在这里插入图片描述
时间复杂度: O(n) = k + (n − k)log2 k

8、二叉树的链式结构

8.1 基本概念

用链表来表示一棵二叉树, 通常的方法是链表中每个结点由三个域组成:数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。
在这里插入图片描述

8.2 结构和接口

typedef int BTDataType;

typedef struct BinTreeNode
{
	struct BinTreeNode* left;
	struct BinTreeNode* right;
	int val;
}BTNode;
//创建新节点
BTNode* BuyBTNode(BTDataType x);
//前序遍历
void PreOrder(BTNode* root);
//中序遍历
void InOrder(BTNode* root);
//后序遍历
void PostOrder(BTNode* root);
//树的大小
int TreeSize(BTNode* root);
//树的深(高)度
int TreeHeight(BTNode* root);
//二叉树叶子结点个数
int TreeLeafSize(BTNode* root);
//树第k层的节点
int TreeKLevel(BTNode* root, int k);
//查找值为x的节点
BTNode* TreeFind(BTNode* root, int x);
//层序遍历
void TreeLevelOrder(BTNode* root);
//判断是否是完全二叉树
bool TreeisComplete(BTNode* root);
//销毁
void TreeDestroy(BTNode* root);

8.3 二叉树的创建

//创建二叉树新节点
BTNode* BuyBTNode(BTDataType x)
{
	BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
	if (newnode == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	newnode->val = x;
	newnode->left = 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;
}

创建二叉树如下
在这里插入图片描述

8.4 二叉树的遍历

前序遍历

//前序遍历: 根 左子树 右子树
void PreOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}

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

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

中序遍历

//中序遍历:左子树 根 右子树
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}

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

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

后序遍历

//后序遍历:左子树 右子树  根
void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	PostOrder(root->left);
	PostOrder(root->right);
	printf("%d ", root->val);
}

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

8.5 二叉树节点个数

如果该root为NULL,则root不是节点,返回0,否则返回左子树和右子树的节点的总数+1即可

int TreeSize(BTNode* root)
{
	//静态变量不存在栈帧,存在于静态区
	//局部静态成员变量只会被初始化一次
	//static int size = 0;
	/*if (root == NULL) 
		return 0;
	else 
		++size;

	TreeSize(root->left);
	TreeSize(root->right);

	return size;*/
	//以上方法有线程安全的风险
	
	return root == NULL? 0 : TreeSize(root->left) + TreeSize(root->right)+1;
}

8.6 二叉树的深度/高度

如果root是NULL,则root不是节点,返回0,否则返回左子树和右子树的最大深度

int TreeHeight(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	int leftDepth = TreeHeight(root->left);
	int rightDepth = TreeHeight(root->right);
	return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
}

8.7 二叉树叶节点个数

int TreeLeafSize(BTNode* root)
{
	//0个节点
	if (root == NULL)
	{
		return 0;
	}
	//一个节点
	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}

	//返回左子树叶节点和右子树叶节点个数总和
	return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}

8.8 二叉树第k层节点个数

根节点起始位置在第一层,递归k-1次即可到达第k层,此时统计左子树和右子树的总数即可

int TreeKLevel(BTNode* root, int k)
{
	assert(k > 0);

	if (root == NULL) 
		return 0;

	if (k == 1)
		return 1;

	//k>1 分治 转化为子问题求解
	return TreeKLevel(root->left, k - 1) + TreeKLevel(root->right, k - 1);
}

8.9 二叉树查找值为x的节点

BTNode* TreeFind(BTNode* root, int x)
{
	if (root == NULL)
		return NULL;

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

	没找到——>递归子问题
	这样写会造成返回值丢失(能找到指定节点,但是无法返回该节点)
	//TreeFind(root->left, x);
	//TreeFind(root->right, x);


	//为了解决返回值丢失和减少递归次数
	//应当创建节点接收返回值,减少递归次数,降低时间复杂度
	BTNode* ret1 = TreeFind(root->left, x);
	//如果左节点找到了(有返回值)直接返回即可
	if (ret1)
		return ret1;

	BTNode* ret2 = TreeFind(root->right, x);
	//如果右节点找到了(有返回值)直接返回即可
	if (ret2)
		return ret2;

	return NULL;//整个树都没找到,返回NULL
}

8.10 层序遍历

利用队列实现一层一层遍历(FIFO)
本质上是广度优先遍历(bfs)

void TreeLevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);

	if (root != NULL)
	{
		QueuePush(&q, root);
	}
	//从上至下
	while (!QueueEmpty(&q))
	{
		BTNode* top = QueueFront(&q);
		QueuePop(&q);

		if (top)
		{
			printf("%d ", top->val);
			//从左向右
			if (top->left)
			{
				QueuePush(&q, top->left);
			}
			if (top->right)
			{
				QueuePush(&q, top->right);
			}
		}
		else printf("N ");
	}
	printf("\n");
	QueueDestroy(&q);
}

在这里插入图片描述

8.11 判断是否为完全二叉树

利用层序遍历完成

//用层序遍历实现
bool TreeisComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root != NULL)
	{
		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 != NULL)
		{
			QueueDestroy(&q);
			return false;
		}
	}
	QueueDestroy(&q);
	return true;
}

8.12 销毁

从根节点开始,递归左子树和右子树销毁所有节点

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

	TreeDestroy(root->left);
	TreeDestroy(root->right);
	free(root);
}

8.13 源代码

.h头文件

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>4
#include<stdbool.h>
typedef int BTDataType;

typedef struct BinTreeNode
{
	struct BinTreeNode* left;
	struct BinTreeNode* right;
	BTDataType val;
}BTNode;
BTNode* BuyBTNode(BTDataType x);
//前序遍历
void PreOrder(BTNode* root);
//中序遍历
void InOrder(BTNode* root);
//后序遍历
void PostOrder(BTNode* root);
//树的大小
int TreeSize(BTNode* root);
//树的深度
int TreeHeight(BTNode* root);
//二叉树叶子结点个数
int TreeLeafSize(BTNode* root);
//树第k层的节点
int TreeKLevel(BTNode* root, int k);
//查找值为x的节点
BTNode* TreeFind(BTNode* root, int x);
//层序遍历
void TreeLevelOrder(BTNode* root);
//判断是否为完全二叉树
bool TreeisComplete(BTNode* root);
//销毁
void TreeDestroy(BTNode* root);

.c源文件

#include "Bintree.h"
#include "Queue.h"
static int size = 0;
//创建二叉树新节点
BTNode* BuyBTNode(BTDataType x)
{
	BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
	if (newnode == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	newnode->val = x;
	newnode->left = newnode->right = NULL;
	return newnode;
}
//前序遍历: 根 左子树 右子树
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;
	}
	PostOrder(root->left);
	PostOrder(root->right);
	printf("%d ", root->val);
}

int TreeSize(BTNode* root)
{
	//静态变量不存在栈帧,存在于静态区
	//局部静态成员变量只会被初始化一次
	//static int size = 0;
	/*if (root == NULL) 
		return 0;
	else 
		++size;

	TreeSize(root->left);
	TreeSize(root->right);

	return size;*/
	//以上方法有线程安全的风险
	
	return root == NULL? 0 : TreeSize(root->left) + TreeSize(root->right)+1;
}

int TreeHeight(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	int leftDepth = TreeHeight(root->left);
	int rightDepth = TreeHeight(root->right);
	return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
}

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);
}

int TreeKLevel(BTNode* root, int k)
{
	assert(k > 0);

	if (root == NULL) 
		return 0;

	if (k == 1)
		return 1;

	//k>1 分治 转化为子问题求解
	return TreeKLevel(root->left, k - 1) + TreeKLevel(root->right, k - 1);
}

BTNode* TreeFind(BTNode* root, int x)
{
	if (root == NULL)
		return NULL;

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

	没找到——>递归子问题
	这样写会造成返回值丢失(能找到指定节点,但是无法返回该节点)
	//TreeFind(root->left, x);
	//TreeFind(root->right, x);


	//为了解决返回值丢失和减少递归次数
	//应当创建节点接收返回值,cong'er 减少递归次数,降低时间复杂度
	BTNode* ret1 = TreeFind(root->left, x);
	//如果左节点找到了(有返回值)直接返回即可
	if (ret1)
		return ret1;

	BTNode* ret2 = TreeFind(root->right, x);
	//如果右节点找到了(有返回值)直接返回即可
	if (ret2)
		return ret2;

	return NULL;//整个树都没找到,返回NULL
}
//层序遍历一般采用队列实现
//本质上是广度优先遍历(bfs)
void TreeLevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);

	if (root != NULL)
	{
		QueuePush(&q, root);
	}
	//从上至下
	while (!QueueEmpty(&q))
	{
		BTNode* top = QueueFront(&q);
		QueuePop(&q);

		if (top)
		{
			printf("%d ", top->val);
			//从左向右
			if (top->left)
			{
				QueuePush(&q, top->left);
			}
			if (top->right)
			{
				QueuePush(&q, top->right);
			}
		}
		else printf("N ");
	}
	printf("\n");
	QueueDestroy(&q);
}
//用层序遍历实现
bool TreeisComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root != NULL)
	{
		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 != NULL)
		{
			QueueDestroy(&q);
			return false;
		}
	}
	QueueDestroy(&q);
	return true;
}

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

	TreeDestroy(root->left);
	TreeDestroy(root->right);
	free(root);
}

测试文件

#include"Bintree.h"

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;
}

int main()
{
	BTNode* root = CreateTree();
	PreOrder(root);
	printf("\n");
	InOrder(root);
	printf("\n");
	PostOrder(root);
	printf("\n");
	printf("二叉树的节点个数:%d \n", TreeSize(root));

	printf("二叉树的第3层节点个数:%d \n", TreeKLevel(root,3));
	printf("二叉树的大小是:%d \n", TreeSize(root));
	printf("二叉树的高度是:%d\n", TreeHeight(root));
	if (TreeFind(root, 5))
		printf("找到了\n");
	else
		printf("没找到\n");
	printf("二叉树的叶子节点个数:%d \n",TreeLeafSize(root));
	TreeLevelOrder(root);
	if (TreeisComplete(root)) printf("完全二叉树");
	else printf("不完全二叉树");
	printf("\n");
	TreeDestroy(root);
	root = NULL;//外部置空

	return 0;
}

总结和思考

  • 二叉树是非线性数据结构,相较于栈队列链表等线性结构更加抽象。为了更好的理解,可以尝试手动画递归展开图,将接口的每个栈帧都具象化。
  • 至于二叉树笔试题目,我们可以先手动推导一遍,再去核对公式,有利于更深刻理解公式的原理。
    END

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

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

相关文章

免费SSL证书申请流程开启HTTPS,以及3个月到期解决方法

阿里云免费SSL证书申请流程2024年最新申请教程&#xff0c;阿里云免费SSL证书品牌是Digicert&#xff0c;免费单域名证书&#xff0c;一个阿里云账号可以免费申请20张SSL免费证书&#xff0c;免费时长为3个月&#xff08;之前是一年免费时长&#xff09;&#xff0c;免费SSL证书…

【Java并发】变量的内存存储、线程安全分析

要理解原因&#xff0c;首先要清楚局部变量是什么&#xff1f;局部变量的存储方式是什么&#xff1f; 局部变量&#xff0c;从名字上就可以知道&#xff0c;它是只在特定作用域内可见并且只能在该作用域内使用的变量。也就意味着不同作用域的局部变量是不共享的。在多线程环境下…

Apache Tomcat与反向代理

Apache Tomcat 是一个开源的 Java Servlet 容器&#xff0c;主要用于部署和运行基于 Java 的 Web 应用程序。Tomcat 提供了一个环境&#xff0c;让开发者能够使用 Java 编写的 Web 应用程序在 Web 服务器上运行。下面是对 Tomcat 的详细介绍&#xff1a; Tomcat 的历史 Tomca…

C++学习笔记——三角形面积

一、题目描述 二、代码 #include <iostream> #include <bits/stdc.h> using namespace std; int main() {double a0,b0,c0;cin >> a >> b >> c;double p(abc)/2;double dp * (p-a)* (p-b)* (p-c);cout<< fixed << setprecision(3)&l…

QT 简易网页信息抓取程序模板基础代码

有些网页爬不了&#xff0c;只是一个简单的代码。 项目结构 NetBugBaseCode.pro #------------------------------------------------- # # Project created by QtCreator 2024-08-26T15:13:10 # #-------------------------------------------------QT core gui netw…

抖音小红书爆款预定,Tiktok爆火的短视频玩法,Ai生成宝宝走秀视频,萌翻全场

大家好&#xff0c;我是方知有&#xff0c;每天分享一个互联网副业&#xff0c;喜欢的朋友可以关注~ 今天给大家分享在Tiktok爆火的短视频玩法&#xff0c;现在抖音小红书制作这类型视频的人数还不多&#xff0c;大家可以赶快操作起来&#xff0c;这个玩法就是用Ai生成宝宝走秀…

论坛测试报告1.0

版本号&#xff1a; 作者&#xff1a; 日期&#xff1a; 目录 1 引言1.1 项目概述1.2 文档概述1.2.1 编写目的1.2.2 读者对象 1.3 产品需求和设计文档 2 测试执行2.1测试工具2.2制定测试计划2.3设计测试用例2.4执行测试用例 3.测试结果4.遗留风险5.测试结果评估 1 引言 1.1 项…

【kubernetes】Pod生命周期-启动钩子、停止钩子

一&#xff0c;Pod的生命周期 pod从开始创建到终止退出的时间范围称为Pod生命周期。pod生命周期包含以下几个重要流程&#xff1a; 初始化容器&#xff08;initContainers&#xff09;。 一个pod可以拥有任意数量的init容器。init容器是按照顺序以此执行的&#xff0c;并且仅…

HAL库:中断 方式按键检测:抬起执行、按下执行、长按短按检测、延时执行

目录 HAL库&#xff1a;中断 方式按键检测&#xff1a;抬起执行、按下执行、长按短按检测、延时执行 注意事项&#xff1a; 初始化部分&#xff1a; 回调函数部分 HAL库&#xff1a;中断 方式按键检测&#xff1a;抬起执行、按下执行、长按短按检测、延时执行 注意事项&am…

如何在CMD/PowerShell中使用命令行管理IIS

使用通用IIS命令行管理工具&#xff1a; C:\Windows\System32\inetsrv\appcmd.exe 查看现有站点 1 appcmd list site 添加一个新站点 1 appcmd add site /name:"My New Site" /id:2 /bindings:http/*:81: /physicalPath:"c:\inetpub\mynewsite" /name …

559. N 叉树的最大深度(迭代法)

目录 一&#xff1a;题目&#xff1a; 二&#xff1a;代码&#xff1a; 三&#xff1a;结果&#xff1a; 一&#xff1a;题目&#xff1a; 给定一个 N 叉树&#xff0c;找到其最大深度。 最大深度是指从根节点到最远叶子节点的最长路径上的节点总数。 N 叉树输入按层序遍历…

学习系列三:V8目标检测与分割自动化标注

学习系列三&#xff1a;YOLOv8目标检测与分割自动化标注 提示&#xff1a;本次文章主要介绍yolov8目标检测与自动化标注(较简单&#xff0c;通用性比较强&#xff0c;标签格式txt)&#xff0c;yolov8实例分割与自动化标注(程序较复杂&#xff0c;自动化标注效果有待提升,标签格…

SE11 没有激活的名称表存 No active nametab exists for

背景&#xff1a; SE11中减少某个非空表的字段的长度后&#xff0c;在SE14中的操作不当&#xff0c;并且对该表进行了删除重建的操作后&#xff0c;发生SE11激活该表报错。 原因&#xff1a; 出现了一些未知原因&#xff0c;导致该表在底层数据库存在&#xff0c;但是运行时对…

柔版印刷版市场前景:预计2030年全球市场规模将达到20.9亿美元

一、当前市场状况 目前&#xff0c;柔版印刷版市场呈现出较为稳定的发展态势。随着全球经济的逐步复苏&#xff0c;包装印刷等领域对柔版印刷版的需求持续增长。柔版印刷版具有环保、高效、印刷质量高等特点&#xff0c;在食品包装、标签印刷等行业中得到广泛应用。 全球前四…

微信聊天记录删除怎么恢复?两个抱藏方法快速恢复数据

微信作为我们日常沟通的重要工具&#xff0c;存储了大量的聊天记录&#xff0c;这些记录往往承载着珍贵的记忆或重要的工作信息。我们在日常使用微信时不小心左滑删除了和领导的聊天记录&#xff01;里面还有很重要的工作内容&#xff0c;删除的微信聊天记录怎么恢复啊&#xf…

环境问题处理:Python写工具,转换excel内容合并到xml中(openpyxllxml)

问题描述 提示报错&#xff0c;但是没有像java代码的解决方案推荐。 Note&#xff1a;PycharmProjects\项目名\venv\Scripts 创建项目时自带的脚本&#xff0c;也包含python.exe 查看python文件有输出路径&#xff0c;使用python也能打开python3.8&#xff0c;但是无法查找pyt…

Java学习_22_网络编程

文章目录 前言网络编程网络编程三要素IPInetAddress 端口号协议OSI七层模型TCP/IP模型UDP协议TCP协议 综合练习多发多收接收和反馈上传文件上传文件重名问题上传文件多线程问题上传文件线程池问题BS架构 总结 前言 博客仅记录个人学习进度和一些查缺补漏。 学习内容&#xff1…

软考:一个月拿下软件设计师

前言 软考我满打满算也就准备了一个月不到&#xff0c;期间也走了不少弯路&#xff1b;特地做个博客记录一下&#xff0c;也给其它备考的人一些建议。 我是24年上半年参加的考试&#xff0c;说实在的这年下午题有几道不好写。 只要上午成绩和下午成绩都过45就算及格了。 正文…

【Git保姆级使用教程】Git从入门到精通超级详细的使用教程,一套教程带你搞定Git(高见龙版本)。

目录 Git下载与安装设置GitGit的用户设置 使用Git新增初始Repository将文件交给Git管控(将文件加入暂存区)查看Git文件记录查找commit记录更改提交commit的记录撤销提交commit 将某些文件取消Git版控Git中删除或更改文件名Git查看某个文件的某一行代码是谁写的.git目录中有什么…

Tower for Mac Git客户端管理软件

Mac分享吧 文章目录 效果一、下载软件二、开始安装1、双击运行软件&#xff0c;将其从左侧拖入右侧文件夹中&#xff0c;等待安装完毕2、应用程序显示软件图标&#xff0c;表示安装成功 三、运行测试1、打开软件&#xff0c;测试2、克隆项目&#xff0c;测试 安装完成&#xf…