目录
1、 二叉树的存储结构
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
1.1 顺序结构
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树
会有空间的浪费
。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面的章节会专门讲
解。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
1.2 链式结构
二叉树的链式存储结构是指用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的
方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩
子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链
和三叉链
,当前我们学习中一般都
是二叉链,学到高阶数据结构如红黑树等会用到三叉链,三叉链想必与二叉链多了一个指向父亲的指针
// 二叉链
struct BinaryTreeNode
{
struct BinTreeNode* pLeft; // 指向当前节点左孩子
struct BinTreeNode* pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
};
// 三叉链
struct BinaryTreeNode
{
struct BinTreeNode* pParent; // 指向当前节点的双亲
struct BinTreeNode* pLeft; // 指向当前节点左孩子
struct BinTreeNode* pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
};
2、 链式二叉树的创建
2.1 结构定义
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
2.2 创建一个树结点
BTNode* CreateBTNode(BTDataType x)
{
BTNode* newNode = (BTNode*)malloc(sizeof(BTNode));
assert(newNode);
newNode->data = x;
newNode->left = NULL;
newNode->right = NULL;
return newNode;
}
2.3 创建和链接
BTNode* n1 = CreateBTNode(1);
BTNode* n2 = CreateBTNode(2);
BTNode* n3 = CreateBTNode(3);
BTNode* n4 = CreateBTNode(4);
BTNode* n5 = CreateBTNode(5);
BTNode* n6 = CreateBTNode(6);
// 链接
// 1
// 2 4
// 3 # 5 6
// # # # # # #
n1->left = n2;
n1->right = n4;
n2->left = n3;
n4->left = n5;
n4->right = n6;
3、 二叉树遍历
每棵树都有三个部分,根,左子树,右子树
3.1 前中后序遍历(深度优先遍历)
先序:根 -> 左子树 -> 右子树
中序:左子树 -> 根 -> 右子树
后序:左子树 -> 右子树 -> 根
观察顺序,我们可以发现,遍历过程中根(root)
的在什么位置,就是什么遍历,如:前面就是前序。
同时我们只要直到前序+中序,或中序+后序就可以还原出完整二叉树
为什么前序+后序不能还原出来?
前序和后序都能确定出根节点,但是不能区分出根的左右子树,所以得出还原出二叉树必须要确定根节点和根节点的左右区间
3.1.1 前序代码
// 前序
// 根 -> 左子树 -> 右子树
void PrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");
return;
}
printf("%d ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
3.1.2 中序代码
// 中序
// 左子树 -> 根 -> 右子树
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
3.1.3 后序代码
// 后序
// 左子树 -> 右子树 -> 根
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
3.2 层序遍历(广度优先遍历)
思路:
- 我们需要一个队列来实现
- 首先需要入一个数据(需要判空),后面的数据就更加方便的出栈和入栈
- 出队头数据并打印,在依次把该结点的左右结点给入到队列里(空结点不能入队)
- 重复上面的步骤直到队列为空,就可以一层一层的遍历整棵树了。
- 这里的原理就是队列的先进先出,每一个节点出队后都会带着左右子树进队列
- 主要思想:上一层节点带下一层节点,空节点不带
// 层序
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
// 不为空,根就要入进去
if (root)
QueuePush(&q, root);
// 上一层带下一层,空就不带
// 队列为空,就层序出完数据
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
printf("%d ", front->data);
if (front->left)
QueuePush(&q, front->left);
if (front->right)
QueuePush(&q, front->right);
}
printf("\n");
QueueDestroy(&q);
}
3.3 判断是否完全二叉树
完全二叉树性质:前n-1层满的,最后一层不满,但是最后一层节点是从左到右连续的
思路:
- 和层序区别是:空节点也要入队
- 重复着上一层带下一层的思路,遇到空节点就结束
- 在剩下的队列数据里,看是否还有非空的数据
- 还有数据证明:不是完全二叉树
- 没有数据:就是完全二叉树
这里很巧妙的利用了完全二叉树和队列的性质,先进先出,遇到空后,就能判断是否是完全二叉树了
// 是否完全二叉树
bool isCompeleteBinTree(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
QueuePush(&q, root);
// 每次都带下一层节点进来,直到头部数据为空就终止循环
while (QueueFront(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
// 剩下的队列的里,还有数据就不是完全二叉树,只有空就是完全二叉树
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front)
{
QueueDestroy(&q);
return false;
}
}
QueueDestroy(&q);
return true;
}
3.4 是否平衡二叉树
一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值
不超过 1
。
注意下图就不是平衡二叉树,虽然他的根节点左右子树高度是一样的,但是他左右子树的子树高度差不满足
- 返回值是bool型,形参为根
- 终止条件:遇到NULL就是真
- 单层逻辑:求出当前节点的左右子树的高度,求绝对值是否大于1,大于就不是,小于就继续递归直到为NULL返回true,两个子树都为true,就是平衡二叉树
bool isBalanced(struct TreeNode* root){
if (root == NULL)
return true;
// 调用最大深度函数
int leftDepth = maxDepth(root->left);
int rightDepth = maxDepth(root->right);
// 写法1
// 三个条件有一个不满足就不是,根左右子树都满足就是平衡二叉树
return abs(leftDepth - rightDepth) < 2 && isBalanced(root->left) && isBalanced(root->right);
// 写法2
return abs(leftDepth - rightDepth) > 1 ? false : isBalanced(root->left) && isBalanced(root->right);
4、 树相关的操作
4.1 节点个数
- 返回值是int型,形参为根
- 终止条件:遇到NULL就返回
- 单层逻辑:左子树和右子树的数量+1(根节点)
// 节点个数
int TreeSize(BTNode* root)
{
return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}
4.2 叶子个数
- 返回值是int型,形参为根
- 终止条件:遇到NULL就返回
- 单层逻辑:左右子树都是NULL,就+1
// 叶子节点个数
int LeafSize(BTNode* root)
{
if (root == NULL)
return 0;
if (root->left == NULL && root->right == NULL)
return 1;
return LeafSize(root->left) + LeafSize(root->right);
}
4.3 树的最大深度
- 返回值是int型,形参为根
- 终止条件:遇到NULL就返回
- 单层逻辑:左右子树深度相比谁更深,返回深的子树在+1(根节点)
// 树的最大深度
int MaxDepth(BTNode* root)
{
if (root == NULL)
return 0;
int leftDepth = MaxDepth(root->left);
int rightDepth = MaxDepth(root->right);
if (leftDepth > rightDepth)
return leftDepth + 1;
else
return rightDepth + 1;
}
4.4 销毁
- 返回值是int型,形参为根
- 终止条件:遇到NULL就返回
- 单层逻辑:后序编辑逻辑,先free完左子树,在free右子树,最后free根
// 销毁
void TreeDestroy(BTNode** root)
{
if (root == NULL)
return;
free((*root)->left);
free((*root)->right);
free(*root);
*root = NULL;
}
5、 总结
学习树逻辑基本上都是递归,递归有三部曲一定要弄清楚
- 确定递归函数的参数和返回值
- 确定终止的条件
- 确定单层递归的逻辑