目录
一,二叉树的存储结构
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。今天着重讲解顺序结构。链式结构,下一篇博客细说。
1. 顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
2 . 链式存储
二叉树的链式存储结构是指用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链。
二,二叉树的结构介绍
1, 二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结 构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统 虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
2, 堆的概念及结构
如果有一个关键码的集合K = {k0 ,k1 ,k2 ,…, },把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足: = 且 >= ) i = 0,1, 2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。
三,堆的理论实现
1,堆的两种经典算法
1.1堆向下调整算法
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整 成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
int array[ ] = {27,15,19,18,28,34,65,49,25,37};
接下来就是调整的过程示意图:
1.2堆的向上调整算法
还是刚刚这组样例,在28的右子树插入一个10,破坏了堆的结构,想要重新调整成堆,得依靠向上调整算法。一步一步爬上去
2,堆的建立
下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的 子树开始调整,一直调整到根节点的树,就可以调整成堆。
int a[ ] = {1,5,3,8,7,6};
3,建堆的时间复杂度
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的 就是近似值,多几个节点不影响最终结果):
这里是重点精髓:**************************************************
如果你选择向下建堆的方式,则时间复杂度为O(n),
选择向上的建堆方式,时间复杂度是O(n*log(n)),
两种方式的时间复杂度区别很大的。
这是为什么呢?
看下图,就明白了
四,堆的代码实现
依旧是老规矩,函数的实现和声明分离开来,之前一些博客都是这样实现的,条理清晰
先来头文件的函数定义声明:
1,初始化及销毁函数
这两个函数较为简单,置为空,置为0即可。
void HeapInit(HP* php) //初始化
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
void HeapDestory(HP* php) //销毁
{
assert(php);
free(php->a);
php->a = NULL;
php->capacity = php->size = 0;
}
2,堆的插入函数
插入数据的时候,首先得考虑是否为空,是否为满了,空或者满的情况下,需要进行动态内存开辟,也就是扩容一下,
扩容成功后,需要进行数据的赋值。
但是这只是插入数据,此时还不是堆的结构,需要使用向上调整算法去调整结构,把它调整成堆,大堆还是小堆可以全凭自己喜好。都没问题的。修改<符号即可
为什么使用向上调整算法?
因为插入都是在最后一个位置插入,也就是尾插,你需要慢慢爬上去,这个应该可以理解吧
void HeapPush(HP* php, HPDataType x) //插入X继续保持堆形态
{
assert(php);
if (php->size == php->capacity)
{
int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, newCapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc fail:");
exit(-1);
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
向上调整函数,AdjustUp函数:孩子比父节点小的时候,进行交换,把小的数据调整上去,最后会形成小堆结构。
每一次调整过后,把父节点的赋值给孩子,进行下一次循环,直到把堆的结构调整正确为止。
传递过来的整型child是数组的下标,可不是具体的数据值。
void Swap(HPDataType* a, HPDataType* b)
{
HPDataType tmp = *b;
*b = *a;
*a = tmp;
}
void AdjustUp(HPDataType* a, int child) //调整成小堆
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
3,堆的判空函数
这种函数,最合适的就是用布尔,返回值要么true,要么false。语句也比较简单。
bool HeapEmpty(HP* php) //堆的判空
{
assert(php);
return php->size == 0;
}
4,堆的删除函数
堆的删除是删除堆顶的元素,
为什么是删除堆顶?
因为这样的目的,是找出次大或者次小的值,如果是大堆,删除堆顶的数据,接下来依旧保持堆的结构,那么下一个堆顶就是次大的,第二大的数据。同理,小堆也是如此,找出次小的数据。这是删除堆顶数据的意义。
如果是直接删除堆顶,那么堆的结构就全乱了,父子关系也会混乱的。
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
而且向下调整算法的时间复杂度是O(logn),可不是O(n),上面那是向下建堆是O(n),
这次只是把一个数向下调整而已。
原来我们找一个次小的数需要遍历一遍,O(n)的时间复杂度。现在只需要O(logn),也就是十亿个数只需要30次,便能找到次小的数。。。这就是二叉树的魅力。。。。。
void HeapPop(HP* php) //删除
{
assert(php);
assert(!HeapEmpty(php));
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
向下调整算法AdjustDown函数:形参是,传递一个数组,一个整型n是数组大小,parent是数组下标,要调整的父节点。
由于是向下调整,父节点要和两个子树之中最小的进行交换,所以不妨,先检测两个左右孩子的大小,找出最小的孩子,防止越界,加一个限制条件,然后,进行和父节点的交换。
直到把堆调整成功为小堆。向下调整,上面有算法介绍。
void AdjustDown(HPDataType* a, int n, int parent)
{
int minChild = parent * 2 + 1;
while (minChild < n)
{
if (minChild + 1 < n && a[minChild + 1] > a[minChild])//找出小或者大的那个孩子,根据自己定
{
minChild++;
}
if (a[minChild] > a[parent])
{
Swap(&a[minChild], &a[parent]);
parent = minChild;
minChild = parent * 2 + 1;
}
else
{
break;
}
}
}
5,返回堆顶的函数和打印函数
返回堆顶的函数较为简单,判断不为空之后,直接返回即可。
打印函数也较为简单。
HPDataType HeapTop(HP* php) //返回堆顶的元素----找次大或者次小的 时间复杂度O(logN)
{
assert(php);
assert(!HeapEmpty(php));
return php->a[0];
}
void HeapPrint(HP* php) //打印
{
for (int i = 0; i < php->size; ++i)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
6,展示效果
每一个插入数据,然后建成了小堆,然后删除之后,还是堆,完全符合上面函数的意义。
想建成大堆,把判断父子大小的 < 符号修改一下即可。
五,利用堆的思想进行排序
首先明白一点,建堆并不是排序,那根本不是顺序,堆根本不是严格意义的由大到小或者由小到大。请读者领悟,堆在上面定义,已经说明白了,符合那种结构就是堆,可没说是有序的。
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1. 建堆 升序:建大堆
为什么是大堆?不是小堆?
答:因为如果建小堆升序,选完最大的数之后,接下来的次序全乱了,父子关系乱了,必须得重新建堆,然后非常复杂的。所以大堆最合适。
第一个和最后一个位置交换,把最后一个不看作堆里面的,向下调整,选出次大的,依次处理。
选完最大的之后,其余数组下标不变,依旧是堆的结构。
降序:建小堆
和上面同理,利用小堆,
第一个和最后一个位置交换,把最后一个不看作堆里面的,向下调整,选出次小的,依次处理。
选完最小的,剩下依旧是堆的结构。更加方便。循环即可
2. 利用堆删除思想来进行排序 建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
模拟插入的过程,开始向下调整建堆,然后选择排序,依次选数,从后往前排。
为什么选择向下调整?上面已经说过了,向下调整建堆,时间复杂度低。
这才是在建堆的基础上,完成了排序。
倒数第一个非叶子节点(最后一个节点的父亲)
开始向下调整,直到调整到根。n-1是最后一个节点,(n-1-1)/2是父亲。
void HeapSort(int* a, int n)
{
for (int i = (n-1-1)/2; i >=0; --i)
{
AdjustDown(a, n,i); //向下调整建大堆
}
int i = 1; //选数
while (i < n)
{
Swap(&a[0], &a[n - i]);
AdjustDown(a, n - i, 0);
++i;
}
}
展示效果:完全符合要求。。。
关注老爷,小白写东西不易,请老爷小手抬一抬,搞个赞!!!!!!