【数据结构——树与堆】

目录

一、树的概念

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

  • 子树是不相交的
  • 除了根节点外,每个结点有且仅有一个父节点
  • 一棵N个结点的树有N-1条边

Tips:树的子集之间不能有交集,否则就不是树

树的层数我们一般从1开始:

从第一层开始算: 空树高度为0,只有根节点高度为1

从第零层开始算: 空树高度为-1,只有根节点高度为0

所以我们倾向于第一种

树的构成: 根+N棵子树(N>=0)

二、树的定义

1.已知树的度

c 复制代码
#define N 4
struct TerrNode
{
	int val;//
	struct TreeNode* subs[N];//定义一个指针数组
};

2.树的度未知

(1)顺序表表示法

c 复制代码
struct TreeNode
{
	int val;
	SeqList subs;//用顺序表动态开辟空间,顺序表内部存struct TreeNode*孩子的指针
	//这种方式用C++写会更好一点
	//vector<struct TreeNode*>subs;
}

(2)左孩子右兄弟表示法

无论一个父亲节点有多少个孩子,leftchild都只指向左边第一个孩子,剩下的孩子用rightBother挨个访问,即可走完整个树

c 复制代码
struct TreeNode
{
	int val;
	struct TreeNode* leftchild;
	struct TreeNode* rightBrother;
};

数组存储: 只适用于满二叉树和完全二叉树

非完全二叉树也可以用数组存,但是不适合,因为会存在很多空间浪费(要补齐)

非完全二叉树: 链式存储

三、二叉树

1.概念

二叉树: 树的一个子集,限定了树的度最大为二

组成: 为空或者由一个根节点加上两棵称为左子树和右子树的二叉树组成

Tips:

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

2.特殊的二叉树

(1)满二叉树

满二叉树: 二叉树的每一层的结点数都达到最大值,最后一层都是叶子节点

已知高度求结点数: 一个高度为k的满二叉树,一共有2k-1个节点

已知结点数求高度: 如果这棵树有N个节点,那么这棵树的高度为log2(N+1)

(2)完全二叉树

完全二叉树: 高度为k的一棵二叉树,前k-1层都是满的,只有最后一层不满且从左到右连续

3.二叉树的存储

(1)链表

适合存储非完全二叉树

(2)数组

a.逻辑结构与物理结构

这里我们采用逻辑结构与物理结构结合的方式去理解

用数组存储的优势: 用数组存储更简单,可以用下标算父子关系,快速找到孩子和父亲

局限性: 由于数组空间是连续的,因此只适合满二叉树和完全二叉树,如果存别的需要空出位置,会造成很多的空间浪费

b.用下标算父子关系

已知父亲在数组中的下标为i:

左孩子下标:2*i+1

右孩子下标:2*i+2

已知孩子在数组中的下标为j:

父亲在数组中的下标:(j-1)/2

Tips:这里由于除法取整,因此偶数不管是 j-1/2 还是 j-2/2 结果是一致的

四、堆

物理上: 数组

逻辑上: 完全二叉树

1.堆的概念

堆的概念: 一个完全二叉树

大堆: 任何一个父亲>=孩子

小堆: 任何一个父亲<=孩子

Tips:小堆和大堆不代表着升序和降序,堆只要求了父子之间的大小关系,但是兄弟之间(同一层之间)并没有确定大小关系

2.堆的定义

c 复制代码
typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;

3.初始化

c 复制代码
void HPInit(HP* php)
{
	assert(php);
	php->a=NULL;
	php->size=php->capacity=0;
}

4.销毁

c 复制代码
void HPDesTroy(HP* php)
{
	assert(php);
	free(php->a);
	php->a=NULL;
	php->size=php->capacity=0;
}

5.插入

(1)向上调整

  1. 先将元素插入到堆的末端,即最后一个孩子之后
  2. 插入之后如果堆的性质遭到破坏,将新插入节点顺着其双亲往上调整至合适位置即可
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;
	}
}

Tips:这里的循环条件不建议用parent>=0,因为这里parent不会小于0

(2)交换

c 复制代码
void Swap(HPDataType* p1,HPDataType* p2)
{
	HPDataType tmp=*p1;
	*p1=*p2;
	*p2=tmp;
}

(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* temp=(HPDataType*)realloc(php->a,newcapacity*sizeof(php->a));
		if(temp==NULL)
		{
			perror("realloc fail\n");
			return;
		}
		php->capacity=newcapacity;
		php->a=temp;
	}
	php->a[php->size]=x;//在堆末尾插入数据
	php->size++;//增加有效元素个数
	AdjustUP(php->a,php->size-1);//要插入的数据位置为size-1
}

6.删除

这里的删除指的是删除堆顶的数据

不能只是单纯前移一位数组数据挪动覆盖删除,这样会导致父子关系混乱

  1. 将堆顶元素与堆中最后一个元素进行交换
  2. 删除堆中最后一个元素
  3. 将堆顶元素向下调整到满足堆特性为止

(1)向下调整

向下调整算法: 如果是小堆(大堆)的话,先比较该元素所在位置的左右子树的大小,取小(大)的那个与该元素进行比较,若小于(大于)该元素,则交换位置

到叶子节点截止: 当算出来左孩子的范围超出数组的范围

c 复制代码
AdjustDown(HPDataType* a,int n,int parent)
{
	//假设法,先假设左孩子小
	int child=parent*2+1;
	while(child<n)//如果child>=n,说明孩子不存在,调整到叶子节点了
	{
		//找出小的那个孩子
		if(child+1<n&&a[child+1]<a[child])//如果右孩子小于左孩子,同时判断一下右孩子是否小于n,否则会有越界风险
		{
			++child;
		}
		//基于小堆情况
		//如果孩子小于父亲
		if(a[child]<a[parent])
		{
			Swap(&a[child],&a[parent]);
			parent=child;//将元素下标改为其原孩子的下标
			child=parent*2+1;//继续计算该元素当前位置的左孩子下标
		}
		else
			break;//父亲小于孩子或者超出数组范围就break
	}
}

(2)删除堆顶数据

c 复制代码
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);//从根的位置向下调整,因此传0
}

时间复杂度最好情况是O(1),最坏情况是O(logN),效率很高

7.返回堆顶元素

c 复制代码
HPDataType HPTop(HP* php)
{
	assert(php);
	assert(php->size>0);
	return php->a[0];
}

8.判空

c 复制代码
bool HPEmpty(HP* php)
{
	assert(php);
	return php->size==0;
}

9.堆排序

(1)建堆

❌降序建大堆:根节点不变,以剩下的数字重新作为堆找出次大的,会导致兄弟变父子、兄弟叔侄等关系混乱,需要重新建堆,代价太大

❌升序建小堆:根节点不变,以剩下的数字重新作为堆找出小的,也会导致关系混乱

✅降序建小堆:先首尾交换找出最小的,再向下调整,不断选出次小的

✅升序建大堆:先首尾交换找出最大的,再向下调整,不断选出次大的

a.向上调整建堆:模拟插入
c 复制代码
for(int i=1;i<n;i++)//循环从1开始,默认i=0时就是个堆,然后不断插入向上调整
{
	AdjustUp(a,i);
}

时间复杂度: O(N*logN)

特点: 结点数量多的层调整次数多,结点数量少的层调整次数少

b.向下调整建堆

如果左右子树都是大堆,可直接向下调整

如果不是,采取倒着向下调整的方法:由于叶子结点既可以看做大堆也可以看做小堆,因此我们从倒数第一个非叶子结点开始调

c 复制代码
for(int i=(n-1-1)/2;i>=0;i--)//最后一个结点的父亲结点为起始调整结点
{
	AdjustDown(a,n,i);
}

时间复杂度: O(N)

每一层的结点数: 等比数列

**每一层的结点的向下调整次数:**等差数列

c.时间复杂度

满二叉树:

F(h)=20+21+22+...+2(h-2)+2(h-1)

F(h)=2h-1

F(h)=2h-1=N

h= log ⁡ 2 ( N + 1 ) \log_2(N+1) log2(N+1)

完全二叉树:

最坏情况:只有一个叶子节点

F(h)=20+21+22+...+2(h-2)+1

F(h)=2(h-1)-1+1

F(h)=2(h-1)-1+1=N

h= log ⁡ 2 N \log_2N log2N+1

不论是向上调整还是向下调整,每次插入调整后都往后走一层,一共走高度次

这里我们忽略细节,可将二者的时间复杂度都看做O( log ⁡ 2 N \log_2N log2N)

(2)排序

c 复制代码
int end=n-1;
while(end>0)
{
	Swap(&a[0],&a[end]);
	AdjustDown(a,end,0);
	--end;
}

时间复杂度: O(N*logN)

调整次数类似向上调整建堆

10.Pop K问题(N个数找最大的前K个)

方法1:

建一个N个数的大堆(O(N)),Pop K次(O(K*logN))

如果N远大于K:时间复杂度为O(N)

缺点: 当N很大时,空间消耗太大,分次解决效率太低

方法2:

用前K个数,建一个小堆(O(K)),剩下数据跟堆顶数据比较,如果比堆顶的数据大,就替代堆顶进堆(覆盖根位置,然后向下调整)(O(K*log(N-K)),最后小堆中的K个数据就是最大的前K个数

时间复杂度: O(N)

c 复制代码
int k;
printf("请输入k:");
scanf("%d",&k);
int* kminheap=(int*)malloc(sizeof(int)*K)
if(kimheap==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",&kimheap[i]);
}
//建有k个数的小堆
for(int i=(k-1-1)/2;i>=0;i++)
{
	AdjustDown(kimheap,k,i);
}
//读取剩下的N-k个数
int x;
while(fscanf(fout,"%d",&x)>0)
{
	if(x>kiminheap[0])//剩余数据大于堆顶数据
	{
		kminheap[0]=x;//该数据直接覆盖堆顶数据,代替堆顶进堆
		AdjustDown(kminheap,k,0);//向下调整
	}
}
//打印输出最大前k个数
printf("最大前k个数:");
for(int i=0;i<k;i++)
{
	printf("%d ",kminheap[i]);
}
printf("\n");

五、完整代码

Heap.h

c 复制代码
#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 HPInit(HP* php);//初始化
void HPDesTroy(HP* php);//销毁
void Swap(HPDataType* p1, HPDataType* p2);//交换
void AdjustUP(HPDataType* a, int child);//向上调整
void HPPush(HP* php, HPDataType x);//插入数据
AdjustDown(HPDataType* a, int n, int parent);//向下调整
void HPPop(HP* php);//删除堆顶数据
HPDataType HPTop(HP* php);//返回堆顶元素
bool HPEmpty(HP* php);//判空

Heap.c

c 复制代码
#include"Heap.h"
//初始化
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;
}
//交换
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;
	}
}
//插入数据
void HPPush(HP* php, HPDataType x)
{
	assert(php);
	//判断数组是否已满
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
		HPDataType* temp = (HPDataType*)realloc(php->a, newcapacity * sizeof(php->a));
		if (temp == NULL)
		{
			perror("realloc fail\n");
			return;
		}
		php->capacity = newcapacity;
		php->a = temp;
	}
	php->a[php->size] = x;//在堆末尾插入数据
	php->size++;//增加有效元素个数
	AdjustUP(php->a, php->size - 1);//要插入的数据位置为size-1
}
//向下调整
AdjustDown(HPDataType* a, int n, int parent)
{
	//假设法,先假设左孩子小
	int child = parent * 2 + 1;
	while (child < n)//如果child>=n,说明孩子不存在,调整到叶子节点了
	{
		//找出小的那个孩子
		if (child + 1 < n && a[child + 1] < a[child])//如果右孩子小于左孩子,同时判断一下右孩子是否小于n,否则会有越界风险
		{
			++child;
		}
		//基于小堆情况
		//如果孩子小于父亲
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;//将元素下标改为其原孩子的下标
			child = parent * 2 + 1;//继续计算该元素当前位置的左孩子下标
		}
		else
			break;//父亲小于孩子或者超出数组范围就break
	}
}
//删除堆顶数据
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);//从根的位置向下调整,因此传0
}
//返回堆顶元素
HPDataType HPTop(HP* php)
{
	assert(php);
	assert(php->size > 0);
	return php->a[0];
}
//判空
bool HPEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

test.c

以下可用作测试参考

c 复制代码
#include"Heap.h"
void TestHeap1()
{
	int a[] = { 4,6,5,1,8,3,2,7,9 };
	HP hp;
	HPInit(&hp);
	//建堆
	for (size_t i = 0; i < sizeof(a) / sizeof(int); i++)
	{
		HPPush(&hp, a[i]);//一个一个插入,然后向上调整
	}
	//Pop的应用意义
	//从小到大的打印印堆中所有数据
	while (!HPEmpty(&hp))
	{
		printf("%d ", HPTop(&hp));
		//若想排序:a[i++]=HPTop(&hp); 将数据从小到大放入数组
		HPPop(&hp);
	}
	//找出最大的前k个数据
	int k = 0;
	scanf("%d", &k);
	while (k--)
	{
		printf("%d ", HPTop(&hp));
		HPPop(&hp);
	}
	printf("\n");
}
void HeapSort(int* a, int n)
{
	//降序建小堆
	//升序建大堆
	//向上调整建堆
	/*for(int i=1;i<n;i++)//循环从1开始,默认i=0时就是个堆,然后不断插入向上调整
	{
		AdjustUp(a,i);
	}*/
	//向下调整建堆
	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;
	}
}
//建堆测试
void TestHeap()
{
	int a[] = { 1,5,8,6,9,3,7 };
	HeapSort(a, sizeof(a) / sizeof(int));
}
int main()
{
	TestHeap();
	TestHeap1();
	return 0;
}
相关推荐
BomanGe21 小时前
NSK超重载滚珠丝杠W5020SS技术规格详解
经验分享·规格说明书
郝学胜-神的一滴1 小时前
CMake 017:彩色日志输出实战
linux·c语言·开发语言·c++·软件工程·软件构建·cmake
怪味&先森2 小时前
读书小结—《认知觉醒》
笔记
Navigator_Z2 小时前
LeetCode //C - 1096. Brace Expansion II
c语言·算法·leetcode
luj_17682 小时前
FreeDOS vs MS-DOS PC-DOS 对比解析
服务器·c语言·开发语言·经验分享·算法
Bomangedd2 小时前
重载极速模块MCM08005H10K00详解
经验分享·规格说明书
杨先生哦2 小时前
2026 热端攻防:AI 驱动 Web 前端安全全景透析
前端·笔记·安全·web安全
RH2312112 小时前
2026.6.10 数据结构 二叉树
数据结构
江苏勃曼工业控制技术有限公司3 小时前
NSK紧凑型FA系列滚珠丝杠技术详解
经验分享·规格说明书