目录
需要先创建一颗二叉树,然后才能学习其相关的基本操作,考虑到我们刚刚接触二叉树,
为了能够先易后难地进行讲解,这里将手动创建一颗简单的二叉树,用来方便大家学习。
等二叉树结构了解的差不多后,后期会研究二叉树的真正的创建方式。
(后面的层序遍历跟着的那篇OJ就是用前序遍历构建二叉树)
1.二叉树概念
复习链接:
数据结构与算法⑩(第四章_上)树和二叉树和堆的概念及结构_GR C的博客-CSDN博客
二叉树是什么?① 空树 ② 非空:根节点、根节点的左子树与根节点的又子树组成的。
从概念中我们不难看出,二叉树的定义是递归式的。因此后续基本操作中,基本都是按照该概念来实现的,来看 A 的左子树,把 B 看作为根节点,又是颗二叉树。所以可以通过采用递归的手法来实现二叉树。
2.二叉树定义
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
typedef char BTDataType;
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* left; // 记录左节点
struct BinaryTreeNode* right; // 记录右节点
BTDataType data; // 存储数据
} BTNode;
//创建新节点
BTNode* CreateNode(BTDataType x)
{
BTNode* new_node = (BTNode*)malloc(sizeof(BTNode));
if (new_node == NULL)
{
printf("malloc failed!\n");
exit(-1);
}
new_node->data = x;
new_node->left = new_node->right = NULL;
return new_node;
}
//手动创建二叉树
BTNode* CreateBinaryTree()
{
BTNode* nodeA = CreateNode('A');
BTNode* nodeB = CreateNode('B');
BTNode* nodeC = CreateNode('C');
BTNode* nodeD = CreateNode('D');
BTNode* nodeE = CreateNode('E');
BTNode* nodeF = CreateNode('F');
nodeA->left = nodeB; // A
nodeA->right = nodeC; // B C
nodeB->left = nodeD; // D E F
nodeC->left = nodeE;
nodeC->right = nodeF;
return nodeA;
}
int main()
{
BTNode* root = CreateBinaryTree();
}
3.二叉树深度优先遍历
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历,就是按照某种特定的规则,一次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。 访问节点所做的操作要看具体的应用问题。遍历是二叉树上最重要的运算之一,也是二叉树上进行其他运算的基础。
二叉树遍历(Traversal):沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。 按照规则,二叉树的遍历有:前序 / 中序 / 后序 的递归或非递归遍历。除了前序、中序和后续遍历外,我们还可以对二叉树进行层序遍历。
比如二叉树的中序遍历:
3.1二叉树前序遍历
前序遍历(Preorder Traversal):访问根节点的操作发生在遍历其右子树之前。即:首先访问根结点,然后遍历左子树,最后遍历右子树(根左右)。
代码实现前序遍历:
//二叉树前序遍历
void PreOrder(BTNode* root)
{
//首先判断根是否为空,为空就返回
if (root == NULL)
{
printf("NULL "); // 暂时打印出来,便于观察
return;
}
//走到这里说明不为空,根据前序遍历,先访问根节点
printf("%c ", root->data);
//然后遍历左子树(利用递归)
PreOrder(root->left);
//最后遍历右子树(利用递归)
PreOrder(root->right);
// A
// B C
// D E F 前序:根 左 右
//执行输出: A B D NULL NULL NULL C E NULL NULL F NULL NULL
}
① 首先判断根是否为空,如果根为空,则返回。这里为了表示,我们把空节点以 " Ø " 打印出来。
② 如果跟不为空,这说明有数据。由于是前序遍历(Preorder),前序遍历是先访问根节点,然后遍历左子树,最后再遍历右子树。所以,我们这里先要访问的是根节点,我们把根节点的数据打印出来。
③ 然后我们需要遍历左子树,这时我们利用递归就可以实现。将根节点 root 的左数 left 传入 PreOrder 函数(将其左树看作根),一直递归下去,直到碰到 root == NULL 则返回。
④ 最后,遍历完左子树后遍历右子树。利用递归,方法同上。
3.2二叉树中序遍历
递归的中序和后序和前序差不多 顺序换一下就行
//二叉树中序遍历
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->left);
printf("%c ", root->data);
InOrder(root->right);
// A
// B C
// D E F 中序:左 根 右
//执行输出:NULL D NULL B NULL A NULL E NULL C NULL F NULL
}
3.3二叉树后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%c ", root->data);
// A
// B C
// D E F 后序:左 右 根
//执行输出:NULL NULL D NULL B NULL NULL E NULL NULL F C A
}
4.二叉树的几个接口函数
下面我们实现几个接口函数
// 二叉树结点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子结点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层结点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
4.1 求二叉树结点个数
和上面遍历一样,采用分治的思想:是空就返回0,不是空就返回左子树的结点和右子树的结点+1(本身)。
int TreeSize(BTNode* root)
{
return root == NULL ? 0
: TreeSize(root->left) + TreeSize(root->right) + 1;
}
4.2 求二叉树叶子结点个数
同理,是空返回1,是叶子返回0,不是空也不是叶子的话就求其左子树的叶子+右子树的叶子
int TreeLeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
if (root->left == NULL && root->right == NULL)
{
return 1;
}
return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
4.3求二叉树第k层结点个数
求当前树的第k层结点个数=其左子树第k-1层结点个数+其右子树第k-1层结点个数,k=1时返回1
比如上图K=3,求A的第3层结点个数就是求B的第2层的节点+C的第2层的结点。
int TreeKLevelSize(BTNode* root, int k)
{
assert(k > 0);
if (root == NULL) //空结点返回0
{
return 0;
}
if (k == 1)
{
return 1;
}
return TreeKLevelSize(root->left, k - 1)
+ TreeKLevelSize(root->right, k - 1);
}
4.4查找树里面值为x的那个结点
思路:空就返回空,找到了就返回这个结点,没找到就到左子树和右子树去找,都没找到就返回空
BTNode* TreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
BTNode* lret = TreeFind(root->left, x);
if (lret)
{
return lret;
}
BTNode* rret = TreeFind(root->right, x);
if (rret)
{
return rret;
}
return NULL;
}
比如要找E的位置:(找不到前都返回空)
4.5 二叉树销毁
这里采用后序遍历的销毁方式,因为用前序和中序要保存结点。
// 一般,如果选择一级指针,存在野指针问题,调用BinaryTreeDestory的人,把指针置空
// 这样保持接口的一致性
void BinaryTreeDestory(BTNode* root)
{
if (root == NULL)
{
return;
}
BinaryTreeDestory(root->left);
BinaryTreeDestory(root->right);
//printf("%p %c\n", root, root->data);
free(root);
root = NULL;
}
前面函数的代码和测试:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef char BTDataType;
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* left; // 记录左节点
struct BinaryTreeNode* right; // 记录右节点
BTDataType data; // 存储数据
} BTNode;
//创建新节点
BTNode* CreateNode(BTDataType x)
{
BTNode* new_node = (BTNode*)malloc(sizeof(BTNode));
if (new_node == NULL)
{
printf("malloc failed!\n");
exit(-1);
}
new_node->data = x;
new_node->left = new_node->right = NULL;
return new_node;
}
//手动创建二叉树
BTNode* CreateBinaryTree()
{
BTNode* nodeA = CreateNode('A');
BTNode* nodeB = CreateNode('B');
BTNode* nodeC = CreateNode('C');
BTNode* nodeD = CreateNode('D');
BTNode* nodeE = CreateNode('E');
BTNode* nodeF = CreateNode('F');
nodeA->left = nodeB; // A
nodeA->right = nodeC; // B C
nodeB->left = nodeD; // D E F
nodeC->left = nodeE;
nodeC->right = nodeF;
return nodeA;
}
//二叉树前序遍历
void PreOrder(BTNode* root)
{
//首先判断根是否为空,为空就返回
if (root == NULL)
{
printf("NULL "); // 暂时打印出来,便于观察
return;
}
//走到这里说明不为空,根据前序遍历,先访问根节点
printf("%c ", root->data);
//然后遍历左子树(利用递归)
PreOrder(root->left);
//最后遍历右子树(利用递归)
PreOrder(root->right);
// A
// B C
// D E F 前序: 根 左 右
//执行输出: A B D NULL NULL NULL C E NULL NULL F NULL NULL
}
//二叉树中序遍历
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->left);
printf("%c ", root->data);
InOrder(root->right);
// A
// B C
// D E F 中序:左 根 右
//执行输出:NULL D NULL B NULL A NULL E NULL C NULL F NULL
}
//二叉树后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%c ", root->data);
// A
// B C
// D E F 后序:左 右 根
//执行输出:NULL NULL D NULL B NULL NULL E NULL NULL F C A
}
int TreeSize(BTNode* root)
{
return root == NULL ? 0
: TreeSize(root->left) + TreeSize(root->right) + 1;
}
int TreeLeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
if (root->left == NULL && root->right == NULL)
{
return 1;
}
return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
int TreeKLevelSize(BTNode* root, int k)
{
assert(k > 0);
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return TreeKLevelSize(root->left, k - 1)
+ TreeKLevelSize(root->right, k - 1);
}
// 查找树里面值为x的那个节点
BTNode* TreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
BTNode* lret = TreeFind(root->left, x);
if (lret)
{
return lret;
}
BTNode* rret = TreeFind(root->right, x);
if (rret)
{
return rret;
}
return NULL;
}
// 一般,如果选择一级指针,存在野指针问题,调用BinaryTreeDestory的人,把指针置空
// 这样保持接口的一致性
void BinaryTreeDestory(BTNode* root)
{
if (root == NULL)
{
return;
}
BinaryTreeDestory(root->left);
BinaryTreeDestory(root->right);
//printf("%p %c\n", root, root->data);
free(root);
root = NULL;
}
int main()
{
BTNode* root = CreateBinaryTree();
PreOrder(root);
printf("\n");
InOrder(root);
printf("\n");
PostOrder(root);
printf("\n");
printf("TreeSize:%d\n", TreeSize(root));
printf("TreeLeafSize:%d\n", TreeLeafSize(root));
printf("TreeKLevelSize:%d\n", TreeKLevelSize(root, 3));
printf("TreeFind:%p\n", TreeFind(root, 'E'));
printf("TreeFind:%p\n", TreeFind(root, 'F'));
printf("TreeFind:%p\n", TreeFind(root, 'X'));//打印0 :找不到
BinaryTreeDestory(root);
root = NULL;
return 0;
}
5.二叉树广度优先遍历
5.1层序遍历
层序遍历(Level Traversal):设二叉树的根节点所在的层数为1的情况下,
从二叉树的根节点出发,首先访问第1层的树根节点,然后再从左到右访问第2层上的节点。
接着是第3层的节点……以此类推,自上而下、从左向右地逐层访问树的节点。
该如何实现层序遍历呢? 可以利用队列的性质来实现。
之前再讲过队列,这里你可以选择自己实现一个队列。
如果不想实现就直接复制即可,因为这里重点要学的是层序遍历。
这里就先不实现了,下一篇写写二叉树OJ,下下篇再实现(最下面已附链接)
6.笔试选择题
练习1:
2-3树是一种特殊的树,它满足两个条件:
(1) 每个内部结点有两个或三个子结点
(2) 所有的叶结点到根的距离相同
如果一颗2-3树有10个结点,那么它有( )个叶结点。
A.7
B.8
C.7 或 8
D.6
练习2:
设某种二叉树有如下特点:每个结点要么是叶子结点,要么有2棵子树。假如一棵这样的二叉树中有m(m>0)个叶子结点,那么该二叉树上的结点总数为( )
A.2m+1
B.2(m-1)
C.2m-1
D.2m
练习3:
设根结点的深度为1,则一个拥有n个结点的二叉树的深度一定在( )区间内
A.[log(n + 1),n]
B.[logn,n]
C.[log(n + 1),n - 1]
D.[log(n + 1),n + 1]
练习4:
对任意一颗二叉树,设N0、N1、N2分别是度为0、1、2的结点数,则下列式子中一定正确的是( )
A.N0 = N2 + 1
B.N1 = N0 + 1
C.N2 = N0 + 1
D.N2 = N1 + 1
练习5:
二叉树的( )遍历相当于广度优先遍历,( )遍历相当于深度优先遍历
A.前序 中序
B.中序 前序
C.层序 后序
D.层序 前序
练习6:
如果一颗二叉树的前序遍历的结果是ABCD,则满足条件的不同的二叉树有( )种
A.13
B.14
C.15
D.16
答案:
1.答案:D
根据题目意思,每一个非叶子节点至少有两个孩子节点,并且叶子节点都在同一层,所以,假设树的高度为h, 则二三树种最小的节点个数为满二叉树的个数:2^h - 1, 最大个数: (3^h - 1) / 2。所以 2^h - 1 < 10 < (3^h - 1) / 2, h为3,结构是1(3(2,2,2))。所以叶节点个数为6
2.答案:C
根据二叉树的性质,在任意的二叉树中,度为0的节点比度为2的节点多了1个----见课件
现在叶子节点为m个,即度为0的节点有m个,那度为2的节点个数就为m-1个
而题目说该二叉树中只有度为2和度为0的节点 ,因此总的节点数就为:m+m-1 = 2m-1
3答案:A
最大深度: 即每次只有一个节点,次数二叉树的高度为n,为最高的高度
最小深度: 此树为完全二叉树, 如果是完全二叉树
根据二叉树性质,完全二叉树的高低为 h = log(n+1)向上取整
4答案:A
节点总数N: N = N0 + N1 + N2
度和边的关系: N - 1 = 0 * N0 + 1 * N1 + 2 * N2
上面两个式子可以推出: N0 + N1 + N2 - 1 = N1 + 2 * N2
可得: N0 = N2 + 1
5答案:D(前中后序遍历都可以称为深度优先遍历,但是单选的话就选前序)
广度优先需要把下一步所有可能的位置全部遍历完,才会进行更深层次的遍历,
层序遍历就是一种广度优先遍历。
深度优先是先遍历完一条完整的路径(从根到叶子的完整路径),才会向上层折返,
再去遍历下一个路径,前序遍历就是一种深度优先遍历。
6答案:B
首先这棵二叉树的高度一定在3~4层之间:
三层:
A(B(C,D),()), A((),B(C,D)), A(B(C,()),D), A(B((),C),D),
A(B,C(D,())), A(B,C((),D))
四层:
如果为四层,就是单边树,每一层只有一个节点,除过根节点,其他节点都有两种选择,
在上层节点的左边还是右边,所以2*2*2共8种,总共为14种。
本篇完。(附下两篇链接)
下一篇写写二叉树的OJ题,穿越回来复习顺便贴个链接:
数据结构与算法⑮(第四章_下)二叉树OJ(力扣:144,965,104,110,226,100,101,572)_GR C的博客-CSDN博客
(想了想层序遍历后面还是更新了,不过还是建议按文章序号先看OJ)