今天来说一下树,树是一种非线性结构,比线性结构复杂得多。
树(Tree)
什么是树?
通过上图,我们发现,树这种存储结构很像我们生活中的树,每个元素我们称为节点,用来连线相邻节点之间的关系,称为父子关系。
如下图,A节点就是B节点的父节点,B节点是A节点的子节点。B、C、D这三个节点的父节点是同一个节点,所以它们之间互称为兄弟节点。我们把没有父节点的节点叫作根节点,也就是图中的节点E。我们把没有子节点的节点叫作叶子节点或者叶节点,比如图中的G、H、I、J、K、L都是叶子节点。
关于树的几个概念,高度、深度、层:
节点的高度:节点到叶子节点的最长路径(边数)
节点的深度:根节点到这个节点所经历的边数
节点的层数:节点的深度+1
树的高度:跟节点的高度
如下图:
二叉树
树的结构多种多样,比较常用的有二叉树。
顾名思义,二叉树就是每个节点最多有两个叉,也就是每个节点最多有两个子节点。如下图三个二叉树:
这个图里,有两个比较特殊的二叉树,分别是编号2和3这两个。
其中编号2,叶子节点都在最底层,除了叶子节点外,其他所有节点都有两个子节点,这种树叫做满二叉树。
编号3,叶子节点都在最底层,最后一层子节点都往左排列,并且除了最后一层节点,其他层的子节点个数都达到最大,称为完全二叉树。
满二叉树特征很明显,但是完全二叉树并不是很明显。那么为什么我们还要将完全二叉树特别说明呢,为什么偏偏最后一层子节点全都靠右排列就叫完全二叉树?如果靠右就不能叫完全二叉树了吗?这个定义的由来或者目的是什么?
要了解完全二叉树定义的由来,我们先了解如何存储一颗二叉树。
要存储一颗二叉树,我们有两种方法,一种基于指针或者引用的二叉链式存储法,另一种是基于数组的顺序存储法。
链式存储法:
如下图,每个节点有三个字段,其中一个存储数据,另外是两个指向左右子节点的指针。
顺序存储法:
基于数组的顺序存储法,我们把根节点存储在下标i = 1的位置,那左子节点存储在下标2 * i = 2的位置,右子节点存储在2 * i + 1 = 3的位置。以此类 推,B节点的左子节点存储在2 * i = 2 * 2 = 4的位置,右子节点存储在2 * i + 1 = 2 * 2 + 1 = 5的位置。如下图是一棵完全二叉树的顺序存储:
上面介绍的是一棵完全二叉树的顺序存储,我们发现i=0的存储位置并没有使用,造成了空间浪费。如果是非完全二叉树,则会浪费更多的空间,如下图:
所以,如果某棵二叉树是一棵完全二叉树,那用数组存储无疑是最节省内存的一种方式。因为数组的存储方式并不需要像链式存储法那样,要存储额外的左右子节点的指针。这也是为什么完全二叉树会单独拎出来的原因,也是为什么完全二叉树要求最后一层的子节点都靠左的原因。
二叉树的遍历
二叉树的遍历,经典的方法有三种,前序遍历、中序遍历、后序遍历。其中,前、中、后序,表示的是节点与它的左右子树节点遍历打印的 先后顺序。
二叉树遍历java代码实现:
/** * 前序遍历 */ public void preOrder(TreeNode node){ if (node != null){ System.out.println(node.value); preOrder(node.leftNode); preOrder(node.rightNode); } } /** * 中序遍历 */ public void inOrder(TreeNode node){ if (node != null){ preOrder(node.leftNode); System.out.println(node.value); preOrder(node.rightNode); } } /** * 后序遍历 */ public void postOrder(TreeNode node){ if (node != null){ preOrder(node.leftNode); preOrder(node.rightNode); System.out.println(node.value); } }
遍历时间复杂度O(n)。