树是一种非线性的数据结构,它由一个根结点和n(>=0)个子树构成,之所以叫做树,是因为它很像生活中的树倒过来的样子
注意:
结点的度:一个结点拥有孩子的个数就叫该结点的度,比如A的度为6,E的度为0;
叶结点或终端结点:度为0的结点就是叶结点,比如B,H,I,J......就是叶结点
分支结点或非终端结点:度不为0的结点就是分支结点,比如A,C,D......就是分支结点
父结点:若一个结点含有子节点,则称该结点为其子结点的父结点,比如A就是B的父结点
子结点:一个结点其子树的根结点称为该结点的子结点,比如B就是A的子节点
兄弟结点:两个结点的父节点是同一个结点称为这两个结点是兄弟结点,比如I和J就是兄弟结点
树的度:一颗树中所有结点的度中最大值就是树的度,比如上面的树的度是6
结点的层次:从根结点开始定义,根为第一层,往下依次递增,比如I结点所在的层次为3
树的高度或深度:树中结点的最大层次,比如上面的树的高度为3
堂兄弟结点:两个结点的父结点在同一层,并且这两个结点不为兄弟结点,称这两个结点为堂兄弟结点,比如H和I是堂兄弟结点
结点的祖先:从根结点到该结点的路径上所有的结点都叫该结点的祖先,比如I的祖先是A和D
子孙:以某结点为根的子树下,所有的结点都是该节点的子孙,比如所有的结点都是A的子孙
森林:m(>0)颗互不相交的树的集合叫做森林
typedef int TreeDataType;
#define SIZE 5
struct Node
{
TreeDataType val;
struct Node* a[SIZE];
};
这样定义的问题是,实际上每个结点的子节点树是不确定,有可能某些结点的子节点没有SIZE个,就造成了空间的浪费
在使用中,我们更常用名为左孩子右兄弟的结构表示法:
typedef int TreeDataType;
typedef struct Node
{
TreeDataType val;
struct Node* leftChild;
struct Node* rightBrother;
}Node;
度数<=2的树叫做二叉树
规定二叉树的层数从1开始
- 一颗非空二叉树,第i层最多有个结点
- 深度为h的二叉树最多有个结点
- 一颗满二叉树,有N个结点,它的高度是
- 一颗完全二叉树,有N个结点,它的高度范围是
- 对于任何一颗二叉树,如果它度数为0的结点的个数为,度数为2的结点的个数为,则
- 若将一个二叉树从上到下,从左到右按照数组的方式依次编号,则父结点和子结点之间的关系:
1)假设父结点的下标为i,由父结点算子结点:左孩子为2*i+1,右孩子为2*i+2
2)假设子结点下标为j,由子结点算父结点:父结点为(j-1)/2
二叉树的存储结构由两种:
所谓用顺序结构存储,就是用数据存储,但是用数组存储的二叉树最好是满二叉树或完全二叉树,因为这两个二叉树的结点是连续的,正好与数组连续相对应;如果是普通的二叉树用数组存储,数组中间有的位置需要空出来
链式结构存储就是用链表将数据串起来;通常有二叉链,三叉链,二叉链是一个结点中两个指针,一个指针指向左子树,另一个指向右子树;而三叉链在二叉链的基础上又加了一个指向父节点的指针,目前我们只考虑二叉链。
结构定义:
//二叉链
typedef int BTNDataType;
typedef struct BinaryTreeNode
{
BTNDataType val;//值
struct BinaryTreeNode* leftChild;//指向左孩子
struct BinaryTreeNode* rightChild;//指向右孩子
}BinaryTreeNode;
- 堆是一种完全二叉树,分为大堆和小堆
- 由于完全二叉树的特性,堆中的数据适合用数组来进行存储
typedef int HeapDataType;
typedef struct Heap
{
HeapDataType* a;
int size;//有效数据个数
int capacity;//容量
}Heap;
//初始化
void HeapInit(Heap* php);
//销毁
void HeapDestroy(Heap* php);
//入数据
void HeapPush(Heap* php, HeapDataType x);
//出数据
void HeapPop(Heap* php);
//判空
bool HeapEmpty(Heap* php);
//获取堆顶数据
HeapDataType HeapTop(Heap* php);
//获取堆的数据个数
int HeapSize(Heap* php);
//初始化
void HeapInit(Heap* php)
{
assert(php);
php->a = NULL;
php->capacity = php->size = 0;
}
我们以大堆为例,小堆同理
我们把插入的数据往上调整的过程叫做向上调整算法
void Swap(HeapDataType* p1, HeapDataType* p2)
{
HeapDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//向上调整算法
void AdjustUp(HeapDataType* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (parent - 1) / 2;
}
else
{
break;
}
}
}
//入数据
void HeapPush(Heap* php, HeapDataType x)
{
assert(php);
//判断是否需要扩容
if (php->capacity == php->size)
{
int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HeapDataType* tmp = (HeapDataType*)realloc(php->a, sizeof(HeapDataType) * newCapacity);
if (tmp == NULL)
{
perror("HeapPush:realloc fail");
exit(-1);
}
php->capacity = newCapacity;
php->a = tmp;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
很多人会想,出数据不就是size--吗?在之前的顺序表,链表等数据结构中,出数据的确就只是移除数据;但是,到了这个阶段,我们得考虑到,出数据不仅仅是为了出数据了,要进一步想,我做这个操作有什么作用?
在堆中,如果出数据就只是移除堆尾的元素,那出了数据有什么意义呢?
因此,在堆中,我们的出数据是出掉堆顶的数据,那这样做有什么意义呢?根据堆的性质,堆顶的数据是数组中的最大值(最小值),将最大值(最小值)删除,接下来就可以筛选次大值(次小值)了......往下一一筛选,是不是就能降序(升序)我们的数据
那么是不是直接出掉堆顶的数据呢?如果直接出掉堆顶的数据,此时所有结点的父子关系都乱了,且此时的二叉树不一定是一个堆了;因此,我们出数据的操作是,先将堆顶数据和堆尾数交换,再出掉堆尾数据,就相当于出掉堆顶数据,且此时根结点的子树的关系不变
void AdjustDown(HeapDataType* 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[parent] < a[child])
{
Swap(&a[parent], &a[child]);
parent = child;
child = child * 2 + 1;
}
else
{
break;
}
}
}
因此我们的出数据代码就是:
//出数据
void HeapPop(Heap* php)
{
assert(php);
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
//判空
bool HeapEmpty(Heap* php)
{
assert(php);
return php->size == 0;
}
//获取堆顶数据
HeapDataType HeapTop(Heap* php)
{
assert(php);
return php->a[0];
}
//获取堆的数据个数
int HeapSize(Heap* php)
{
assert(php);
return php->size;
}
//销毁
void HeapDestroy(Heap* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->capacity = php->size = 0;
}
想要排序数据:
void TestHeap()
{
int a[] = { 3, 4,7,2,1,8,9,22,73,24 };
Heap hp;
HeapInit(&hp);
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
HeapPush(&hp, a[i]);
}
while (!HeapEmpty(&hp))
{
HeapDataType ret = HeapTop(&hp);
printf("%d ", ret);
HeapPop(&hp);
}
printf("\n");
}
堆的常见应用:
- 堆排序
- TOPK问题
上面我们用堆的插入和删除操作完成了数据的排序,但实际上改变的只是堆里面的数据,对外面的a数组并没有改变,当然了,我们也可以在最后拷贝到原数组;但其实不需要那么麻烦,在下篇博客,我将会讲讲堆真正强大的功能——堆排序
还有一个非常经典的问题,在N个数据中,取出最大的前K个数:
我们常见的思路是,将该数据弄成一个大堆,然后再PopK次
时间复杂度是:,也就是,好像效率也还行
但如果N取非常大,10亿,甚至100亿呢?此时我们的内存是存不下这么多数据的,这些数据只能放在文件中,此时我们的思路是:
可能有人疑惑的是为什么是建小堆,如果是建大堆,由于大堆的性质,堆顶的数是最大值,如果这N个数中的最大值在第一次建堆时进去了,那么后面比较时就没有数据比堆顶数据更大,我们的堆就不能完成更新;只有建小堆,堆顶的数据是这K个数中最小的,那么最大的前K个数一个会将其他数排挤出去
时间复杂度:
具体代码的实现也会在下篇博客详解
需要堆的实现的源码的小伙伴可以去我的Gitee主页获取
Heap/Heap · baiyahua/LeetCode - 码云 - 开源中国 (gitee.com)