数据结构--二叉树详解

news2024/11/15 8:22:05

一,概念

1,结点的度:一个结点含有子树的个数称为该结点的度

2, 树的度:一棵树中,所有结点度的最大值称为树的度;

3,叶子结点或终端结点:度为0的结点称为叶结点; 

4,双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点,

5,孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点;

6,根结点:一棵树中,没有双亲结点的结点;

7,树的高度或深度:树中结点的最大层次;

二,二叉树的分类

1,满二叉树

每层的结点数都达到最大值,则这棵二叉树就是满二叉树。满二叉树是一种特殊的完全二叉树

2,完全二叉树

从上到下,从左到右一次排列

三,二叉树的基本性

1,若规定根节点的层数为1,则一颗非空二叉树的第i层上最多有2^i-1个节点

2, 如规定根结点的二叉树的深度为为1,则深度为k的二叉树的最大节点数是2^-1

3,对任意一颗二叉树,如果其叶节点的个数为n0,度为2的非叶节点个数为n2,则有n0=n2+1

4,具有n个节点的完全二叉树的深度为log2(n+1)上取整

5,对于具有n个节点的完全二叉树,如果按照从上至下从左至右所有节点从0开始编号,如果父节点的下标为i,则孩子节点为2i+1,2i+2。但是如果2i+1>=n,左无左孩子,若2i+2>=n,否则没有右孩子

6,知道孩子的下标,父节点的下标为(i-1)/2,如果i=0,则无则无双亲节点 

7, 二叉树存储分为顺序存储,和链式储存,链式储存是通过一个一个节点的引用起来的。

以链式储存为例:

例如:

public class BinaryTree {
    public TreeNode root;
    static class TreeNode{
        public char val;
        public TreeNode left;
        public TreeNode right;
        public TreeNode(char val) {
            this.val = val;
        }
    }
}

四,二叉树的遍历

分类:

二叉树的遍历分为大体四种:前序遍历,中序遍历,后序遍历,层序遍历。前三种遍历的方式又可以写为递归的形式和非递归的形式

前序遍历:

二叉树中的每一棵树都要符合先遍历根,然后遍历左树,最后是右树(根左右)

递归:

如果根为空,直接return。先打印根,然后打印左树,最后是右树

public void preOrder(TreeNode root){
    if (root==null){
        return;
    }
    System.out.print(root.val+" ");
    preOrder(root.left);
    preOrder(root.right);
}
非递归:
法一:

借助栈。先将根放到栈中,然后弹出,记录下来(赋值给cur),并打印。然后先将cur的右根放到栈中,再放cur的左根。在再弹出栈顶元素也就是左根,重复上述步骤(记录下来,并打印,将其右左树再放到栈中)

注意:

1,一定是先放右树,再放左树

2,将左右树放到栈中时要分别判断左右树是否为空,如果为空则不进栈

public void preOrder2(TreeNode root){
    Stack<TreeNode> stack=new Stack<>();
    stack.push(root);
    TreeNode cur=stack.pop();
    stack.push(cur.right);
    stack.push(cur.left);
    System.out.print(cur.val+" ");
    while (!stack.isEmpty()){
        cur=stack.pop();
        if (cur.right!=null){
            stack.push(cur.right);
        }
        if (cur.left!=null) {
            stack.push(cur.left);
        }
        System.out.print(cur.val+" ");
    }
}
法二:

借助栈。将root赋值给cur,进入两次循环,内循环是找到cur最左边的树并把过程中经过的每一个节点放到栈中,直到找到null。因为这里是前序遍历,所以每找到一个节点就要打印出来。当找到null时,走出循环。这时弹出栈顶元素,cur等于栈顶元素的右树(通过前面步骤,已知栈顶元素的左树为空)。然后cur开始外循环。由于内循环的条件是cur!=null,所以当右树为空时,不进入内循环,直接再次弹出栈顶元素。如果不为空是,则进入内循环,寻找它的左树……

public void preOrder3(TreeNode root){
    Stack<TreeNode> stack=new Stack<>();
    TreeNode cur=root;
    while (cur!=null||!stack.isEmpty()){
        while (cur!=null){
            stack.push(cur);
            System.out.print(cur.val+" ");
            cur=cur.left;
        }
        TreeNode old=stack.pop();
        cur=old.right;
    }
    System.out.println();
}

中序遍历:

二叉树中的每一棵树都要符合先遍历左树,然后遍历根,最后是右树(左根右)

递归:
public void midOrder(TreeNode root){
    if (root==null){
        return;
    }
    midOrder(root.left);
    System.out.print(root.val+" ");
    midOrder(root.right);
非递归:

与前序遍历非递归的法二原理相似,只是根打印的位置不相同,所以在走内循环时不打印节点,走完后,在打印,这样保证先打印的是左树。

public void midOrder2(TreeNode root){
    Stack<TreeNode>stack=new Stack<>();
    TreeNode cur=root;
    while (cur!=null||!stack.isEmpty()){
        while (cur!=null){
            stack.push(cur);
            cur=cur.left;
        }
        TreeNode old=stack.pop();
        System.out.print(old.val+" ");
        cur=old.right;
    }
    System.out.println();
}

后序遍历:

二叉树中的每一棵树都要符合先遍历左树,然后遍历右树,最后是根(左右根)

递归:
public void postOrder(TreeNode root){
    if (root==null){
        return;
    }
    postOrder(root.left);
    postOrder(root.right);
    System.out.print(root.val+" ");
}
非递归:

与中序遍历非递归思路相似,只是根打印的位置不相同,所以在走内循环,及走完内循环后均不打印,完成内循环后,直接判断栈顶元素右树是否为空,如果为空,就可以弹出栈顶元素,并且打印。但是如果不为空,就要先走右树(因为后序遍历的顺序是左右根,已知没有左树,所以要先打印右树)。

注意:要把每次遍历完的节点储存一下。因为每次内循环走完左树为空时,到判断栈顶元素A右树时(在右树不为空的情况下),这时会开始遍历右树,当遍历完右树后,又会回到这个起点(判断该栈顶元素A是否有右树),这时就会进入死循环,所以这里的判断条件进行丰富,即栈顶元素既有右树且之前没有遍历过【注意栈顶元素A,只是举了一个例子,方便理解!】

public void postOrder2(TreeNode root){
    Stack<TreeNode>stack=new Stack<>();
    TreeNode cur=root;
    TreeNode prev=null;
    while (cur!=null||!stack.isEmpty()){
        while (cur!=null){
            stack.push(cur);
            cur=cur.left;
        }
        TreeNode old=stack.peek();
        if (old.right==null||prev==old.right){
            stack.pop();
            System.out.print(old.val+" ");
            prev=old;
        }else {
            cur=old.right;

        }
    }
    System.out.println();
}

层序遍历:

一层一层的进行遍历

这里我们用到了队列,先把根放进去,弹出时,记录下来(赋值到ret中)并打印,然后根据ret,将ret的左树和右树也放到队列里面,重复上述步骤(弹出,记录下来,并打印,将其左右树再放到队列中),循环上述步骤,直到队列为空,则遍历完成。需要注意的是:将左右树放到队列中时要分别判断左右树是否为空,如果为空则不进队列,只有不为空时,才能放入。

法一:
public void levelOrder(TreeNode root){
    Queue<TreeNode> queue=new LinkedList<>();
    if (root==null){
        return;
    }
    queue.offer(root);
    while (!queue.isEmpty()){
        TreeNode ret=queue.peek();
        if (ret.left!=null){
            queue.offer(ret.left);
        }
        if (ret.right!=null){
            queue.offer(ret.right);
        }
        System.out.print(queue.poll().val+" ");
    }
    System.out.println();
}
法二:

这种方法是将每一层的节点放到一个链表中,然后将每一层的的链表放到一个“大的链表”中。先将根放到队列中,计算这一层的大小size,则决定着这一层的的链表的大小。然后循环size次,从而将这一层的每个元素均放到该层链表中。然后将这一层的每个元素的左右树再放到队列中,重复上述步骤,直到链表为空。

public List<List<Character>> levelOrder2(TreeNode root){
    List<List<Character>> ret=new LinkedList<>();
    if (root==null){
        return ret;
    }
    Queue<TreeNode> queue=new LinkedList<>();
    queue.offer(root);
    while (!queue.isEmpty()){
        int size= queue.size();
        List<Character> list=new LinkedList<>();
        while (size>0){
            TreeNode node=queue.peek();
            if (node.left!=null){
                queue.offer(node.left);
            }
            if (node.right!=null) {
                queue.offer(node.right);
            }
            list.add(queue.poll().val);
            size--;
        }
        ret.add(list);
    }
    return ret;

}

五,求二叉树的简单性质

1,一棵树的节点个数

法一:

我们遍历二叉树时,遍历了每个节点,所以只需要将遍历中打印的步骤改为count++,就可以得到节点的个数

public static  int sizeNode;
public void size2(TreeNode root) {
    if (root==null){
        return ;
    }
    sizeNode++;
    size2(root.left);
    size2(root.right);
}

法二:

一棵树的节点个数=这棵树的左子树的节点个数+右子树的节点个数+1.(这个1是根节点)

public int size(TreeNode root){
    if (root==null){
        return 0;
    }
    return 1+size(root.left)+size(root.right);
}

2,求叶子节点的个数

法一:

叶子结点的性质是左子树和右子树均为null,所以当遇到这样的节点时,返回1。整颗树的叶子结点个数=左子树叶子节点的个数+右子树叶子结点个数

public int getLeafNodeCount(TreeNode root){
    if (root==null){
      return 0;
    }
   if (root.left==null&&root.right==null){
       return 1;
   }
   return getLeafNodeCount(root.left)+getLeafNodeCount(root.right);
}

法二:

也可以遍历二叉树,找到左子树和右子树均为null的节点,count++.

public static  int sizeLeafNode;
public void getLeafNodeCount2(TreeNode root){
    if (root==null){
        return ;
    }
    if (root.left==null&&root.right==null){
       sizeLeafNode++;
    }
    getLeafNodeCount2(root.left);
    getLeafNodeCount2(root.right);

}

3,获取k层节点的个数

我们每递归一层时,让参数k-1,这样当k==1时,就是k层的节点,我们只需要返回1,整颗树的k层结点个数=左子树的k层节点的个数+右子树的k层结点个数

public int getKLevelNodeCount(TreeNode root,int k){
   if (root==null ){
       return 0;
   }
   if (k==1){
       return 1;
   }
   return getKLevelNodeCount(root.left,k-1)
           +getKLevelNodeCount(root.right,k-1);
}

4,树的高度

树的高度=左子树高度和右子树高度的最大值+1

public int getHeight(TreeNode root){
    if (root==null){
        return 0;
    }
    int leftHeight=getHeight(root.left);
    int rightHeight=getHeight(root.right);
    return Math.max(leftHeight,rightHeight)+1;
}

5,找到某个节点

如果找到该节点,返回该节点的根。左子树递归完后,如果找到了,直接返回,如果没有找到,再右子树递归,这样可以提高效率

public TreeNode find(TreeNode root,char val){
    if (root==null){
        return null;
    }
    if (root.val==val){
        return root;
    }
    TreeNode ret=find(root.left,val);
    if (ret!=null){
        return ret;
    }
    ret=find(root.right,val);
    if (ret!=null){
        return ret;
    }else {
        return null;
    }
}

六,简单应用

1,检查两棵树是否相同

如果两棵树均为空,则相同。因为我们需要用递归来实现,所以写的时候我们用if语句(两棵树一定不相同的条件)来快速排除

排除条件

(1)如果一棵树为空,一棵树不为空,直接返回false

(2)如果对应节点的值不一样,直接返回false

当两棵树对应的左树与右树均相同,则两棵树相同

public boolean isSameTree(TreeNode p,TreeNode q){
    if (p==null&&q==null){
        return true;
    }
    if (p==null&&q!=null||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,第二棵树是否是第一棵树的子树

我们先找到第一棵树是否有节点与第二棵树的根结点一致,如果有相同的节点,调用方法一,看两棵树是否相同。我们分别从左树和右树中寻找,只要有一边找到了,就说明第二棵树是第一棵树的子树

public boolean isSubTree(TreeNode root,TreeNode subRoot){
    if (root==null&&subRoot!=null){
        return false;
    }
    if (root.val== subRoot.val){
       return isSameTree(root,subRoot);
    }
    return isSubTree(root.left,subRoot)
            ||isSubTree(root.right,subRoot);
}

3,翻转二叉树

当根为空,或者根的左右树均为空,则直接返回根。如果不是,交换左右树

private void swap(TreeNode root){
    TreeNode tmp=root.left;
    root.left=root.right;
    root.right=tmp;
}
public TreeNode reverseTree(TreeNode root){
    if (root==null||root.left==null&&root.right==null){
        return root;
    }
    swap(root);
    reverseTree(root.left);
    reverseTree(root.right);
    return root;
}

4,判断一棵二叉树是否是平衡二叉树

即:所有节点的高度差小于等于一

法一:

算左右树的高度,如果差的绝对值小于1,且左树和右树每一棵子树的左右树的高度差的绝对值小于1,则是平衡二叉树

public boolean isBalanced(TreeNode root){
    if (root==null){
        return true;
    }
    int leftHeight=getHeight(root.left);
    int rightHeight=getHeight(root.right);
    return Math.abs(leftHeight-rightHeight)<=1
            &&isBalanced(root.left)&&isBalanced(root.right);
}

法二(优化):

重写计算书的高度的方法,如果树的左右树的高度差小于1,则返回该树的高度,如果高度差大于1,返回-1,最后看树的高度是否大于0,如果大于0,则说明每一棵树的左右子树的高度差均小于1,如果小于0,则说明有树的左右子树的高度差均大于1,则不是平衡二叉树

public int getHeight2(TreeNode root){
   if (root==null){
       return 0;
   }
   int left=getHeight2(root.left);
   if (left<0){
       return -1;
   }
   int right=getHeight2(root.right);
   if (right<0){
       return -1;
   }
   if (Math.abs(left-right)<=1){
       return Math.max(left,right)+1;
   }else {
       return -1;
   }
}
public boolean isBalanced2(TreeNode root){
    if (root==null){
        return true;
    }
    return getHeight2(root)>0;

}

5,对称二叉树

如果根为空或者根的左树右树均为空,则是对称二叉树。

(1)如果跟的左树,右树一个为空一个不为空,则不是对称二叉树

(2)如果根的左树和右树的值不一样,则不是对称二叉树

然后判断左右,这两棵树是否镜面对称,我们再写一个子方法。

(1)如果这两棵树的左右树均为空,则是对称二叉树

(2)如果一棵树的左树,与一棵树的右树,一颗为空,一颗不为空,则不是对称二叉树

(3)如果一棵树的右树,与一棵树的左树,一颗为空,一颗不为空,则不是对称二叉树

(4)如果一棵树的左树,与另一棵树的右树的值不相同,或者一棵树的右树,与一棵树的左树的值不相同,则不是对称二叉树

private boolean isSymmetricChild(TreeNode p,TreeNode q){
    if (p.left==null&&p.right==null&&q.left==null&&q.right==null){
        return true;
    }
    if (p.left!=null&&q.right==null
            ||p.left==null&&q.right!=null
            ||p.right!=null&&q.left==null
            ||p.right==null&&q.left!=null){
        return false;
    }
    if (p.left.val!=q.right.val
            ||p.right.val!=q.left.val){
        return false;
    }
    return isSymmetricChild(p.left,q.right)
            &&isSymmetricChild(p.right,q.left);
}

public boolean isSymmetric(TreeNode root){
    if (root==null||root.left==null&&root.right==null){
        return true;
    }
    if (root.left==null&&root.right!=null
            ||root.left!=null&&root.right==null){
        return false;
    }
    if (root.left.val!=root.right.val){
        return false;
    }
    return isSymmetricChild(root.left,root.right);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1946464.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++内存管理(候捷)第四讲 笔记

上中下三个classes分析 Loki allocator的三个类&#xff0c;从低阶到高阶分别为&#xff1a;Chunk, FixedAllocator, SmallObjAllocator Chunk&#xff1a;pData指针&#xff0c;指向分配的一个chunk&#xff0c;firstAvailableBlock_索引&#xff0c;指向第一个可用区块是第几…

WHAT - 一个 Github 仓库的 License 如何解读

目录 一、背景二、解读许可证说明的作用常见的开源许可证类型使用他人代码仓库时需要注意的事项结论 实践作为开发者1. 选择许可证类型2. 在 README 文件中编写许可证信息 作为使用者1. 确定权限2. 了解和遵守条款 总结 一、背景 我们经常在一些 Github 仓库里看到 License 部…

GAMES104:05游戏引擎中的渲染系统2:渲染中的光照、材质和shader-学习笔记

文章目录 一、渲染方程及其挑战二、基础光照解决方案-简化版简化光源简化材质简化阴影 三、基于预计算的全局光照3.1挑战和计算思路傅里叶变换球谐函数&#xff08;Spherical Harmonics&#xff09; 3.2 SH Lightmap&#xff1a;预计算 GI3.3 探针 Probe&#xff1a;Light Prob…

uni-app:踩坑路---关于使用了transform导致fixed定位不生效的问题

前言&#xff1a; 继续记录&#xff0c;在上篇文章中&#xff0c;弹出框遮罩层在ios上没有正确的铺盖全屏&#xff0c;是因为机型的原因&#xff0c;也和我们的代码结构有相关的问题。今天再来展示另外一个奇葩的问题。 这次我使用了在本篇博客中的弹出框组件CustomDialog.vue…

Matlab进阶绘图第65期—带分组折线段的柱状图

带分组折线段的柱状图是在原始柱状图的基础上&#xff0c;在每组柱状图位置处分别添加折线段&#xff0c;以进行对比或添加额外信息。 由于Matlab中未收录带分组折线段的柱状图的绘制函数&#xff0c;因此需要大家自行设法解决。 本文使用自制的BarwithGroupedLine小工具进行…

景区AR导航营销系统:技术解决方案与实施效益分析

随着旅游市场的竞争日益激烈&#xff0c;景区需要不断创新以吸引游客。景区 AR 导航将虚拟画面与现实场景相结合&#xff0c;为游客提供了更加直观、生动的导航服务。对于景区而言&#xff0c;这一创新技术无疑是吸引游客目光、提升景区知名度的有力武器。通过独特的 AR 导航体…

AI/机器学习(计算机视觉/NLP)方向面试复习2

1. 用pytorch写一个self-attention 继承pytorch.nn.Module的类 代码&#xff1a; import torch import torch.nn as nn import torch.nn.functional as Fclass SelfAttention(nn.Module):def __init__(self, embed_size): # (B,T,C)super(SelfAttention, self).__init__()sel…

Zabbix监控案例

文章目录 一、监控linux TCP连接状态TCP端口的十一种连接状态自定义监控项监控示例二、监控模板监控tcp连接监控nginx 一、监控linux TCP连接状态 TCP&#xff0c;全称Transfer Control Protocol&#xff0c;中文名为传输控制协议&#xff0c;它工作在OSI的传输层&#xff0c;…

亚马逊AWS节点运行器扩展对Sui支持,简化区块链部署路径

亚马逊AWS在其节点运行器&#xff08;Node Runners&#xff09;服务中扩展了对Sui的原生支持&#xff0c;为基础设施开发者提供了一条新的、便捷的路径来建立Sui节点。Sui基金会还加入了AWS的Web3激活提供商计划&#xff08;Web3 Activate Provider Program&#xff09;&#x…

抖音矩阵管理系统解决方案:一站式服务

在当今社交媒体蓬勃发展的时代&#xff0c;抖音作为一款短视频平台&#xff0c;凭借其独特的魅力和庞大的用户群体&#xff0c;已成为众多企业、个人乃至网红达人展示自我、推广品牌的重要舞台。然而&#xff0c;随着抖音账号数量的不断增加&#xff0c;如何高效、专业地管理这…

开源安全信息和事件管理(SIEM)平台OSSIM

简介 OSSIM&#xff0c;开源安全信息和事件管理&#xff08;SIEM&#xff09;产品&#xff0c;提供了经过验证的核心SIEM功能&#xff0c;包括事件收集、标准化和关联。 OSSIM作为一个开源平台&#xff0c;具有灵活性和可定制性高的优点&#xff0c;允许用户根据自己的特定需…

SpringBoot上传超大文件导致OOM,完美问题解决办法

问题描述 报错: Caused by: java.lang.OutOfMemoryError at java.io.ByteArrayOutputStream.hugeCapacity(ByteArrayOutputStream.java:123) ~[?:1.8.0_381] at java.io.ByteArrayOutputStream.grow(ByteArrayOutputStream.java:117) ~[?:1.8.0_381] at java.…

【BUG】已解决:ERROR: No matching distribution found for PIL

ERROR: No matching distribution found for PIL 目录 ERROR: No matching distribution found for PIL 【常见模块错误】 【解决方案】 欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 欢迎来到我的主页&#xff0c;我是博主英杰&#xff0c;211科班出身&#xff0…

CSS(四)——CSS Text(文本)

CSS Text(文本&#xff09; 文本颜色 颜色属性被用来设置文字的颜色。 颜色是通过CSS最经常的指定&#xff1a; 十六进制值 - 如: &#xff03;FF0000 一个RGB值 - 如: RGB(255,0,0) 颜色的名称 - 如: red 一个网页的背景颜色是指在主体内的选择&#xff0c;即<body…

C++之类与对象(2)

前言 今天将步入学习类的默认成员函数&#xff0c;本节讲解其中的构造函数和析构函数。 1.类的默认成员函数 在 C 中&#xff0c;如果一个类没有显式定义某些成员函数&#xff0c;编译器会自动为该类生成默认的成员函数。以下是编译器可能会生成的默认成员函数&#xff1a; 默…

Fork软件笔记:一键拉取仓库所有模块

Fork是一个好用的git工具&#xff0c;只是没有中文而已&#xff08;不过不用翻译也能看使用&#xff09;。 工具下载地址&#xff1a;https://fork.dev/ 界面展示&#xff1a; 当项目中仓库模块比较多时&#xff0c;可以看到每个模块都是一个分页&#xff0c;每一个都要手动切换…

Linux云计算 |【第二阶段】AUTOMATION-DAY2

主要内容&#xff1a; 部署GitLab、配置管理GitLab、CI/CD概述、Jenkins概述、部署Jenkins&#xff08;初始化、拷贝插件&#xff09; 一、GitLab概述 GitLab 是一个基于 Web 的 Git 仓库管理工具&#xff0c;它提供了一个集成的开发环境和代码管理平台。GitLab 不仅支持 Git…

深入探讨:如何在Shopee平台上安全运营多个店铺?

在Shopee平台上&#xff0c;卖家如何避免店铺被关联&#xff1f;这是一个关乎账号安全和业务持续性的重要问题。Shopee严格规定每个卖家只能拥有一个店铺&#xff0c;以维护市场竞争公平和购物体验的稳定性。如果你开了多个店铺被平台判定关联&#xff0c;会面临冻结或封禁的风…

AI数字人+城市交通大数据可视化平台,让交通管理与服务更简便、更智能

如今&#xff0c;AI数字人作为科技革命和产业革命的重要驱动力&#xff0c;AI数字人接入城市交通大数据可视化平台&#xff0c;可以有效地将各硬件与业务系统进行深度融合&#xff0c;完成业务闭环。依托AI数字人的应用&#xff0c;使城市交通大数据可视化平台的使用复杂度大幅…

【北京迅为】《i.MX8MM嵌入式Linux开发指南》-第三篇 嵌入式Linux驱动开发篇-第五十章 Linux设备树

i.MX8MM处理器采用了先进的14LPCFinFET工艺&#xff0c;提供更快的速度和更高的电源效率;四核Cortex-A53&#xff0c;单核Cortex-M4&#xff0c;多达五个内核 &#xff0c;主频高达1.8GHz&#xff0c;2G DDR4内存、8G EMMC存储。千兆工业级以太网、MIPI-DSI、USB HOST、WIFI/BT…