数据结构—二叉树(BinaryTree)的入门原理以及Java实现案例

·  阅读 154

「这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战」。

本文详细介绍了二叉树的基本概念,以及各种二叉树,以及二叉树的Java实现方式,包括顺序结果和链式结构的实现。

二叉树是一种特殊的树,其定义为:二叉树是n(n>=0)个节点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根节点和两棵互不相交的、分别称为根节点的左子树和右子树组成。

1 二叉树的定义

二叉树是一种特殊的树,其定义为:二叉树是n(n>=0)个节点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根节点和两棵互不相交的、分别称为根节点的左子树和右子树组成。 如果不是太清楚树的的概念的,可以看这篇文章:数据结构—树(Tree)的入门原理以及Java实现案例

如下图,就是一颗二叉树: 在这里插入图片描述

2 二叉树的特性

三个特性:

  1. 每个节点最多有两棵子树,所以二叉树中不存在度大于2的节点。注意不是只有两棵子树,而是最多有。没有子树或者有一棵子树都是可以的。
  2. 左子树和右子树是有顺序的,次序不能任意颠倒。
  3. 即使树中某节点只有一棵子树,也要区分它是左子树还是右子树。

如下案例:一个3个节点树,对于普通的树和二叉树,分别有几种形态? 在这里插入图片描述 普通树,首先有树1形态,而后续四种情况对于普通树是没有区分的,因此只有两种情况;而对于二叉树,则以五种情况都有。

3 特殊的二叉树

3.1 斜二叉树

所有的节点都只有左子树的二叉树叫左斜树。所有节点都是只有右子树的二叉树叫右斜树。这两者统称为斜树。

左斜树:

在这里插入图片描述

右斜树:

在这里插入图片描述

3.2 满二叉树

在一棵二叉树中,如果所有分支节点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树,又称完美二叉树。 在这里插入图片描述

满二叉树的特点:

  1. 叶子只能出现在最下一层。出现在其他层就不可能达成平衡。
  2. 非叶子节点的度一定是2。否则就是“缺胳膊少腿”了。
  3. 在同样深度的二叉树中,满二叉树的节点个数最多,叶子数最多。

3.3 完全二叉树

完全二叉树:除去最后一层叶子节点,就是一颗满二叉树,并且最后一层的节点只能集中在左侧,满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。 在这里插入图片描述 完全二叉树的特点:

  1. 叶子节点只能出现在最下两层。
  2. 最下层的叶子一定集中在左部连续位置。
  3. 倒数二层,若有叶子节点,一定都集中在右部连续位置。
  4. 如果节点度为1,则该节点只有左孩子,即不存在只有右子树的情况。
  5. 同样节点数的二叉树,完全二叉树的深度最小。

3.4 平衡二叉树

平衡二叉树又被称为AVL树(区别于AVL算法),它是具有如下性质:

  1. 它一定是一棵二叉排序树;
  2. 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

平衡二叉树作为重点和难点,此处不多赘述,后面的文章会单独讲。

4 二叉树的性质

共有大概六条性质,这些性质可以被直接用来实现二叉树:

  1. 在二叉树的第i层上至多有2^(i-1)个节点(i≥1);

  2. 深度为k的二叉树至多有2^k-1个节点(k≥1),最少有k个节点;

  3. 对于任意一棵二叉树,如果其叶子节点数为N0,且度数为2的节点总数为N2,则N0=N2+1;

  4. 具有n个节点的完全二叉树的深度为|log2n+1|(|x|表示不大于x的最大整数)。

  5. 有N个节点的完全二叉树各节点如果用顺序方式存储,若I为节点编号(从1开始),则节点之间有如下关系:

    1. 如果 I = 1,则节点I是二叉树的根;I > 1,则其父节点的编号为 I/2,左子节点编号为 2 * I (如果存在),右子节点编号为 2 * I + 1(如果存在);
    2. 如果 2 * I <= N,则其左孩子(即左子树的根节点)的编号为 2 * I ;若 2 * I > N,则无左右孩子;
    3. 如果 2 * I + 1 <= N,则其右孩子的节点编号为 2 * I + 1;若 2 * I + 1 > N,则无右孩子。
  6. 有N个节点的完全二叉树各节点如果用顺序方式存储,若I为节点编号(从0开始),则节点之间有如下关系:

    1. 如果 I = 0,则节点 I 是二叉树的根;I > 0,则其父节点的编号为 (I-1)/2,左子节点编号为 2 * I + 1(如果存在),右子节点编号为 2 * I + 2(如果存在);

    2. 如果 2 * I + 1 <= N,则其左孩子(即左子树的根节点)的编号为 2 * I +1 ;若 2 * I + 1 > N,则无左右孩子;

    3. 如果 2 * I + 2 <= N,则其右孩子的节点编号为 2 * I + 2;若 2 * I + 2 > N,则无右孩子。

在这里插入图片描述

上图是已经编号了的完全二叉树,具有N=10个节点,设I从1开始,下面来验证各个特性:

  1. I = 1 的节点,确实是根节点;如果 I = 5 > 0,那么父节点编号为5/2,即2。
  2. 如果I=5,2 * 5 = 10,5编号的节点的左孩子为2 * 5 = 10编号;如果I=6,2 * 6 > 10,6编号的节点的无左孩子。
  3. 如果 I = 4,2 * 4 + 1 < 10,4编号的节点的右孩子为2 * 4 + 1 = 9 编号;如果I=5,2 * 5 + 1 > 10,5编号的节点的无右孩子。

最后一个特性:给定n个节点,能构成f(n)种不同的二叉树。f(n)为卡特兰数的第N项:

在这里插入图片描述

5 二叉树的存储结构

5.1 顺序存储结构

5.1.1 顺序存储结构的概述

顺序存储结构对树这种一对多的关系结构实现起来是比较困难的。但是二叉树是一种特殊的树,由于它的特殊性,使得用顺序存储结构也可以实现。

二叉树的顺序存储结构就是用一维数组存储二叉树中的节点,并且节点的存储位置,也就是数组的下标要能体现节点之间的逻辑关系,比如双亲与孩子的关系,左右兄弟的关系等。

此时,完全二叉树的规律性和优越性就显现了出来:

在这里插入图片描述

由于完全二叉树的特性,可以将上图的完全二叉树从上到下,从左到右的遍历,然后顺序存放进数组对应索引的位置中:

在这里插入图片描述

对于一般的二叉树,则可以 “借用” 完全二叉树的的思路,将空出来的节点位置置空:

在这里插入图片描述

如上图的普通二叉树,存储时,将其“转换”为完全二叉树,不存在的节点使用null填充:

在这里插入图片描述

极端情况下,一棵深度为k的右斜树,它只有k个结点,却需要分配2^k-1个存储单元空间,这明显会浪费很多空间。

在这里插入图片描述

因此,顺序存储结构只适用于完全二叉树或者满二叉树。

5.1.2 顺序存储结构的简单实现

提供一个二叉树的顺序存储结构的简单实现,节点不允许为null。

可以看到,子节点和父节点的添加、获取,都是依靠的二叉树性质的第五条的公式,这里把要被实现的二叉树看成了完全二叉树,还是比较简单的,但是可能会浪费内存空间。

/**
 * 二叉树的顺序存储结构的简单实现
 */
public class ArrayBinaryTree<E> {

    /**
     * 深度
     */
    private int deep;
    /**
     * 容量,也是节点数量
     */
    private int capacity;
    /**
     * 底层数组
     */
    private Object[] elements;

    /**
     * 节点真正数量
     */
    private int size;


    /**
     * 指定树的深度,初始化数组
     *
     * @param deep 树深度
     */
    public ArrayBinaryTree(int deep) {
        this.deep = deep;
        this.elements = new Object[capacity = (int) Math.pow(2, deep) - 1];
    }

    /**
     * 指定树的深度和根节点
     *
     * @param deep
     * @param root
     */
    public ArrayBinaryTree(int deep, E root) {
        this(deep);
        addRoot(root);
    }


    /**
     * 添加根节点
     *
     * @param root 根节点数据
     */
    public void addRoot(E root) {
        checkNullData(root);
        elements[0] = root;
        size++;
    }


    /**
     *
     * 添加子节点
     *
     * @param parentIndex 父节点索引
     * @param data        节点数据
     * @param left        是否是左子节点,true 是;false 否
     * @return 添加成功后子节点的索引
     */
    public int addChild(int parentIndex, E data, boolean left) {
        checkParentIndex(parentIndex);
        checkNullData(data);
        int childIndex;
        if (left) {
            childIndex = parentIndex * 2 + 1;
        } else {
            childIndex = parentIndex * 2 + 2;
        }
        addChild(childIndex, data);
        size++;
        return childIndex;
    }

    /**
     * 添加子节点
     *
     * @param childIndex 子节点索引
     * @param data       子节点数据
     */
    private void addChild(int childIndex, E data) {
        if (elements[childIndex] != null) {
            throw new IllegalStateException("该父节点已经存在该子节点");
        }
        elements[childIndex] = data;
    }


    /**
     * 是否是空树
     *
     * @return true 是 ;false 否
     */
    public boolean isEmpty() {
        return elements[0] == null;
    }


    /**
     * 返回节点数
     *
     * @return 节点数
     */
    public int size() {
        return size;
    }


    /**
     * 获取索引为index的节点的父节点
     *
     * @param index 索引
     * @return 父节点数据
     */
    public E getParent(int index) {
        if (index == 0) {
            return null;
        }
        return (E) elements[(index - 1) / 2];
    }

    /**
     * 获取索引为index的节点的右子节点
     *
     * @param index 索引
     * @return 右子节点数据
     */
    public E getRight(int index) {
        if (2 * index + 1 >= capacity) {
            return null;
        }
        return (E) elements[index * 2 + 2];
    }

    /**
     * 获取索引为index的节点的左子节点
     *
     * @param index 索引
     * @return 左子节点
     */
    public E getLeft(int index) {
        if (2 * index + 1 >= capacity) {
            return null;
        }
        return (E) elements[2 * index + 1];
    }


    /**
     * 获取根节点
     *
     * @return 根节点数据
     */
    public E getRoot() {
        return (E) elements[0];
    }

    /**
     * 获取节点出现的首个索引位置
     *
     * @param data 节点数据
     * @return 节点索引, 或者-1--不存在该节点
     */
    public int indexOf(E data) {
        for (int i = 0; i < capacity; i++) {
            if (elements[i].equals(data)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * 检查子节点是否已经存在
     *
     * @param message 消息
     */
    private void checkChild(int childIndex, String message) {
        if (elements[childIndex] == null) {
            throw new IllegalStateException(message);
        }
    }


    /**
     * 数据判null
     *
     * @param data 添加的数据
     */
    private void checkNullData(E data) {
        if (data == null) {
            throw new NullPointerException("数据不允许为null");
        }
    }

    /**
     * 检查父节点是否存在
     *
     * @param parentIndex 父节点索引
     */
    private void checkParentIndex(int parentIndex) {
        if (elements[parentIndex] == null) {
            throw new NoSuchElementException("父节点不存在");
        }
    }
}

复制代码

5.2 链式存储结构

采用链式存储结构更加的灵活,为树节点设计一个数据域和两个引用变量,一个保存左子结点的引用,另一个保存右子节点的引用,我们称这样的链表叫做二叉链表。如果有需要还可在加在一个保存父节点引用变量。

5.2.1 链式存储结构的简单实现

下面是一个没有保存父节点引用的二叉树链式存储结构的简单实现,查找其父节点是比较困难的,需要遍历整个树,因此建议加上父节点的引用。

/**
 * 二叉树的链式存储结构的简单实现
 */
public class LinkedBinaryTree<E> {

    /**
     * 外部保存根节点的引用
     */
    private BinaryTreeNode<E> root;

    /**
     * 树节点的数量
     */
    private int size;

    /**
     * 内部节点对象
     *
     * @param <E> 数据类型
     */
    public static class BinaryTreeNode<E> {

        //数据域
        E data;
        //左子节点
        BinaryTreeNode<E> left;
        //右子节点
        BinaryTreeNode<E> right;

        public BinaryTreeNode(E data) {
            this.data = data;
        }

        public BinaryTreeNode(E data, BinaryTreeNode<E> left, BinaryTreeNode<E> right) {
            this.data = data;
            this.left = left;
            this.right = right;
        }

        @Override
        public String toString() {
            return data.toString();
        }
    }

    /**
     * 空构造器
     */
    public LinkedBinaryTree() {
    }

    /**
     * 构造器,初始化root节点
     *
     * @param root 根节点数据
     */
    public LinkedBinaryTree(E root) {
        checkNullData(root);
        this.root = new BinaryTreeNode<>(root);
        size++;
    }

    /**
     * 添加子节点
     *
     * @param parent 父节点的引用
     * @param data   节点数据
     * @param left   是否是左子节点,true 是;false 否
     */
    public BinaryTreeNode<E> addChild(BinaryTreeNode<E> parent, E data, boolean left) {
        checkNullParent(parent);
        checkNullData(data);
        BinaryTreeNode<E> node = new BinaryTreeNode<>(data);
        if (left) {
            if (parent.left != null) {
                throw new IllegalStateException("该父节点已经存在左子节点,添加失败");
            }
            parent.left = node;
        } else {
            if (parent.right != null) {
                throw new IllegalStateException("该父节点已经存在右子节点,添加失败");
            }
            parent.right = node;
        }
        size++;
        return node;
    }

    /**
     * 是否是空树
     *
     * @return true 是 ;false 否
     */
    public boolean isEmpty() {
        return size == 0;
    }


    /**
     * 返回节点数
     *
     * @return 节点数
     */
    public int size() {
        return size;
    }

    /**
     * 获取根节点
     *
     * @return 根节点 ;或者null--表示空树
     */
    public BinaryTreeNode<E> getRoot() {
        return root;
    }

    /**
     * 获取左子节点
     *
     * @param parent 父节点引用
     * @return 左子节点或者null--表示没有左子节点
     */
    public BinaryTreeNode<E> getLeft(BinaryTreeNode<E> parent) {
        return parent == null ? null : parent.left;
    }

    /**
     * 获取右子节点
     *
     * @param parent 父节点引用
     * @return 右子节点或者null--表示没有右子节点
     */
    public BinaryTreeNode<E> getRight(BinaryTreeNode<E> parent) {
        return parent == null ? null : parent.right;
    }


    /**
     * 数据判null
     *
     * @param data 添加的数据
     */
    private void checkNullData(E data) {
        if (data == null) {
            throw new NullPointerException("数据不允许为null");
        }
    }


    /**
     * 检查父节点是否为null
     *
     * @param parent 父节点引用
     */
    private void checkNullParent(BinaryTreeNode<E> parent) {
        if (parent == null) {
            throw new NoSuchElementException("父节点不能为null");
        }
    }
}

复制代码

6 总结

本文为大家介绍了二叉树的入门知识,比如二叉树的概念,特性,性质等,这些东西很多是死的,但是需要我们理解记忆,最后介绍了二叉树的存储结构,以及Java语言的简单实现,对于更加特殊的二叉树,比如红黑树,它们还有自己的独特实现,在后续文章中会介绍。

另外,在实现案例中,并没有树的遍历,以及整颗树的创建等操作,这部分内容较多,将在后续的文章中单独介绍,大家可以关注文章更新。最后,如果大家还不是太清楚树的的概念的,可以看这篇文章:数据结构—树(Tree)的入门原理以及Java实现案例

相关文章:

  1. 《大话数据结构》
  2. 《算法》
  3. 《算法图解》

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

分类:
后端
分类:
后端