赞
踩
二叉树与普通的树的本质上的区别实际上只有一个——子结点的数量。
普通的树:任意数量的子结点
二叉树:只有两个子结点,也称为左孩子和右孩子结点。
二叉树一共有五种形态:
1.空二叉树。
2.只有一个根结点。
3.根结点只有左子树。
4.根结点只有右子树。
5.根结点既有左子树又有右子树。
那么根据这五种形态就可以延申一个问题:如果有三个结点的二叉树,它能够有几种形态呢?
我们根据以上的五种形态,可以画出如下的形态
满二叉树:除了第一层,其他层的结点都有两个子节点
完全二叉树:除了最后一层外,每层的节点数都达到最大,并且最后一层的节点都集中在左侧。(注:完全二叉树和满二叉树的关系相当于正方形和长方形的关系)
二叉搜索树:一种特定类型的二叉树,对于每个节点,左子树中的所有节点的值都小于该节点的值,右子树中的所有节点的值都大于该节点的值。
二叉树有几个重要的性质和公式,这是基于二叉树的特性的,有助于理解其结构和行为。以下是一些常见的二叉树相关公式和性质:
对于一棵具有 (h) 的高度的完全二叉树,节点的数量 (n) 的范围是:
一棵完全二叉树的叶子节点 (L) 的数量与高度 (h) 的关系为:
这适用于从高度为0到高度为(h)的索引。
对于包含 (n) 个节点的二叉树,内部节点(即有至少一个子节点的节点)数量 (I) 和叶子节点数量 (L) 的关系为:
且总有:
这是因为内部结点仅仅比叶子节点少一个根节点
因此我们可以得出:
对于每个节点,其深度(从根节点到该节点的边数) (d) 与树的高度 (h) 的关系是:
在完全二叉树中,如果父节点为 (i),则:
度为0的永远比度为2 的多一个,即 N0=N2+1
叶子节点的个数——即度为0的个数N0
在二叉搜索树中:
对于一棵 AVL 树(自平衡的二叉搜索树),任何节点的两个子树的高度差至多为 1。这个性质确保了 AVL 树的高度始终保持在 (O(log n))。
我们知道,对于树来说,实际上它的子结点本身也是一棵树,那么我们通常就会使用递归的方法来构建树。
递归,其中函数在其定义的过程中调用自身。
递归通常由两个基本部分组成:
递归基(Base Case):
递归步骤(Recursive Step):
既然我们二叉树可以使用递归,那么递归都有哪些优点呢?
代码简洁性:
自然表达:
但是使用递归就会有一些缺点浮现。
但总的来说,由于树的结构并不是特别复杂,并且往往调用的函数是其本身,其缺点也就微不足道了。
接下来我们就对二叉树的一系列操作进行解析。
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
二叉树可以使用数组和链表两种形式来进行存储,但是针对二叉树的特点,只有满二叉树或者完全二叉树适合使用数组存储。其他形式的树用数组存储反而会浪费空间,所以我们使用链表存储。
BTNode* BuyNode(BTDataType x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
if (node == NULL)
{
perror("malloc fail");
return NULL;
}
node->data = x;
node->left = NULL;
node->right = NULL;
return node;
}
这里我们使用三位运算符可以更加直观和简短地计算树的深度
int TreeHeight1(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int left = TreeHeight(root->left);//左子树的高度
int right = TreeHeight(root->right);//右子树的高度
return left > right ? left + 1 : right + 1;//当前树的高度=左右子树最大值+1
}
对于二叉树的遍历,这其中大有说法。
同时它也是递归思想的经典案例,我们进行仔细分析。
我们知道树的结构,从结点来看,二叉树可以看作父亲结点、左孩子结点、右孩子结点,我们需要获得孩子节点只需要直接指向它的左孩子结点或者右孩子结点即可;而从横面来看,二叉树是具有层数的,每一层的结点数都与2的倍数有关;
那么我们要遍历所有的结点的时候,我们就衍生出了三种不同的遍历方法,它们的不同是根据访问父结点的顺序来决定的:前序遍历(先序遍历)、中序遍历、后序遍历。举个例子,先序遍历就是最先访问父结点,再访问左孩子结点,最后访问右孩子结点。
鉴于递归的特点,当我们访问孩子结点的时候,又可以将其再作为一个父节点,从而再去访问它的孩子结点,一直递归下去,直到遍历完所有的结点。
接下来我们分别编写三种遍历的代码。
先序遍历:父节点->左孩子结点->右孩子结点
//前序遍历
void PreOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
中序遍历:左孩子结点->父节点->右孩子结点
//中序遍历
void MidOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
MidOrder(root->left);
printf("%d ", root->data);
MidOrder(root->right);
}
后序遍历:左孩子结点->右孩子结点->父节点
//后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
通过上述的代码我们其实可以发现,printf("%d ", root->data);
这个语句实际上就是父节点的位置,而PostOrder(root->left)
; 和PostOrder(root->right);
就是左孩子结点和右孩子结点的分别位置。从书面上来看,使用递归的写法使得代码十分简洁易懂,并且具有很强的逻辑性帮助理解二叉树的本质。
你以为遍历就这么完了吗?其实并没有。我们来看一个例子。
现在请你根据这个二叉树写出它中序遍历之后的排列顺序。
你以为的:中序遍历就是按照左->父->右的顺序直接遍历就行了,
H->D->I->B->E->A->F->C->G
但是很遗憾,这样写是错的。因为空结点NULL也是结点,我们不能忽视它。
你再以为的:既然空结点也是结点,那只需要将E、F、G的左右孩子结点(即空结点)也表示出来即可。
H->D->I->B->NULL->E->NULL->A->NULL->F->NULL->C->NULL->G->NULL
但是很遗憾,这样写也是错的。你忽视掉了H和I。
实际上的:
NULL->H->NULL->D->NULL->I->B->NULL->E->NULL->A->NULL->F->NULL->C->NULL->G->NULL
据此我们可以知道的是,遍历的时候我们不能忽视空结点。在实际解决的时候我们需要先访问所有的结点,如果访问完某个结点为空才可以跳过它去访问下一个结点,而不是忽略空结点。
除了以上三种遍历以外,还有一种遍历方法,这种方式是直接一层一层地进行遍历,也叫做层序遍历。
层序遍历:第一层->第二层->第三层->…->第n层
//层序遍历 //思路:首先建一个队列,根节点入队,然后出队,打印队首元素,左右子树入队,直到队列为空 void LevelOrder(BTNode* root) { Queue q;//首先建一个队列 QInit(&q);//初始化队列 if (root == NULL) { return NULL; } if (root) { QPush(&q, root);//根节点入队 } while (!QEmpty(&q))//队列不为空 { BTNode* front = QFront(&q);//队首元素 printf("%d ", front->data);//打印队首元素 if (front->left) //左子树入队 { QPush(&q, front->left); } if (front->right) //右子树入队 { QPush(&q, front->right); } } QDestroy(&q);//销毁队列 }
//销毁二叉树
void DestroyTree(BTNode* root)
{
if (root == NULL)
return;
DestroyTree(root->left);
DestroyTree(root->right);
free(root);
}
以上仅仅是对二叉树典型概念和用法的大致讲解,实际上二叉树涉及到的内容还有很多,例如线索二叉树的构建、树与二叉树之间的转换等等,而这些概念将会在后续的补充中分开进行讲解。
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。