数据结构——二叉树

树的结构:

概念:

从结构上来看,树是一种非线性的数据结构,是一个由n(n≥0)个结点组成的集合

结构上的特点:

①有一个特殊的结点,叫做根结点,根节点没有前驱结点

②除了根结点以外的结点被分为互不相交的集合(子树,由子树再去细化为具体的叶子结点)

③树是递归定义的

这种就不是树,因为同一层的结点之间不能相交

关于树的一些概念(重要)

结点的度:一个结点含有子树的个数称为该结点的度(如结点10的度是3)

树的度:一棵树中,最大的结点的度称为树的度 (如图树的度为7)

树的高度:根结点到最底层叶子结点的层数就是树的高度(如图树的高度是4)

叶子结点:没有子树的结点,即度为 0 的结点 (如 2 3 5 7 8 9 11 12 13 14都是叶子结点)

双亲结点:有子树的结点,即度不为 0 的结点

树的层次:从根结点开始为第1层,依次往下类推

深度:具体到某个叶子结点所在的层数即是深度 (如图结点12的深度是4,结点10的深度是3)

树的结构定义:

cpp 复制代码
class Tree{
  static class TreeNode{
    public TreeNode child;        //子树
    public TreeNode nextbro;      //同一根结点的兄弟结点  
    public char val;              //树的值  
    public TreeNode(char val)     
    {
        this.val = val;
    }
  }
    public TreeNode root;
}
   

二叉树

1.概念

度最大为2的树就是二叉树

2.结构特点

二叉树要么为空,要么由就是一个根结点和左右两棵子树组成。

满二叉树和完全二叉树

满二叉树一颗二叉树如果每一层的结点数都达到最大数目,则称之为满二叉树(第k层的最大节点数为2的k次方个)

完全二叉树 :完全二叉树是一种特殊的二叉树,对于一个有n个结点的二叉树,其结点从0~n依次对应则称这种数为完全二叉树


二叉树的性质:

  1. 若规定 根结点的层数为 1 ,则一棵 非空二叉树的第 i 层上最多有 2^(i-1)
    (i>0) 个结点
  2. 若规定只有 根结点的二叉树的深度为 1 ,则 深度为 K 的二叉树的最大结点数是2^k-1个
    (k>=0)
  3. 对任何一棵二叉树 , 如果其 叶结点个数为 n0, 度为 2 的非叶结点个数为 n2, 则有 n0 n2 1
  4. 具有 n 个结点的完全二叉树的深度 k 为 log 2 (n+1) 上取整

看到这可能对于第三个性质会有疑问,为什么?

二叉树的创建

用穷举法创建一棵树:

java 复制代码
public class Tree{
    
    static class TreeNode{

        private TreeNode left;
        private TreeNode right;
        public char val;
        
        public TreeNode(char val)
        {
            this.val = val;
        }
    }
    public TreeNode root;

public static void main(String[] args){
        Tree tree = new Tree(A);
        TreeNode A =  new TreeNode('a');
        TreeNode B =  new TreeNode('b');
        TreeNode C =  new TreeNode('c');
        TreeNode D =  new TreeNode('d');
        TreeNode E =  new TreeNode('e');
        A.left = B;
        B.left = C;
        B.right = D;
        A.right = E;
    }
}

现在我们穷举法创建的树长这样

是否真的长这样,我们接下来用遍历验证一下


二叉树的遍历:

**前序遍历:**前序遍历是从树的根开始遍历,然后是遍历树的左子树,最后遍历右子树(根左右)

**中序遍历:**中序遍历是从树的左子树开始遍历,然后遍历树的根结点,最后遍历右子树(左根右)

**后序遍历:**后序遍历是从树的左子树开始遍历,然后遍历树的右子树,最后遍历根结点(左右根)

**层序遍历:**层序遍历就是一棵有N个结点的树,从上到下,从左往右依次遍历对应结点(了解即可)

用递归实现前三种遍历:

前序遍历:

java 复制代码
 //前序遍历
    public void preOrder(TreeNode root)
    {
        if(root == null)
        {
            return;
        }
        System.out.print((root.val)+" ");
        preOrder(root.left);
        preOrder(root.right);
    }

我们再来看上面举的那个例子,然后理解一下递归的过程

直到root结点走到最底层的左子树为空,开始返回结点,然后再进行右子树递归

以此类推,每次递归都会先进行左子树递归,然后遇到左子树为空就返回到上一层递归,再进行右子树遍历,再遍历右子树的左子树,为空就再返回,再遍历右子树....

举一反三,中序遍历就是前序遍历的区别就是:前序遍历递归是先打印根结点------左子树------右子树,而中序遍历递归先打印左子树------根结点------右子树。同理,后序遍历就是先打印左子树------右子树------根结点。

java 复制代码
 //前序遍历
    public void preOrder(TreeNode root)
    {
        if(root == null)
        {
            return;
        }
        System.out.print((root.val)+" ");
        preOrder(root.left);
        preOrder(root.right);
    }

    //中序遍历
    public void inOrder(TreeNode root)
    {
        if(root == null)
        {
            return;
        }
        inOrder(root.left);
        System.out.print((root.val)+" ");
        inOrder(root.right);
    }

    //后序遍历
    public void postOrder(TreeNode root)
    {
        if(root == null)
        {
            return;
        }
        postOrder(root.left);
        postOrder(root.right);
        System.out.print((root.val)+" ");
    }

用非递归的方法进行三种遍历:

144. 二叉树的前序遍历 - 力扣(LeetCode)

我们仍以上面我们穷举法创建的树为例子,这个问题我们用栈来解决。

思路:定义一个引用先进行左子树遍历,每次往左走一步把一个左子树结点压入栈,然后把左子树结点的值放到链表中,当左子树为空的时候,获取栈顶的结点(此时栈顶的元素就是当前子树的根结点) ,然后往右走一步遍历右子树,之后在右子树让引用再次往左走一步遍历左子树

java 复制代码
public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        if(root == null)
        {
            return list;
        }
        Stack<TreeNode> stack = new Stack<>();
        TreeNode cur = root;
        while(cur != null||!stack.isEmpty())
        {
            while(cur!= null)
            {
                stack.push(cur);
                list.add(cur.val);
                cur = cur.left;
            }
            TreeNode top = stack.pop();
            cur = top.right;
        }
        return list;
    }

中序遍历与前序遍历大差不差,中序遍历和前序遍历的差别就在于中序遍历不需要定义引用,并且在每次遍历到左子树为空时才将栈顶元素弹出,然后才将弹出去的结点的值放到list中去(前序遍历是每次压栈的同时把结点的值放到list)

注:每次获取栈顶元素都是当前子树的根结点,将当前的栈顶元素弹出之后,下一个栈顶元素就是上一层树的根节点。

145. 二叉树的后序遍历 - 力扣(LeetCode) (非递归实现)

后序遍历相较于前面两种遍历方式就比较复杂那么一点,后序遍历的难点就在于解决先遍历左子树然后遍历右子树。

当左子树遍历完成,如何让引用直接走到右子树?

这时候只能用到另一个引用top,这个引用直接peek()栈顶元素(相当于子树的根),然后当cur遍历完左子树之后并且右子树不为空的情况下,直接用top往右遍历然后赋值给cur,这样就能达到目的,当左右子树都遍历过之后,将栈顶元素弹出,并放到list

最大的坑就再弹出栈并且放元素的这一步,这一步很多人只考虑了当右子树为空的情况,当栈顶元素弹出之后,再次peek()的是上一层树的根结点,再遍历右子树,可是此时的右子树已经遍历过了,然后就陷入死循环...

死循环的原因:

这就得用一个引用prev来记录右子树,在左子树遍历完之后,先检验右子树是否为空之后还要再检验右子树是否已经被打印,如果打印过就直接将栈顶元素弹出去

java 复制代码
public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        if(root == null)
        {
            return list;
        }
        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 top = stack.peek();
            if(top.right == null|| top.right== prev)   //后面的表达式是确保右边的子树是被打印过的,当右子树被打印过后就直接弹出栈顶元素
            {
                stack.pop();
                list.add(top.val);
                prev = top;
            }  
            else{
                cur = top.right;
            }          
        }
        return list;
    }
相关推荐
ChoSeitaku1 分钟前
链表交集相关算法题|AB链表公共元素生成链表C|AB链表交集存放于A|连续子序列|相交链表求交点位置(C)
数据结构·考研·链表
偷心编程2 分钟前
双向链表专题
数据结构
香菜大丸3 分钟前
链表的归并排序
数据结构·算法·链表
jrrz08283 分钟前
LeetCode 热题100(七)【链表】(1)
数据结构·c++·算法·leetcode·链表
oliveira-time14 分钟前
golang学习2
算法
@小博的博客30 分钟前
C++初阶学习第十弹——深入讲解vector的迭代器失效
数据结构·c++·学习
南宫生1 小时前
贪心算法习题其四【力扣】【算法学习day.21】
学习·算法·leetcode·链表·贪心算法
懒惰才能让科技进步2 小时前
从零学习大模型(十二)-----基于梯度的重要性剪枝(Gradient-based Pruning)
人工智能·深度学习·学习·算法·chatgpt·transformer·剪枝
Ni-Guvara2 小时前
函数对象笔记
c++·算法
泉崎2 小时前
11.7比赛总结
数据结构·算法