树
一.树概念及结构
1.树的概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
- 有一个特殊的结点,称为根结点,根节点没有前驱结点
- 除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
- 因此,树是递归定义的。
注意:树形结构中,子树之间不能有交集,否则就不是树形结构
2.树的相关概念
- 节点的度 :一个节点含有的子树的个数称为该节点的度。上图中,A的度为6
- 叶节点或终端节点:度为0的节点成为叶节点。B C P Q为叶节点
- 非终端节点或分支节点:度不为0的节点。
- 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点。A是B的父节点
- 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点。B是A的子节点
- 兄弟节点:具有相同父节点的节点互称为兄弟节点。D和E是兄弟节点,J和K不死兄弟节点
- 树的度:一棵树中,大的节点的度称为树的度。上图树的度是6
- 节点的层次:从根开始定义起,根为第1层,根的字节点是第二层。
- 树的高度或深度:树中节点的大层次。上图树的高度为4
- 堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:J和k
- 节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
- 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
- 森林:多棵互不相交的树的集合称为森林
3.树的表示
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中常用的孩子兄弟表示法
typedef int Datatype
typedef struct Node
{
Datatype data;
struct Node *child; //左孩子
struct Node *brother;//右兄弟
}
4.树在实际中的运用(表示文件系统的目录树结构)
二. 二叉树概念及结构
1.概念
一棵二叉树是结点的一个有限集合,该集合
(1)或者为空
(2)或由一个根节点加上两棵别称为左子树和右子树的二叉树组成
(3)二叉树不存在度大于2的节点
2.现实中的二叉树
3.特殊的二叉树
(1)满二叉树:一个二叉树,如果每层的节点都达到最大值,则这个二叉树就是满二叉树
(2)完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由完全二叉树引申来的.。满二叉树是完全二叉树的特殊形式
完全二叉树的特点:
a.若设二叉树的深度为h,除第h层外,其它各层(1到h-1层)的结点数都达到最大个数,第h层所有的结点都连续集中在最左边,这样的二叉树称为完全二叉树。
b.对于一颗具有n个节点的二叉树按层序编号,如果编号为i的节点与同样深度的满二叉树中编号为i的节点在二叉树中位置完全相同,则这棵树被称为完全二叉树。
c.叶子结点只能出现在最下层和次下层,最下层的叶子结点集中在树的左部,倒数第二层若存在叶子结点,一定在右部连续位置
4.二叉树的性质
二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树注意:对于任意的二叉树都是由以下几种情况复合而成的
假设数据用数组储存,i表示数组下标:
(1)若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
(2)若 2i+1<n ,左孩子序号为 2i+1 ;若 2i+1 >=n,则无左孩子
(3)若2i+2<n,右孩子序号:2i+2;若 2i+2>=n 则无右孩子
5.二叉树的储存结构
(1)顺序结构:顺序结构储存就是用数组来储存,一般使用数组只适合储存完全二叉树,
因为不是完全二叉树会有空间浪费,而现实中使用中只有堆才会使用数组来存储,关于堆我们后面的章节会专门讲解。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
(2)链式储存:二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面课程学到高阶数据结构如红黑树等会用到三叉链。
typedef int Datatype
//二叉链
struct BinaryTreeNode{
BinaryTreeNode * leght; //指向当前节点的左孩子
BinaryTreeNode * right; //指向当前节点的右孩子
Datatype data; //储存当前节点的数据
}
//三叉链
struct BinaryTreeNode{
BinaryTreeNode * parent; //指向当前节点的父节点
BinaryTreeNode * leght; //指向当前节点的左孩子
BinaryTreeNode * right; //指向当前节点的右孩子
Datatype data; //储存当前节点的数据
}
三. 二叉树顺序结构及实现
1.二叉树的顺序结构
普通的二叉树是不适合用顺序储存的,因为会存在大量的空间浪费。而完全二叉树更使适合用顺序储存。现实中我们通常把堆(一种二叉树)用顺序结构来储存。需要注意的是这里的堆与操作系统的虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
2.堆的概念和结构
如果 有一个整数的集合,把他们按完全二叉树的顺序储存结构储存,arr[ ] = { 1,2 ,3, 4, 5, 6, 7, 8,9 }。我们把根节点最大的堆叫做大堆,根节点最小的堆叫做小堆,这里的arr是小堆。堆都会满足以下关系:
- (1) a[0] = 2, a[0^2+1], a[0^2+2], a[0] 是 a[1] 和 a[2] 的父节点。
2.左孩子用 n*2+1 找父节点,右孩子用 n*2+2 找父节点。 - 可以通过 (n-1)/2 和(n-2)/2来找父节点。—>优化成,(当前下标-1)/2 找父节点。
- 堆中某个节点的值总是不大于父节点的值。
- 堆总是一颗完全的二叉树。
3.堆的实现
推荐使用堆的向上调整算法,把堆向下调整法的逻辑说给大家,这里详讲堆的向上调整法(和堆的插入一起讲)。
(1)堆的向下调整算法:
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
arr[]={ 27, 15, 19, 18, 28, 34, 65, 49, 25, 37}
(2)对的向上调整算法
插入数之前,该二叉树是堆。再插入一个10到数组的尾上,再进行向上调整算法,直到满足堆。
(3)堆的创建
下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。
(4)建堆时间复杂度
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响终结果)
因此,建堆的时间复杂度为 O( N )
(5) 堆的插入
----- 我们把数 5, 3, 6, 8, 2, 7 依次存入堆中-----用小堆( 根节点的值不大于任意的子 节点)
向上调整的步骤如下( 先插入后调整 ,直到满足堆):
- 存入 5,则 arr[0] = 5;存入 3,则 arr[1] = 3
- arr[0] > arr[1] ,父子交换,则 arr[0] = 3,arr[1] = 5
- 存入 6,则 arr[2] = 6,大于arr[0] = 3,符合小堆的规定
- 存入 8,则,arr[3] = 8,大于arr[0] = 3符合小堆的规定
- 存入2,则 arr[4] = 2,不符合小堆的规定,,需要进行父子交换,如下:
- (1)(4-1)/2=1,则 arr[4]的父节点是 arr[1] = 5,父子交换后,arr[1] = 2, arr[4] = 5
- (2)(1-1)/2=0, arr[1]父节点是arr[0] = 3,父子交换后,arr[0] = 2, arr[1] = 3
- 存入7,则 arr[5] = 7,大于arr[2] = 6,符合小堆的规定
- 数组为 int arr[] = { 2, 5, 3, 6, 8, 7, };
代码如下:
//arr是数组指针,n是下标,n=依次是 1, 2, 3, 4, 5
void AdjustUpper(Hp* arr, n)
{
int child = n - 1;//子节点的下标
int parent = (n-1) / 2 ;//根据子节点的下标找父节点的下标
while (child > 0)//循环结束的条件
{
//判断父与子的大小
if (arr[parent] > arr[child])
{
//子父的值交换
Hpdatatype tmp = arr[child];
arr[child] = arr[parent];
arr[parent] = tmp;
//父与子的下标交换
child = parent;
parent = (child - 1) / 2;
}
//上述的child 与 parent不成立,则说明每个数都符合小堆规定,父子换值结束
else
{
break;
}
}
}
(6)堆的删除
堆删除是删除堆顶的数据,将堆顶的数据最后一个数据一换,然后删除数组后一个数据,再进行向下调整算法,保持堆的完整性(这里还是以小堆为例)
步骤如下:
- 找到 arr[0] 的子节点 arr[1] 和 arr[2],并找出最小的值 arr[1]
- 找到 arr[1] 的子节点 arr[3] 和 arr[4],并找出最小的值 arr[4]
- 找到 arr[4] 的子节点 arr[9] 和 arr[10],并找出最小的值 arr[10]
代码演示:
int parent = 0;
//先假设左孩子小
int child = parent * 2 + 1;
while (child < p->size-1)
{
//验证假设
if (child+1<p->size-1 && p->data[child+1] < p->data[child])
{
child++;
}
//父子交换值
if (p->data[parent] > p->data[child])
{
int tmp = p->data[child];
p->data[child] = p->data[parent];
p->data[parent] = tmp;
//子父交换下标
parent = child;
child = parent * 2 + 1;//和上面保持一致,假设左孩子小
}
//不成立则说明子都比父大,堆完成
else
{
break;
}
}
(7)堆应用:a.堆排序
堆排序即利用堆的思想来进行排序,----大堆排升序----小堆排降序
这里用大堆排升序举例,步骤:
- 先把数组变成大堆(这里代码演示的是直接优化好的 变堆 )
- 把 arr[0] 和 arr[n] 交换值,再用向下调整算法
- 把 arr[1] 和 arr[n-1] 交换值,再用向下调整算法
- …
代码演示:
//值交换函数
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int main()
{
int arr[] = { 5, 17, 4, 20, 16, 3 };
//在原数组内用向上调整法->大堆
for (int i = 1; i < sizeof(arr) / sizeof(int); i++)
{
int child = i;
int parent = (i - 1) / 2;
while (child > 0)
{
if (arr[child] > arr[parent])
{
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//排序
for (int j = sizeof(arr) / sizeof(int) - 1; j > 0; j--)
{
//从后往前排,把最大值放数组尾部
Swap(&arr[0], &arr[j]);
//向下调算法
int parent = 0;
int child = 0 * 2 + 1;//假设左孩子大
while (child < j)//左孩子要在未排序的范围内
{
//右孩子要在未排序的范围内
if (child + 1 < j-1 && arr[child] < arr[child+1])
{
//假设不成立就换成右孩子
child++;
}
//父子交换值
if (arr[child] > arr[parent])
{
Swap(&arr[child], &arr[parent]);
//父子换下标
parent = child;
child = parent*2 + 1;//继续假设左孩子大
}
else
{
break;
}
}
}
return 0;
}
(8)堆应用:b.Top-k问题
TOP-K问题:即求数据结合中前K个大的元素或者小的元素,一般情况下数据量都比较大
比如:专业前10,世界500强…
对于吧Top-K问题,能想到的简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了( 可能数据都不能一下子全部加载到内存中 ),最佳的方式就是用堆来解决,基本
思路如下( 升序:建大堆; 降序:建小堆):
- 用数据集合中前K个元素来建堆前k个 大的元素;建小堆,则前k个小的元素;建大堆则前k个大的元素
- 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
- 将剩余 K-N 元素依次与堆顶比完之后,堆中剩余的K个元素就是所求的前K个最小或最大元素
四.二叉树链式结构及实现
待更新