数据结构——树

 本文为个人学习笔记,如有错误,欢迎批评指正,我们一起学习呀。如有侵权,请联系删除。

今日格言:今天应做的事没有做,明天再早也是耽误了。

树的概念

树是一种非线性的结构,它是由n个节点组成的一个具有层次关系的集合。为什么把它叫作树呢?因为你一眼看去, 它就像是一棵倒挂的树,也就是根朝上,叶朝下。

根节点:根节点是一个特殊的节点,因为它没有直接前驱

除了根节点之外,其余节点都被分为M个不相交的集合,每一棵子树的根节点有且仅有一个前驱,有0个或者多个后继。每棵树都能看成都能看成根和它的子树构成。因此,树是递归定义的。

所以在树中,子树都是不相交的,图中的标出的树,其根节点都不止一个前驱,不符合数的定义

树的结构(其实下面的概念不用死记硬背,联系实际就很容易理解啦)

 节点的度:一个结点含有子树的个数称为这个节点的度,比如图中的A的度是6(其实我们也可以直接看它有多少条线延伸出去)。

树的度:树的度就是在这棵树中最大的节点的度。在这棵树中,树的度为6.

叶节点(终端结点):度为0的节点即为叶节点。

父节点(双亲节点):一个节点含有子节点,那么这个点就称为 该子节点的父节点,A是B的父 节点

子节点(孩子节点):与父节点相对应,一个节点含有的子树的根节点称为该节点的子节点,B是A的子节点

兄弟节点:具有相同父节点的节点互为兄弟节点,B和C是兄弟节点

堂兄弟节点:双亲节点在同一层上的节点互为堂兄弟结点,H和I是堂兄弟节点。

这里要区分一下兄弟节点和堂兄弟节点,大家也可以联想一下实际,虽然说亲戚关系的区分总是让人头痛,但是你的亲兄弟和堂兄弟还是很好区分的吧

节点的层次:从根定义开始,根为第一层,以此类推

树的高度(深度):树中节点的最大层次,该树的高度为4

结点的祖先:从根到该节点所经的分支上的所有节点,所有A是所有节点的祖先

子孙:以某节点为根的子树中任一节点都成为该节点的子孙,所有节点都是A的子孙

森林:由m棵互不相交的的多棵树的集合称为森林

树的表示方法

这里只介绍一种——孩子兄弟表示法

下面是代码形式

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

下面是图像,能更清楚的了解这种方法的表示方式

一般使用最多的就是二叉树,下面让我们一起来了解二叉树吧

二叉树

概念:
一棵二叉树是节点的一个有限集合,该集合或者为空,或者是由一个根节点加上两棵左右子树的二叉树构成,这里需要注意的是,子树在左边和在右边是不同的情况,不然我们为什么要多此一举,给他们都起一个名字呢

二叉树的特点:
从上面的图中我们也可以看出

 (1)每个节点最多有两棵子树,不存在度大于2的树(其实二叉树挺像支持计划生育的树)

(2)二叉树的子树有左右之分,所以子树的顺序不能颠倒

 特殊的二叉树

满二叉树:

树的每一层节点数都达到最大值,即每一层都是满的,那么这棵树可以称为满二叉树。

完全二叉树:

完全二叉树是效率很高的树,对于深度为k,有n个节点的二叉树,当且仅当其每一个节点都与深度为k的满二叉树中编号从1~n的节点一一对应时称为完全二叉树。上面是它的概念,我的理解就是除了最后一层外,其余都是满二叉树,最后一层节点是从左到右按顺序排列的

满二叉树其实是特殊的完全二叉树

二叉树的性质

ps:二叉树的性质在做题中应用还是很多的,大家要记住哦

(1)若规定根节点的层数为1,那么一棵非空二叉树的第i层上最多有 2^(i-1)个节点,其实最大的情况就是完全二叉树的情况

(2)若规定根节点的层数为1,那么深度为h的二叉树的节点个数为2^h-1(即为等比数列求和)

(3)对于任何一棵二叉树,如果度为0的节点个数为n0,度为2的节点个数为n2,那么n0=n2+1,对于完全二叉树而言,度为1的节点个数最大为1。

(4)若规定根节点的层数为1,具有n个节点的满二叉树深度为h=log(n+1)

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

a.i>0,起父节点的序号为(i-1)/2;i=0,无父节点

b.2*i+1<n,左孩子:2*i+1

c.2*i+2<n,右孩子,2*i+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; // 当前节点值域
};

图形表示

 堆

堆是一棵完全二叉树。堆分为大堆和小堆,关于大堆和小堆的定义这里就不说了,我说说自己的理解(如有理解不到位的地方,请指出)。

大堆:

根节点总是最大的完全二叉树,是一个大堆

小堆:

与大堆相对应,根节点总是最小的完全二叉树,是一个小堆

堆的实现

 我们现在已经知道了堆是一棵完全二叉树,并且完全二叉树可以用数组来表示,那么堆就用数组储存。所以定义了三个变量,分别是存储数据的数组a,数组元素个数size以及数组容量大小capacity,之所以定义size和capacity是为了后面插入数据是扩容方便。

1.堆的初始化

代码表示

void HeapInit(HP* php) {
	assert(php);
	php->size = 0;
	php->capacity = 4;
	HeapDataType* a = (HeapDataType*)malloc(sizeof(HeapDataType)*4);
	if (a == NULL) {
		printf("error\n");
		return;
	}
	
	php->a = a;

}

2.堆的销毁

对于堆的销毁,我们只需要将数组的空间释放,并将指针置空,空间和元素个数置零即可。

代码表示

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

 3.堆的判空

对于堆的判空,我们可以直接利用之前设置的变量size来判断是否为空,通过size,我们就可以知道这个数组里面是否还有元素

bool HeapEmpty(HP* php) {
	assert(php);
	return php->size == 0;
}

4.判断堆中的数据个数

这里和堆的判空类似,我们可以直接通过size得出数组中还剩下多少数据 

代码表示

int HeapSize(HP* php) {
	assert(php);
	return php->size;
}

5.取堆顶的元素

因为堆是按照数组存储的,所以我们只需要返回数组中下标为0的元素即可

HeapDataType HeapTop(HP* php) {
	assert(php);
	return php->a[0];
}

6.堆的插入

堆的插入就上点难度了

step 1:将所要插入的数据插入到数组的末端(尾插);

step 2:调整堆中数据,是其符合大堆或者小堆的要求(我们插入数据时,可能运气超好,插入数据后依旧满足要求。但是,我们不能全靠运气啊,还是要有应对特殊情况的能力的);

因此这里就要用到向上调整法(从最后一个非叶子节点开始调整),这里以小堆为例:

那么我们现在来看看代码是如何实现的吧

void AdjustUp(HeapDataType* a, int child) {
	int parent = (child - 1) / 2;//这里运用了二叉树的性质哦,不记得的话可以往上翻翻
	while (child > 0) {
		if (a[child] < a[parent]) {
			HeapDataType temp = a[child];
			a[child] = a[parent];
			a[parent] = temp;
			child = parent;
			parent = (child - 1) / 2;
		}
		else {
			break;
		}
	}
}

准备工作完成后,我们现在就可以正式开始插入过程啦! 

代码表达

void HeapPush(HP* php, HeapDataType x) {
	assert(php);
	//插入数据前我们还得判断数组是不是已经满了,如果满了,则需要扩容
	if (php->size == php->capacity) {
		HeapDataType* temp = (HeapDataType*)realloc(php->a, sizeof(HeapDataType) * php->capacity * 2);
		if (temp == NULL) {
			printf("error\n");
			return;
		}
		php->a = temp;
		php->capacity *= 2;
	}
	php->a[php->size] = x;
	php->size++;
	AdjustUp(php->a, php->size - 1);//减一是因为要与数组的下标相对应

}

7.堆的删除

堆的删除一般是删除对堆顶的元素进行删除,那么你是不是想的是,直接对下标为0的元素删除就好了。可是现实很骨感, 这样做不能达到预期的效果,反而会让你的辈分升高。因为你直接删除之后,堆的结构就会混乱,也就会产生我原本是你兄弟,但是现在是你爸爸的结果,这可不辈分又升高了吗!

那么正确的删除步骤是什么呢?

step 1 :先将堆顶元素与堆尾元素互换位置

step 2 :删除堆尾元素

step 3:从堆顶开始,进行向下调整

向下调整算法:
前提:左右子树必须是堆

因为我们只是交换了堆顶元素和堆尾元素,那么其余元素的关系并没有改变,因此可以采用向下调整法。

下面具体介绍向下调整算法

代码表达

void AdjustDown(HeapDataType* a, int root,int n) {
	int parent = root;
	int child = 2 * parent + 1;//假设最小的是左子树
	if (child+1<n&&a[child] > a[child + 1]) {//防止越界访问
		child++;//如果右孩子的值比左孩子更小,那么就换成右孩子
	}
	while (child < n) {
		if (a[child] < a[parent]) {
			HeapDataType temp = a[child];//交换二者的值
			a[child] = a[parent];
			a[parent] = temp;
			parent = child;//更新parent和child的位置
			child = 2 * parent + 1;

		}
		else {
			break;
		}
	}
}

准备工作完毕,正式开始堆的删除 

void HeapPop(HP* php) {
	assert(php);
	assert(!HeapEmpty(php)); // 确保堆非空

	// 直接把最后一个元素覆盖堆顶,然后调整
	php->a[0] = php->a[php->size - 1];
	php->size--;
	AdjustDown(php->a, 0, php->size); // 从 root=0 开始调整
}

完整代码(这个代码只适用于小堆,但是大堆的逻辑是差不多的,可以自己尝试一下) 

Heap.h
#pragma once
#include<assert.h>
#include<stdio.h>
#include<string.h>
#include<stdbool.h>
#include<stdlib.h>
typedef int HeapDataType;
typedef struct Heap{
	HeapDataType* a;
	int size;
	int capacity;

}HP;
void HeapInit(HP* heap);
void HeapDestory(HP* heap);
bool HeapEmpty(HP* heap);
int HeapSize(HP* heap);
HeapDataType HeapTop(HP* heap);
void AdjustUp(HeapDataType*a,int child);
void HeapPush(HP* heap,HeapDataType x);
void AdjustDown(HeapDataType* a, int root,int n);
void HeapPop(HP* heap);
Heap.c
#include"heap.h"
void HeapInit(HP* php) {
	assert(php);
	php->size = 0;
	php->capacity = 4;
	HeapDataType* a = (HeapDataType*)malloc(sizeof(HeapDataType)*4);
	if (a == NULL) {
		printf("error\n");
		return;
	}
	
	php->a = a;

}
void HeapDestory(HP* php) {
	assert(php);
	php->size = php->capacity = 0;
	free(php->a);
	php->a = NULL;
}
bool HeapEmpty(HP* php) {
	assert(php);
	return php->size == 0;
}
int HeapSize(HP* php) {
	assert(php);
	return php->size;
}
HeapDataType HeapTop(HP* php) {
	assert(php);
	return php->a[0];
}
void AdjustUp(HeapDataType* a, int child) {
	int parent = (child - 1) / 2;//这里运用了二叉树的性质哦,不记得的话可以往上翻翻
	while (child > 0) {
		if (a[child] < a[parent]) {
			HeapDataType temp = a[child];
			a[child] = a[parent];
			a[parent] = temp;
			child = parent;
			parent = (child - 1) / 2;
		}
		else {
			break;
		}
	}
}
void HeapPush(HP* php, HeapDataType x) {
	assert(php);
	//插入数据前我们还得判断数组是不是已经满了,如果满了,则需要扩容
	if (php->size == php->capacity) {
		HeapDataType* temp = (HeapDataType*)realloc(php->a, sizeof(HeapDataType) * php->capacity * 2);
		if (temp == NULL) {
			printf("error\n");
			return;
		}
		php->a = temp;
		php->capacity *= 2;
	}
	php->a[php->size] = x;
	php->size++;
	AdjustUp(php->a, php->size - 1);//减一是因为要与数组的下标相对应

}
void AdjustDown(HeapDataType* a, int root,int n) {
	int parent = root;
	int child = 2 * parent + 1;//假设最小的是左子树
	if (child+1<n&&a[child] > a[child + 1]) {//防止越界访问
		child++;//如果右孩子的值比左孩子更小,那么就换成右孩子
	}
	while (child < n) {
		if (a[child] < a[parent]) {
			HeapDataType temp = a[child];//交换二者的值
			a[child] = a[parent];
			a[parent] = temp;
			parent = child;//更新parent和child的位置
			child = 2 * parent + 1;

		}
		else {
			break;
		}
	}
}
void HeapPop(HP* php) {
	assert(php);
	assert(!HeapEmpty(php)); // 确保堆非空

	// 直接把最后一个元素覆盖堆顶,然后调整
	php->a[0] = php->a[php->size - 1];
	php->size--;
	AdjustDown(php->a, 0, php->size); // 从 root=0 开始调整
}

 更多内容,敬请期待。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值