树和二叉树
树的概念和结构
什么是树
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树的原因是它看起来像一颗倒挂的树,根朝上,叶朝下。
注意:在树形结构中,子树之间不能有交集,否则就就不是树形结构
与树有关的概念
- 节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的子树位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)棵互不相交的树的集合称为森林;
树的表示方法
树结构要储存相对比较麻烦,既然保存值域,也要保存结点和结点之间的关系,树的表示法有:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等,其中,最常用的是孩子兄弟表示法
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 第一个孩子结点
struct Node* _pNextBrother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};
树在实际中的应用
用来表示系统中的目录
在平时,树的结构用的并不多,下面着重介绍二叉树相关知识
二叉树的概念和结构
概念
一棵二叉树的结点是一个有限的集合,该集合或者为空,或者由一根节点加上两棵分别成为左子树和右子树的二叉树构成
- 二叉树不存在度大于2的结点
- 二叉树的子树有左右之分,次序不可以颠倒,因此二叉树是有序二叉树
特殊二叉树
满二叉树
一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是
说,如果一个二叉树的层数为K,且结点总数是 2k-1,则他就是满二叉树
结点总数推导:
完全二叉树
对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
通俗来讲,完全二叉树前k-1行都是满的,但是最后一行不一定满,但必须从左到右连续,不然就不满足完全二叉树的条件;
如图为非完全二叉树
完全二叉树节点个数没有确定值,只是一个范围;最多的情况是满二叉树情况,为2k-1,最少情况是第k行即最后一行最少为1个,数量为2k-1-1+1=2k-1个
二叉树的性质
- 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2i-1 个结点.
- 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是 2h-1.
- 对任何一棵二叉树, 如果度为0其叶结点个数为n0 , 度为2的分支结点个数为n2 ,则有n0 =n2 +1
举个栗子:对于此二叉树
度为0的节点为H , I ,E , F , G , n0 = 5 , 度为2的节点为A , B , C , D , 则 n0 = n2 + 1
- 若规定根节点的层数为1,具有n个结点的满二叉树的深度h=log2(n+1)
总结点个数:2h-1 = n ,则推出 h = log2(n+1)
二叉树的储存结构
二叉树的存储结构一般有两种:顺序结构和链式结构
顺序结构
现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
用下标表示一个树中父子关系的公式:
leftchild = parent * 2 + 1
rightchild = parent * 2 + 2
parent = ( child - 1 ) / 2
堆的概念及结构
如果有一个关键码的集合K = { k0,k1 ,k2 ,…,kn-1}把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki<=K2i+1,Ki<=K2i+2则称为小堆(或大堆)。小堆是所有的父亲结点都小于等于孩子节点,大堆是所有的父亲结点都大于等于孩子结点
堆的复杂度
堆的实现
堆的向下调整算法
向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
实现方法:从根开始往下调整,选出左孩子和右孩子中较小的和父亲比较,若比父亲小,则与父亲交换,以小的孩子的位置继续往下调;如果小的孩子比父亲大,则停止
举一个例子:现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。
int array[] = {27,15,19,18,28,34,65,49,25,37};
堆向下调整算法图解:
代码实现:
void Swap(int* px, int* py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1;//默认左孩子
while (child < n) //
{
//选出左右孩子中小/大的那个
if (child+1 < n && a[child +1] > a[child])//大堆的话换成 > child+1<n 判断右孩子是否越界
{
++child;
}
//1. 如果小孩子比父亲小 ,则交换,继续向下调整
//2. 大 结束调整
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;//交换完,孩子的值给父亲,然后孩子重新算
child = parent * 2 + 1;
}
else
{
break;
}
}
堆的创建
当所给的数据不满足左右子树都是大堆/小堆时,就不能用向下调整算法,这时,我们可以从左后一棵子树开始调整,也就是从倒数第一个非叶子结点向上调整
举个例子:
int array[] = {27,37,28,18,19,34,65,25,49,15};
我们进行调整时,从最后一棵树的父亲节点开始,使用堆向上调整算法。最后一个父节点在数组中的位置为 ( n -1 -1 ) / 2,其中,n - 1 为最后一个叶子结点.
void HeapSort(int* a, int n)
{
for (int i = (n-1 -1) / 2; i >= 0; --i)//从最后一个结点父亲开始算
{
AdjustDown(a, n, i);
}
}
这样我们就可以将这些数据排列成大堆或者小堆,进而方便堆数据进行排序
堆的初始化
void HeapInit(HP* php, HPDateType* a, int n)
{
assert(php);
php->a = (HPDateType*)malloc(sizeof(HPDateType)*n);
if (php->a == NULL)
{
printf("malloc fail\n");
exit(-1);
}
memcpy(php->a, a, sizeof(HPDateType)*n);
for (int i = (n - 2) / 2; i >= 0; --i)
{
AdjustDown(php->a, n, i);
}
php->size = n;
php->capacity = n;
}
堆的销毁
void HeapDestory(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
堆的向上调整算法
void AdjustUp(int* a, int child)
{
int parent = (child- 1 )/ 2;
while (child > 0)
{
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
堆的插入
堆的插入是插入到堆的末尾,然后再进行堆的向上调整算法
void HeapPush(HP* php, HPDateType x)//插入x,保持继续是堆
{
assert(php);
if (php->size == php->capacity)//判断是否需要扩容
{
HPDateType* tmp = (HPDateType*)realloc(php->a, php->capacity * 2*sizeof(HPDateType));
if (php->a == NULL)
{
printf("realloc fail\n");
exit(-1);
}
php->capacity *= 2;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
堆的删除
堆的删除时将堆顶的数据删除,操作是将堆顶的数据和堆尾的数据交换,然后
--size
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
Swap(&php->a[0], &php->a[php->size - 1]);
--php->size;
AdjustDown(php->a, php->size, 0);
}
堆的应用
堆排序
- 升序
升序要建大堆
如果排升序建小堆的话,那么最小的数在堆顶,然后要选出第二小的数,但是原先的堆的数据结构已经乱了,就要重新建堆,而建堆的时间复杂度为O(N),这样每次建堆的代价非常大,而且上一次建的堆一点用也没有,所以排升序建小堆的意义不大。
建大堆时,最大的数在堆顶,将堆顶数据和堆尾数据交换,然后用堆的向下调整算法,这时不把堆尾的数算在堆里,这样我们就选出的第二大的数,以此类推堆向下调整算法的时间复杂度为O(logN),算上最开始建堆,时间复杂度为O(N*logN),远小于建小堆的复杂度。
2. 降序
相反,降序就要建小堆,不再赘述.
topK问题
求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大.
解决方法:
用数据集合中前K个元素来建堆(大堆或小堆),然后用剩下N-K个元素来与堆顶的元素进行比较,如果比堆顶元素大(小),则替换堆顶元素,当所有数据都比较完之后,剩下的K个元素就是最大或最小的十个元素。