一、树
1.1、概念
树是一种非线性的结构(比线性结构更为复杂),它是由n(n>0)个有限结点组成的一个具有层次关系的集合。与现实世界中的参天大树相似,但在数据结构中,树结构类似是一棵倒挂的树,根朝上,叶朝下。
根据上图,分析以下概念:
其中有一个特殊的结点——根结点,无前驱结点。如:A结点
除根结点外,其余结点被分成M(上图M=3)个互不相交的集合(如果相交,此结构变为更为复杂的图结构),其中每个集合又是类似树结构的子树,每颗子树的根结点只有一个前驱,可以有0个或多个后驱。 如:B、C、D为三个集合,其中包含除根节点A之外的所有结点,且互不相交。
除根节点之外,每个节点有且只有一个父节点。
一颗树若有N个结点,则该树有N - 1条边。(除根结点无双亲,其余结点均有一个父节点,则有N - 1条边)
1.2、树的相关概念
根据下图对概念进行简要的解释:
- 节点的度:一个节点含有的子树个数称为节点的度。如:A的度为6、B的度为0
- 树的度:一棵树中,最大节点的度称为树的度。如:树的度为6
- 叶节点或终端节点:度为0的节点称为叶节点。如:B、C、H、I、P、Q......
- 非终端节点或分支节点:除根节点之外度不为0的节点。如:D、E、F、G......
- 双亲节点或父节点:一个节点含有子树,该节点称为子树的父节点。如:A是B的父节点
- 孩子节点或子节点:一个节点含有的根被称为根节点的子节点。如:B是A的子节点
- 兄弟节点:具有相同父节点的节点互称为兄弟节点。如:B、C、D...互被称为兄弟节点
- 堂兄弟节点:父节点在同一层节点互为堂兄弟节点。如:H、I、K、N互为堂兄弟节点
- 节点的层次:从根开始定义,根一般为第一层,根的子节点为第二层,以此类推。
- 树的高度或深度:树中节点的最大层次。如:树的深度为4
- 节点的祖先:从根到该节点所经分支上的所有节点。如:A是所有节点的祖先
- 子孙:以根节点的子树任意节点都成称为该节点的子孙。如:所有节点是A节点的子孙
- 森林:由M棵互不相交的树的集合称为森林;
1.3、树的表示
相对于线性表结构,树结构的存储更为复杂,既需要保留值域,也需要保留节点与节点之间的关系。实际上有多种存储关系:双亲表示法、孩子表示法、孩子双亲表示法、孩子兄弟表示法等。其中我们用树结构(与下面的二叉树区别而开)最常见的孩子兄弟表示法
简要说明:孩子兄弟表示法是从根节点指向第一个孩子和其兄弟之间指向即可,直至指向NULL
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 第一个孩子结点
struct Node* _pNextBrother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};
二、二叉树
2.1、概念
在树结构的基础上,由一个根节点加上俩棵称为左子树、右子树,或者为空树,组成的二叉树结构。
注意:
- 二叉树不存在度大于2的节点;
- 二叉树有左右子树之分,次序不能颠倒,因此二叉树是有序树结构
对于任意二叉树,都是由下列几种情况复合而成的: 一定与线性表区别而开
2.2、特殊的二叉树
注:满二叉树是特殊的完全二叉树,而完全二叉树不一定是满二叉树
(1)、满二叉树:
在一个二叉树中,如果每一层的节点数都达到了最大值,则这个二叉树就是满二叉树。如果一个满二叉树层数为k,且节点总数是2^k - 1,则就是满二叉树。
(2)、完全二叉树:
完全二叉树是一个效率很高的数据结构。对于深度为k、有n个节点的二叉树中,当且仅当每一个节点都与深度为k的满二叉树中编号从1至n的节点一一对应是称为完全二叉树。通俗易懂的解释:深度为k的二叉树中,在第k层次的节点从左向右依次排序,节点之中不能有间隔,且除第k层次之外的层次均为满二叉树
2.3、二叉树的性质
1、若规定根节点的层数为1,则一棵非空二叉树的第k层上最多有2^(k - 1)个结点 (满二叉树第k层)
2、若规定根节点的层数为1,则深度为k的二叉树的最大结点数是2^k - 1 (满二叉树)
3、对任何一棵二叉树,若度为0其叶结点个数为n0,度为2的分支结点个数为n2,有:n0 = n2 + 1
任意二叉树有N个结点;
度为2结点个数n2、度为1结点个数n1、度为0结点个数n0,则有等式1:N = n0 + n1 + n2;
有N个结点,则二叉树有N - 1条边,则有等式2:N - 1 = n1 + 2*n2;
等式1与等式2相结合,得:n0 = n2 + 1
4、若规定根节点的层数为1,具有n个结点的完全二叉树的深度k= log2(n + 1)(最大结点数的反函数),如果是小数则向上取整
5、对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对与序号为k的结点有:(这样的好处:可以通过连续空间数组存储完全二叉树结构)
若k > 0,则 k 位置结点的双亲序号:(k - 1)/2;k = 0,k 为根节点无双亲
若2*k + 1 < n,左孩子序号:2*k + 1;2*k + 1 >=n无左孩子
若2*k + 2 < n,右孩子序号:2*k + 2;2*k + 2 >=n无右孩子
6、若完全二叉树结点总数为偶数:度为1的结点只有一个;若为奇数:无度为1的结点
根据上述性质,请大家解决下列题:
1. 某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为()性质3
A 不存在这样的二叉树
B 200
C 198
D 199
2.下列数据结构中,不适合采用顺序存储结构的是( )性质5
A 非完全二叉树
B 堆(完全二叉树)
C 队列
D 栈
3.在具有 2n 个结点的完全二叉树中,叶子结点个数为( )性质3、6
A n
B n+1
C n-1
D n/2
4.一棵完全二叉树的节点数位为531个,那么这棵树的高度为( )性质4
A 11
B 10
C 8
D 12
5.一个具有767个节点的完全二叉树,其叶子节点个数为()性质3、6
A 383
B 384
C 385
D 386
答案:
1.B
2.A
3.A
4.B
5.B
2.4、二叉树的存储结构
1、顺序结构存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费 (如下图非完全二叉树顺序存储)。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面的会专门讲解。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
2、链式结构存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是二叉树结构的孩子表示法,链表中每个结点由三个域组成:数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链;而三叉链是在结点上多加上了双亲域的存储方法,即孩子双亲表示法,在以后的红黑树中会遇到。
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
};
// 三叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pParent; // 指向当前节点的双亲
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
};
三、二叉树的顺序结构及实现
上述已经介绍了顺序结构的概念,是运用完全二叉树的性质构建的物理是连续结构数组,逻辑是一颗二叉树。
3.1、堆的概念及结构
前提区分:这里的堆是属于数据结构,与malloc、realloc在堆空间上申请的物理空间不一样
在一个关键码的集合k = {k0,k1,k2……kn-1},把所有元素按照完全二叉树的顺序存储方式存储在一个一维数组中,并满足每个父节点均小于等于孩子结点(或每个父节点均大于等于孩子结点)
则称该结构为小堆(大堆)。堆中,根节点被称为堆顶,堆顶最大称为大根堆,堆顶最小称为小根堆。
堆的性质:
- 堆中某节点的值总是小于或大于其父节点的值
- 堆是一颗完全二叉树,但完全二叉树不一定是堆 (需要有序排列才可成堆)
- 堆中每一条路径一定是升序或者降序
3.2、堆的实现
与之前的线性结构均不相同,堆中需要通过从堆顶向下按照堆的规则排序,而不是一味的通过升序或降序初始化堆。因此顺序结构的堆中,最重要的就是向下调整算法(AdjustDown):该算法就是从堆中任意元素与其子节点按照小堆或大堆的性质进行比较,但子节点之间也需比较,再进行交换,直至所有节点均比较完成。在代码中体现是将给定数组创建为堆结构,从最后一个父节点开始,对堆进行初始化。
typedef int DataType;
typedef int (*HPCPY)(DataType left, DataType right);
// 此时少了一种自定义的小堆或大堆方法,则可以使用回调函数解决
typedef struct Heap {
DataType* array;// 顺序表申请堆
int capacity;// 顺序结构堆的容量
int size;
HPCPY pCompare;// 传入大堆或小堆放法
}Heap;
堆的操作:初始化、插入删除元素、获取堆顶元素、堆中元素个数、判空、销毁
注:接口代码中,运用堆结构中创建函数指针变量,运用回调函数进行大小堆的创建。
堆实现接口: gitee: Heap.h / Heap.c
数据结构 DS/2022-03-12 二叉树 · vipover/学习基本代码 - 码云 - 开源中国 (gitee.com)
3.3、堆的应用
(1)、运用堆结构排序
通过建立堆结构对数据进行排序:
1、建堆: 升序:建大堆; 降序:建小堆
2、利用堆删除思想进行排序: 堆创建完毕后,最后一个元素与堆顶交换,再通过传入除最后一个元素之前的元素个数进行向下调整排序(因为堆顶元素一定是满足最大或最小的元素),直至所有元素经过排序即可。
排序接口: gitee: Sort.h / Sort.c
学习基本代码: 自我学习的代码上传备份 - Gitee.com
// 利用堆思想进行排序
void HeapSort(int array[], int size)
{
// 1.建立堆——>大小堆?
for (int root = (size - 2) / 2; root >= 0; root--)
HeapAdjust(array, size, root);
// 2.利用堆删除思想排序
int end = size - 1;
while (end) {
Swap(&array[0], &array[end]);
// ####传入end是经过交换,end位置是堆顶最小元素,则不需要在进行排序####
HeapAdjust(array, end, 0);
end--;
}
}
(2)、Top-K问题
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大
1. 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素3. 用堆排序思想完成Top-K问题
排序接口: gitee: TopK.h / TopK.c
学习基本代码: 自我学习的代码上传备份 - Gitee.com
四、二叉树的链式结构及实现
二叉树中,堆结构只适用于完全二叉树,但不一定所有二叉树均为完全二叉树。
概念一定要切记,因为下来的所有步骤均是围绕概念进行的递归操作
根据二叉树的概念,二叉树是由:
1、空树;
2、根节点 + 左子树 + 右子树;
但左子树和右子树依然是一颗二叉树,则在二叉树中,本质上是一个递归的过程
学习链式二叉树的初步就是学习二叉树的遍历规则,接下来给大家依次介绍链式二叉树的知识要点
4.1 二叉树遍历
前序:在二叉树遍历中,我们对同一颗二叉树可能有不同的遍历方式,如下图二叉树
对于不同的遍历结果,可以为:1 2 3 4 5 6、1 2 4 3 5 6、6 5 4 3 2 1 等等。因此我们需要有一定的规则和操作下去遍历二叉树,下列介绍二叉树遍历的方法。
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次
按照规则,二叉树的遍历有:前序、中序、后序的递归结构遍历
1. 前序遍历(Preorder Traversal)——访问根结点的操作发生在遍历其左右子树之前。
2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。由于被访问的结点必是某子树的根,所以N(Node)L(Left subtree)R(Right subtree)又可解释为:根、根的左子树和根的右子树。NLR、LNR、LRN分别又称为先根遍历、中根遍历和后根遍历。
创建二叉树链式结构:
typedef int BTDataType;
typedef struct BTNode
{
struct BTNode* left;
struct BTNode* right;
BTDataType data;
}BTNode;
1、前序遍历规则:先遍历根节点 、 再遍历左子树 、 再遍历右子树
// 前序遍历
void PreOrder(BTNode* root)
{
if (NULL == root)
return;
printf("%d ", root->data); // 打印根节点
PreOrder(root->left);
PreOrder(root->right);
}
2、中序遍历规则:先遍历左子树 、 再遍历根节点 、 再遍历右子树
// 中序遍历
void InOrder(BTNode* root)
{
if (NULL == root)
return;
InOrder(root->left);
printf("%d ", root->data); // 打印根节点
InOrder(root->right);
}
3、后序遍历规则:先遍历左子树、 再遍历右子树 、 再遍历根节点在
// 后续遍历
void PostOrder(BTNode* root)
{
if (NULL == root)
return;
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data); // 打印根节点
}
注意: 遍历过程重要的是理解递归过程是如何进行的,如前序打印:打印 1 根节点后,递归到以 2 为根节点的左子树,经过打印 2 节点后,接着递归到以 3 为根节点的左子树中,以此类推,直到最后一个叶子节点。
前序遍历结果:1 2 3 4 5 6
中序遍历结果:3 2 1 5 4 6
后序遍历结果:3 2 5 6 4 1
4.2 二叉树的实现操作
在链式二叉树中,其中大部分操作都是通过运用的递归过程而实现,对二叉树的操作包含:二叉树的创建、销毁、遍历、高度、叶子节点个数、所有节点个数、第k层节点个数等等;最为重要的是需要理解创建过程和层序遍历的过程。
二叉树的创建是在给定数组中包含不需要的元素,从而实现空树的情况,那么可以利用二叉树前序遍历的思想,按照根->左子树->右子树的创建顺序进行递归,遇见空树元素返回空即可。
二叉树的层序遍历规则为从上到下、从左至右依次遍历,但是在二叉树的概念和操作中并没有包含这种操作,此时就可以利用先进先出的原则,利用队列结构实现层序遍历的操作;设置二叉树类型的队列结构(BinaryTreeQueue.c / BinaryTreeQueue.h),将二叉树根节点放入队列中,判断其左右子树是否存在,存在放入队列;打印队头元素后该元素出队列,直至所有队列元素均出队列,从而实现层序遍历操作。
链式二叉树接口: gitee: BinaryTree.c / BinaryTree.h
数据结构 DS/2022-03-12 二叉树 · vipover/学习基本代码 - 码云 - 开源中国 (gitee.com)
至此,二叉树结构的基本操作和概念就介绍完了,具体的细节步骤在接口代码中。