目录
一、树
顾名思义,结构即为树,由一个根节点分出多个节点,这几个节点再继续往下连接其他节点形成一个个子树。不过这棵树是根朝上,叶朝下的。一个根不限制连接多少个节点,把第二层的几个节点也看成根节点,最终形成一整个树结构。形象的图可以搜到,这里就不写了。
来点形式主义
概念
树是一种非线性的数据结构,它是由n(n >= 0)个有限节点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一颗倒挂的数,也就是说它是根朝上,而叶朝下的。
~ 有一个特殊的节点,称为根节点,根节点没有前驱节点。
~ 除根节点外,其余节点被分成M(M > 0)个互不相交的集合T1,T2....TM, 其中每一个集合Ti(1 <= i <= M)又是一棵结构与树类似的子树。每棵子树的根节点有且只有一个前驱,可以有0个或多个后继节点。
~ 因此,树是递归定义的。
接下来要看一下树的一些定义
节点的度:一个节点含有的子树的个数称为该节点的度,如上图:A的为6
叶节点或终端节点:度为0的节点称为叶节点,如上图:B、C、H、I.....等节点为叶节点
非终端节点或分支节点:度不为0的节点;如上图:D、E、F、G....等节点为分支节点
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点。如上图, A是B的节点
孩子节点或者子节点:一个节点含有的子树的根节点称为该节点的子节点。如上图:B是A的子节点
兄弟节点:具有相同父节点的节点互称为兄弟节点。如上图:B、C是兄弟节点
树的度:一棵树中,最大的节点的度称为树的度。如上图:树的度为6
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推。
树的高度或深度:树中节点的最大层次。如上图,树的高度为4
堂兄弟节点:双亲在同一层的节点互为堂兄弟。如上图:H、I互为堂兄弟节点
节点的祖先:从根到该节点所经分支上的所有节点。如上图:A是所有节点的祖先
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
森林:由m(m > 0)棵互不相交的树的集合称为森林
这些概念是树中常用名词,无论是为了做题还是为了工作,都应当记住。
二、二叉树
二叉树的每个节点最多有两个子节点,分为左子树和右子树。二叉树中又有满二叉树和完全二叉树。
满二叉树是每一层的节点数都达到最大。所以如果高度为h的满二叉树,它的节点数应当是2 ^ h - 1。
完全二叉树:完全二叉树是一个效率比较高的结构,由满二叉树变种而来。要求除去最后一层外其他层节点数达到最大,最后一层节点数至少为1,且节点必须连续,比如A为根节点,BC作为第二层节点,B和C下各一个左子树或者右子树就是不连续,就不是完全二叉树。
二叉树的特点
1、若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2 ^ (i - 1)个节点。
2、若规定根节点的层数为1,则深度为h的二叉树的最大节点数是2 ^ h - 1。
3、对任何一棵二叉树,如果度为0,其叶节点个数为n0,度为2的分支节点个数为n2,则有n0 = n2 + 1。(度为0总比度为2的多一个)
4、若规定根节点的层数为1,具有n个节点的满二叉树的深度,h = log2(n + 1)。
具体推算过程就不写了。
三、这篇重点在于堆的实现
二叉树有大堆和小堆之分,大堆就是父节点大于等于子节点,小堆就是父节点小于等于子节点。这篇写大堆。
该用数组还是链表体现二叉树?
假设是链表
插入第一个后,需要用一个指针指向第二个。根节点如果有好几个子节点,那就得需要多个指针,可以在创建结构体时,里面放入多个指针,也可以简单点,建立一个指针数组,大小就是树的度。不过这样或许还是不够好,另有一个方法,创建结构体后,里面放入一个child和brother指针,child指向自己的子节点,brother则指向兄弟节点。比如A为祖先,下有3个子节点,child指向节点B,B的brother指向C,C的brother指向d。BCD三个节点如果有子节点,那就用child指向子节点,这样就方便了。
现在想一下push功能,这是不是相当于尾插功能?所以我们需要第三个指针来指向尾部。插入后新数字需要和之前的数字比较。可是他该如何和其他元素比较?插入的新元素应当是brother或者child指向的数字,想访问指向新数字的节点,这里就得需要一个prev指针指向前面了吧?
所以可以发现一个问题,我没办法随意的访问其他节点元素。push或者pop时就总会有些麻烦,那么现在转向数组。
假设是数组
数组可以随意访问其他元素,只要找到下标之间的规律即可。不过数组又该如何体现二叉树结构?这个并不是难事,我们需要脱离树状结构这个臆想图,真正在电脑里面存储时都是一块块空间,我们只需要使空间里的数字符合规律,可以简单地访问其他元素即可。现在以数组来完成二叉树。
调用整个树之前先创建好空间,所以初始化函数里就不写malloc了。
头文件里
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
HPDataType size;
HPDataType capacity;
}HP;
Heap.c(包括push和pop,向上/下比较法)
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
先放上这两个函数。然后开始push和pop的代码。
现在的情况是已经存入了一些数据,要继续存入。
该怎么体现树状结构?我们确定了要使用数组,数组为空的时候,push一个就是放在下标0位置处,push第二个就需要和数组里的元素进行比较来确定谁当祖先。之后一个个存入,我们都需要比较,放好位置,所以搞清楚这个做法也就完成了push函数的思路。进行比较的时候,应该跟谁比较?怎么去访问?如果是链表,我们可以通过指针,不过数组就需要找规律。先前已经说过,抛掉两个树状结构的臆想图,一个根节点只能访问两个子节点,那么就把两个子节点放在根节点之后,通过下标转换来找到子节点。比如根节点下标为0,两个子节点下标分别是1和2,1这个节点继续分支,占据了3和4下标,2这个节点继续分支,占据5和6下标,这样找到规律即可随机访问。那么我们开始写push和pop函数。
插入之前先判断为不空
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)
{
perror("realloc fail");
exit(-1);
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;
php->size++;
}
先写出来这些代码。现在已经在尾部插入一个数据了,那么开始判断大小吧。兄弟节点没必要比较,如果新数字比父节点大,那么自然也就比兄弟节点大;如果小,那就不需要动位置。之后一步步往上挪,整个过程都不需要跟兄弟节点比较,只跟父节点比较,大于就交换位置,直到祖先节点,如果比根节点大,那么新插入的数据就称为新的祖先。和父节点交换位置,父节点也不需要再跟其他节点进行比较。这是向上比较法。两个比较的对象,一个下标是新插入的位置,另一个则是通过规律来寻找,规律就是(i - 1) / 2,就能找到它的父节点,交换完后继续向上找父节点。
void Swap(HPDataType* s1, HPDataType* s2)
{
HPDataType tmp = *s1;
*s1 = *s2;
*s2 = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
while (child)
{
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)
{
perror("realloc fail");
exit(-1);
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
向上比较函数里,判断条件是child > 0。为什么不能是 >= 0?当最后和祖先节点比较时,如果还是更大,那么child就变成下标0位置了,这时候整个过程应当结束,如果是>= 0,while还会继续,就会越界访问了。
那么数组为空时,这个push函数有没有效?还是可以的,因为child为0,循环进不去,UP函数就会break了。再插入一个数据,进入函数,就进行比较,排好位置,会发现整个结构会按照大堆方向排列。写一个测试代码
void HeapPrint(HP* php)
{
assert(php);
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
void TestHeap1()
{
int arr[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
HP hp;
HeapInit(&hp);
for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
{
HeapPush(&hp, arr[i]);
}
HeapPrint(&hp);
HeapDestroy(&hp);
}
int main()
{
TestHeap1();
return 0;
}
结果就是
所以没问题
接下来看pop函数。
删除根部元素如何删除?尾部元素很好删除,就是尾删功能,下标最后一个删除,而且也没有改变节点之间的关系,因为它是一个子节点,但是根部元素的删除确实有点麻烦。因为这相当于删除祖先,我们就需要再找一个节点当祖先。这里能不能挪动覆盖?把49及之后的数据往前挪一次,这样头部就删除了,但是挪动数据时间复杂度为O(N),,且这样挪动的话,节点之间的关系就乱了,很有可能子节点比父节点数值大,所以不能挪动。那如果是把49覆盖到65上,然后再往后找数值放到49这个位置呢?其实也不好做,画图仔细想想,会发现挪动的每个数据的下标不好找规律。
现在有另一个方法。把最后一个数字和第一个数字调换一下,这两个下标很好找,尾删一下,也就是size--,此时根部位置的数字变成了尾部数字,这时根部位置的数字一定比它现在的子节点的数要小。要改成正确的顺序其实就可以参照push函数的做法,不过这里是向下比较法。换过来后,和第二层的数字比较,找出大的那个,让它来做新的祖先,然后一层层向下探索。
void AdjustDown(HPDataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
//确认child指向大的那个孩子并且child要小于size
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* php)
{
assert(php);
assert(php->size > 0);
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
在之前的测试代码再加上几行
void TestHeap1()
{
int arr[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
HP hp;
HeapInit(&hp);
for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
{
HeapPush(&hp, arr[i]);
}
HeapPrint(&hp);
int k = 5;
while (k--)
{
printf("%d ", HeapTop(&hp));
HeapPop(&hp);
}
HeapDestroy(&hp);
}
难题攻克了,我们把剩下的点一一写完
HPDataType HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
HPDataType HeapSize(HP* php)
{
assert(php);
return php->size;
}
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
最后,还有一个问题,有的时候给的接口里面会有创建堆这个函数,关于这个之后再写。先放上所有的代码
Heap.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
HPDataType size;
HPDataType capacity;
}HP;
void HeapInit(HP* php);
void HeapDestroy(HP* php);
void HeapPrint(HP* php);
void HeapPush(HP* php, HPDataType x);
void HeapPop(HP* php);
HPDataType HeapTop(HP* php);
HPDataType HeapSize(HP* hp);
bool HeapEmpty(HP* hp);
Heap.c
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
void HeapPrint(HP* php)
{
assert(php);
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
void Swap(HPDataType* s1, HPDataType* s2)
{
HPDataType tmp = *s1;
*s1 = *s2;
*s2 = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
while (child)
{
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)
{
perror("realloc fail");
exit(-1);
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
void AdjustDown(HPDataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
//确认child指向大的那个孩子并且child要小于size
if (child + 1 < n && a[child + 1] < a[child])
{
++child;
}
//1、孩子大于父亲,交换,继续向下调整
//2、孩子小于父亲,则调整结束
if (a[child] < a[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);
}
HPDataType HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
HPDataType HeapSize(HP* php)
{
assert(php);
return php->size;
}
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
Test.c
#include "Heap.h"
/*void TestHeap1()
{
int arr[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
HP hp;
HeapInit(&hp);
for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
{
HeapPush(&hp, arr[i]);
}
HeapPrint(&hp);
int k = 5;
while (k--)
{
printf("%d ", HeapTop(&hp));
HeapPop(&hp);
}
HeapDestroy(&hp);
}*/
void TestHeap2()
{
int arr[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
HP hp;
HeapInit(&hp);
for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
{
HeapPush(&hp, arr[i]);
}
HeapPrint(&hp);
while (!HeapEmpty(&hp))
{
printf("%d ", HeapTop(&hp));
HeapPop(&hp);
}
HeapDestroy(&hp);
}
int main()
{
//TestHeap1();
TestHeap2();
return 0;
}
结束。下一篇再补上创建堆的接口。