【数据结构】二叉树


一、树的概念以及结构

1、树的概念

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因
为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
有一个特殊的结点,称为根结点,根节点没有前驱结点
除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、 T2、……、 Tm,其中每一个集合Ti(1 <= i
<= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
因此,树是递归定义的

数据结构中的树可以看作是现实生活中是树和人类的血缘关系组合而成的

结构如下图所示:
在这里插入图片描述

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

比如下面的样例:
在这里插入图片描述
以上3个树都是错误的样例

同时,图中说明了树最基本的3个点:

1、子树不能相交
2、除根节点以外,每个节点有且仅有一个父节点
3、一个树有n个节点就有n-1条边

2、树的相关概念

我们拿出一个树图出来,方便大家观察

在这里插入图片描述

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

2、 叶节点或终端节点:度为0的节点称为叶节点; 如上图: B、 C、 H、 I…等节点为叶节点

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

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

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

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

7、树的度:一棵树中,最大的节点的度称为树的度;如上图:树的度为6

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

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

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

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

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

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

补充:树的高度推荐大家从1开始记录,因为当树为空也就是空树的时候0比-1合理一些,当然不强求大家非要这样来

以上就是总结出来的一些树的相关概念,或许还有许多概念没有提到,欢迎大家在评论区补充

3、树的表示

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

同样,树也是由一个结构体来定义的:

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

这里采用左孩子,右兄弟的方法
在这里插入图片描述
代码如下:
在这里插入图片描述
这段代码如果加上注释的一行就是递归打印出整个树,如果不加就是打印一行节点

二、二叉树的概念以及结构

1、概念

一棵二叉树是结点的一个有限集合,该集合:
1 . 或者为空
2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成

如下图所示:

在这里插入图片描述
可以看出:

1 . 二叉树不存在度大于2的结点
2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

注意:对于任意的二叉树都是由以下几种情况复合而成的:
在这里插入图片描述

前几年在网上就有着这么几张图,标题是:程序员和非程序员眼中的树

在这里插入图片描述
这就是典型的二插树

2、特殊的二插树

1 . 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说:如果一个二叉树的层数为K,且结点总数是2^k-1,则它就是满二叉树
在这里插入图片描述

  1. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K 的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对 应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
    在这里插入图片描述

知识点

1、完全二叉树的节点数量范围是【2 ^ (k-1), 2 ^ k - 1】
2、满二叉树是特殊的完全二叉树。所以说:满二叉树一定是完全二叉树,完全二插树并不是满二叉树

3、堆的概念以及结构

看看大佬对堆的介绍:
在这里插入图片描述

所以说堆的性质就是:

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

接下来我们仔细看看小根堆与大根堆的物理图与逻辑图:(网上找的图,有点模糊,希望各位不要建议)
在这里插入图片描述
大家起时可以看到规律:

用数组存储节点值是按照堆的高度,一行一行来存储节点值的

大根堆:任何节点的值>=孩子的值;
小根堆:任何节点的值<=孩子的值;

在这里插入图片描述

由此有大佬推出了父子关系公式:

知识点:

liftchild = parent * 2 + 1
左孩子节点=父节点乘以2加上1
rightchild = parent * 2 + 2
右孩子节点=父节点乘以2加上2
parent = (child-1) / 2
父节点=孩子节点先减1再除以2

4、堆的实现

堆的结构

typedef int HPDataType;

typedef struct HeapTree
{
	HPDataType* a;//整型指针
	int size;//有效节点个数
	int capacity;//堆的最大节点容量
}HP;//重命名

函数接口的声明

//不管如何操作,堆在被调用函数接口操作之后还是一个小(根)堆或者大(根)堆
void HPInit(HP* ps);//初始化
void HPDestroy(HP* ps);//销毁空间
void HPPrint(HP* ps);//打印
void HPPush(HP* ps, HPDataType x);//插入节点,插入之后一定是小(根)堆或者大(根)堆
void HPPop(HP* ps);//删除堆顶数据
HPDataType HPTop(HP* ps);//提取堆顶的数据
bool HPEmpty(HP* ps);//判空
int HPSize(HP* ps);//堆有多少个节点值

堆的初始化

void HPInit(HP* ps)
{
	assert(ps);
	ps->a = NULL;//开始置空
	ps->size = ps->capacity = 0;//有效节点个数与容量都为0
}

销毁空间

void HPDestroy(HP* ps)
{
	assert(ps);
	free(ps->a);//堆的物理结构也就是存储结构是数组,所以说空间开辟是连续的,释放空间是一次性释放完的
	ps->a = NULL;//指针置空
	ps->size = ps->capacity = 0;//个数与容量重新清0
}

打印堆(就是打印数组)

void HPPrint(HP* ps)//打印
{
	assert(ps);
	for (int i = 0; i < ps->size; ++i)//循环打印
	{
		printf("%d ", ps->a[i]);//数组a的第i个数据
	}
	printf("\n");
}

交换节点值

void Swap(HPDataType* child, HPDataType* parent)//传值调用不会交换外面的两个值,所以说用传址调用
{
	HPDataType tmp = *child;
	*child = *parent;
	*parent = tmp;
}

返回堆顶数据

HPDataType HPTop(HP* ps)
{
	assert(ps);
	assert(!HPEmpty(ps));//为空断言生效
	return ps->a[0];//数组a中第0个下标的元素就是堆顶数据
}

堆是否为空

bool HPEmpty(HP* ps)
{
	assert(ps);
	return ps->size == 0;//如果有效节点个数为0,就表明堆为空
}

堆剩余节点个数

int HPSize(HP* ps)
{
	assert(ps);
	return ps->size;//直接返回
}

删除堆顶数据(向下调整方法)

这里要用到向下调整方法
我们先看删除堆顶数据的代码:

void HPPop(HP* ps)//删除堆顶数据(找次大或者次小),把最小或者最大的值删除之后,最上面的值就是次大值或者次小值
{
	assert(ps);
	assert(!HPEmpty(ps));//堆为空,断言生效
	Swap(&ps->a[0], &ps->a[ps->size - 1]);//将堆顶的值,与堆最后的值就行交换
	ps->size--;//删除最后的值
	Justdown(ps->a, ps->size,0);//向下转换
}

这里的Justdown(ps->a, ps->size,0);//向下转换就是一个向下调整函数
我们来仔细分析分析:

在这里插入图片描述
也就是说要删除堆顶的数据时,把堆顶和堆尾的数据调换,然后删除堆尾数据。将调换后的堆顶数据进行向下调整:

void Justdown(HPDataType* a, int size, int parent)//向下转换
{
	int midchild = parent * 2 + 1;//我们假设左节点为两个节点中的小节点,那么左节点的下标就是父节点下标乘以2加上1
	while (midchild < size)//这里不要用父节点判断是否向下调整完毕,如果没有下面的break会判断失误,出不了循环
	{
		//if (midchild + 1 < size && a[midchild + 1] > a[midchild])//求取大堆最上面的节点
		if (midchild + 1 < size && a[midchild + 1] < a[midchild])//小节点的坐标加1要小于size,不然就相当于越界了
		//如果右节点的值小于左节点,那么midchild下标加1,得到两个节点中的小节点
		{
			midchild++;
		}
		//if (a[midchild] > a[parent])//求取大堆最上面的节点
		if (a[midchild] < a[parent])
		//如果小节点的值小于父节点的值就进去开始向下调整
		{
			Swap(&a[midchild], &a[parent]); //交换父节点与小节点的值
			parent = midchild;//把小节点的下标赋给父节点
			midchild = parent * 2 + 1;//此时新的小节点下标就为父节点乘以2加上1
		}
		else
		{
			break;//如果小节点的值大于等于父节点就不用进行操作(这是针对小根堆的注释)
		}
	}
}

插入堆数据(向上调整法)

因为不确定插入数据之后堆还是大堆或者小堆,所以要采用向上调整法
先看看插入数据代码:

void HPPush(HP* ps, HPDataType x)
{
	assert(ps);
	if (ps->size == ps->capacity)//如果有效节点个数等于容量
	{
		int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		//如果是第一次,那么新容量直接为4,否则就是旧容量的2倍
		HPDataType* tmp = (HP*)realloc(ps->a, newcapacity * sizeof(HPDataType));
		//动态内存扩容新容量个空间大小
		if (tmp == NULL)
		{
			perror("realloc fail\n");
			exit(-1);
		}
		ps->a = tmp;//将扩容空间地址赋值给ps->a
		ps->capacity = newcapacity;//赋值新容量
	}
	ps->a[ps->size] = x;//将数据插入到堆尾
	ps->size++;//size++;
	Justup(ps->a, ps->size - 1);//向上调整方法
}

接下来我们看看向上调整方法:
在这里插入图片描述

void Justup(HPDataType* a, int child)//向上转换
{
	int parent = (child - 1) / 2;//父节点下标等于子节点下标减1然后除以2
	while (child > 0)
	{
		if (a[child] < a[parent])//小堆
		//如果子节点值小于父节点值,就进行向上调整
		//if (a[child] < a[parent])//大堆
		{
			Swap(&a[child], &a[parent]);//交换父子节点值
			child = parent;//父节点下标赋值给子节点下标
			parent = (child - 1) / 2;//父节点下标等于子节点下标减1然后除以2
		}
		else
		{
			break;//如果子节点值大于父节点值,直接跳出(注释针对的是小堆)
		}
	}
}

5、完整代码

Heap.h

#pragma once

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

typedef int HPDataType;

typedef struct HeapTree
{
	HPDataType* a;//整型指针
	int size;//有效节点个数
	int capacity;//堆的最大节点容量
}HP;//重命名

//不管如何操作,堆在被调用函数接口操作之后还是一个小(根)堆或者大(根)堆
void HPInit(HP* ps);//初始化
void HPDestroy(HP* ps);//销毁空间
void HPPrint(HP* ps);//打印
void HPPush(HP* ps, HPDataType x);//插入节点,插入之后一定是小(根)堆或者大(根)堆
void HPPop(HP* ps);//删除堆顶数据
HPDataType HPTop(HP* ps);//提取堆顶的数据
bool HPEmpty(HP* ps);//判空
int HPSize(HP* ps);//堆有多少个节点值


Heap.c

#define _CRT_SECURE_NO_WARNINGS
#include"Heap.h"
void HPInit(HP* ps)
{
	assert(ps);
	ps->a = NULL;//开始置空
	ps->size = ps->capacity = 0;//有效节点个数与容量都为0
}

void HPDestroy(HP* ps)
{
	assert(ps);
	free(ps->a);//堆的物理结构也就是存储结构是数组,所以说空间开辟是连续的,释放空间是一次性释放完的
	ps->a = NULL;//指针置空
	ps->size = ps->capacity = 0;//个数与容量重新清0
}

void HPPrint(HP* ps)//打印
{
	assert(ps);
	for (int i = 0; i < ps->size; ++i)//循环打印
	{
		printf("%d ", ps->a[i]);//数组a的第i个数据
	}
	printf("\n");
}

void Swap(HPDataType* child, HPDataType* parent)//传值调用不会交换外面的两个值,所以说用传址调用
{
	HPDataType tmp = *child;
	*child = *parent;
	*parent = tmp;
}

void Justup(HPDataType* a, int child)//向上转换
{
	int parent = (child - 1) / 2;//父节点下标等于子节点下标减1然后除以2
	while (child > 0)
	{
		if (a[child] < a[parent])//小堆
		//如果子节点值小于父节点值,就进行向上调整
		//if (a[child] < a[parent])//大堆
		{
			Swap(&a[child], &a[parent]);//交换父子节点值
			child = parent;//父节点下标赋值给子节点下标
			parent = (child - 1) / 2;//父节点下标等于子节点下标减1然后除以2
		}
		else
		{
			break;//如果子节点值大于父节点值,直接跳出(注释针对的是小堆)
		}
	}
}

void HPPush(HP* ps, HPDataType x)
{
	assert(ps);
	if (ps->size == ps->capacity)//如果有效节点个数等于容量
	{
		int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		//如果是第一次,那么新容量直接为4,否则就是旧容量的2倍
		HPDataType* tmp = (HP*)realloc(ps->a, newcapacity * sizeof(HPDataType));
		//动态内存扩容新容量个空间大小
		if (tmp == NULL)
		{
			perror("realloc fail\n");
			exit(-1);
		}
		ps->a = tmp;//将扩容空间地址赋值给ps->a
		ps->capacity = newcapacity;//赋值新容量
	}
	ps->a[ps->size] = x;//将数据插入到堆尾
	ps->size++;//size++;
	Justup(ps->a, ps->size - 1);//向上调整方法
}

void Justdown(HPDataType* a, int size, int parent)//向下转换
{
	int midchild = parent * 2 + 1;//我们假设左节点为两个节点中的小节点,那么左节点的下标就是父节点下标乘以2加上1
	while (midchild < size)//这里不要用父节点判断是否向下调整完毕,如果没有下面的break会判断失误,出不了循环
	{
		//if (midchild + 1 < size && a[midchild + 1] > a[midchild])//求取大堆最上面的节点
		if (midchild + 1 < size && a[midchild + 1] < a[midchild])//小节点的坐标加1要小于size,不然就相当于越界了
		//如果右节点的值小于左节点,那么midchild下标加1,得到两个节点中的小节点
		{
			midchild++;
		}
		//if (a[midchild] > a[parent])//求取大堆最上面的节点
		if (a[midchild] < a[parent])
		//如果小节点的值小于父节点的值就进去开始向下调整
		{
			Swap(&a[midchild], &a[parent]); //交换父节点与小节点的值
			parent = midchild;//把小节点的下标赋给父节点
			midchild = parent * 2 + 1;//此时新的小节点下标就为父节点乘以2加上1
		}
		else
		{
			break;//如果小节点的值大于等于父节点就不用进行操作(这是针对小根堆的注释)
		}
	}
}

void HPPop(HP* ps)//删除堆顶数据(找次大或者次小),把最小或者最大的值删除之后,最上面的值就是次大值或者次小值
{
	assert(ps);
	assert(!HPEmpty(ps));//堆为空,断言生效
	Swap(&ps->a[0], &ps->a[ps->size - 1]);//将堆顶的值,与堆最后的值就行交换
	ps->size--;//删除最后的值
	Justdown(ps->a, ps->size,0);//向下转换
}

HPDataType HPTop(HP* ps)
{
	assert(ps);
	assert(!HPEmpty(ps));//为空断言生效
	return ps->a[0];//数组a中第0个下标的元素就是堆顶数据
}

bool HPEmpty(HP* ps)
{
	assert(ps);
	return ps->size == 0;//如果有效节点个数为0,就表明堆为空
}

int HPSize(HP* ps)
{
	assert(ps);
	return ps->size;//直接返回
}


test.c

#define _CRT_SECURE_NO_WARNINGS
#include"Heap.h"

//void Test1()
//{
//	int a[] = { 50,32,100,65,70,60 };
//	HP p;
//	HPInit(&p);
//	for (int i = 0; i < sizeof(a) / sizeof(int); ++i)
//	{
//		HPPush(&p, a[i]);
//	}
//	HPPush(&p, 10);
//	HPPrint(&p);
//
//	/*HPPop(&p);
//	HPPrint(&p);
//
//	HPPop(&p);
//	HPPrint(&p);*/
//	HPDestroy(&p);
//
//}
void PrintTopK(int* a, int n, int k)
{
	HP p;
	HPInit(&p);
	for (int i = 0; i < n; ++i)
	{
		scanf("%d", &a[i]);
	}
	for (int i = 0; i < n; ++i)
	{
		HPPush(&p, a[i]);
	}
	for(int i=0;i<k;i++)
	{
		printf("%d ",HPTop(&p));
		HPPop(&p);
	}
	printf("\n");
	HPPrint(&p);
	
	HPDestroy(&p);
}

void TestTopk()
{
	HP p;
	int n = 10;
	int* a = (int*)malloc(sizeof(int) * n);
	int k = 3;
	PrintTopK(a, n, k);
	return 0;
}

int main()
{
	TestTopk();
	return 0;
}

结束语

到目前为止二叉树的堆差不多结束了,下期就讲讲堆的排序以及TOPK问题,让我们下期见吧!

  • 24
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 18
    评论
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值