• 第五章 树和二叉树(上)【24王道数据结构笔记】


    1.树的概念

    1.1 树的基本定义 

     

    树:n(n>=0)个节点的有限集合,是一种逻辑结构,当n=0时为空树,且非空树满足:

    • 有且仅有一个特定的称为根的节点.
    • 当n>1时,其余结点可分为m (m >0) 个互不相交的有限集合T1,T2,...,Tm,其中每个集合本身又是一棵树,并且称为根结点的子树。

    树是一种递归的数据结构 

    非空树特点:

    • 有且仅有一个根节点
    • 没有后继的结点称为“叶子结点”(或终端节点)
    • 有后继的结点称为“分支结点” (或非终端结点)
    • 除了根节点外,任何一个结点都有且仅有一个前驱
    • 每个结点可以有0个或多个后继

    1.2 基本术语 

    • 祖先结点:自己的之上都是祖先节点。
    • 子孙结点:自己的之下都是子孙节点。
    • 双亲结点 (父节点) :和自己相连的上一个就是父节点。
    • 孩子结点:和自己相连的下面一个。
    • 兄弟结点:我自己同一个父节点的。
    • 堂兄弟结点:同一层的节点。

    属性:

    • 结点的层次(深度)--从上往下数,默认从 1开始
    • 结点的高度-一从下往上数
    • 树的高度 (深度)-一总共多少层
    • 结点的度--有几个孩子(分支),叶子结点 的度=0
    • 树的度一-各结点的度的最大值
    • 两个结点之间的路径——从上往下经历的结点
    • 路径长度——经过的边的的条数

    有序树和无序树

    • 有序树--逻辑上看,树中结点的各子树从左至右是有次序的,不能互换
    • 无序树--逻辑上看,树中结点的各子树从左至右是无次序的,可以互换

    选用有序树还是无序树具体看你用树存什么,是否需要用结点的左右位置反映某些逻辑关系

    1.3.森林

    森林是m(>=0)棵互不相交的树的集合。

    m可为0——空森林

    2. 树的常考性质 

    • 常见考点1:结点数=总度数+1

    结点的度——结点有几个孩子(分支),最后再加上根结点

    • 常见考点2:度为m的树、m叉树 的区别 

     树的度——各结点的度的最大值,m叉树——每个结点最多只能有m个孩子的树

    • 常见考点3:度为m的树第 i 层至多有 mi-1 个结点(i≥1) m叉树第 i 层至多有 mi-1 个结点(i≥1)

    • 常见考点4: 高度为h的m叉树至多有\frac{m^{h}-1}{m-1}个结点。

    等比数列求和公式:

    • 常见考点5: 高度为h的m叉树至少有h个结点;高度为h、度为m的树至少有h+m-1个结点。
    • 常见考点6: 具有n个结点的m叉树的最小高度为\log_{m}[n(m-1)+1](考点4反解)

    高度最小的情况——所有结点都有m个孩子

     3. 二叉树

     ​​​

    3.1 二叉树的定义

    二叉树是n (n>=0)个结点的有限集合

    1. 或者为空二叉树,即n =0。
    2. 或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。

    特点:

    • 每个结点至多只有两棵子树
    • 左右子树不能颠倒 (二叉树是有序树)
    • 二叉树可以是空集合,根可以有空的左子树和空的右子树
       

    注意区别:度 为2的有序树(至少有一个结点度为2) 

    二叉树的五种状态

    3.2 特殊二叉树

     3.2.1 满二叉树

    一棵深度为k且有2^{k-1}个结点的二叉树称为满二叉树。特点:

    • 每一层上的结点数都达到最大,不存在度为 1 的结点
    • 叶子全部在最低层。
    • 按层序从1开始编号,结点i的左孩子为 2i,右孩子为 2i+1;结点i的父节点为\left \lfloor i/2 \right \rfloor(向下取整)
    • i<=\left \lfloor i/2 \right \rfloor为分支结点,i>\left \lfloor i/2 \right \rfloor为叶子结点

     

    3.2.2 完全二叉树

    深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号为1~n的结点一一对应时,称之为完全二叉树。特点:

    • 只有最后两层可能有叶子结点,
    • 最多只有一个度为1的结点(0个或1个),如果某结点只有一个孩子, 那么一定是左孩子
    • 按层序从1开始编号,结点i的左孩子为 2i,右孩子为 2i+1;结点i的父节点为[i/2]
    • i<=\left \lfloor i/2 \right \rfloor为分支结点,i>\left \lfloor i/2 \right \rfloor为叶子结点

     

    3.2.3 二叉排序树:

    一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:

    • 左子树上所有结点的关键字均小于根结点的关键字;
    • 右子树上所有结点的关键字均大于根结点的关键字;
    • 左子树和右子树又各是一棵二叉排序树。

    二叉排序树可用于元 素的排序、搜索 

     3.2.4 平衡二叉树

    树上任一结点的左子树和右子树的深度之差不超过1。

    平衡二叉树能有 更高的搜索效率

    3.3 二叉树的性质

    3.3.1 二叉树的常考性质

    常见考点1:设非空二叉树中度为0、1和2的结点个数分别为n_{0}n_{1},和n_{2},则n_{0}=n_{2}+1(叶子结点比二分支结点多一个)

    常见考点2:二叉树第i层至多有2^{i-1}个结点 (i>=1);m叉树第i层至多有m^{i-1}个结点 (i>=1)

    常见考点3:高度为h的二叉树至多有2^{h}-1个结点(满二叉树);高度为h的m叉树至多\frac{m^{h}-1}{m-1}结点 

    3.3.2 完全二叉树的常考性质

    常见考点1:(注意此处是完全二叉树才具备的)

     

    常见考点2:对于完全二叉树,可以由总结点数 n 推出度为 0、1 和 2 的结点个数n_{0}n_{1}n_{2}

     推导过程:
            因为:n_{0} = n_{2}+1:所以n_{0} + n_{2}为奇数
            又因为:n=n_{0} +n_{1}+n_{2}
            所以:若完全二叉树有偶数n个节点,则n_{1}为1;n_{0}\frac{n}{2}n_{2}\frac{n}{2}-1
            若完全二叉树有奇数n个节点,则n_{1}为0;n_{0}\frac{n+1}{2}n_{2}\frac{n+1}{2}-1 

     

    3.4二叉树存储实现

    3.4.1 二叉树的顺序存储:

    二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来;

    可以让第一个位置空缺,保 证数组下标和结点编号一致 

    定义一个长度为 MaxSize 的数组 t ,按照从上至下、从左至右的顺序依次存储完全二叉树中的各个结点,代码实现如下

    1. #define MaxSize 100
    2. struct TreeNode{
    3. ElemType value; //结点中的数据元素
    4. bool isEmpty; //结点是否为空
    5. }
    6. // 初始化时所有结点标记为空
    7. void InItTree(TreeNode t[])
    8. {
    9. for (int i=0; i
    10. t[i].isEmpty = true;
    11. }
    12. }
    13. main(){
    14. TreeNode t[MaxSize];
    15. InItTree(t);
    16. }

    几个重要常考的基本操作:

    • i 的左孩子:2i
    • i 的右孩子:2i+1
    • i 的父节点:i/2
    • i 所在的层次:\log_{2}(n+1)或者\log_{2}n+1

    若完全二叉树中共有n个结点,则

    • 判断i是否有左孩子?  2i\leqslant n ?
    • 判断i是否有右孩子?  2i+1\leqslant n ?
    • 判断i是否是叶子/分支结点?  i>n/2 ?

    注:如果不是完全二叉树,依然按层序将各节点顺序存储,那么无法从结点编号反映 出结点间的逻辑关系

     二叉树的顺序存储中,一定要把二叉 树的结点编号与完全二叉树对应起来

     最坏情况:高度为 h 且只有 h 个结点的单 支树(所有结点只有右孩子),也至少需 要 2h-1 个存储单元

    结论:二叉树的顺序存储结构,只适合存 储完全二叉树

    3.4.2 二叉树的链式存储 

    包含数据域和左右孩子指针,n个结点的二叉链表共有 n+1 个空链域,可以用于构造 线索二叉树

    1. //二叉树的结点
    2. struct ElemType{
    3. int value;
    4. };
    5. typedef struct BiTnode{
    6. ElemType data; //数据域
    7. struct BiTNode *lchild, *rchild; //左、右孩子指针
    8. }BiTNode, *BiTree;
    9. //定义一棵空树
    10. BiTree root = NULL;
    11. //插入根节点
    12. root = (BiTree) malloc (sizeof(BiTNode));
    13. root->data = {1};
    14. root->lchild = NULL;
    15. root->rchild = NULL;
    16. //插入新结点
    17. BiTNode *p = (BiTree) malloc (sizeof(BiTNode));
    18. p->data = {2};
    19. p->lchild = NULL;
    20. p->rchild = NULL;
    21. root->lchild = p; //作为根节点的左孩子

    优点:找到指定结点 p 的左/右孩子——超简单

    缺点:找指定结点 p 的 父结点只能从根开始遍历寻找

    解决:三叉链表——方便 找父结点(根据实际需求决定要不要加父结点指针)

    1. struct ElemType{
    2. int value;
    3. };
    4. typedef struct BiTnode{
    5. ElemType data; //数据域
    6. struct BiTNode *lchild, *rchild; //左、右孩子指针
    7. struct BiTNode *parent; // 父结点指针
    8. }BiTNode, *BiTree;

    4.二叉树的先中后序遍历

    • 遍历:按照某种次序把所有结点都访问一遍。
    • 层次遍历:基于树的层次特性确定的次序规则

    二又树的递归特性:

    • 要么是个空二叉树
    • 要么就是由“根节点+左子树+右子树”组成的二叉树 

    4.1二叉树的先中后遍历

    4.1.1 二叉树先中后序遍历的递归实现

    • 先序遍历:根左右(NLR)
    1. typedef struct BiTnode{
    2. ElemType data;
    3. struct BiTNode *lchild, *rchild;
    4. }BiTNode, *BiTree;
    5. void PreOrder(BiTree T){
    6. if(T!=NULL){
    7. visit(T); //访问根结点
    8. PreOrder(T->lchild); //递归遍历左子树
    9. PreOrder(T->rchild); //递归遍历右子树
    10. }
    11. }

    C++实现

    1. void traversal(TreeNode* cur, vector<int>& vec) {
    2. if (cur == NULL) return;
    3. vec.push_back(cur->val); // 中
    4. traversal(cur->left, vec); // 左
    5. traversal(cur->right, vec); // 右
    6. }
    7. vector<int> preorderTraversal(TreeNode* root) {
    8. vector<int> result;
    9. traversal(root, result);
    10. return result;
    11. }
    12. };
    • 中序遍历:左根右 (LNR)
    1. typedef struct BiTnode{
    2. ElemType data;
    3. struct BiTNode *lchild, *rchild;
    4. }BiTNode, *BiTree;
    5. void InOrder(BiTree T){
    6. if(T!=NULL){
    7. InOrder(T->lchild); //递归遍历左子树
    8. visit(T); //访问根结点
    9. InOrder(T->rchild); //递归遍历右子树
    10. }
    11. }
    • 后序遍历:左右根(LRN)
    1. typedef struct BiTnode{
    2. ElemType data;
    3. struct BiTNode *lchild, *rchild;
    4. }BiTNode, *BiTree;
    5. void PostOrder(BiTree T){
    6. if(T!=NULL){
    7. PostOrder(T->lchild); //递归遍历左子树
    8. PostOrder(T->rchild); //递归遍历右子树
    9. visit(T); //访问根结点
    10. }
    11. }

    4.1.2 用栈实现了二叉树先中后序的迭代遍历(非递归)。

    基本思路:将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记,只有空节点弹出的时候,才将下一个节点放进结果集如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。

    迭代法中序遍历

    1. vector<int> inorderTraversal(TreeNode* root) {
    2. vector<int> result;
    3. stack st;
    4. if (root != NULL) st.push(root);
    5. while (!st.empty()) {
    6. TreeNode* node = st.top();
    7. if (node != NULL) {
    8. st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
    9. if (node->right) st.push(node->right); // 添加右节点(空节点不入栈)
    10. st.push(node); // 添加中节点
    11. st.push(NULL); // 中节点访问过,但是还没有处理,加入空节点做为标记。
    12. if (node->left) st.push(node->left); // 添加左节点(空节点不入栈)
    13. } else { // 只有遇到空节点的时候,才将下一个节点放进结果集
    14. st.pop(); // 将空节点弹出
    15. node = st.top(); // 重新取出栈中元素
    16. st.pop();
    17. result.push_back(node->val); // 加入到结果集
    18. }
    19. }
    20. return result;
    21. }

    迭代法前序遍历 (注意此时我们和中序遍历相比仅仅改变了两行代码的顺序)

    1. vector<int> preorderTraversal(TreeNode* root) {
    2. vector<int> result;
    3. stack st;
    4. if (root != NULL) st.push(root);
    5. while (!st.empty()) {
    6. TreeNode* node = st.top();
    7. if (node != NULL) {
    8. st.pop();
    9. if (node->right) st.push(node->right); // 右
    10. if (node->left) st.push(node->left); // 左
    11. st.push(node); // 中
    12. st.push(NULL);
    13. } else {
    14. st.pop();
    15. node = st.top();
    16. st.pop();
    17. result.push_back(node->val);
    18. }
    19. }
    20. return result;
    21. }

    迭代法后序遍历

    1. class Solution {
    2. public:
    3. vector<int> postorderTraversal(TreeNode* root) {
    4. vector<int> result;
    5. stack st;
    6. if (root != NULL) st.push(root);
    7. while (!st.empty()) {
    8. TreeNode* node = st.top();
    9. if (node != NULL) {
    10. st.pop();
    11. st.push(node); // 中
    12. st.push(NULL);
    13. if (node->right) st.push(node->right); // 右
    14. if (node->left) st.push(node->left); // 左
    15. } else {
    16. st.pop();
    17. node = st.top();
    18. st.pop();
    19. result.push_back(node->val);
    20. }
    21. }
    22. return result;
    23. }
    24. };

     4.2 二叉树的层序遍历

    算法思想:

    • 1.初始化一个辅助队列
    • 2.根结点入队
    • 3.若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
    • 4.重复3直至队列为空

    1. //二叉树的结点(链式存储)
    2. typedef struct BiTnode{
    3. ElemType data;
    4. struct BiTNode *lchild, *rchild;
    5. }BiTNode, *BiTree;
    6. //链式队列结点
    7. typedef struct LinkNode{
    8. BiTNode *data; // 存结点指针而不是结点
    9. typedef LinkNode *next;
    10. }LinkNode;
    11. typedef struct{
    12. LinkNode *front, *rear;
    13. }LinkQueue;
    14. //层序遍历
    15. void LevelOrder(BiTree T){
    16. LinkQueue Q;
    17. InitQueue (Q); //初始化辅助队列
    18. BiTree p;
    19. EnQueue(Q,T); //将根节点入队
    20. while(!isEmpty(Q)){ //队列不空则循环
    21. DeQueue(Q,p); //队头结点出队
    22. visit(p); //访问出队结点
    23. if(p->lchild != NULL)
    24. EnQueue(Q,p->lchild); //左孩子入队
    25. if(p->rchild != NULL)
    26. EnQueue(Q,p->rchild); //右孩子入队
    27. }
    28. }

    C++实现

    队列法

    1. vectorint>> levelOrder(TreeNode* root) {
    2. queue que;
    3. if (root != NULL) que.push(root);
    4. vectorint>> result;
    5. while (!que.empty()) {
    6. int size = que.size();
    7. vector<int> vec;
    8. // 这里一定要使用固定大小size,不要使用que.size(),因为que.size是不断变化的
    9. for (int i = 0; i < size; i++) {
    10. TreeNode* node = que.front();
    11. que.pop();
    12. vec.push_back(node->val);
    13. if (node->left) que.push(node->left);
    14. if (node->right) que.push(node->right);
    15. }
    16. result.push_back(vec);
    17. }
    18. return result;
    19. }

    递归法

    1. void order(TreeNode* cur, vectorint>>& result, int depth)
    2. {
    3. if (cur == nullptr) return;
    4. if (result.size() == depth) result.push_back(vector<int>());
    5. result[depth].push_back(cur->val);
    6. order(cur->left, result, depth + 1);
    7. order(cur->right, result, depth + 1);
    8. }
    9. vectorint>> levelOrder(TreeNode* root) {
    10. vectorint>> result;
    11. int depth = 0;
    12. order(root, result, depth);
    13. return result;
    14. }

    4.3 由遍历序列构造二叉树

    • 一个前序遍历序列可能对应多种二叉树形态。
    • 同理,一个后序遍历序列、一个中序遍历序列、一个层序遍历序列也可能对应多种二叉树形态。

    结论:若只给出一棵二叉树的 前/中/后/层序遍历序列 中的一种,不能唯一确定一棵二叉树。

    由二叉树的遍历序列构造二叉树:

    4.3.1 前序+中序遍历序列

    由 前序+中序遍历序列 构造二叉树:由前序遍历的遍历顺序(根节点、左子树、右子树)可推出根节点,由根节点在中序遍历序列中的位置即可推出左子树与右子树分别有哪些结点。

    4.3.2 后序+中序遍历序列

    由 后序+中序遍历序列 构造二叉树:由后序遍历的遍历顺序(左子树、右子树、根节点)可推出根节点,由根节点在中序遍历序列中的位置即可推出左子树与右子树分别有哪些结点。

    4.3.3 层序+中序遍历序列

    由 层序+中序遍历序列 构造二叉树:由层序遍历的遍历顺序(层级遍历)可推出根节点,由根节点在中序遍历序列中的位置即可推出左子树与右子树分别有哪些结点。

    前序、后序、层序 序列的两两组合无法唯一 确定一科二叉树

  • 相关阅读:
    【Mysql】Mysql基本概念和操作
    c++ vector用法 入门必看 超详细
    git 学习笔记
    小程序wx:if 和hidden的区别?
    typora操作手册
    关于视频封装格式和视频编码格式的简介
    【C++提高编程】第一章 模板:函数模板|类模板|
    继认证后弄清Spring Security实现授权
    【Linux成长史】Linux编辑器-gcc/g++使用
    学习linux从0到工程师(命令)-4
  • 原文地址:https://blog.csdn.net/zhendong825/article/details/134466119