Bootstrap

数据结构-二叉树

1.树

1.1什么是树

树是一种非常重要的非线性的数据结构,它在计算机科学和软件工程中有广泛的应用。树是由n个有限节点组成的集合,形状非常像一颗倒挂的树。有一个特殊的节点称为根节点,根节点没有前驱节点,其余节点分为若干个互不相交的子集,这些子集本身就是一棵树,称为根的子树。

注意:树中,子树之间不能有交集,否则就不是树

1.2树涉及的术语

节点:树中的一个元素

根节点:树的最顶层节点,没有前驱(父节点),如上图A就是根节点

父节点:一个节点的上层节点,如上图B的父节点是A,J的父节点是F

子节点:一个节点的下层节点,如上图E的子节点是I,B的子节点是E,F

子孙节点:以某节点为根的子树中任何一个节点都称为该节点的子孙,如上图所有节点都是A的子孙

兄弟节点:具有相同父节点的节点,如上图E,F是一对兄弟节点

堂兄弟节点:双亲在同一层的节点的节点,如上图F,G是一对堂兄弟节点

叶子节点:没有子节点的节点(或度为0的节点),如上图的I,J,K,H都是叶子节点

:一个节点的子节点的个数,如上图A的度是3,E的度是1,K的度是0

深度:根节点到某个节点的路径长度

高度:从某个节点到最远节点的路径长度

路径:从一个节点到另一个节点的序列

树的高度或深度:树中节点最大的高度,如上图该树的高度为4

1.3树的表现形式

树的表现形式有很多,如双亲表示法,孩子表示法,双亲孩子表示法,孩子兄弟表示法等,最常用的就是孩子兄弟表示法

Class TreeNode{
    int value;
    //第一个孩子引用
    TreeNode firstChild;
    //下一个兄弟引用
    TreeNode nextBrother;
}

1.4树的应用

1.文件系统使用树结构来组织文件和目录。每个目录是一个节点,文件时叶子节点

2.数据库索引,B树和B+树常用于数据库索引,以提高数据检索的效率

3.路由算法,在计算机网络中。树结构用于网络拓扑,帮助设计路由算法

4.决策树,在机器学习中,决策树用来分类和回归任务

2.二叉树

2.1什么是二叉树

二叉树是一种特殊的树,其中每个节点最多有2个子节点,这两个子节点通常称为左子节点和右子节点,二叉树是节点的一个有限的集合。

对于任意二叉树都是有以下几种情况符合而成的:

2.2二叉树的性质

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

2.若规定只有根节点的深度为1,则深度为k的二叉树的最大节点数是(2^k)-1(k>=0)

3.对于任意一颗二叉树,如果其叶子节点个数为n0,度为2的非叶子节点个数为n2,则有n0=n2+1,对于任何一颗二叉树(非空),叶子节点的个数永远比度为2的节点个数多1

证明:

4.具有n个节点的完全二叉树的深度k=log2(n+1)向上取整

5.对于具有n个节点的完全二叉树,如果按照从上到下从左到右的顺序对所有节点从0开始编号,则对于序号i的节点有:

若i>0,父节点:(i-1)/2  

若i=0,i为根节点,无父节点

若2i+1<n,左孩子序号:2i+1,否则无左孩子

若2i+2<n,右孩子序号:2i+2,否则无右孩子

2.3特殊的二叉树

1.满二叉树

一棵树二叉树,如果每层的节点数都达到最大值,则这颗二叉树就是满二叉树。也就是说,如果一颗二叉树的层数为K,且节点个数为(2^K)-1,则他就是一颗满二叉树

2.完全二叉树:

完全二叉树是效率非常高的数据结构,完全二叉树出最后一层外,每一层都被填满,并且所有节点都尽可能地向左对齐

3.平衡二叉树

任何节点的左右子树高度差不超过1,例如AVL树

4.二叉搜索树(BST)

对于每个节点,其左子树所有节点的值小于该节点的值,右子树所有节点的值大于该节点的值

2.4二叉树的存储结构

二叉树的存储结构分为:顺序存储和类似于链表的链式存储

二叉树的链式存储时通过一个一个节点引用起来的,具体如下:

//孩子表示法
class TreeNode{
    int val;
    //左孩子引用
    TreeNode left;
    //右孩子引用
    TreeNode right;
}

//孩子双亲表示法
class TreeNode{
    int val;
    //左孩子引用
    TreeNode left;
    //右孩子引用
    TreeNode right;
    //当前节点的父节点
    TreeNode parent;
}

我们主要以孩子表示法来构建二叉树

3.二叉树的基本操作

假定创建一棵二叉树

public class BinaryTree{
    public static class TreeNode{
        TreeNode left;
        TreeNode right;
        int value;
        TreeNode(int value){
            this.value=value;
        }
    }
    private TreeNode root;
    public void createBinaryTree(){
        TreeNode node1=new TreeNode(1);
        TreeNode node2=new TreeNode(2);
        TreeNode node3=new TreeNode(3);
        TreeNode node4=new TreeNode(4);
        TreeNode node5=new TreeNode(5);
        TreeNode node6=new TreeNode(6);
        
        root=node1;
        node1.left=node2;
        node2.left=node3;
        node1.right=node4;
        node4.left=node5;
        node4.right=node6;
        
    }
}

该树的形状如下:

 

3.1二叉树的创建

二叉树的创建通常是给定数组,然会对数组进行遍历而创建的,二叉树的创建通常可以通过递归或者迭代的方式来实现。

代码实现:

    static class TreeNode{
        TreeNode left;
        TreeNode right;
        Character val;
        public TreeNode(Character val){
            this.val=val;
        }
    }
    public static int i=0;    
    public static TreeNode createBinaryTree(String str){
        if(str==null||str.length()==0){
            return null;
        }
        TreeNode root=null;
        if(str.charAt(i)!='#'){
            root=new TreeNode(str.charAt(i));
            i++;
            root.left=createBinaryTree(str);
            root.right=createBinaryTree(str);
        }else{
            i++;
        }
        return root;
    }

3.2二叉树的遍历

遍历就是沿着某条搜索路线,依次对树中每个节点均作一次访问,遍历是二叉树最重要的操作之一,是二叉树进行其他运算的基础

1.二叉树的前序遍历(先序遍历):首先访问根节点,再访问根的左子树,最后访问根的右子树

2.二叉树的中序遍历:首先访问根的左子树,再访问根节点,最后访问根的右子树

3.二叉树的后序遍历:首先访问根的左子树,再访问根的右子树,最后访问根节点

前序遍历打印的第一个节点一定是根节点,后序遍历打印的第一个节点一定是根节点,但是中序遍历打印的中间位置的节点不一定是根节点,中序遍历中根节点左侧打印的节点为左子树的节点,右侧打印的节点为右子树的节点

前序遍历代码实现:

 public void preorderTraversal(TreeNode root){
        if(root==null){
            return;
        }
        System.out.print(root.value+" ");
        preorderTraversal(root.left);
        preorderTraversal(root.right);
 }

 中序遍历代码实现:

public void inorderTraversal(TreeNode root){
        if(root==null){
            return;
        }
        inorderTraversal(root.left);
        System.out.print(root.value+" ");
        inorderTraversal(root.right);
}

 后序遍历代码实现:

public void postorderTraversal(TreeNode root){
        if(root==null){
            return;
        }
        postorderTraversal(root.left);
        postorderTraversal(root.right);
        System.out.print(root.value+" ");
}

3.3获取树中节点的个数

采用子问题的思想,获取树中节点的个数,可以分解为根节点+左子树节点个数+右子树节点个数

代码编写:

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

 

3.4获取叶子节点的个数

叶子节点的特点是没有子节点,也就是left和right孩子都为空,叶子节点的计算可以通过遍历思路来实现,也可以通过子问题思想将叶子节点个数转化为左子树叶子节点个数+右子树叶子节点个数

遍历思路代码编写:

    private static int leafSize=0;
    public int getLeafSize(TreeNode root){
        if(root==null){
            return 0;
        }
        if(root.left==null&&root.right==null){
            leafSize++;
        }
        getLeafSize(root.left);
        getLeafSize(root.right);
        return leafSize;
    }

 子问题思想代码实现:

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

3.5获取第K层节点的个数

当根节点为空时,自然第k曾就不存在了,同样当k<=0时,k层的节点个数也就无意义了,自然返回0。通过创建队列queue用于存放每层的节点,每次循环都要将上一次queue中的元素全部删除,这样while循环每完成一次,就代表开始下一层了,即queue存放了新的一层的元素,当循环次数等于k-1时,queue中的元素个数就是第k层的节点数。while的第一次循环,queue中只有一个元素,将该元素出队列,观察该元素的左右孩子是否为空,如果不为空,将其入队列,这样queue中存放了第二层的节点。while的第二次循环,将queue中的元素依次出队列,并分别判断出队列的元素的左右孩子是否为空,不为空,则进行入队列操作,for循环结束,queue就已经存放了第三层的元素,依次类推

     public int getKLevelNodeCount(TreeNode root, int k) {
        if (root == null || k <= 0) {
            return 0;
        }
        if (k == 1) {
            return 1;
        }
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        int level = 1;
        int count = 0;
        while (!queue.isEmpty()) {
            int size = queue.size();
            if (level == k) {
                count = size;
                break;
            }
            level++;
            for (int i = 0; i < size; i++) {
                TreeNode node = queue.poll();
                if (node.left != null) {
                    queue.offer(node.left);
                }
                if (node.right != null) {
                    queue.offer(node.right);
                }
            }
        }
        return count;
    }

3.6获取二叉树的高度

树的高度可以采用子问题的思想:求根节点左子树,右子树高度的较大值+1

代码编写:

    public int getTreeHeight(TreeNode root){
        if(root==null){
            return 0;
        }
        int leftHeight=getTreeHeight(root.left)+1;
        int rightHeight=getTreeHeight(root.right)+1;
        return leftHeight>rightHeight?leftHeight:rightHeight;
    }

 

3.7检测值为value的元素是否存在

思路:遍历二叉树,看是否存在value元素

代码编写:

    public boolean contains(TreeNode root,int value){
        if(root==null){
            return false;
        }
        if(root.value==value){
            return true;
        }
        return contains(root.left,value)|| contains(root.right,value);
    }

 

3.8层序遍历

层序遍历:即从上到下从左到右依次打印树中的元素(每一层从左到右打印,从第一层开始到最后一层)

    public void levelOrderTraversal(TreeNode root){
        if(root==null){
            return;
        }
        System.out.print(root.value+" ");
        Queue<TreeNode> queue=new LinkedList<>();
        queue.offer(root);
        while(!queue.isEmpty()){
            TreeNode node=queue.poll();
            if(node.left!=null){
                System.out.print(node.left.value+" ");
                queue.offer(node.left);
            }
            if (node.right!=null){
                System.out.print(node.right.value+" ");
                queue.offer(node.right);
            }
        }
    }

3.9判断一棵树是不是完全二叉树

完全二叉树的特点是,从上到下、从左到右,每一层的节点都被填满,直到最后一层。并且最后一层的节点都尽可能地集中在左侧。

思路:

在遍历过程中,一旦遇到第一个空节点,标记这个状态。从这个空节点开始,如果再遇到非空节点,则说明该树不是完全二叉树,因为完全二叉树的最后层的节点应该尽可能地集中在左侧。如果所有节点都被访问,且没有在遇到第一个空节点后出现非空节点,则该树是完全二叉树

代码实现:

    public boolean isCompleteTree(TreeNode root) {
        if (root == null) return true;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        boolean end = false; // 用来标记是否遇到了第一个null节点
        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            if (node == null) {
                end = true; // 标记遇到了第一个null节点
            } else {
                if (end) return false; // 如果已经标记了end,说明之前已经出现了null节点,此时不应该再出现非null节点
                queue.offer(node.left);
                queue.offer(node.right);
            }
        }
        return true;
    }

二叉树涉及的知识和内容还有很多,再次我们不仅一一进行列举说明。

;