👀先看这里👈
😀作者:江不平
📖博客:江不平的博客
📕学如逆水行舟,不进则退
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
❀本人水平有限,如果发现有错误的地方希望可以告诉我,共同进步👍
🏐1.树
🏀1.1树是什么
树是一种数据结构,它是由n(n≥0)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
树的特点:
每个节点有零个或多个子节点;没有父节点的节点称为根节点;每一个非根节点有且只有一个父节点;除了根节点外,每个子节点可以分为多个不相交的子树。
注意在树这个结构中,子树不能有交集,否则就不是树形结构,换句话说就是不能成环
🏀1.2为什么会有树
日常生活中,很多事物都可以用树来描述,例如书的目录、工作单位的组织架构等等。树是计算机中非常重要的一种数据结构,树存储方式可以提高数据的存储、读取效率。
🏀1.3怎么表示树
树的表示方法比起线性表来说就复杂了,因为我们并不知道一个结点包含几个孩子结点,也有人用顺序表将每个结点存储起来。实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。比较推荐的是兄弟孩子表示法。结点结构为下图所示。
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 第一个孩子结点
struct Node* _pNextBrother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};
可以看到这里树的定义是递归定义的,结点中包含了孩子和兄弟结点的地址
🏐2.二叉树
树的种类有很多,比如普通的二叉树、完全二叉树、满二叉树、线索二叉树、哈夫曼树、二叉排序树、平衡二叉树、AVL平衡二叉树、红黑树、B树、B+树、堆等。今天我们主要来了解一下二叉树和堆。
🏀2.1二叉树是什么
树有很多种,每个节点最多只能有两个子节点的称为二叉树。 或者说树中节点的度最大为2的树就叫做二叉树
①在二叉树中,一个节点最多有两颗子树,二叉树节点的度<=2
②二叉树的有左右之分,且子树的次序不能颠倒,因此二叉树是有序树
如果该二叉树的所有叶子节点都在最后一层,并且结点总数= 2^n -1, n为层数,则我们称为满二叉树。
如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树。
🏀2.2二叉树的存储结构
⚽2.2.1二叉树的顺序结构
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
- 顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空
间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面的章节会专门讲解。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
这种顺序结构存储更方便我们找到父子间的关系,可以看到非完全二叉树进行存储时,数组要空出没有的结点,因为不这样做无法通过数组知道结点间的位置关系。还会造成空间的浪费,非完全二叉树更适合下面讲的链式结构存储。
⚽2.2.2二叉树的链式结构
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。
struct BinaryTreeNode
{
struct BinTreeNode* pLeft; // 指向当前节点左孩子
struct BinTreeNode* pRight; // 指向当前节点右孩子
BTDataType data; // 当前节点值域
}
🏐3.堆
我们可以看到完全二叉树非常适合顺序存储,避免了空间浪费,现实中我们通常把使用顺序结构的数组来存储一种特殊二叉树——叫做堆
🏀3.1堆是什么
一组集合,按照完全二叉树的顺序结构进行排列存储在一个一维数组中,并满足每一个子结点都不大于(或者不小于)父亲节点,堆顶数据为这一组数据中最大(或最小)的数据,称为大根堆(或小根堆)。堆总是一颗完全二叉树。
🏀3.2堆的实现
堆的构建和初始化
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
⚽3.2.1打印堆
🏓3.2.1.1直接遍历打印堆,没改变顺序
void HeapPrint(HP* php)
{
assert(php);
for (int i = 0; i < php->size; ++i)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
🏓3.2.1.2改变顺序,升序/降序打印堆
想让堆升序打印出来我们就用小堆,降序打印出来我们就用大堆,那么我们怎么控制这个堆是大堆还是小堆呢,在插入数据建堆时控制向上调整的条件是>还是<即可,同时还要在删除数据时控制向下调整的条件是>还是<
void TestHeapSort()
{
// 升序打印 -- 小堆
// 降序打印 -- 大堆
HP hp;
HeapInit(&hp);
int a[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
for (int i = 0; i < sizeof(a) / sizeof(int); ++i)
{
HeapPush(&hp, a[i]);
}
while (!HeapEmpty(&hp))
{
printf("%d ", HeapTop(&hp));
HeapPop(&hp);
}
printf("\n");
}
之前呢我们也经常排序数组中的数据,这里堆也实现一下
🏓3.2.1.3不是打印有序数据而是直接改变数组中值的顺序
void Heapsort(int* a, int n)
{
HP hp;
HeapInit(&hp);
for (int i = 0; i < n; ++i)
{
HeapPush(&hp, a[i]);
}
int i = 0;
while (!Heapempty(&hp))
{
a[i++] = HeapTop(&hp);
HeapPop(&hp);
}
HeapDestroy(&hp);//要考虑内存释放问题
}
上面写法有两个不好的地方
- 建了两个数组,之前有一个,后来建堆又一个。空间复杂度大
- 要先写一个数据结构Hp,要用Pop,Push什么的
我们可以在原有数组上直接进行调整建堆避免空间浪费,详见下面的堆排序内容
在了解堆排序内容后再回来看,就可以直接像下面这么写
void TestHeapSort()
{
int a[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
HeapSort(a, sizeof(a) / sizeof(int));
}
⚽3.2.2插入数据
插入数据后,要想让它还是堆,我们要进行调整
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 (parent >= 0)这种不行,parent一直为零会死循环
while (child > 0)
{
//if (a[child] < a[parent])//小堆
if (a[child] > a[parent])//大堆
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType)*newcapacity);
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
⚽3.2.3删除数据
思路:删除数据的时候,挪动数据覆盖删除会让剩下的数据构不成堆,所以我们这里将删除的数据跟最后一个数据调换位置,–size后进行向下调整A的justDown
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustDown(HPDataType* a, int size, int parent)
{
int child = parent * 2 + 1;//遇到比较选大小的时候,我们经常先定义假设出一个,如果不是就调换即可,这样控制起来比较简单
while (child < size)
{
// 选出左右孩子中小/大的那个
if (child+1 < size && a[child+1] > a[child])
{
++child;
}
// 孩子跟父亲比较
if (a[child] > a[parent])//小堆
//if ( hp[child] > hp[parent] )//大堆
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapPop(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);
}
删除数据的时候只删除堆顶的数据,删除其他位置的数据没有意义,因为最大和最小的值都在堆顶,显然我们关心的肯定是最大最小的数据,然后在不断得到次小次大的……
⚽3.2.4获取堆顶的数据
HPDataType HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
⚽3.2.5数据的个数与堆的销毁
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);//就比初始化多了个free
php->a = NULL;
php->size = php->capacity = 0;
}
🏀3.3堆的应用
⚽3.3.1堆排序
🏓3.3.1.1向上调整建堆
void UpHeap(int* a, int n)
{
// O(N*logN)
for (int i = 1; i < n; ++i)
{
AdjustUp(a, i);
}
}
🏓3.3.1.2向下调整建堆
我们先要保证左右子树是堆才可以进行向下调整,为了普适性,可以对于任何数组进行向下调整建堆,我们从倒数第一个非叶子结点开始建堆,因为叶子结点不需要调整
void DownSort(int* a, int n)
{
// O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)//初始化改成n-1也可以,多走几次而已
{
AdjustDown(a, n, i);
}
}
🏓3.3.1.3堆排序
在原有数组上建堆我们了解后,有个堆这是第一步,我们还要排序,这是第二步,至于排升序还是降序根据采用的建堆方式来确定
排升序
我们稍加分析可知,排升序假设用小堆,我们取出堆顶数据后剩下的部分构不成堆了,无法再进行堆方面的操作,除非重新建堆,但这样的话我们就需要不断的建堆排序,复杂度可想而知高了许多,所以排升序我们要建大堆,那么我们建大堆用向上和向下调整都可
void HeapSort(int* a, int n)
{
// O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)//初始化改成n-1也可以,多走几次而已
{
AdjustDown(a, n, i);
}
// O(N*logN)
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
那么建完大堆后,进行的操作就是将第一个数据与最后一个进行交换,再向下调整(注意这里就不是向上向下都可以选择了,这里不是建堆,而是用这个方法达到我们选数据的目的
🏐4.一些小点
- 别混淆了,有些绕,向上调整和向下调整只是方法,能达到我们的目的就用,比如排序时建堆和选数两次的使用
- 建大堆和小堆,用向上调整和向下调整都可以,不是固定的,改条件即可
以上就是二叉树和堆的相关内容啦,觉得还不错的铁汁点赞关注一下吧😀