数据结构之二叉树1—二叉树的基础概念和遍历方式

目录

一、前言

1.关于递归

2.树的基本概念

1)为什么要有树结构?

2)数据结构常用的树结构

3)树的基本概念

二、二叉树

1.两种特殊的二叉树

1)满二叉树

2)完全二叉树

3)完全二叉树的节点编号

2.二叉树的存储方式

3.二叉树的遍历

1)遍历:

2)四大遍历方式

4.二叉树代码实现

1)节点类的定义

2)前中后序遍历

3)统计节点个数

⭐99^100and100^99大小

4)元素是否存在

5)二叉树高度

三、leetcode题型(二叉树遍历)

1.Num102:二叉树的层序遍历(借助队列)

2.三大遍历的非递归实现(借助栈)

1)Num144:前序遍历

2)Num94:中序遍历 

3)Num145:后序遍历 


一、前言

1.关于递归

1)递归的应用场景

a.大问题可以拆分成小问题

b.拆分后的小问题和大问题除了数据规模,解决思路全一样

c.存在递归终止条件,拆分拆到底的条件

2)如何写递归程序:不要纠结内部运行,学会使用递归的语义解决问题

2.树的基本概念

1)为什么要有树结构?

      二叉树就是树形结构(天然的查找语义),树形结构可以更加高效的进行查找和搜索。如电脑中的文件系统就是一个树形结构。

      如果文件是线性结构会怎么样:线性结构是所有文件逻辑上连续,成一条直线排列,换句话说电脑中假设有100w个文件,都在一个位置存储,检索特点的某个文件,就要遍历这个集合n次,O(n),如果是,根据文件夹查找特定文件,时间复杂度实际上就是文件树的深度:O(logN)

2)数据结构常用的树结构

a.BST:二分搜索树【二叉树的元素查找】

b.平衡二分搜索树:AVL(严格平衡),红黑树(非严格平衡)

c.堆

d.并查集

e.字符串:线段树;字典树(Trie)

3)树的基本概念

      树是一种非线性的数据结构,由n(n>=0)个有限节点组成一个具有层次关系的集合,把他叫做树是因为它看起来像一颗倒挂的树,也就是说它是根朝上,叶朝下,子树是不能相交的,除了根节点外,每个节点有且仅有一个父节点,一棵N个节点的树有N-1条边,树是递归定义的。

a.树中的每个单元称为节点

b.节点的度:该节点含有的子树的个数就称为节点都度

c.树的度:该树中最大的节点的度就是该树的度

d.叶子节点:度为0的节点,也称为终端节点

e.根节点:树中没有父亲节点的节点:A就是根节点

f.关于节点的层次:从根开始计算,根节点是第一层

g.树的高度:节点的最大层次

二、二叉树

每个节点最多只有两个子树,二叉树中节点的度不超过2,两个子树有左右之分。

数据结构之二叉树1—二叉树的基础概念和遍历方式_第1张图片

1.两种特殊的二叉树

1)满二叉树

a.概念

      一个二叉树如果每一层的节点树都能达到最大值(所有非叶子节点的度都为2),则这个二叉树是满二叉树。

b.特点:对一棵二叉树来讲

1)高度为k,则该二叉树最多有2^k-1个节点【节点最多的情况就是满二叉树】

2)层次为k,第k层最多有2^(k-1)个节点

3)边长和节点个数关系:边长=节点个数-1。设度为0的节点个数为n0,度为1的节点个数n1,度为2的节点个数n2,则:n0=n2+1(叶子节点的个数=度为2的节点个数+1),设总的个数为n,n=n0+n1+n2,边长=n-1。

2)完全二叉树

a.概念

      完全二叉树是效率很高的数据结构,它实际上就是满二叉树“缺了个右下角”,在完全二叉树中,度为1的节点若存在,只可能有一个度为1的节点,且只有左子树没有右子树,不存在只有右子树没有左子树的节点(节点要靠左排列!)。

b.问题1:

      一个完全二叉树第六层有8个叶子节点,则该完全二叉树至多有多少个节点(求该完全二叉树全部的节点个数)

分析:已知第六层8个叶子节点,让完全二叉树前六层拉满,存在第七层。前6层而言,最多的节点个数:2^6-1=63;第6层总共有2^(6-1)=32,有8个叶子节点,非叶子节点=32-8=24个,这24个节点的子树就是第七层的节点,这24节点都有左右子树,第7层节点个数:24*2=48,总的节点个数=前六层拉满的节点+第7层的节点=63+48=111。

c.问题2:

500,500,1,0

分析:总结点1000,1024之内(2^10-1=1023,2^9-1=511),二叉树高度为10,前九层节点个数拉满:511,第十层节点个数:1000-511=489(全是叶子节点),第九层非叶子节点=第10层节点个数/2=488/2+1(度为2的节点+度为1的节点)=245,第九层总节点个数:2^(9-1)=256,第九层叶子节点=256-245=11。综上,总叶子节点=11+489=500,非叶子节点=1000-500=500,度为2的节点=499,度为1的节点=1。

3)完全二叉树的节点编号

      若根节点从1开始编号,设父节点的编号为k,则左子树2k,右子树2k+1;若根节点从0开始编号,左子树2k+1,右子树2k+2。

2.二叉树的存储方式

      同链表一样,有顺序存储和链式存储(引用)。顺序存储是将二叉树采用数组的方式存储(只能存储完全二叉树,在堆章节介绍),普通二叉树采用引用方式存储。

class Node{

E val; Node left;  Node right;//左右孩子表示法,一般都可以使用该方法来存储二叉树的节点

}

class Node{//之后平衡树会用到

E val; Node left; Node right;

Node parent;//父节点地址

}

3.二叉树的遍历

1)遍历:

      按照一定的顺序“访问根据不同的场景,访问的需求是不同的,如打印节点值或是计算节点个数)”这个集合的所有元素,不重复,不遗漏。

2)四大遍历方式

      对于二叉树这种非线性结构而言,遍历比线性结构就复杂得多,有四大遍历方式(对于二叉树来讲,遍历操作是其他操作的基础):前中后序遍历、层序遍历。

      注意:在写前三种遍历方式时可以借用栈结构,保证做到不重不漏不出错,此时的“访问”就是输出结点的值

a.前序遍历:【preOrder】

      先访问根节点,递归访问左子树,递归访问右子树,“根左右”,第一次访问根节点就可以输出节点值。

b.中序遍历:【inOrder】

      先递归访问左子树,然后访问根节点,最后递归访问右子树,“左根右”。

c.后序遍历:【postOrder】

      先递归访问左子树,递归访问右子树,再访问根节点,"左右根"。

拓展:后序的转置输出恰好是前序遍历的镜像:根右左

d.层序遍历:【levelOrder】

      按照二叉树的层次一层层访问节点,先左再有。

数据结构之二叉树1—二叉树的基础概念和遍历方式_第2张图片

A先入队列,输出打印A,A左右不为空,左右孩子BC入队,此时A就可以出队了,此时队首B,处理B,B先做一个输出,B有左孩子D无右孩子,D入队同时B出队,此时队首C,处理C先让C输出,处理左右子树是EF,入队,这时C处理完了,出队,此时队首D.......

注意:1.队列为空,层序遍历就处理完毕  2.队列中保存的都是下一层要处理的元素

eg:

数据结构之二叉树1—二叉树的基础概念和遍历方式_第3张图片

前序:ABDEGHCF

中序:DBGHEACF

后序:DHGEBFCA

层序:ABCDEFGH

4.二叉树代码实现

数据结构之二叉树1—二叉树的基础概念和遍历方式_第4张图片

1)节点类的定义

public class MyBinTree {
    private static class TreeNode{//创建内部类
        char val;
        //左子树根节点
        TreeNode left;
        //右子树根节点
        TreeNode right;
        public TreeNode(char val){
            this.val=val;
        }
    }
}

      同链表一样,传递一棵二叉树,传入该树的根节点即可,通过根节点的不同遍历方式取得所有节点值。

2)前中后序遍历

/**
 * 基础二叉树实现
 * 使用左右孩子表示法
 */
public class MyBinTree {
    private static class TreeNode{//创建内部类
        char val;
        //左子树根节点
        TreeNode left;
        //右子树根节点
        TreeNode right;
        public TreeNode(char val){
            this.val=val;
        }
    }

    /**
     * 创建一个二叉树,返回根节点
     * @return
     */
    public static TreeNode build(){
        TreeNode nodeA=new TreeNode('A');
        TreeNode nodeB=new TreeNode('B');
        TreeNode nodeC=new TreeNode('C');
        TreeNode nodeD=new TreeNode('D');
        TreeNode nodeE=new TreeNode('E');
        TreeNode nodeF=new TreeNode('F');
        TreeNode nodeG=new TreeNode('G');
        TreeNode nodeH=new TreeNode('H');
        nodeA.left=nodeB;
        nodeA.right=nodeC;
        nodeB.left=nodeD;
        nodeB.right=nodeE;
        nodeE.right=nodeH;
        nodeC.left=nodeF;
        nodeC.right=nodeG;
        return nodeA;
    }

    /**
     * 先序遍历:根左右
     * 传入一个二叉树根节点,就可以按照以先序遍历的方式输出节点值
     * @param root
     */
    public static void preOrder(TreeNode root){
        //边界条件-判空
        if(root==null){
            return;
        }
        //先打印根节点的值
        System.out.print(root.val+" ");
        //按照先序遍历的方式递归访问左树
        preOrder(root.left);
        //按照先序遍历的方式递归访问右树
        preOrder(root.right);
    }

    /**
     * 中序遍历:左根右
     * 传入一棵二叉树的根节点,就能按照中序遍历的方式来输出结果集
     * @param root
     */
    public static void inOrder(TreeNode root){
        if(root==null){
            return;
        }
        //先递归访问左子树
        inOrder(root.left);
        System.out.print(root.val+" ");
        inOrder(root.right);
    }
    public static void postOrder(TreeNode root){
        if(root==null){
            return;
        }
        //先递归访问左子树
        postOrder(root.left);
       //再递归访问右子树
        postOrder(root.right);
        //打印根节点
        System.out.print(root.val+" ");
    }
    public static void main(String[] args) {
        TreeNode root=build();
        System.out.println("前序遍历的结果为:");
        preOrder(root);
        System.out.println();
        System.out.println("中序遍历结果为:");
        inOrder(root);
        System.out.println();
        System.out.println("后序遍历结果为:");
        postOrder(root);
    }
}

3)统计节点个数

a.总共节点个数

/**
 * 传入一棵二叉树的根节点,就能统计出当前二叉树中一共有多少个节点,返回节点数
 * 此时的访问就不再是输出节点值,而是统计节点个数
 * @param root
 * @return 当前二叉树的节点个数
 */
public static int getNodes(TreeNode root){
    if(root==null){
        return 0;
    }
    //当前根节点不为空,说明当前根节点要统计一次
    //然后加上左右子树的节点数就是整棵树的节点数
    return 1+getNodes(root.left)+getNodes(root.right);

}
    public static void main(String[] args) {
        TreeNode root=build();
        System.out.println("当前二叉树一共有"+getNodes(root)+"个节点数");
    }

b.叶子节点个数

/**
 * 求一棵二叉树叶子节点的个数
 * 传入一棵二叉树根节点,就能统计出当前二叉树叶子节点个数
 * @param root
 * @return
 */
public static int getLeafNodes(TreeNode root){
    if(root==null){
        return 0;
    }
    if(root.left==null&&root.right==null){
        //说明二叉树只有根节点,这个根节点就是叶子节点
        return 1;
    }
    //当前数不为空且存在子树,则返回子树的叶子节点数
    return getLeafNodes(root.left)+getLeafNodes(root.right);
}
public static void main(String[] args) {
        TreeNode root=build();
        System.out.println("当前二叉树一共有"+getLeafNodes(root)+"个叶子节点");
}

递归函数实际上具体的处理过程都在终止条件,递归过程只是将多个结果拼起来而已。

c.第k层节点个数⭐

      以root为节点的第k层节点个数=以root.left为根节点的第k-1层节点个数+以root.right为根节点的第k-1层节点个数。

/**
 * 求出以root为根节点的二叉树第k层节点个数
 * @param root
 * @param k
 * @return
 */
public static int getKLevelNodes(TreeNode root,int k){
    if(root==null||k<=0){
        return 0;
    }
    if(k==1){
        return 1;
    }
    //二叉树不为空且k>=2
    //以root为根节点的第k层=以root.left为根节点的k-1层+以root.right为根节点的k-1层
    return getKLevelNodes(root.left,k-1)+getKLevelNodes(root.right,k-1);
}

⭐99^100and100^99大小

99^100大一些,在二进制、八进制、十进制、十六进制等这些进制中,最高效的实际上是三进制,和3越近数越大。2^4,3^3,4^2;2^5,3^4,4^3。

4)元素是否存在

在二叉树中判断给定元素是否存在:遍历,这里访问是判断节点值是否与给定值相等

/**
 * 判断当前二叉树中是否包含指定元素val
 * 若存在返回true,否则返回false
 * @param val
 * @return
 */
public static boolean contains(TreeNode root,char val){
    if(root==null){
        return false;
    }
    if(root.val==val){
        return true;
    }
    //二叉树不为空且根节点值不是val,在子树中继续寻找
    return contains(root.left,val)||contains(root.right,val);
}

5)二叉树高度

 /**
     * 传入一个以root为根节点的二叉树,求出该树的高度
     * @param root
     * @return
     */
    public static int height(TreeNode root){
        if(root==null){
            return 0;
        }
        return 1+Math.max(height(root.left),height(root.right));
//        return 1+(height(root.left)>height(root.right)?height(root.left):height(root.right));//法2
//        int leftHeight=height(root.left);//法1
//        int rightHeight=height(root.right);
//        int max=Math.max(leftHeight,rightHeight);
//        return 1+max;
    }

三、leetcode题型(二叉树遍历)

1.Num102:二叉树的层序遍历(借助队列)

借助双端队列实现遍历过程。

Deque queue=new LinkedList<>();

分析:

1.队列为空,层序遍历就处理完毕

2.队列中保存的都是下一层要处理的元素

queue.size():

      计算出当前层的元素个数,按这个元素个数循环,将队列中当前层元素从队列中取出并放入temp集合同时将下一层元素加入队列。

queue.add()和queue.offer()

      都是用来向队列中添加元素,容量已满情况下add是抛出异常,offer是返回false。

/**
 * 二叉树的层序遍历
 * 用二维数组保存:一层用一个小集合存储,最后大集合存储所有层
 */
public class Num102_LevelOrder {
    public List> levelOrder(TreeNode root) {
        List> ret=new ArrayList<>();
        if(root==null){
            return ret;
        }
        //借助队列实现遍历过程-双端队列
        Deque queue=new LinkedList<>();
        queue.offer(root);//根节点入队
        while(!queue.isEmpty()){
            //使用一个temp数组保存当前层的元素
            List temp=new ArrayList<>();
            //取出当前层所有元素添加进temp中
            int size= queue.size();//当前层元素的个数(不能写到循环里)
            for (int i = 0; i < size; i++) {
                TreeNode cur=queue.poll();//依次从队列中取队首元素
                temp.add(cur.val);//队首元素的值添加到当前层集合中
                if(cur.left!=null){
                    queue.offer(cur.left);//cur左树不为空就将左树入队
                }
                if(cur.right!=null){
                    queue.add(cur.right);
                }
            }
            ret.add(temp);
        }
        return ret;
    }
}

2.三大遍历的非递归实现(借助栈)

Deque stack=new ArrayDeque<>();

1)Num144:前序遍历

      第一次走到根节点时就可以访问。入栈时先入右子树C再入左子树B,这样才能先出左子树,B才能在C上面【因为栈是一个LIFO的结构,先遍历的节点后入】

/**
 * 借助栈实现前序遍历
 */
public class Num144_PreOrderNonRecursion {
    public List preorderTraversal(TreeNode root) {
        List ret=new ArrayList<>();
        if(root==null){
            return ret;
        }
        //双端队列
        Deque stack=new ArrayDeque<>();
        stack.push(root);
        while(!stack.isEmpty()){
            TreeNode cur=stack.pop();
            //先访问根节点
            ret.add(cur.val);
            //先压入右孩子
            if(cur.right!=null){
                stack.push(cur.right);
            }
            //再压入左孩子
            if(cur.left!=null){
                stack.push(cur.left);
            }
        }
        return ret;
    }
}

2)Num94:中序遍历 

第二次访问根节点才能输出

public class Num94_InOrderNonRecursion {
    public List inorderTraversal(TreeNode root) {
        List ret=new ArrayList<>();
        if(root==null){
            return ret;
        }
        //当前走到的节点
        TreeNode cur=root;
        Deque stack=new ArrayDeque<>();
        while(cur!=null||!stack.isEmpty()){
            //不管三七二十一,先一路向左走到根
            while(cur!=null){
                stack.push(cur);
                cur=cur.left;
            }
            //cur为空,说明走到了null
            //此时栈顶就存放了左树为空的节点
            cur=stack.pop();
            ret.add(cur.val);
            //继续访问右子树
            cur=cur.right;
        }
        return ret;
    }
}

3)Num145:后序遍历 

      引用一个prev引用保存上一个被完全处理过的节点,当cur第二次访问出队时,看右子树是否为空或被我们访问过(利用prev是否==cur.right),不为空且没有处理过的话根节点再压回栈中,继续处理右子树,为空或者访问过就需要输出并更新prev。

public class Num145_PostOrderNonRecursion {
    public List postorderTraversal(TreeNode root) {
        List ret=new ArrayList<>();
        if(root==null){
            return ret;
        }
        TreeNode cur=root;
        //保存上一个完全处理过的节点(左右根都处理过的节点)
        TreeNode prev=null;
        Deque stack=new ArrayDeque<>();
        while(cur!=null||!stack.isEmpty()){
            //先一路向左走到根
            while(cur!=null){
                stack.push(cur);
                cur=cur.left;
            }
           //此时cur走到null,栈顶存放了左树为null的节点,栈顶元素出队给cur,第二次访问
            cur=stack.pop();
            //判断右树是否为空或者被我们访问过
            if(cur.right==null||prev==cur.right){
                ret.add(cur.val);
                //当前节点cur就是最后处理的根节点,cur已经处理完毕,更新prev引用,变为cur,进行下一轮循环
                prev=cur;
                cur=null;
            }else{
                //此时右树不为空且没有处理过
                //根节点再压回栈中,继续处理右子树
                stack.push(cur);
                cur=cur.right;
            }
        }
        return ret;
    }
}

下一篇:数据结构之二叉树2—二分搜索树_林纾y的博客-CSDN博客

相关:二叉树的搜索与回溯问题(leetcode)_林纾y的博客-CSDN博客

二叉树1—二叉树的遍历_林纾y的博客-CSDN博客_从上到下打印二叉树

二叉树2—对称性递归问题_林纾y的博客-CSDN博客

你可能感兴趣的:(数据结构,数据结构,java,b树,队列,栈)