真正的勇士,就是在看清生活的真相后,依旧慷慨面对他所遭受的苦难与挫折。
大学究竟教会了我们什么呢?或许答案只有一个,看清自己,与自己和解,和自己坐下来谈一谈。
人生或许本就没有什么意义,但我们还是要生活,不是吗?
文章目录
- 一、树
- 1.1 树的介绍
- 1.2 树的重要概念
- 1.3 树的表示(左孩子右兄弟)
- 二、二叉树
- 2.1 二叉树的介绍
- 2.2 二叉树的性质
- 三、二叉树的顺序结构及实现
- 3.1 二叉树的顺序结构
- 3.2 堆的概念及结构
- 3.3 堆(底层就是顺序表)的实现
- 3.3.1 堆结构体设计+堆的初始化+堆的销毁
- 3.3.2 堆的插入(附:向上调整算法)+堆的删除(附:向下调整算法)
- 3.3.3 取堆顶数据+堆的大小+堆的判空
- 3.3.4 测试接口
一、树
1.1 树的介绍
树是一种非线性的数据结构,它是一种由有限个结点组成的具有层状结构的集合,把它叫做树是因为它看起来像一颗倒挂起来的树,叶子朝下,根root朝上。
其中最上面的结点称之为根节点,而且每一棵子树之间是不能有交集的,否则就不是树状结构了,下面的Linux目录的结构就是我们的树形结构。
1.2 树的重要概念
1.结点的度: 一个结点含有的子树的个数称为该节点的度
2.叶结点或终端结点: 子树个数为0的结点
3.双亲结点或父节点: 如果一个结点有子结点,则这个结点称为子节点的父节点
4.孩子结点或子结点: 一个结点含有的子树的根结点就是这个结点的子结点
5.结点的层次: 从根开始定义,根为第一层,根的子节点为第二层,一次类推
6.结点的祖先: 从树的根节点开始一直到这个结点所经过的路径上所有的结点就是这个结点的祖先。
7.子孙: 以某一结点作为根结点的子树下的所有结点都是这个根结点的子孙
8.森林: 多棵互不相交的树组成的集合称之为森林。
例如Linux中的ls指令其实就是将当前所处根结点的所有子节点全部列出来
1.3 树的表示(左孩子右兄弟)
树的结构在表示时,不仅要存储值,还要链接其每个结点之间的关系,但我们不知道每个结点的度是多少,但不要担心,问题总会有解决的办法的,下面就来说一下左孩子右兄弟表示法的精妙所在。
我们不管那么多,每个结点只能有一个孩子,剩下的子节点就跟父节点没关系了,全靠他的第一个孩子来连接,直到一个孩子的brother指到空指针,这一层就完事了。
后面的也依次类推。
二、二叉树
2.1 二叉树的介绍
一棵二叉树是结点的一个有限集合,该集合可以为空或由两棵子树构成,子树分别称为左子树和右子树,并且二叉树中结点的度是不可以超过2的,也就是任意一个结点的子节点个数必须小于等于2。
任意的一棵二叉树都是由下面的几种情况复合而成的。
只要一棵树的最大度小于等于2,我们就可以把这棵树称之为二叉树,空树,只有根节点等都可以称之为二叉树。
当然二叉树中也有一些特殊的树,分别是完全二叉树和满二叉树。
满二叉树就是除叶结点之外的结点的度都是2.
完全二叉树其实就是特殊的满二叉树,只要满足最后一行是连续的叶结点,中间不可以空开,除最后一层外其他层满足满二叉树的特点,这样的树我们称之为完全二叉树。但完全二叉树最后一层最少都得有一个结点。
2.2 二叉树的性质
1.满二叉树的结点个数:2^h-1(h代表树的层数)
2.完全二叉树的结点个数:最多个数:2^h-1 最少个数:2^(h-1)
3.对于任何一棵二叉树,假设叶结点个数为n0,度为2的结点个数为n2,则有结论n0=n2+1,也就是叶结点个数永远比度为2结点个数多1
4.完全二叉树度为1的结点个数要么是1要么是0.
5.父子结点关系:parent=(child-1) / 2 leftcihld=parent2+1 rightchild=parent2+2
6.满二叉树高度为h,节点数是n,h=log(n+1),我们后面会经常用高度来判断算法的时间复杂度。
三、二叉树的顺序结构及实现
3.1 二叉树的顺序结构
现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
3.2 堆的概念及结构
如果有一个关键码的集合K = { , , ,…, },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足: <= 且 <= ( >= 且 >= ) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
其实堆就是完全二叉树
其实为什么要存在逻辑结构这种东西呢?
其实吧实际在内存当中的存储结构就两种,一种是数组一种是链表,但由于我们生活中的存储模式可不简简单单只有这两种结构,所以我们将这两种基本的存储结构抽象成我们想要的结构,用算法来将其进行实现。
所以其实逻辑结构存在的最大意义就是方便我们理解,如果没有逻辑结构,我们对着生涩的数组裸写代码,是非常容易写错的,所以我们需要逻辑,需要它将抽象的东西变得生动化,可视化,形象化。
3.3 堆(底层就是顺序表)的实现
3.3.1 堆结构体设计+堆的初始化+堆的销毁
typedef int HPDataType;
typedef struct Heap
{
HPDataType* array;
int size;
int capacity;
}HP;
其实这里堆结构体就是一个顺序表,我们定义的结构体和顺序表也没什么区别
void HeapInit(HP*php)
//这里初始化开不开辟空间都可以,我们可以选择这里开空间,后面用realloc修改空间大小,也可以这里不开空间,直接后面realloc修改空间
//因为当realloc接收的指针为NULL时,他的作用和malloc是一样的,所以这里开不开辟空间都是可以的
{
assert(php);
php->array = NULL;
php->size = php->capacity = 0;
}
初始化这里也没什么新奇的东西和之前讲解的链表顺序表等,没什么区别
void HeapDestroy(HP* php)
{
assert(php);
free(php->array);
php->array = NULL;
php->size = php->capacity = 0;
}
释放掉动态开辟的数组空间,然后将指针置为空,其他变量置为0即可。
3.3.2 堆的插入(附:向上调整算法)+堆的删除(附:向下调整算法)
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->array, sizeof(HPDataType) * newCapacity);
if (tmp == NULL)
{
perror("realloc fail\n");
exit(-1);
}
php->array = tmp;
php->capacity = newCapacity;
}
php->array[php->size] = x;
php->size++;
AdjustUp(php->array, php->size - 1);
}
在插入数据之前我们还是需要先检查空间是否满,如果满了,我们直接扩容就好了。最后我们调用向下调整的接口进行实现堆的调整
void AdjustUp(HPDataType* array, int child)//传过来你插入的孩子的下标
{
int parent = (child - 1) / 2;
while (child > 0)//child会被赋值到祖先的位置,这时parent已经越界了,我们的向上调整也就结束了,所以child>0
{
if (array[child] > array[parent])
//如果想要调整为小堆的话,我们只要调整这里的比较符号就可以了,保证树中所有的父亲都小于等于孩子
{
Swap(&array[child], &array[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
我们每一次插入新的数据,我们都让这个新的数据和他的父节点进行比较,我们这里默认建的是大堆,所以只要每次插入的孩子结点大于父节点时,我们就将这个子节点向上调整到parent下标的位置,parent继续向上调整到新的parent的位置,等到child的位置到达祖先的位置也就是根节点root的位置时,我们向上调整的循环也就结束了。
多说一句,大家可能有点蒙,为什么child-1除以2就是parent的结点了,不用分情况讨论吗?child的下标既有可能是奇数,也有可能是偶数啊,你不用区分一下吗?
其实是不用区分的,因为/求的是商,我们的偶数和奇数下标经过上面的运算过后,其实结果是一样的。所以在利用孩子找父节点时,只要减1再除以2就OK了。
void HeapPop(HP* php)
{
assert(php);
assert(php->size > 0);//括号内表达式若判断为假,直接报错,粗暴的方式来解决
Swap(&php->array[0], &php->array[php->size - 1]);
php->size--;
//AdjustDownMe(php->array, php->size -1);
AdjustDownTeach(php->array, php->size, 0);
}
我们再删除堆顶数据时,利用了一个小技巧就是,我们将堆顶数据用堆中最小辈分的数据覆盖掉,然后依次向下调整数据的位置,以便保证堆还是大堆,为什么这样做是可行的呢?这样做可以保持根的某一个子树结构不变,我们只要调整另一子树的数据就可以了,而且这个调整还是递归式的,我们的算法时间复杂度是logN。
要知道logN可是非常快的,所以这个算法是非常牛逼的。
void AdjustDownTeach(HPDataType* array, int n, int parent)
{
int child = parent * 2 + 1;//上来我们就先假设最大的孩子是左孩子
while (child<n)//我们想的是循环结束的条件,写的是循环继续的条件
{
//保证有右孩子的同时,看看我们的假设是否正确,错误就调整
if (child + 1 < n && array[child + 1] > array[child])
//如果假设错误,我们将孩子改为右孩子,并且你也有可能没有右孩子,没有右孩子,默认左孩子就是最大的
//这里其实不用担心没有孩子的问题,因为如果parent没有孩子,child被赋值过后肯定大于n了直接跳出循环了就。
{
++child;//将下标自增,左孩子就变为右孩子
}
if (array[parent] < array[child])
{
Swap(&array[parent], &array[child]);
parent = child;
child = parent * 2 + 1;//这里再重新假设左孩子是大的,下一次循环就是先看看我们的假设是否正确,若不正确就进行调整。
}
else
{
break;
}
}
}
这里的代码延续了我们之前再做栈和队列的一些面试题时所用到的技巧了,就是假设法,如果假设错了,我们直接利用if语句进行调整就OK了,向下调整也简单,只要我们的parent小于他的子节点,那它就得往下走,不能占顶部的位置,这算法和前面的向上调整也是比较相似的。
3.3.3 取堆顶数据+堆的大小+堆的判空
这几个接口真的是简单的要死,我不想说了,写了这么多数据结构了,今天见的属实是简单的要死。
HPDataType HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->array[0];
}
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
3.3.4 测试接口
void TestHeap1()
{
int array[] = { 27,15,19,18,28,34,65,49,25,37 };
HP heap;//建一个堆
HeapInit(&heap);
for (int i = 0; i < sizeof(array) / sizeof(int); i++)
{
HeapPush(&heap, array[i]);//我们的插入模块儿里面,每一次插入之后,都会再重新向上调整一次,以此来保证我们的堆是大堆。
}
HeapPrint(&heap);
HeapPop(&heap);
HeapPrint(&heap);
//topK快
int k = 5;
while (k--)
{
printf("%d ", HeapTop(&heap));//求出topk个数据,大堆中最大的前5个数据
HeapPop(&heap);
}
HeapDestroy(&heap);
}
void TestHeap2()
{
int array[] = { 27,15,19,18,28,34,65,49,25,37 };
HP heap;//建一个堆
HeapInit(&heap);
for (int i = 0; i < sizeof(array) / sizeof(int); i++)
{
HeapPush(&heap, array[i]);//我们的插入模块儿里面,每一次插入之后,都会再重新向上调整一次,以此来保证我们的堆是大堆。
}
HeapPrint(&heap);
//topK快
int k = 5;
while (!HeapEmpty(&heap))//利用堆顶数据,我们可以打印出来这个数组的降序内容
{
printf("%d ", HeapTop(&heap));//求出topk个数据,大堆中最大的前5个数据
HeapPop(&heap);
}
HeapDestroy(&heap);
}
int main()
{
//TestHeap1();
TestHeap2();//测试过后我们的数组就被我们排成降序的了
return 0;
}
这里说几句吧,我们可以利用HeapTop接口和HeapPop接口来组合解决topK问题,然后再测试接口里面我们是单独先创建了一个数组,然后利用堆的插入接口,讲这个数组的内容重新插入到我们动态开辟的数组array,这样就实现我们大堆的搭建了。