1、什么树
首先我们要知道,树是一种非线性表结构。通过下面的图我们可以更加直观的了解到什么是树。
通过上面的图我们可以发现,“树”这种数据结构和我们现实生活中的树很像。上图中的每一个元素我们称之为“节点”;用来连接相邻节点之间的关系,我们叫作“父子关系”。
如下图所示,A节点就是B节点的父节点,B节点是A节点的子节点。B、C、D这三个节点的父节点是同一个节点,所以它们之间互称为兄弟节点。我们把没有父节点的节点叫作根节点,也就是图中节点E。我们把没有子节点的节点叫作叶子节点或者叶节点,比如图中G、H、I、J、K、L都是叶子节点。
除此之外,关于“树”,还有三个比较重要的概念:高度、深度、层。它们的定义是这样的:
(1)节点的高度 = 节点到叶子节点最长路径。(即以叶子节点为基准)
(2)节点的深度 = 跟节点到这个节点所经历的边的个数。(即以根节点为基准)
(3)节点的层数 = 节点的深度 + 1.
(4)树的高度 = 根节点的高度。
下面的图可以更好的帮助我们理解这三个概念
2、二叉树
树结构多种多样,不过我们最常用的还是二叉树。
二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点分别是左子节点和右子节点。不过,二叉树并不要求每个节点都要有两个子节点,有的节点只有左子节点,有的节点只有右子节点。下面列举了一些二叉树。
这个图里面,有两个比较特殊的二叉树,分别是编号2和编号3这两个。
其中编号2的二叉树中,叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点,这种二叉树就叫作满二叉树。
编号3的二叉树中,叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这个二叉树叫作完全二叉树。
总结如下:
1、满二叉树
(1)所有的叶子节点都在最后一层。
(2)除了叶子节点之外,每个节点都有左右两个子节点。
2、完全二叉树
(1)叶子节点只在最后两层出现。
(2)最后一层的叶子节点优先靠左排列。
(3)除了最后一层之外,其他层的节点个数达到最大。
通过下面的图可以帮助我们进一步的了解完全二叉树。
通过上面的介绍,我们知道了什么是棵完全二叉树,但是大家肯定会疑问为什么完全二叉树要这样要求,想要知道这个原因,那么下面就需要了解一下二叉树的存储。
3、二叉树的存储
1、链式存储法
该方法是一种比较简单和直观的方法。从下图中我们可以清楚地看到,每个节点都有三个字段,其中一个存储数据,另外两个是指向左右子节点的指针。我们只要确定根节点,就可以通过左右子节点的指针,把整棵树串起来。这种存储方式我们比较常用。大部分二叉树代码都是通过这种结构实现的。
2、顺序存储法
我们把根节点存储在下标 i = 1的位置,那左子节点存储在下标为 2*i = 2的位置,右子节点存储在 2*i + 1 = 3 的位置。以此类推,B节点的左子节点存储在 2*i = 2*2 = 4的位置,右子节点存储在 2*i + 1 = 2*2+1 = 5的位置。
总结上面所说的内容,如果节点X存储在数组中下标为 i 的位置,下标为 2*i 的位置存储的就是左子节点,下标为 2*i + 1 的位置存储的就是右子节点。反过来,下标为 i/2 的位置存储就是它的父节点。通过这种方式,我们只要知道根节点存储的位置(一般情况下,为了方便计算子节点,根节点会存储在下标为1的位置),这样就可以通过下标计算,把整棵树都串起来。
不过,前面所举的例子是一棵完全二叉树,所以仅仅“浪费”了一个下标为0的位置。如果是非完全二叉树,其实会浪费比较多的数组存储空间。比如下面所举的例子
所以,如果某棵二叉树是一棵完全二叉树,那用数组存储无疑是最节省内存的一种方式。因为数组的存储方式并不需要像链式存储那样,要存储额外的左右子节点的指针。这也是为什么完全二叉树要求最后一层的子节点都靠左的原因。
4、二叉树的遍历
前面介绍了二叉树的基本定义和存储方法,现在我们来看二叉树中非常重要的操作,二叉树的遍历。
(1)前序遍历:对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最后打印它的右子树。
(2)中序遍历:对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后打印它的右子树。
(3)后序遍历:对于树中任意节点来说,先打印它的左子树,然后再打印它的右子树,最后打印这个节点本身。
实际上,二叉树的前、中、后序遍历就是一个递归的过程。具体的实现,我会在下一篇博文中给出。
从前面画的前、中、后序遍历的顺序图,可以看出来,每个节点最多会被访问两次,所以遍历操作的时间复杂度,跟节点的个数n成正比,也就是说二叉树遍历的时间复杂度是O(n)。