目录
一、树
1.树的概念及结构
1.1树的概念
1.2树的相关概念
1.3树的表示
2.二叉树的概念及结构
2.1二叉树的概念
2.2特殊的二叉树
2.3二叉树的性质
2.4二叉树的存储结构
3.二叉树的顺序结构及实现
3.1二叉树的顺序结构
3.2堆的概念及结构
二、堆的实现
0.定义堆
1.初始化堆
2.堆的创建
3.堆的删除
4.堆的判空问题
5.堆的大小
6.堆的销毁
三、堆的应用
1.堆排序
2.topK问题
总结
一、树
1.树的概念及结构
1.1树的概念
树是一种非线性的数据结构,它是由N(N>=0)个有限节点组成一个具有层次关系的集合,叫它为树是因为它看起来像一颗倒挂的树,根朝上,叶子朝下
- 树有一个特殊的节点,称为根节点,根节点没有前驱节点
- 除了根节点外,其余节点被分为M个互不相交的集合,其中每一个集合又是一颗结构与树类似的子树,每棵子树的根节点有且只有一个前驱,可以有0或多个后继,因此树是递归定义的。
1.2树的相关概念
- 节点的度:一个节点含有的子树的个数称为该节点的度
- 叶节点或终端节点:度为0的节点称为叶子节点
- 树的度:一个树中,最大的节点的度称为树的度
1.3树的表示
树需要保存值域,也需要保存节点和节点之间的关系,其中树有很多表示方法,如双亲表示法,孩子表示法,双亲孩子表示法以及孩子兄弟表示法,常用孩子兄弟表示法
typedef int DataType;
struct Node
{
struct Node * child;
struct Node * brother;
DataType data;
};
2.二叉树的概念及结构
2.1二叉树的概念
一棵二叉树是节点的一个有限集合,该集合或者为空,或者由一个根节点加上两棵分别称为左子树和右子树的二叉树组成
从上图可以看出,二叉树不存在度大于2的节点;二叉树有左右之分,次序不能颠倒,因此二叉树是有序树
2.2特殊的二叉树
- 满二叉树:一个二叉树,如果每次的节点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为k,且节点总数为2^K-1,则它就是满二叉树
- 完全二叉树:完全二叉树是效率很高的数据结构,对于深度为K的有N个节点的二叉树,当且仅当每个节点都与深度为K的满二叉树从1到N的节点一一对应,就成为完全二叉树
2.3二叉树的性质
- 若规定根节点为第一层,则一棵非空二叉树的第i层上最多有2^(i-1)个节点
- 若规定根节点为第一层,则深度为h的二叉树的最大节点数是2^h-1
- 对于任意一棵二叉树,如果度为0的叶子节点个数为N0,度为2的分支节点为N2,N0 = N2+1
- 若规定根节点的层数为1,具有n个节点的满二叉树的深度,h=log2(n+1)
- 对于一个具有n个节点的完全二叉树,按照从上至下从左至右的数组顺序对所有节点从0开始编号,对于序号为i的节点:
- 若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无右孩子
2.4二叉树的存储结构
二叉树一般可以使用两种存储结构,一种是顺序结构,一种是链式结构
1.顺序存储
顺序结构存储就是用数组来存储,一般数组只适合表示完全二叉树,不是完全二叉树会有空间的浪费,现实中只有堆使用数组来存储,二叉树顺序存储在物理结构上是一个数组,在逻辑上是一颗二叉树。
2.链式存储
二叉树的链式存储是使用链表表示一颗二叉树,即用链来指示元素的逻辑关系,通常是链表中的每个节点由三个域组成,数据域和左右指针域,左右指针分别用来给出左右孩子所在链节点的存储地址,链式结构分为二叉链和三叉链,一般使用二叉链,红黑树等会用到三叉链
typedef int BTDataType;
//二叉链
struct BinaryTreeNode
{
struct BinaryTreeNode * leftchild;
struct BinaryTreeNode * rightchild;
BTDataType data;
};
//三叉链
struct BinaryTreeNode
{
struct BinaryTreeNode *parent;
struct BinaryTreeNode * leftchild;
struct BinaryTreeNode * rightchild;
};
3.二叉树的顺序结构及实现
3.1二叉树的顺序结构
普通的二叉树一般不使用数组存储,因为会造成大量的空间浪费,完全二叉树适合顺序存储。
3.2堆的概念及结构
如果有一个关键码的集合K={k0,k1,k2,k3...kN-1},把它所有的元素按完全二叉树的顺序存储方式存储到一维数组中,并满足:Ki<=K2i+1,且Ki<=K2i+2(或者小于)则称为小堆,或者大堆,将根节点最大的堆叫做最大堆或者大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
堆中某个节点的值总是不大于或不小于父节点的值;
堆总是一颗完全二叉树
二、堆的实现
0.定义堆
堆的逻辑结构是一棵完全二叉树,物理结构为一个数组,要想实现堆的插入删除,需要再定义size和capacity两个值,分别表示当前完全二叉树中存储的有效数据和完全二叉树的容量。
typedef int HPDataType;
typedef struct
{
HPDataType * a;
int size;
int capacity;
}HP;
1.初始化堆
void HeapInit(HP * php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
2.堆的创建
堆的创建需要先插入节点,插入节点后需要保证仍然是一个堆,则需要调整顺序,即根据定义是大堆或小堆进行调整
void Swap(HPDataType * p1, HPDataType * p2)
{
HPDataType * tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustUp(HPDataType * a,int child)
{
int parent = (child-1)/2;
while(child>0) //直到比到根节点结束
{
if(a[child] < a[parent]) //如果要创建小堆 if(a[child] >a[parent])进行调整
{
Swap(&a[child],&a[parent]); //传递的是数组的值,所以类型为HPDataType,传递的是一个地址,所以用一个指针接收,将child指向的值换为parent指向的值,下标没变
child = parent; //向上走,更换下标
parent = (child -1)/2; //重新找到parent节点
}
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 = realloc(php->a,sizeof(HPDataType)*newCapacity);
if(tmp ==NULL)
exit(-1);
php->a = tmp;
php->capacity = newCapacity;
}
//先插入,再调整
php->a[php->size] = x;
php->size++;
//如果默认建大堆,则插入的节点向上调整
//向上调整的时候,调整的是数组的顺序
//以及知道当前数据的下标
AdjustUp(php->a,php->size-1);
}
//形参为定义的堆,要实现堆结构的数组,数组的个数
void HeapCreate(HP * php,HPDataType * a,int n)
{
assert(php);
HeapInit(php);
for(int i = 0;i<n;i++)
{
//依次插入,插入到哪个堆,以及数据
HeapPush(php,a[i]);
}
}
3.堆的删除
同堆的插入问题,删除节点后需要仍然保持堆的有序问题,对于顺序表来说,头删时间复杂度为O(N),尾删时间复杂度为O(1),所以删除堆顶数据时,先和最后一个交换顺序,删除表尾的数据,然后再根据为大堆或小堆,进行调整堆的顺序。
void AdjustDown(HPDataType * a, int n, int parent)
{
int child = 2*parent +1;
while()
{
//此时可能左孩子大,也可能右孩子大,需要和大的交换,上面定义左孩子
//也有可能没有右孩子,如果没有就不++
if(a[child] <a[child+1] && child+1 <n)
{
++child;
}
//向下交换
if(a[parent] <a[child]) //此时,如果要调整为小堆 箭头 >
{
Swap(&a[parent],&a[child]);
parent = child;
child = 2*parent +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);
}
4.堆的判空问题
bool HeapEmpty(HP * php)
{
assert(php);
return php->size ==0;
}
5.堆的大小
int HeapSize(HP * php)
{
assert(php);
return php->size;
}
6.堆的销毁
void HeapDestroy(HP * php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
三、堆的应用
1.堆排序
堆排序即利用堆的思想进行排序,总共分为两个步骤:
- 建堆 升序 建大堆 降序 建小堆
- 利用堆删除思想进行排序
2.topK问题
即求数据中前K个最大的元素或者最小的元素,一般数据量较大。对于topK问题,最简单的就是排序,但是数据量非常大的时候,排序不可取,最佳方式用堆解决,基本思路如下:
前K个最大的元素,建立大堆
前K个最小的元素,建立小堆
HPDataType HeapPop(HP * php)
{
asser(php);
assert(php->size >0);
return php->a[0];
}
void TestTopK()
{
//根据现有的数据建立堆 建立大或小在push中定义
int a[] = {27,15,19,18,28,34,65,49,25,37};
HP hp;
HeapInit(&hp);
for(int i = 0; i<sizeof(a)/sizeof(int);i++)
{
HeapPush(hp,a[i]);
}
//取前5个
int k = 5;
while(k--)
{
printf("%d" ,HeapTop(&hp);
HeapPop(&hp);
}
HeapDestroy(&hp);
}
总结
本文主要介绍了树的概念及性质,堆的概念及增删改查的接口,堆的应用topK问题,技术有限,如有错误请指正。