二叉树的链式结构
普通的二叉树没有实际价值,真正有意义的是搜索二叉树!
搜索二叉树满足左子树都比根节点小,右子树都比根节点大;
普通二叉树的增删查改也没有太大的意义,因为对于普通的二叉树来说没有特定的限制,我们不知道将数据插入到哪里有意义,因此这里我们不研究二叉树的增删查改;
二叉树的遍历
二叉树的遍历指的是按照某种特定的规则,依次对二叉树中的结点进行相应的操作,并且每个结点只操作一次;
二叉树的遍历包括四种:前序遍历、中序遍历、后续遍历、层序遍历
其中,前序/中序/后序属于递归遍历;
在遍历的时候,我们需要用递归的方式来看树:根结点、左子树、右子树;
前、中、后序遍历指的是结点和左子树、右子树之间的遍历顺序不一样;
接下来我们分别给出前序遍历、中序遍历、后序遍历的代码:
// 前序遍历
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL");
return;
}
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
// 中序遍历
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
// 后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
二叉树的节点数
计算二叉树的节点个数,我们可以通过递归的方式:节点数 = 左子树的节点数 + 右子树的节点数 + 1,因此我们可以得到下面的函数:
int TreeSize(BTNode* node)
{
if (node == NULL)
{
return 0;
}
return TreeSize(node->left) + TreeSize(node->right) + 1;
}
二叉树的高度
对于实现二叉树的高度,我们也可以借鉴上面实现的二叉树的节点数的递归的方式:二叉树的高度 = 左子树和右子树之间的高度最大值 + 1;
那么根据上面的概念我们可以写出下面的代码:
int TreeHigh(BTNode* node)
{
if (node == NULL)
{
return 0;
}
return TreeHigh(node->left) < TreeHigh(node->right)
? TreeHigh(node->right) + 1 : TreeHigh(node->left) + 1;
}
当该节点为空结点的时候直接返回;
但是这种写法有一种严重的错误:我们每次返回时,都得计算左子树的高度和右子树的高度,然后再比较返回较大值; 比较的时候并没有记录下当前左子树和右子树的高度!仅仅只是进行了比较,因此最后返回的时候还得重新计算高度!
这种情况的弊端是非常大的,因为当节点越靠近下面,被调用的次数就越多(因为递归),且每下次层调用的次数就是上一层的两倍!
因此最好的写法是我们这里记录下左子树和右子树的值,然后直接进行比较返回;
int TreeHigh(BTNode* node)
{
if (node == NULL)
{
return 0;
}
int BTNodeLeft = TreeHigh(node->left);
int BTNodeRight = TreeHigh(node->left);
return BTNodeLeft > BTNodeRight ? BTNodeLeft + 1 : BTNodeRight + 1;
}
二叉树第K层的节点个数
规律:根的第K层的结点个数 = 第 K-1 层的结点的左孩子个数 + 第 K-1 层的结点的右孩子个数;
实现代码如下所示:
int TreeKLevel(BTNode* node, int k)
{
// k这里指的是层数,必须>0
assert(k > 0);
if (node == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return TreeKLevel(node->left, k - 1) + TreeKLevel(node->right, k - 1);
}
二叉树查找值为x的结点
查找思路:先遍历根节点,再遍历左子树,最后遍历右子树(如果先找到就直接返回,最后找不到就直接返回 --- 类似先序遍历);
实现的代码如下所示:
BTNode* BinaryFind(BTNode* node, int x)
{
// return NULL 不一定是直接返回到外面:
// 对于递归来说大部分是为了返回到上一层!
if (node == NULL)
return NULL;
if (node->data == x)
return node;
// 在左子树查找x
BTNode* rleft = BinaryFind(node->left, x);
if (rleft)
{
return rleft;
}
BTNode* rright = BinaryFind(node->right, x);
if (rright)
{
return rright;
}
}
练习题
1. 相同的树
https://leetcode.cn/problems/same-tree/
示例代码如下:
bool isSameTree(TreeNode* p, TreeNode* q) {
if (p == NULL && q == NULL)
return true;
if (p == NULL || q == NULL)
return false;
if (p->val != q->val)
return false;
// 若比较两个数字相等,则没有意义,因为还要像下面拿结果!
// 但是若比较不相等,则当时就可以直接获得结果
// 上面三种情况将根节点的情况判断完毕
// 下面判断的是左子树和右子树的情况
return isSameTree(p->left,q->left) &&
isSameTree(p->right,q->right);
}
2. 前序遍历
这里的题目要求是输入一个树,返回的是前序遍历结果的数组!
这里有几个注意点:
- 由于我们需要返回存储数据的数组,因此需要自己开辟一个数组,如果直接在函数体内的栈区定义一个数组,那么函数调用后直接被销毁!指针就成了一个野指针!
- 使用malloc开辟数据我们需要知道数据的个数,这里我们通过自己实现一个size;
- 要注意前序遍历时的下标:当左子树遍历完后,我们把对应的数据存储到数组之后,因为由于递归此时的i会变为2!(递归回到最上面的一层)所以我们可以通过两种方式来解决:定义全局变量或者指针,因为全局变量可能会被其他因素修改,所以下面我们采用的是指针的方式:
// 计算树的节点个数
int BinarySize(struct TreeNode* root)
{
return root == NULL? 0 :BinarySize(root->left) + BinarySize(root->right) + 1;
}
// 前序遍历
// pi是对应的下标
void PreOrder(struct TreeNode* root, int* a, int* pi)
{
if (root == NULL)
return ;
a[(*pi)++] = root->val;
PreOrder(root->left, a, pi);
PreOrder(root->right, a, pi);
}
int* preorderTraversal(struct TreeNode* root, int* returnSize) {
*returnSize = BinarySize(root);
int* a = (int*)malloc(sizeof(int) * (*returnSize));
int i = 0;
PreOrder(root, a, &i);
return a;
}
3. 另一棵树的子树
bool isSameTree(struct TreeNode* p, struct TreeNode* q) {
if (p == NULL && q == NULL)
return true;
if (p == NULL || q == NULL)
return false;
if (p->val != q->val)
return false;
// 若比较两个数字相等,则没有意义,因为还要像下面拿结果!
// 但是若比较不相等,则当时就可以直接获得结果
// 上面三种情况将根节点的情况判断完毕
// 下面判断的是左子树和右子树的情况
return isSameTree(p->left,q->left) &&
isSameTree(p->right,q->right);
}
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot) {
if (root == NULL)
return false;
if (isSameTree(root,subRoot))
return true;
return isSubtree(root->left,subRoot) || isSubtree(root->right,subRoot);
}
这里我们借用了上面的《相同的树》,还是采用递归的思想,如果根节点不满足,接下来分别从左子树和右子树寻找;
4. 对称二叉树
bool compare(struct TreeNode* left, struct TreeNode* right)
{
if(left == NULL && right == NULL)
return true;
if (left == NULL || right == NULL)
return false;
if (left->val != right->val)
return false;
return compare(left->left,right->right) && compare(left->right,right->left);
}
bool isSymmetric(struct TreeNode* root) {
// 先判断根节点
if (root == NULL)
return true;
// 到这里左右两个结点都存在
return compare(root->left,root->right);
}
5. 二叉树的遍历
#include <stdio.h>
// 根据输入的字符串将这棵树还原出来
// 再进行中序遍历
struct TreeNode
{
struct TreeNode* left;
struct TreeNode* right;
char val;
};
// 将前序传入的字符串改为树
// 函数
struct TreeNode* createTree(char* s, int* pc)
{
if (s[*pc] == '#')
{
(*pc) ++;
return NULL;
}
struct TreeNode* root = (struct TreeNode*)malloc(sizeof(struct TreeNode));
// 根节点
root->val = s[*pc];
(*pc)++;
root->left = createTree(s, pc);
root->right = createTree(s, pc);
return root;
}
void InOrder(struct TreeNode* root)
{
if (root == NULL)
return;
InOrder(root->left);
printf("%c ",root->val);
InOrder(root->right);
}
int main() {
char a[100];
scanf("%s",a);
int c = 0;
struct TreeNode* root = createTree(a, &c);
InOrder(root);
return 0;
}
这里我们需要实现:将按照 前序遍历形式的字符串改为二叉树的格式;
- 首先根节点不能为NULL,如果根节点为NULL则直接返回;
- 如果根节点不为NULL,那么malloc一个根节点再将其值放入之中;
- pc对应输入的下标,这里由于递归pc不能设置为局部变量,因此我这里传入的是指针类型;
二叉树的层序遍历
二叉树的层序遍历是遍历树的每一层的节点;
思路:我们可以使用一个队列,队列用于存放二叉树的节点,当一个节点入队列的时候,我们保存其值,自己出队列,然后再让它的左孩子和右孩子入队列...循环上面操作即可完成遍历;
但是直接使用队列存放二叉树结点的话,占用的空间太大,因此这里我们考虑使用存放二叉树结点的指针!
判断二叉树是否为满二叉树
完全二叉树的性质:按层序遍历,非空结点一定连续!
代码整体思路:初始化一个队列用于层序遍历
- 当跟结点为非NULL的时候将其插入到队列当中;
- 对结点进行pop,然后push其左右节点(左右为NULL也push进队列);
- 当pop之后的结点为NULL的时候,此时我们找到NULL,跳出循环;
- 跳出后如果之后有结点都为非NULL,说明不是完全二叉树;
- 对队列剩下的依次遍历:如果为NULL就进行pop;不为空就直接返回false;
二叉树的销毁
采用后序遍历的思路:先销毁左子树,再销毁右子树,最后销毁根节点;
如果采用前序遍历来销毁:需要先保存左右孩子节点的指针,不然直接销毁根节点,找不到左右孩子;
深度优先遍历和广度优先遍历(了解)
在二叉树中,深度优先遍历通常为前序、中序、后续遍历;
对于深度优先遍历,具有这种特点:它会尽可能深入每一个分支,直到到达尽头,然后回溯到上一个节点继续搜索。(通常借助递归来完成)
广度优先遍历通常是层序遍历,这种遍历的特点是:首先访问所有邻居节点,然后再访问每个邻居节点的邻居节点,层层递进的搜索策略。(通常借助队列来完成)