目录
前言
一、树
1、树的概念与结构
2、树的相关术语
3、二叉树
4、满二叉树
5、完全二叉树
6、二叉树的存储
1、顺序结构
2、链式结构
二、堆
1、堆的结构
2、堆的初始化
3、入堆(大根堆)
4、出堆(大根堆)
5、判断堆是否为空
6、取堆顶元素
7、获取堆中元素个数
三、整体代码
前言
树型结构是实现堆的基础,因此前面此篇会先介绍树,然后基于树型结构实现顺序结构堆。因为此篇完全基于自己的理解编写,所以如果出现一些错误还望大家指出,谢谢!
一、树
1、树的概念与结构
- 树中有且只有一个特殊的节点,称为根节点,根节点没有前驱节点
- 除根结点外,其余结点被分成 M(M>0) 个互不相交的集合,其中每⼀个集合⼜是⼀棵结构与树类似的⼦树。每棵⼦树的根结点有且只有⼀个前驱,可以有 0 个或多个后继节点
【注意】
- ⼦树是不相交的(如果存在相交就是图了,图以后得课程会有讲解)
- 除了根结点外,每个结点有且仅有⼀个⽗结点
- ⼀棵N个结点的树有N-1条边
2、树的相关术语
- ⽗结点/双亲结点:若⼀个结点含有⼦结点,则这个结点称为其⼦结点的⽗结点; 如上图:1 是 2 的⽗结点
- ⼦结点/孩⼦结点:⼀个结点含有的⼦树的根结点称为该结点的⼦结点; 如上图: 2 是1 的孩⼦结点
- 结点的度:⼀个结点有⼏个孩⼦,他的度就是多少;⽐如 1 的度为3,4 的度为2,9 的度为0
- 树的度:⼀棵树中,最⼤的结点的度称为树的度; 如上图:树的度为 3
- 叶⼦结点/终端结点:度为 0 的结点称为叶结点; 如上图: 5、6、7、8... 等结点为叶结点
- 分⽀结点/⾮终端结点:度不为 0 的结点; 如上图: 2、3、4... 等结点为分⽀结点
- 兄弟结点:具有相同⽗结点的结点互称为兄弟结点(亲兄弟); 如上图: 2、3 是兄弟结点
- 结点的层次:从根开始定义起,根为第 1 层,根的⼦结点为第 2 层,以此类推;
- 树的⾼度或深度:树中结点的最⼤层次; 如上图:树的⾼度为 3
- 结点的祖先:从根到该结点所经分⽀上的所有结点;如上图: 1 是所有结点的祖先
- 路径:⼀条从树中任意节点出发,沿⽗节点-⼦节点连接,达到任意节点的序列;⽐如1到 8 的路径为: 1-4-8;
- ⼦孙:以某结点为根的⼦树中任⼀结点都称为该结点的⼦孙。如上图:所有结点都是1的⼦孙
- 森林:由 m(m>0) 棵互不相交的树的集合称为森林
3、二叉树
二叉树是树型结构中一种特殊树,二叉树由⼀个根结点加上两棵别称为左⼦树和右⼦树的⼆叉树组成或者为空
二叉树的特点
- ⼆叉树不存在度⼤于 2 的结点
- ⼆叉树的⼦树有左右之分,次序不能颠倒,因此⼆叉树是有序树
4、满二叉树
满二叉树是一种特殊的二叉树,满二叉树的每一层节点都到达最大个数,也就是⼆叉树的层数为 K ,且结点总数 ,则它就是满⼆叉树
5、完全二叉树
完全二叉树是由满二叉树引导出来的,也就是满二叉树是一种特殊的完全二叉树。完全二叉树除最后一层外的每一层都必须是满节点数,并且最后一层的节点必须要是从左至右依次放置
完全二叉树的性质
- 若规定根结点的层数为 k ,则⼀棵⾮空⼆叉树的第 i 层上最多有 个结点
- 若规定根结点的层数为 k ,则深度为 h 的⼆叉树的最⼤结点数是
- 若规定根结点的层数为 k ,具有 n 个结点的满⼆叉树的深度 h = log2 (n + 1)
6、二叉树的存储
⼆叉树⼀般可以使⽤两种结构存储,⼀种顺序结构,⼀种链式结构
1、顺序结构
2、链式结构
⼆叉树的链式存储结构是指,⽤链表来表⽰⼀棵⼆叉树,即⽤链来指⽰元素的逻辑关系
//链式结构节点
struct node
{
int val;//储存数据
struct node* left;//左子树
struct node* right;//右子树
};
二、堆
堆的特性
- 堆中某个结点的值总是不⼤于或不⼩于其⽗结点的值
- 堆总是⼀棵完全⼆叉树
因为堆总是一颗完全二叉树,因此最好使用顺序结构来实现堆,完全⼆叉树更适合使⽤顺序结构存储
顺序结构二叉树性质
- 若 i>0 , i 位置结点的双亲序号: (i-1)/2 ; i=0 , i 为根结点编号,⽆双亲结点
- 若 2i+1<n ,左孩⼦序号: 2i+1 , 2i+1>=n 否则⽆左孩⼦
- 若 2i+2<n ,右孩⼦序号: 2i+2 , 2i+2>=n 否则⽆右孩⼦
1、堆的结构
//堆的结构
struct Heap
{
int* heap;//堆
int size;//堆的有效元素个数
int capacity;//堆的容量
};
2、堆的初始化
//堆的初始化
void HeapInit(struct Heap* heap)
{
//初始化堆的大小为4,后续不够在扩容
heap->capacity = 4;
heap->size = 0;
//创建堆数组
int* tmp = (int*)malloc(sizeof(int) * heap->capacity);
//是否创建失败
if (tmp == NULL)
{
assert(tmp);
}
heap->heap = tmp;
}
3、入堆(大根堆)
时间复杂度:O( logn )
堆的插是将新数据插⼊到数组的尾上,再进⾏向上调整算法,直到满⾜堆
向上调整算法最多调整树的深度次,具有 n 个结点的满⼆叉树的深度 h = log2 (n + 1),因此时间复杂度为O( logn )
向上调整算法
- 先将元素插⼊到堆的末尾,即最后⼀个孩⼦之后
- 插⼊之后如果堆的性质遭到破坏,将新插⼊结点顺着其双亲往上调整到合适位置即可
向上调整算法 (大根堆)
//向上调整算法(大根堆)
void AdjustUp(int* arr, int pos)
{
//当前调整的位置不能是堆顶
if (pos == 0)
{
return;
}
//寻找双亲节点
int parents = (pos - 1) / 2;
//当前位置与双亲节点进行比较
//如果当前位置的数大于双亲节点,就进行交换,并且继续向上调整
//如果当前位置的数小于双亲节点,表示堆已经构建好了
if (arr[parents] < arr[pos])
{
//交换两个数位置
sweap(&arr[parents], &arr[pos]);
//继续向上调整
AdjustUp(arr, parents);
}
}
入堆(大根堆)
//入堆(大根堆)
void HeapPush(struct Heap* heap, int x)
{
//判断堆是否已经满了
if (heap->size == heap->capacity)
{
//扩容至当前容量的两倍
heap->capacity *= 2;
//创建堆数组
int* tmp = (int*)realloc(heap->heap, sizeof(int) * heap->capacity);
//是否创建失败
if (tmp == NULL)
{
assert(tmp);
}
heap->heap = tmp;
}
//入堆
heap->heap[heap->size] = x;
//向上调整算法,保持堆中大根堆的特性
AdjustUp(heap->heap, heap->size);
//堆中有效元素个数加一
heap->size++;
}
4、出堆(大根堆)
时间复杂度:O( logn )
删除堆是删除堆顶的数据, 删除堆顶的数据,将堆顶的数据根最后⼀个数据⼀换,然后删除数组最后⼀个数据,再进⾏向下调整算法,直到满⾜堆的结构
向下调整算法最多调整树的深度次,具有 n 个结点的满⼆叉树的深度 h = log2 (n + 1),因此时间复杂度为O( logn )
向下调整算法
- 左右⼦树必须是⼀个堆,才能调整
- 将堆顶元素与堆中最后⼀个元素进⾏交换
- 删除堆中最后⼀个元素
- 将堆顶元素向下调整到满⾜堆特性为⽌
向下调整算法 (大根堆)
//向下调整算法(大根堆)
void AdjustDown(int* arr, int size, int pos)
{
//左孩子位置
int child = pos * 2 + 1;
//向下调整算法,直到左孩子位置大于数组个数
if (child < size)
{
//选出左右孩子中最大的那个孩子
if (child + 1 < size && arr[child] < arr[child + 1])
{
child++;
}
//与当前位置进行比较
//如果左右孩子中最大数大于当前位置的数,就进行交换,并且继续向下调整
//如果左右孩子中最大数小于当前位置的数,表示堆已经调整好了
if (arr[child] > arr[pos])
{
//交换两个数的位置
sweap(&arr[pos], &arr[child]);
//继续向下调整
AdjustDown(arr, size, child);
}
}
}
出堆(大根堆)
//出堆——删除堆顶数据(大根堆)
void HeapPop(struct Heap* heap)
{
//堆不能为空
if (HeapEmpty(heap))
{
assert("Heap is NULL!");
}
//将堆顶元素与数组最后一个元素进行交换
sweap(&(heap->heap[0]), &(heap->heap[heap->size - 1]));
//堆中有效元素个数减一
heap->size--;
//向下调整算法,保持堆中大根堆的特性
AdjustDown(heap->heap, heap->size, 0);
}
5、判断堆是否为空
时间复杂度:O( 1 )
直接判断堆中元素个数是否为0, 因此时间复杂度为O( 1 )
//判断堆中是否为空
bool HeapEmpty(struct Heap* heap)
{
//判断堆中是否为空
if (heap->size)
{
return false;
}
return true;
}
6、取堆顶元素
时间复杂度:O( 1 )
堆顶就是数组下标为0的位置,直接返回即可, 因此时间复杂度为O( 1 )
//获取堆顶元素
int HeapTop(struct Heap* heap)
{
//堆不能为空
if (HeapEmpty(heap))
{
assert("Heap is NULL!");
}
//获取堆顶元素
return heap->heap[0];
}
7、获取堆中元素个数
时间复杂度:O( 1 )
//堆中有效元素个数
int HeapSize(struct Heap* heap)
{
//堆中有效元素个数
return heap->size;
}
三、整体代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//交换两个数的位置
void sweap(int* num1, int* num2)
{
int tmp = *num1;
*num1 = *num2;
*num2 = tmp;
}
//向下调整算法(大根堆)
void AdjustDown(int* arr, int size, int pos)
{
//左孩子位置
int child = pos * 2 + 1;
//向下调整算法,直到左孩子位置大于数组个数
if (child < size)
{
//选出左右孩子中最大的那个孩子
if (child + 1 < size && arr[child] < arr[child + 1])
{
child++;
}
//与当前位置进行比较
//如果左右孩子中最大数大于当前位置的数,就进行交换,并且继续向下调整
//如果左右孩子中最大数小于当前位置的数,表示堆已经调整好了
if (arr[child] > arr[pos])
{
//交换两个数的位置
sweap(&arr[pos], &arr[child]);
//继续向下调整
AdjustDown(arr, size, child);
}
}
}
//向上调整算法(大根堆)
void AdjustUp(int* arr, int pos)
{
//当前调整的位置不能是堆顶
if (pos == 0)
{
return;
}
//寻找双亲节点
int parents = (pos - 1) / 2;
//当前位置与双亲节点进行比较
//如果当前位置的数大于双亲节点,就进行交换,并且继续向上调整
//如果当前位置的数小于双亲节点,表示堆已经构建好了
if (arr[parents] < arr[pos])
{
//交换两个数位置
sweap(&arr[parents], &arr[pos]);
//继续向上调整
AdjustUp(arr, parents);
}
}
//堆的结构
struct Heap
{
int* heap;//堆
int size;//堆的有效元素个数
int capacity;//堆的容量
};
//堆的初始化
void HeapInit(struct Heap* heap)
{
//初始化堆的大小为4,后续不够在扩容
heap->capacity = 4;
heap->size = 0;
//创建堆数组
int* tmp = (int*)malloc(sizeof(int) * heap->capacity);
//是否创建失败
if (tmp == NULL)
{
assert(tmp);
}
heap->heap = tmp;
}
//入堆(大根堆)
void HeapPush(struct Heap* heap, int x)
{
//判断堆是否已经满了
if (heap->size == heap->capacity)
{
//扩容至当前容量的两倍
heap->capacity *= 2;
//创建堆数组
int* tmp = (int*)realloc(heap->heap, sizeof(int) * heap->capacity);
//是否创建失败
if (tmp == NULL)
{
assert(tmp);
}
heap->heap = tmp;
}
//入堆
heap->heap[heap->size] = x;
//向上调整算法,保持堆中大根堆的特性
AdjustUp(heap->heap, heap->size);
//堆中有效元素个数加一
heap->size++;
}
//判断堆中是否为空
bool HeapEmpty(struct Heap* heap)
{
//判断堆中是否为空
if (heap->size)
{
return false;
}
return true;
}
//堆中有效元素个数
int HeapSize(struct Heap* heap)
{
//堆中有效元素个数
return heap->size;
}
//出堆——删除堆顶数据(大根堆)
void HeapPop(struct Heap* heap)
{
//堆不能为空
if (HeapEmpty(heap))
{
assert("Heap is NULL!");
}
//将堆顶元素与数组最后一个元素进行交换
sweap(&(heap->heap[0]), &(heap->heap[heap->size - 1]));
//堆中有效元素个数减一
heap->size--;
//向下调整算法,保持堆中大根堆的特性
AdjustDown(heap->heap, heap->size, 0);
}
//获取堆顶元素
int HeapTop(struct Heap* heap)
{
//堆不能为空
if (HeapEmpty(heap))
{
assert("Heap is NULL!");
}
//获取堆顶元素
return heap->heap[0];
}
int main()
{
struct Heap heap;
HeapInit(&heap);
HeapPush(&heap, 1);
HeapPush(&heap, 2);
HeapPush(&heap, 3);
HeapPush(&heap, 4);
HeapPush(&heap, 5);
HeapPush(&heap, 6);
HeapPush(&heap, 7);
HeapPop(&heap);
while (!HeapEmpty(&heap))
{
printf("%d ", HeapTop(&heap));
HeapPop(&heap);
}
int i = 0;
return 0;
}