二叉树以及堆讲解

树的概念及结构

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。之所以把这种结构叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。

  • 有一个特殊的结点,称为根结点,根节点没有前驱结 点
  • 除根结点外,其余结点被分成M(M>0)个互不相交的集合,其中每一个集合又是另一个结构与树类似的子树。每颗子树的根结点有且只有一个前驱,可以有0个或多个后继。因此树是递归定义的。
  • 在这里插入图片描述

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

树的相关概念

在这里插入图片描述

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

叶子节点或终端节点:度为0的节点称为叶子节点,如上图,B,C,H,I,P,Q…等节点为叶子节点

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

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

兄弟结点:具有相同父亲节点的节点称为兄弟节点。如上图,b是c的兄弟节点

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

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

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

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

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

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

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

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

树的表示

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

typedef int DataType;
struct Node{
    struct Node* child;  // 指向孩子的结点
    struct Node* brother; // 指向兄弟的结点
    DataType data;  // 结点中的数据
}

在这里插入图片描述

从上图可以看出来,所谓兄弟孩子表示法,就是让父亲结点的child指向它最左边的孩子节点,再由这个孩子节点的``brother`来指向与它相邻的那个节点。这就是孩子兄弟表示法。

二叉树概念及结构

二叉树故名思意,就是一个节点最多只有两个子节点。一棵二叉树是节点的一个有限集合,该集合或者为空,或者由一个根节点加上两棵别称为左子树和右子树的二叉树组成。

在这里插入图片描述

从上图可以看出来,二叉树不存在度大于2的节点,且二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。

特殊的二叉树
  • **满二叉树:**一个二叉树,如果每一层的结点数都达到了最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为k,且结点总数是2^k-1,则它就是满二叉树。

  • **完全二叉树:**完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。

    **注意:**满二叉树时一种特殊的完全二叉树。

二叉树的性质
  • 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1)个结点

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

  • 任何一棵二叉树,如果度为0,其叶子结点的个数为n0,度为2的分支结点个数为n2,则有n0 = n2 + 1,也就是说度为0的个数比度为2的个数要多一个。

  • 若规定根节点的层数是1,具有n个结点的满二叉树的深度,h = log2(n+1)

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

    • 若i>0,i位置节点的双亲号:(i-1)/2; i = 0, i为根节点编号,无双亲节点
    • 若2i+1 < n,左孩子序号:2i + 1,2i+1 >= n 否则无左孩子
    • 若2i+2 < n,右孩子序号:2i+2,2i+2>=n 否则无右孩子
二叉树的存储

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

顺序结构

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

在这里插入图片描述

由上图可以看出,如果不是完全二叉树,使用顺序存储的方式会浪费很多的空间。

链式存储

二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。通常的方法是链表中的每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该节点左右孩子所在的链结点的存储第hi。链式结构又分为二叉链和三叉链。
在这里插入图片描述

typedef int BTDataType;
// 二叉树
struct BinaryTreeNode{
    struct BinTreeNode* _pLeft; // 指向当前节点左孩子
    struct BintreeNode* _pRight; // 指向当前节点右孩子
    BTDataType _data; // 当前节点值域
};
// 三叉树
struct BinaryTreeNode{
    struct BinTreeNode* parent; // 指向当前节点的双亲
    struct BinTreeNode* pLeft; // 指向当前节点左孩子
    struct BinTreeNode* pRight; // 指向当前节点的右孩子
    BTDataType data; // 当前节点值域
};
二叉树的顺序结构及实现

普通的二叉树是不适合用数组来存储,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

堆的概念及结构

如果有一个关键码的集合K = {k0, k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中。且满足,所有的父亲节点比孩子节点要小或满足,所有的父亲节点比孩子节点要大。将根节点最大的堆叫做大根堆或大堆,将根节点最小的堆叫小根堆。

堆的性质

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

在这里插入图片描述

堆的实现

前面讲了那么多关于堆的内容,到底要怎么实现堆呢?这里就得讲一讲向下调整算法。假如我们有一个数组,从逻辑上将它看作一棵完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。但是这种算法有一个前提:左右子树必须是一个堆,才能调整。例如

// 有一个数组
int arr[] = {27,15,20,30,32,56,90}

这是我们实际存储的结构,也就是说这是用数组来存储的二叉树,它的逻辑结构如下图。

在这里插入图片描述

从上图可以看出,除了27这个根节点,其他的子树都满足小根堆的特点,这时候,使用向下调整算法让这个二叉树变成一个正确的小根堆。过程如下
在这里插入图片描述

也就是说,向下调整算法就是,如果父亲节点比孩子节点大(或小)就把父亲节点和孩子节点交换,再比较,直到整个树变成大根堆或小根堆。

堆的创建

下面我们给出一个数组,这个数组逻辑上可以看做一棵完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点的左右子树不是堆,怎么办呢?这里我们从倒数的第一个非叶子节点的子树开始调成,一直调整到根节点的树,就可以调整成堆。

int arr[] = {1, 5, 3, 8, 7, 6}

在这里插入图片描述

堆的删除和插入
  • 删除堆就是删除堆顶的数据,将堆顶的数据和最后一个数据交换,然后删除数组最后一个数据,再进行向下调整算法。
  • 堆的插入是将一个数据插入到数组尾,再进行向上调整算法,直到满足堆。
实现堆
typedef int HPDataType;
typedef struct Heap {
	int* _arr;
	int _size;
	int _capacity;
}Heap;

// 堆的初始化
void HeapInit(Heap* hp);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);
// 打印堆
void HeapShow(Heap* hp);

按照国际惯例,第一个当然是定义堆的结构,在此次代码中定义了一个可变长的数组_arr,然后定义了一个用来记录堆中元素个数的_size,为了判断堆是否存放满了数据,就定义了_capacity

定义好之后就是对堆进行初始化,也就是给堆赋上初始值。

// 堆的初始化
void HeapInit(Heap* hp) {
	assert(hp);
	hp->_arr = NULL;
	hp->_size = hp->_capacity = 0;

}

然后就是对堆进行增加和删除数据,在前面我们说过,创建堆的时候使用了一种算法,叫做向上调整算法,在删除堆的数据时使用的是向下调整算法,所以在讲堆的插入和删除之前需要先实现向下调整和向上调整算法。

如果用数组表示二叉树,那么父亲节点和孩子节点之间有以下关系:

  • parent = (child -1) / 2
  • leftchild = parent * 2 + 1
  • rightchild = parent * 2 + 2
// 向上调整
void AdjustUp(Heap* hp, int index) {
	assert(hp);
	int child = index;  // 孩子节点的位置
	int parent = (child - 1) / 2;  // 通过孩子节点找到其父亲节点的位置
	while (child > 0) {  // 根节点的位置就是arr[0]
		if (hp->_arr[child] > hp->_arr[parent]) { // 如果孩子节点大于父亲节点,就交换位置
			int tmp = hp->_arr[child];
			hp->_arr[child] = hp->_arr[parent];
			hp->_arr[parent] = tmp;
			child = parent;  // 将之前的父亲节点变成孩子节点继续作比较
			parent = (child - 1) / 2;
		}
		else {
			break;
		}

	}
}

向上调整是通过孩子节点找父亲节点然后去左比较,而向下调整正好相反,是用过父亲节点找到孩子节点,然后再去做比较。

// 向下调整
void AddjustDown(Heap* hp, int index) {
	assert(hp);
	int parent = index; // 父亲节点的位置
	int child = parent * 2 + 1;
	
	while (child< hp->_size) { // 当孩子节点是叶子节点时就跳出循环
		if (hp->_arr[child] < hp->_arr[child + 1]) { // 这里是找出左右孩子中大的那个节点
			child++;
		}
		if (hp->_arr[parent] < hp->_arr[child]) { // 孩子节点和父亲节点作比较
			int tmp = hp->_arr[parent];
			hp->_arr[parent] = hp->_arr[child];
			hp->_arr[child] = tmp;
			parent = child;
			child = parent * 2 + 1;
		}
		else {
			break;
		}
	}
}

了解了向上调整和向下调整算法之后就可以来对堆进行插入和删除操作了。

// 堆的插入
void HeapPush(Heap* hp, HPDataType x) {
	assert(hp);
	if (hp->_size == hp->_capacity) { // 判断堆是否为空或为满,如果是就进行扩容操作
		int newarrsize = hp->_capacity == 0 ? 4 : hp->_capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(hp->_arr,sizeof(HPDataType) * newarrsize);
		if (tmp != NULL) {
			hp->_arr = tmp;
			hp->_capacity = newarrsize;
		}
	}
	hp->_arr[hp->_size] = x; // 把数据插入到数组尾
	AdjustUp(hp, hp->_size); // 向上调整算法,建堆
	hp->_size++;
	
}

前面我们讲了,堆的数据删除就是把堆顶的数据删除掉。

// 堆的删除
void HeapPop(Heap* hp) {
	assert(hp);
	assert(hp->_size >= 0);
	hp->_arr[0] = hp->_arr[hp->_size - 1]; // 数组最后一个数据覆盖掉堆顶的数据
	hp->_size--; // 删除数组最后一个数据
	AddjustDown(hp, 0); // 向下调整,建堆
}

要取出堆顶的数据也很简单,就是数组的第一个数据,就不再赘述。

// 取堆顶的数据
HPDataType HeapTop(Heap* hp) {
	assert(hp);
	assert(hp->_size >= 0);
	return hp->_arr[0];
}
// 堆的数据个数
int HeapSize(Heap* hp) {
	return hp->_size;
}
// 堆的判空
int HeapEmpty(Heap* hp) {
	assert(hp);
	if (hp->_size <= 0) {
		return 1;
	}
	else {
		return 0;
	}
}
//  打印堆
void HeapShow(Heap* hp) {
	int i = 0;
	for (i = 0; i < hp->_size; i++) {
		printf("%d ", hp->_arr[i]);
	}
}

因为我们创建堆时开辟了内存空间,在使用结束后应该释放掉我们开辟的空间。

// 堆的销毁
void HeapDestory(Heap* hp) {
	assert(hp);
	free(hp->_arr);
	hp->_size = hp->_capacity = 0;
}

—end

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_yiyi_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值