一、树的概念
- 树的度——这个节点直接链接的子节点数量
- 树的深度——从根节点到最远的叶子节点距离
- 叶子节点——无子节点的节点
- 父节点——节点的直接所被链接的节点为父节点
- 祖先节点——该节点的父节点及父节点的父节点等等
- 兄弟节点——与该节点共有一个父节点的节点为该节点的兄弟节点
二、二叉树的概念与性质
二叉树具有树的性质,但在树的基础上做了限制
- 二叉树——每个节点最多两个子节点
这里还要介绍两个特殊的二叉树
1.完全二叉树-除了倒数第二层以外,其他节点都是满的,最后一层必须是连续的,也就是说不能只存在右子树
2.满二叉树,最后一层均为叶子节点,其他节点都是满的
因为这两个树比较特殊,所以也存在一些规律,比如可以比较方便的计算树的深度,节点数量范围等等
1.深度为n的满二叉树的节点数量为(2^n)-1
2.深度为n的完全二叉树的节点数量范围是2(n-1)到(2n)-1之间
可以根据以上公式到推出通过节点数量计算深度的公式
链式二叉树的结构
typedef int DataType;
struct Node
{
struct Node* firstChild1; // 第一个孩子结点
struct Node* pNextBrother; // 指向其下一个兄弟结点
DataType data; // 结点中的数据域
};
三、堆的概念
堆其实就是使用完全二叉树的存储结构,子结点的值总是不大于或者不小于父节点,叫做大堆或者小堆
如果把一个数组看出一个堆,那么下标位置为N的节点的父节点为(N-1)/2,下标为N的左孩子为N2+1,其右孩子下标为N2+2。
堆的实现
堆的结构
typedef int HPDataType;//数据类型
typedef struct Heap
{
HPDataType* _a;//
int _size;//当前数据个数
int _capacity;//数组容量
}Heap;
堆的初始化
// 构建一个容量大小为n的堆
void HeapCreate(Heap* hp, int n)
{
HPDataType* a = (HPDataType*)malloc(sizeof(HPDataType) * n);
hp->_a = a;
hp->_capacity = n;
hp->_size = 0;
}
在堆中有两个主要的大小,向上/向下调整
图为数字9的向上调整
通过对每个数据进行调整,我们可以吧一个数调整成一个堆
但是向上调整和向下调整要求堆调整的时候调整目标方向的结构也为堆,我们面对一个无序的数组的话,我们可以从第二层开始向上调整或者倒数第二层开始向下调整来避免这个问题。
// 堆的销毁
void HeapDestory(Heap* hp)
{
free(hp->_a);
hp->_a = NULL;
hp->_capacity = hp->_size = 0;
free(hp);
hp = NULL;
}
//交换
void Swap(HPDataType* p, int a, int b)
{
int tmp = p[a];
p[a] = p[b];
p[b] = tmp;
}
//向上调整//小堆
void AdjustUp(HPDataType*p,int child,int n)
{
int parents = (child - 1) / 2;
while (p[child] > p[parents])
{
Swap(p, child, parents);
child = parents;
parents = (child - 1) / 2;
}
}
//向下调整//小堆
void AdjustDown(HPDataType* p, int parents,int n)
{
int child = parents * 2 + 1;
if (child<n && parents * 2 + 2&& p[child] < p[parents * 2 + 2])
{
child = parents * 2 + 2;
}
while(child < n && p[parents] < p[child])
{
Swap(p, parents, child);
parents = child;
child = parents * 2 + 1;
if (child < n && parents * 2 + 2<n && p[child] < p[parents * 2 + 2])
{
child = parents * 2 + 2;
}
}
}
// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
if (hp->_capacity == hp->_size)
{
hp->_a = (HPDataType*)realloc(hp->_a, sizeof(HPDataType) * (hp->_capacity * 2));
hp->_capacity *= 2;
}
hp->_a[hp->_size++] = x;
AdjustUp(hp->_a, hp->_size - 1,hp->_size);
}
// 堆的删除
void HeapPop(Heap* hp)
{
assert(hp);
assert(hp->_size);
int tmp = hp->_a[0];
hp->_a[0] = hp->_a[hp->_size - 1];
hp->_a[hp->_size - 1] = tmp;
AdjustDown(hp->_a, 0,hp->_size-1);
hp->_size--;
}
// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
assert(hp);
assert(hp->_size);
return hp->_a[0];
}
// 堆的数据个数
int HeapSize(Heap* hp)
{
assert(hp);
return hp->_size;
}
// 堆的判空
int HeapEmpty(Heap* hp)
{
assert(hp);
return hp->_size==NULL;
}
对于堆的删除,我们选择删除堆顶的数据,因为堆顶的数据总是最大或者最小的,我们通过交换堆顶与最后一位的数据,再把堆顶交换过去的数据进行向下排序来完成数据的删除。
堆排序
堆的排序不能直接选择排升序就建小堆这种的形式,因为不能保证一个小堆在数组中的顺序的完全升序的
所以我们选择和删除一样的思路,选择反其道而行之,排升序建立大堆
这样我们每次都可以把最大的选出来放到最后
然后再调整为堆,第二次就能选出次大的,依次循环就能完成排序
代码实现
// 对数组进行堆排序
void HeapSort(int* a, int n)
{
//建大堆
for (int i = 1; i < n; i++)
{
AdjustUp(a, i, n);
}
//向下调整
int end = n - 1;
while (end > 0)
{
Swap(a, 0, end);
AdjustDown(a, 0, end);
end--;
}
}