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

·  阅读 268

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

本文将详细介绍树这种数据结构的基本概念,以及通用的树的Java实现方式,为后面各种树的深入学习打好基础。

树结构和线性结构的最大的不同是,树中的节点具有明显的层级关系,并且一个节点可以对应多个节点。

1 树的概述

1.1 定义

树结构和线性结构的最大的不同是,树中的节点具有明显的层级关系,并且一个节点可以对应多个节点。树的定义如下:

树(Tree)是n(n≥0)个节点的有限集。n=0时称为空树。在任意一棵非空树中:

  1. 有且仅有一个特定的称为根(Root)的节点r;
  2. 当n>1时,其余节点可分为m(m>0)个互不相交的不为空的有限集T1、T2、……、Tm,其中每一个集本身又是一棵树,并且称为根的子树(SubTree)。

从上面树的定义可以看出使用了递归的思想,也就是在树的定义之中还用到了树的概念,根的子树节点,同时作为子树的根节点。递归思想和栈数据结构有关,可以看这篇文章:数据结构—栈(Stack)的原理以及Java实现以及后缀表达式的运算

在这里插入图片描述

如上图,该树具有唯一根节点r,它的子树是以a、b、c为根节点的三棵树。而子树a下面还有一颗子树d。

在这里插入图片描述

从递归中我们发现,一棵树是N个节点和N−1条边的集合,其中的一个节点叫做根。存在N−1条边的结论是由下面的事实得出的,每条边都将某个节点连接到它的父亲,而除去根节点外每一个节点都有一个父亲。

1.2 节点

在这里插入图片描述

在上图中,节点r是根。节点a有一个父亲并且儿子d、e。每一个节点可以有任意多个儿子,也可能是零个儿子。没有儿子的节点称为树叶(leaf);上图中的树叶是e、f、g、h、i、j和k。

具有相同父亲的节点称为兄弟(sibling),比如a、b和c节点都是兄弟节点;用类似的方法可以定义祖父(grandfather)和孙子(grandchild)。

森林:两颗或两颗以上互不相交的树的集合。

1.3 深度和高度

在这里插入图片描述

节点的层次(Level)从根开始定义起,这里规定,根为第一层,根的孩子为第二层。若某节点在第i层,则其子树就在第i+1层。树中节点的最大层次称为树的深度(Depth)或高度,上图中,树的深度为4。

本文约定:从上往下数就是深度,从下往上数就是高度。另外由于不同的参考书对于根节点深度和高度的定义不一样,而节点的深度和高度会受到根节点的深度0还是1的影响,本文取根节点深度为1。一棵树的深度等于它的最深的树叶的深度;该深度总是等于这棵树的高度。

上图中,根节点r的深度为1,高度为4。子节点b的深度为2,高度为2。

1.4 节点的度

在这里插入图片描述

树的节点包含一个数据元素及若干指向其子树的分支。节点拥有的直接子节点数称为该节点的度(De-gree)

度为0的节点称为叶节点(Leaf)或终端节点;度不为0的节点称为非终端节点或分支节点。

除根节点之外,分支节点也称为内部节点。树的度是树内各节点的度的最大值。上图中,因为这棵树节点的度的最大值是节点r和d的度,为3,所以树的度也为3。

1.5 有序性

无序树:树中任意节点的子节点之间没有顺序关系,这种树称为无序树,也称为自由树;有序树:树中任意节点的子节点之间有顺序关系,这种树称为有序树。

2 树的通用实现

树也可以采用顺序存储结构和链式存储结构实现。下面有三种通用的树实现的方法。后面介绍特殊的树时,比如二叉树、红黑树等它们还有自己的特殊实现方式。

2.1 父节点表示法

采用顺序存储结构来实现树,底层采用数组来存储节点。由于子节点具有唯一的父节点,因此,可以采用父节点表示法表示出一棵树的结构。

节点由数据域和父节点的索引位置组成,根节点的父节点索引为-1。

在这里插入图片描述

上图的树,使用最简单的父节点表示法可以表示为:

index(数组索引) data(数据域) parent(父节点索引)
0 r -1
1 a 0
2 b 0
3 c 0
4 d 1
5 e 1
6 f 2
7 g 3
8 h 3
9 i 4
10 j 4
11 k 4

树节点的设计可以是:

/**
 * 树节点的设计
 *
 * @param <E> E类型
 */
private static class TreeNode<E> {
    //数据域
    E e;
    //父节点的索引
    int parent;

    public TreeNode(E e, int parent) {
        this.e = e;
        this.parent = parent;
    }
}

复制代码

这样的存储结构,我们可以根据结点的parent索引很容易找到它的双亲结点,所用的时间复杂度为O(1),直到parent为-1时,表示找到了树结点的根。但是如果要知道孩子节点是什么,那就需要遍历整个数组才行。

2.2 父子节点链表示法

为了能够直观的找到一个节点的孩子节点。在原节点中添加一个子节点链表,原节点保存子节点链表的头索引,同时节点保存着父节点的索引。这需要一种新的孩子节点。

index(数组索引) data(数据域) parent(父节点索引) child(子节点链表头索引)
0 r -1 a->b->c->null
1 a 0 d->e->null
2 b 0 f->null
3 c 0 g->h->null
4 d 1 i->j->k
5 e 1 null
6 f 2 null
7 g 3 null
8 h 3 null
9 i 4 null
10 j 4 null
11 k 4 null

树节点和子节点的设计可以为:

/**
 * 树节点的设计
 *
 * @param <E> E类型
 */
private static class TreeNode<E> {
    //数据域
    E e;
    //父节点的索引
    int parent;

    //第一个子节点的引用
    TreeNodeChild<E> firstChild;

    public TreeNode(E e, int parent, TreeNodeChild<E> firstChild) {
        this.e = e;
        this.parent = parent;
        this.firstChild = firstChild;
    }
}

/**
 * 子节点链表结构
 *
 * @param <E>
 */
private static class TreeNodeChild<E> {
    //子节点的索引
    int index;
    //下一个子节点的索引
    TreeNodeChild<E> next;

    public TreeNodeChild(int index, TreeNodeChild<E> next) {
        this.index = index;
        this.next = next;
    }
}

复制代码

2.3 父子兄弟表示法

对于上面的父节点表示法和父子节点表示法,不能很方便的得知节点的兄弟节点。

实际上,我们观察树形结构后发现,任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,我们设置两个索引,分别指向该结点的第一个孩子和此结点的右兄弟。只需要两个引用就能很方便的表示出节点的唯一对应关系。

为了突破节点数量限制,我们可以使用链表。为了找到父节点,我们还可以添加父节点索引。

data(数据域) parent(父节点引用) child(第一个子节点引用) right(节点右兄弟引用)
r null a null
a r d b
b r f c
c r g null
d a i e
e a null null
f b null null
g c null h
h c null null
i d null j
j d null k
k d null null

节点可以统一设计为如下,不需要额外的节点结构:

/**
 * 树节点的设计
 *
 * @param <E> E类型
 */
private static class TreeNode<E> {
    //数据域
    E e;
    //父节点的引用(可选)
    TreeNode<E> parent;

    //第一个子节点的引用
    TreeNode<E> firstChild;

    //右兄弟节点的引用
    TreeNode<E> rightBrother;
}

复制代码

使用示意图来直观的展示这种表示法的效果:

在这里插入图片描述

图中只标出了子节点和兄弟节点的引用链,红色为子节点引用,黑色为右兄弟节点的引用。我们看到,只需要这两个引用关系就能表示出一颗完整的树。并且需要查找某个节点的子节点或者父节点或者兄弟节点时,不需要完整遍历整颗树。

上面的表示方法,每个节点最多包括两个引用节点(不计算父节点引用),实际上这样的表示方法,把复杂的多子节点的树,表示成了一颗简单的二叉树:

在这里插入图片描述

这种表示法将是以后最常用的方法,二叉树将是我们在以后用到的最多的树种之一。

3 总结

本文作为树这种数据结构的入门文章,详细讲解了树以及各种专业术语的定义和解释,然后讲解了树的通用实现,讲了父节点表示法、父子节点链表示法、父子兄弟表示法。实际上后面的特性化的数据结构比如二叉树、AVL树、红黑树,均有自己的表示方法。但是上面这几种方法作为树的通用实现,还是可以了解一下的。

最后提到了二叉树,二叉树将是我们在以后用到的最多的树种之一,内容非常繁多,在此不做介绍,本文作为树的入门文章,在后续的文章中,将慢慢介绍二叉树等特性化的树。

相关文章:

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

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

分类:
后端
分类:
后端
收藏成功!
已添加到「」, 点击更改