在数据结构的主体中,对于树是有很多分类和用法的,我们这一篇文章将从概念和定义的角度,定性的对这几种分类进行分析,在一些需要熟练掌握代码操作的地方,也会有对应的代码示例
在了解树的几个高阶的分类之前,先来了解游戏啊有关于树的概念和定义
树
树的定义
树是n(n>=1)个节点的有限集合T,并且满足:
①有一个被称为根节点(root)
②其余的节点可以分为m个互不相交的集合,这些集合本身也都是树,并称他们为根节点的子树。每棵子树同样拥有自己的根节点
树的ADT
树结构的基本概念
下面用一颗树进行比方:
节点
节点为树中的元素,包含数据项以及若干指向其子树的分支
在上面的树中:A,B ,C,D,E,F,G,H,I,J都是这棵树的节点(node)
节点的度
节点的度也成为degree(),他的含义为节点的子树科目
在上面的树中 degree(A) = 2,degree(G) = 1
因为这是一颗二叉树,所以任何一个节点的度都不会超过2
树的度
树的度就是节点的度的最大值
同节点的度可知,上面的这棵树为一颗二叉树,所以这棵树的度也为2
叶子节点
节点的度为0的节点被称为叶子节点
例如在上面这颗树中,叶子节点有H J E F I 这五个节点,他们都是没有自己的子树
父节点
一个节点的相连接的上一个节点被称为父节点
例如在上面的这棵树中B的父节点为A ; E的父节点为B
树的链式存储结构
一般的情况下,以二叉树为例子,树的结构一般如下图所示:
struct TreeNode
{
struct TreeNode* leftchild;
int data;
struct TreeNode* rightchild;
}
树的遍历
以这一颗树作为例子
树的层次遍历(树的广度优先遍历)
树的层次遍历其实就是树的每一层的元素从左到右进行遍历
用上面的这棵树举例子就是 A B C D L E F G H I
当我们使用代码对树的广度优先遍历的时候,我们需要结合队列进行书写:
例如:
我们先把A节点放入队列
当我们从把A从队列里出来的时候,要把A节点的三个子节点B C D按顺序入队,当我们要取出B节点的时候,同时需要将B节点的子节点入队
树的前序遍历
①先访问根节点
②在访问左节点,接着访问右节点
对于上面这一棵树而言,它的前序遍历为:
A B L E C F D G I H
示例代码如下:
void FirTrav(TreeNode* ps)
{
if (ps == NULL)
{
return;
}
printf("%d ", ps->data);
FirTrav(ps->lchild);
FirTrav(ps->rchild);
}
树的后序遍历
①先访问左节点,在访问右节点
②最后访问根节点
对于上面这一棵树而言:
L E B F C I G H D A
示例代码为:
void LastTrav(TreeNode* ps)
{
if(ps == NULL)
{
return;
}
LastTrav(ps->lchild);
LastTrav(ps->rchild);
printf("%d ",ps->data);
}
树的遍历的巧记方法
前序遍历的巧记方法
在树的每个节点的左边画一个圈圈,对这个树进行从左边开始往下画线,先路过的圈圈就先写出来,例如:
后序遍历的巧记方法
和前序遍历相似,后序遍历一般是在每个节点的右侧画圈圈然后相连
中序的巧记方法
和前两个相似,只不过是在每个节点的下方画一个圈圈然后进行相连
二叉树
二叉树的定义
根据二叉树的定义可知,二叉树的每一个节点最多有两个子节点
二叉树的性质
性质1
一颗非空二叉树的第i层上最多有2^(i-1)个节点
性质2
一颗高度为k的二叉树,最多有2^k -1 个节点
性质3
对于一颗非空二叉树,如果叶子节点的节点数为a,度为2的节点数为b,那么有a = b + 1成立
二叉树的创建代码
TreeNode* Create()
{
int data = 0;
scanf("%d", &data);
if (data == -1)
{
return NULL;
}
else
{
TreeNode* newnode = (TreeNode*)malloc(sizeof(TreeNode));
if (newnode == NULL)
{
printf("malloc fail!");
exit(-1);
}
newnode->data = data;
printf("请输入%d节点的左节点", data);
newnode->lchild = Create();
printf("请输入%d节点的右节点", data);
newnode->rchild = Create();
return newnode;
}
}
特殊的二叉树
满二叉树
一颗高度为K 并且具有2^k - 1 个节点的二叉树被称为满二叉树
换句话来说,满二叉树就是一颗二叉树中的任意一层的节点数都达到了最大值
下面是两个二叉树的辨析
左边图的二叉树的第四排并没有达到节点数量的最大值,所以左边的这棵树并不是满二叉树
完全二叉树
在满二叉树的最底层自右至左依次(注意:不能跳过任何一个结点)去掉若干个结点得到的二叉树也被称之为完全二叉树。满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。
完全二叉树的特点
(1)所有的叶结点都出现在最低的两层上。
(2)对任一结点,如果其右子树的高度为k,则其左子树的高度为k或k+1。
二叉树的遍历
二叉树的遍历同样的分为前序遍历,后序遍历和中序遍历,由于前面已经用了前序和后序作为举例,所以在这里用中序遍历进行举例
用这一棵树进行举例:
按照在每个节点的下方画一个圈圈进行相连的办法,我们对这颗二叉树进行中序遍历,遍历的图片如下:
所以这一颗树的中序遍历为: B L E A C W X D
二叉树的顺序存储
完全二叉树的存储
以这一颗树作为例子,将这棵树的节点从上到下,从左到右进行序号标识,然后进行顺序存放
普通二叉树的顺序存储
以这一颗二叉树作为例子,和完全二叉树的顺序存储同理,他的顺序存储为:
其中NULL表示一个子树没有两个子树,空的那个子树位置就用NULL进行替换
单只树的顺序存储
还是同理写出顺序存储
在这一颗树的顺序存储中,标红的两个NULL可能不是很好理解,其代表的是A的左子树的左右子树也是NULL为空
哈夫曼树
哈夫曼树在之前的文章中有很详细的讲过
数据结构——哈夫曼树
二叉排序树
二叉排序树的性质:
对于任意的一个节点P而言:
①如果P的左子树为非空子树,则左子树上的所有节点的关键字均小于P节点的关键字的值
②如果P的右子树为非空子树,则右子树上的所有节点的关键字均大于P节点的关键字的值
③节点P的左右子树同样也是二叉排序树
二叉排序树的创建:
例如给出122 99 250 110 200 300 105 230 216这几个数字,构建一颗二叉排序树,排序的结果如下:
排序的注意事项:
①如果需要插入的节点比该根节点小,则放在该根节点的左边,否则放在右边
②在插入节点的时候,需要一层一层的进行比较插入
下面是示例代码:
void insert(Bitree** tree, int x) //指向指针变量的指针,结果是指针tree所指向的值
{
Bitree* temp = NULL;
if ((*tree) == NULL) //判断根节点是否存在
{
temp = (Bitree*)malloc(sizeof(Bitree));
temp->lchild = temp->rchild = NULL; //左右节点制空
temp->a = x;
*tree = temp;
return;
}
if (x < (*tree)->a) //判断是左子树
{
insert(&(*tree)->lchild, x);
}
else if (x > (*tree)->a) //判断是右子树
{
insert(&(*tree)->rchild, x);
}
}
二叉排序树的查找
即如果需要查找的数字在该二叉排列树中,那么则返回该数字所在节点的地址,如果不在该树中,则返回NULL
void Find(Bitree* T,int x)
{
if (T->a == x)
{
printf("找到了");
return;
}
if (T->a > x)
{
Find(T->lchild, x);
}
if (T->a < x)
{
Find(T->rchild, x);
}
}
二叉平衡树(AVL树)
AVL树的性质和定义
①左子树和右子树高度之差的绝对值小于等于1
②左子树和右子树也是平衡二叉树
在树上的每个节点附加上一个数字,这个数字的含义是左子树和右子树的高度差,这个数字被称为这个节点的平衡因子
即平衡因子=节点左子树的高度-节点右子树的高度
而平衡二叉树(AVL树)的每个节点的平衡因子只能是**-1,0,1**
例如下面这棵树就是一颗AVL树:
AVL树调整平衡
LL型、LR型、RL型、RR型的不平衡情况
LL型
LL型的不平衡树采用的是右旋的办法成为AVL树
LR型
LR型的不平衡树采用的是先左旋再右旋的顺序进行转化
RR型
RR型的不平衡采用的是左旋的方法进行转化
RL型
RL型的不平衡采用的是先左旋再右旋的方法转化
平衡树旋转的规则
把平衡因子为中等的变为根,把最小的放在左孩子,把最大的放在右孩子
即找到平衡因子处于三者之间的作为根进行旋转
二叉搜索树(BST)
二叉搜索树的定义
每个根节点的值都比它的左子树的值要大,但是比右子树要小
但是需要注意的是,在二叉搜索树中,不可以有相同的数字
下图是一个二叉搜索树的举例:
代码
struct treeNode
{
int val;
struct treeNode* left;
struct treeNode* right;
}
二叉搜索树的插入
在我们往一颗二叉搜索树中插入元素的时候,我们需要对这颗树的位置进行寻找调整,保证插进去后的树依旧为二叉查找树
问题的简单分析
①先判断这颗二叉查找树是否为空树,如果这颗二叉查找树为空树的话,那么插入的那个新元素就是这颗树的根节点(下面的分析均默认这颗二叉查找树不是空树)
②如果需要插入的这个元素的值小于当前根节点的值,那么就继续寻找当前树的左子树,直到找到为空树就直接插入,递归结束
③如果其大于根节点的值,则寻找右子树,同②
代码
struct treeNode* insert(struct treeNode* root,int val)
{
if(root == NULL)
{
struct treeNode* newnode = (struct treeNode*)malloc(sizeof(struct treeNode));
newnode->val = val;
newnode->left = NULL;
newnode->right = NULL;
return newnode;
}
if(val < root->val)
{
root->left = insert(root->left,val);
}
else if(val > root->val)
{
root->right = insert(root->right,val);
}
return root;
}
二叉搜索树的删除
问题的简单分析
我们首先要分析我们需要删除的节点属于哪一类型的节点:①没有任何孩子的节点 ②有一个孩子的节点 ③有两个孩子的节点
①如果这个节点没有任何的孩子,那么直接删除这个节点即可
②查找需要删除的节点val,如果val小于当前的节点,则递归到左子树继续寻找,如果val大于当前的节点,则递归到右子树进行寻找
③如果需要删除的是当前的根节点,那么需要找到右子树中最小的节点,并将其赋值给当前的节点,再递归到右子树中删除该节点
代码
struct treeNode* findmin(struct treeNode* node)
{
while(node->left)
{
node = node->left;
}
return node;
}
struct treeNode* delete(struct treeNode* root,int val)
{
if(root == NULL)
return NULL;
if(val < root->val)
root->left = delete(root->left,val);
else if(val > root->val)
root->right = delete(root->right,val);
else
{
if(root->left == NULL)
{
struct treeNode* temp = root->right;
free(root);
return temp;
}
if(root->right == NULL)
{
struct treeNode* temp = root->left;
free(root);
return temp;
}
struct treeNode* temp = findmin(root->right);
root->val = temp->val;
root->right = delete(root->right,temp->val);
}
return root;
}
二叉搜索树的查找
这个其实是一个非常容易的事情
问题的简单分析
①先判断这颗二叉查找树是否为空,如果为空树的话,直接返回这个需要查找的数字不在此BST中
②如果这个需要查找的数字小于根节点,那么则递归左子树,反则递归右子树
代码
struct treeNode* findnode(struct treeNode* root,int val)
{
if(root == NULL)
{
return NULL;
}
if(val < root->val)
{
return findnode(root->left,val);
}
if(val > root->val)
{
return findnode(root->right,val);
}
return root;
}