🎄
一、树的概念及结构
1.1树的抽象图形
什么是树?tree?确实,这的确是树。但在我们C语言中树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
然而,就当我上网查找有关倒过来生长的树的图片时,结果却大为震撼。
确实不凡。这证明什么?这证明了树 在数据结构中那必定也是超然地位的存在。扯远了,来个正经点的图(有反转!)
这才叫树嘛。。。
1.2树的结构概念
💡有一个特殊的结点,称为根结点,根节点没有前驱结点。
💡除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继。
💡因此,树是递归定义的。
数据结构里面的树大概可以这样表示
那么这里就有很多关于树的名词需要我们掌握。我来一一列举给大家。
(1)节点的度:一个节点含有的子树的个数称为该节点的度
(2)叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I…等节点为叶节点
(3)非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G…等节点为分支节点
(4)双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点
(5)孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点
(6)兄弟节点:具有相同父节点的节点互称为兄弟节点
(7)树的度:一棵树中,最大的节点的度称为树的度
(8)节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推
(9)树的高度或深度:树中节点的最大层次
(10)堂兄弟节点:双亲在同一层的节点互为堂兄弟
(11)节点的祖先:从根到该节点所经分支上的所有节点;
(12)子孙:以某节点为根的子树中任一节点都称为该节点的子孙
(13)森林:由m(m>0)棵互不相交的树的集合称为森林
1.3树的表示
我们先画一张逻辑图方便我们理解
我们每一个节点都可以用两个指针来表示,一个指向自己的亲儿子,还有一个指向自己的亲兄弟。这种方法是目前表示树的最好的方法,非常的直观简洁高效🐂🍺。代码如下:
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 第一个孩子结点
struct Node* _pNextBrother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};
1.4树在实际中的应用
举个最简单的例子,咱们电脑文件中的目录,是不是就是一个简单的树的结构啊。是的,它的确是。
相信看到这里的巨佬们已经初步对树有了一个基本而又直观的感受。
二、二叉树
2.1小计算
数据结构最重要的是啥,是存储管理数据。我们同过下面这张图片可以看到:
假设parent是父亲节点在数组中的下标。
leftchild = parent * 2 + 1
rightchild = parent * 2 +2。
反过来假设孩子的下标是child,无论是左孩子还是右孩子
parent = (child - 1) / 2
![在这里插入图片描述](https://img-blog.csdnimg.cn/e35f20347ba3448b88fbe77cc281b5b9.png
咱可别小看这个计算,这个公式在下面堆的实现中起到很大很大的作用,但如果你要问我具体有多大,反正就是很大。这两张图相信可以很好的帮助大家理解
2.2性质规律总结
(1)若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1) 个结点.
(2)若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h- 1
(3)对任何一棵二叉树, 如果度为0其叶结点个数为 n0, 度为2的分支结点个数为 n2,则有n0=n2+1
(4). 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=Log2(n+1). (ps:Log2(n+1)是log以2为底,n+1为对数)
(5)对于具有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否则无右孩子
2.3完全二叉树
我们先来看着这张图,非常直观地说明了何为完全二叉树:
我们可以看到完全二叉树中度为1的最多只有一个。
性质见2.2中性质(5).
三、二叉树-堆
说到堆,大家可能会想到操作系统对内存划分中的堆啊,函数栈帧什么的,但实际上,数据结构里面的堆,和这个是两个完全不同的学科,数据结构中的堆在逻辑上是一个完全二叉树,在物理结构上是一个数组
3.1堆的概念及结构
定义:
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储
在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为
小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
🛒堆中某个节点的值总是不大于或不小于其父节点的值;
🛒堆总是一棵完全二叉树。
而堆我们可以将它分成大堆和小堆(咱们说人话,相信定义这东西看起来不是那么通俗易懂)
大堆:树中一个树及子树中,任何一个父亲都大于等于孩子
小堆:树中一个树及子树中,任何一个父亲都小于等于孩子
💎重点:无论是大堆还是小堆,我们插入数据对其他节点没有影响,只是可能会影响从它到根节点路径上节点的关系
根据这个性质我们就可以对完全二叉树-堆进行代码的实现:
3.2代码的实现(大堆)
3.2.1初始化和销毁
之所以把这个两个一块写是因为这两个代码的实现在经历前面练习后对我们来说已经是🍕。
首先第一步创建头文件等等等等。然后定义一个结构体,这一部分不多说了,各位聪明的大佬肯定轻轻松松写出来。
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
初始化:
void HeapInit(HP* hp)
{
assert(hp);
hp->a = NULL;
hp->capacity = hp->size = 0;
}
销毁:
void HeapDestroy(HP* hp)
{
assert(hp);
free(hp->a);
hp->capacity = hp->size = 0;
}
3.2.2插入数据
好,一想到堆的插入我们首先思考的是在哪里插入,但不要急。按照之前数据结构经验,判断空间是否为空以及开辟空间,是我们第一步需要做的。然后我们就要用到一种非常重要的算法–向上调整算法。
⏳向上调整算法:
我们来看一张抽象的图(ps:抽象作者当然用抽象的图)
我们用C表示child。用P表示parent。如果说C比P大,那么我们就将C的位置和P的位置交换,一直交换child为0,也就是在0节点的位置上,我们就停止。这才是大堆存储的方法。我们将这个算法单独写一个接口函数:
void AdjustUp(int* a, int n, int child)// 向上调整
{
assert(a);
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] > a[parent])
{
int tmp = a[child];
a[child] = a[parent];
a[parent] = tmp;
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
这里面注意while循环里面如果你写成了parent>=0虽然也是可以运行出来的,但是并不准确,大概类似于这样:
神奇但是形象。好,实现完向上调整法后。我们在插入数据函数中引用这个接口函数,代码如下:
void HeadPush(HP* hp, HPDataType x)
{
assert(hp);
if (hp->size == hp->capacity)
{
size_t newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
HPDataType* tmp = realloc(hp->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
hp->a = tmp;
hp->capacity = newcapacity;
}
hp->a[hp->size] = x;
hp->size++;
AdjustUp(hp->a, hp->size, hp->size - 1);
}
3.2.3打印
既然我们已经实现了部分功能,我们可以尝试打一下。输入一组乱序数据,看它是否能够实现二叉树的大堆存储
void HeadPrint(HP* hp)
{
for (int i = 0; i < hp->size; ++i)
{
printf("%d ", hp->a[i]);
}
printf("\n");
}
我们输入70, 56, 30, 25, 15, 10, 75这几个数字,打印结果看一下:
可以,成功了。🎉🎉🎉🎉🎉
3.2.4删除数据
删除堆是删除堆顶的数据,这里我们要用特殊的算法,我们先将堆顶的数据和最后一个数据交换,然后删除数组最后一个数据,再进行向下调整算法。
然后我们进行向下调整法,将儿子中小的数据和它的父亲比较,如果小的孩子大于父亲则交换否组,继续向下调整。这里我们来对这个算法进行实现
void AdjustDown(int* a, int n, int parent)
{
assert(a);
int child = parent * 2 + 1;
while (child < n)
{
// 先选出左右孩子中大的
if (child + 1 < n && a[child + 1] > a[child]) // 这里我们就可以找出最大的孩子
{ //如果是小堆那我们就改成<
++child;
}
// 如果小的孩子大于父亲则交换,并继续向下调整
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
向下调整法实现好后,我们就可以对堆数据进行删除操作。方法是交换顶尾数据后,我们将size–就可以堆=对最后一个数据进行删除。代码如下:
void HeapPop(HP* hp)
{
assert(hp);
assert(!HeapEmpty(hp));
Swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
AdjustDown(hp->a, hp->size, 0);
}
同时注意了,我们还要看数组是否为空,如果是空的话,我们再删除就没有意义了,用两端函数实现如下:
bool HeapEmpty(HP* hp)
{
assert(hp);
return hp->size == 0; //size等于0就是空,不等于0就不是
} // 这是个比较表达式所以是bool值
int HeapSize(HP* hp)
{
assert(hp);
return hp->size;
}
这样,我们也实现了删除数据的操作。我们在test函数里实现后,打印看看结果。
圆满完成🎉🎉🎉
3.3堆创建-完整代码
Heap.h
#pragma once
#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 HeapInit(HP* hp);
void HeapDestroy(HP* hp);
void HeadPush(HP* hp, HPDataType x);
void HeapPop(HP* hp);// 删除数据是指删除堆顶的数据即删除这棵树的根
void HeapPrint(HP* hp);
bool HeapEmpty(HP* hp);
int HeapSize(HP* hp);
void Swap(HPDataType* px, HPDataType* py);
Heap.c
#include "Heap.h"
void Swap(HPDataType* px, HPDataType* py)
{
HPDataType tmp = *px;
*px = *py;
*py = tmp;
}
void HeapInit(HP* hp)
{
assert(hp);
hp->a = NULL;
hp->capacity = hp->size = 0;
}
void HeapDestroy(HP* hp)
{
assert(hp);
free(hp->a);
hp->capacity = hp->size = 0;
}
void HeapPrint(HP* hp)
{
for (int i = 0; i < hp->size; ++i)
{
printf("%d ", hp->a[i]);
}
printf("\n");
}
void AdjustUp(int* a, int n, int child)// 向上调整
{
assert(a);
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] > a[parent])
{
/*int tmp = a[child];
a[child] = a[parent];
a[parent] = tmp;*/
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void HeadPush(HP* hp, HPDataType x)
{
assert(hp);
if (hp->size == hp->capacity)
{
size_t newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
HPDataType* tmp = realloc(hp->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
hp->a = tmp;
hp->capacity = newcapacity;
}
hp->a[hp->size] = x;
hp->size++;
AdjustUp(hp->a, hp->size, hp->size - 1);
}
void AdjustDown(int* a, int n, int parent)
{
assert(a);
int child = parent * 2 + 1;
while (child < n)
{
// 先选出左右孩子中大的
if (child + 1 < n && 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* hp)
{
assert(hp);
assert(!HeapEmpty(hp));
Swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
AdjustDown(hp->a, hp->size, 0);
}
bool HeapEmpty(HP* hp)
{
assert(hp);
return hp->size == 0; //size等于0就是空,不等于0就不是
} // 这是个比较表达式所以是bool值
int HeapSize(HP* hp)
{
assert(hp);
return hp->size;
}
test.c
#include "Heap.h"
int main()
{
int a[] = { 70, 56, 30, 25, 15, 10, 75 };
HP hp;
HeapInit(&hp);
for (int i = 0; i < sizeof(a) / sizeof(a[0]); ++i)
{
HeadPush(&hp, a[i]);
}
HeapPrint(&hp);
HeapPop(&hp);
HeapPrint(&hp);
return 0;
}
四、总结
所有的数组都可以表示成完全二叉树,但是他不一定是堆。关于二叉树—堆的部分在这里就要告一段落了,二叉树后面还有很多满意理解的,复杂的,抽象的,排序啊啥的算法。打好基础是为了后面更好的学习。这样,我们下一期不见不散,感谢你们的支持和鼓励。
---------------------------------------------------------------------------------------------------------------------------------------------
我 慢 慢 地写,你 们 慢 慢 地 读