普通链式二叉树没有意义,因而不学增删改查。
学习的目的是
1、为更复杂二叉树搜索->AVL树、红黑树,做铺垫
2、很多二叉树的题,是出在这一块
准备&链式二叉树的结构
自己手动构建一棵二叉树
#include<stdio.h>
#include<stdlib.h>
typedef struct BinaryTreeNode//定义一个节点的结构
{
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
int val;
}BTNode;
BTNode* BuyNode(int x)//创建并初始化一个节点
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
if (node == NULL)
{
perror("malloc fail");
exit(-1);
}
node->val = x;
node->left = NULL;
node->right = NULL;
return node;
}
int main()
{
// 手动构建
BTNode* node1 = BuyNode(1);
BTNode* node2 = BuyNode(2);
BTNode* node3 = BuyNode(3);
BTNode* node4 = BuyNode(4);
BTNode* node5 = BuyNode(5);
BTNode* node6 = BuyNode(6);
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;
return 0;
}
二叉树的操作
二叉树的销毁
//二级指针传递
//一级指针只需要在调用完函数之后,在将root置空就好。
void BinaryTreeDestroy(BTNode** root) {
if (*root == NULL)
return;
// 后序遍历进行销毁,这样子才能往下找到叶子结点,并往上删除
BinaryTreeDestroy(&((*root)->_left));
BinaryTreeDestroy(&((*root)->_right));
free(*root);
*root = NULL; // 将根节点指针置为空,避免出现悬挂指针
}
遍历(递归实现)
递归十分重要,且后续的题目是用二叉树来完成的
前序遍历
前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。即根-左子树-右子树
代码实现
void PrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");//以“#”代替空子树
return;
}
printf("%d ", root->val);
PrevOrder(root->left);
PrevOrder(root->right);
}
下图为模拟递归过程
打印结果为1 2 3 # # # 4 5 # # 6 # #
中序遍历
中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。即左子树-根-右子树
代码实现
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");//以“#”代替空子树
return;
}
InOrder(root->left);
printf("%d ", root->val);
InOrder(root->right) ;
}
打印结果为# 3 # 2 # 1 # 5 # 4 # 6 #
后序遍历
后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。即左子树-右子树-根
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");//以“#”代替空子树
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->val);
}
打印结果为# # 3 # 2 # # 5 # # 6 4 1
层序遍历
把一个结点push进一个队列中,并不会使结点的联系断开
因为我们push的是一个struct,在这个struct里面有int val,strcut* left和right
typedef struct BinaryTreeNode* QDataType;
void BinaryTreeLevelOrder(BTNode* root)
{
//创建并初始化队列
Queue q;
QueueInit(&q);
//把根结点放入
if (root)
{
QueuePush(&q, root);
}
//每次只取队头元素(当前的根结点),
//然后将当前根结点的非空孩子结点存入
//终止条件:队空
while (!QueueEmpty(&q))
{
//取队头元素后pop
BTNode* front = QueueFront(&q);
printf("%d ", front->data);
QueuePop(&q);
//如果有左孩子,则将左孩子的结点push进队列
if (front->left)
{
QueuePush(&q, front->left);
}
if (front->right)
{
QueuePush(&q, front->right);
}
}
QueueDestory(&q);
}
关于递归(复习)
总结递归
1. 确定递归函数和参数返回值
2. 终止条件
3. 单层递归的逻辑
递归的结束
并不是结束程序本身,而是回到调用当前函数的位置。结合函数栈帧理解: 调用一次函数,创建该函数的栈帧,而返回即销毁栈帧。上述的代码中结点的地址是作为参数,以局部变量的形式存在于函数栈帧中。
分治算法
对于一棵二叉树,我们可以看成由许多个小二叉树组成。递归到最后就是一片叶子(可以看作一个根外加两个空结点)。
这种分而治之的思想,就是分治算法的本质:即将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
广度优先遍历与深度优先遍历
广度优先遍历:(Breadth First Search——BFS)是一种先访问同一层节点,再访问下一层节点的遍历方式。层序遍历
深度优先遍历:(Depth First Search——DFS)是一种先访问根节点,然后递归地访问每个子节点的遍历方式。前序遍历
结点及其个数
二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL; // 如果根节点为空,直接返回 NULL
if (root->_data == x)
return root; // 如果根节点的值等于 x,返回根节点
// 递归地在左子树中查找值为 x 的节点
BTNode* leftResult = BinaryTreeFind(root->_left, x);
if (leftResult != NULL)
return leftResult; // 如果左子树中找到了,直接返回结果
// 递归地在右子树中查找值为 x 的节点
BTNode* rightResult = BinaryTreeFind(root->_right, x);
return rightResult; // 返回右子树中查找的结果,可能是 NULL 或者找到的节点
}
二叉树总结点个数
这里的计数器应该是全局变量。如果是局部变量,随着栈帧的创建与销毁,每个计数器在各自的栈帧是独立的,起不到计数的作用。
int TreeSize(BTNode* root)
{
return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
//左右孩子结点都走完了,到父母结点+1返回上去
}
二叉树叶子结点个数
递到叶结点root->left == NULL && root->right == NULL
,
且非叶子结点的空结点也要返回0
int TreeLeafSize(BTNode* root)
{
//只是空,防止传入空指针
if (root == NULL)
{
return 0;
}
//是叶子
if (root->left == NULL && root->right == NULL)
{
return 1;
}
return TreeLeafSize(root->left) + TreeSize(root->right);
}
第k层的结点个数
递到k层才返回1,遇到空结点返回0
当前树的第k层 = 第k-1层的左子树结点个数 + 第k-1层的右子树结点个数
int TreeKLevel(BTNode* root, int k)
{
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return TreeKLevel(root->left,k-1) + TreeKLevel(root->right,k-1);
}
二叉树的高度
其实也就是计算二叉树的深度,在后面
判断二叉树是否是完全二叉树
完全二叉树在队列中只要遇见了第一个NULL,那么队列后面的内容就全是NULL,而非完全二叉树在队列中遇见了第一个NULL后,后面还会遇见非空元素
// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front == NULL)//当遇见第一个NULL时就直接跳出循环来到下面的语
{
break;
}
else
{
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
}
// 遇到空了以后,检查队列中剩下的节点
// 1、剩下全是空,则是完全二叉树
// 2、剩下存在非空,则不是完全二叉树
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front)//如果不为空就返回false
{
QueueDestroy(&q);
return false;
}
}
QueueDestroy(&q);
return true;//全部节点都走完了没有发现非空,说明队列里全是空就返回true.
}
二叉树OJ基础练习题
单值二叉树
bool isUnivalTree(struct TreeNode* root) {
if (root == NULL)
return true;
if (root->left && root->left->val != root->val)
return false;
if (root->right && root->right->val != root->val)
return false;
return isUnivalTree(root->left) && isUnivalTree(root->right);
}
二叉树的最大深度
int maxDepth(struct TreeNode* root) {
if (root == NULL)
return 0;
int leftDepth = maxDepth(root->left);
int rightDepth = maxDepth(root->right);
return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
}
区别
相同的树
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);
}
二叉树的前序遍历
重点是不能忽略了栈帧销毁导致计数器的失效
int TreeSize(struct TreeNode* root)
{
return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}
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) {
//int* returnSize为输出型参数,可以理解为返回给力扣后台的一个参数,这里是让我们返回一个树的大小
// //因此我们需要计算出树的大小
// int n = TreeSize(root);
// //为二叉树的结点开辟数组,以供放入
// int* a = (int*)malloc(sizeof(int)*n);
int n = TreeSize(root);
int* a = (int*)malloc(sizeof(int) * n);
int j = 0;
preorder(root, a, &j);//遍历的同时,并把结点数值放入数组
*returnSize = n;
return a;
}
另一棵树的子树
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) {
//题目给出不存在空树,所以如果root递到空,就说明这一分支没有相同的子树
if (root == NULL)
return false;
if (root->val == subRoot->val)
{
//以相同的结点开始,往下判断子树是否是相同。
if (isSameTree(root, subRoot))
return true;
}
//没找到相同结点(为起始判断),那就继续往下递
//只需要其中一个分支有相同就好,所以用`||`
return isSubtree(root->left, subRoot) || isSubtree(root->right, subRoot);
//此处别写成
//return isSubtree(root->left,subRoot->left) || isSubtree(root->right,subRoot->right);
//被比较的是一个完整的树。。。
}
对称二叉树
跟题目相同的树本质一样,但巧妙地将一棵树拆解成两棵
bool isSymmetricTree(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 isSymmetricTree(p->left, q->right) && isSymmetricTree(p->right, q->left);
}
bool isSymmetric(struct TreeNode* root) {
return isSymmetricTree(root->left, root->right);
//拆成左右两棵树
}
翻转二叉树
struct TreeNode* invertTree(struct TreeNode* root) {
if (root == NULL)
return NULL;
struct TreeNode* left = invertTree(root->left);
struct TreeNode* right = invertTree(root->right);
root->left = right;
root->right = left;
return root;
}
1. 如果当前节点为空,则直接返回空。
2. 递归地翻转左子树和右子树,得到左子树和右子树的翻转结果 `left` 和 `right`。
3. 将当前节点的左子树指针指向 `right`,右子树指针指向 `left`,完成当前节点的翻转。
4. 返回当前节点。
其他关于二叉树选择题(我的错题)
-
N 0 = N 2 + 1 N_0 = N_2+1 N0=N2+1( N 0 N_0 N0为度数为0的结点, N 2 N_2 N2为度数为2的结点)
-
一棵非空的二叉树的先序遍历序列与后序遍历序列正好相反,则该二叉树一定满足,每个结点只有一个孩子,只有一个叶子结点
-
如果一颗二叉树的前序遍历的结果是ABCD,则满足条件的不同的二叉树有( )种
前序确定根,中序找到根确定根的左右子树