前面几篇讲了数据结构的线性结构,对于输入数据,链表的线性访问时间太慢。这时我们可以使用一种新的数据结构——树,它的大部分操作的运行时间平均为O(log N)。
树结构
树是由多个结点和边组成的且不存在任何环型的一种数据结构,它与线性结构的区别在于线性结构的存储是“一对一”的关系,而树结构是“一对多”的关系,一个根结点下可以有多个子结点。树结构的示意图如下
树的术语
- 结点:树中的元素为树的结点。如上图中元素1、元素2都是结点。
- 根结点:树的最顶端结点1为树的根结点。
- 父结点:指向其他结点的结点,为这些结点的父结点。如上图中结点1为结点2和结点3的父结点。
- 子结点:被某个结点指向的结点,为该结点的子结点。如上图中结点2和结点3为节点1的子结点。
- 结点的度:一个结点的子结点数量为该结点的度。如上图中结点1的度为2、结点2的度为3、结点4的度为0。
- 叶子结点:度为0的结点为叶子结点。
- 兄弟节点:具有相同的父结点的所有结点为兄弟结点。如上图中结点2和结点3为兄弟结点。
- 结点的层次:根节点的层次为1,其他节点的层次为父节点层次+1。
- 树的深度:树中所有结点的最大层次为该树的深度。如上图中树的深度为3。
二叉树
二叉树是树结构中使用最多的一种。
二叉树的定义:树中每个结点最多只有两个子结点,即任何一个结点的度都小于等于2。
如下图中,左边的为二叉树,右边的不是二叉树。
左边树中每个结点的子结点个数都小于等于2,满足二叉树的定义,因此左边树是一个二叉树。
右边树根结点1有三个子结点2、6、3,不满足二叉树的定义,所以右边树不是一个二叉树。
左子结点和右子结点
因为二叉树最多只有两个子结点,因此我们通常把左侧的子结点称为左子结点,右侧的子结点称为右子结点。
完美二叉树
完美二叉树 又叫 满二叉树。
定义:在一个二叉树中除了最后一层的结点外,其余每个结点都有两个子结点。
如图是一个满二叉树。
如上图为一个满二叉树,除最后一层的结点4、5、6、7外,其他结点都有两个子结点。
二叉树的性质
- 在二叉树中第 i 层的最多结点个数为2i-1(i >= 1)
- 深度为 K 的二叉树最多有2k - 1个结点(k >= 1)
- 在非空二叉树中,n表示叶子结点的个数,m表示度为2的结点数,则:n = m + 1
二叉树的存储
二叉树存储数据,可以使用数组存储和链表存储。
数组存储
上图使用数组存储,按照从上到下、从左到右的顺序给二叉树结点标上数组下标。之后会发现无法找到各个结点之间的对应关系。
我们可以将二叉树补全为一颗满二叉树,然后按照从上到下、从左到右的顺序给二叉树的结点标上数组下标,如图
我们可以发现各个节点之间好像可以找出对应关系了,即父节点下标 * 2 + 1 为其左子结点的下标值;父节点的下标 * 2 + 2 为其右子结点的下标值。
链表存储
链表存储是二叉树最常用的一种存储方法,把每个节点封装为一个对象,定义left和right两个属性,分别指向其左子结点和右子结点。如图
我们可以通过结点的left和right属性就可以找到其子结点。
树的遍历
树的遍历一共分为三种:先序遍历、中序遍历、后续遍历
先序遍历
先序遍历的顺序:访问根结点 => 访问左子结点 => 访问右子结点,即根左右。在访问左子结点或右子结点时,仍按照这个规则继续访问。
对下图进行先序遍历
先序遍历:
- 先访问根结点35,再访问左子结点,最后访问右子结点
- 左子结点是一颗二叉树,按照根左右的顺序,左子结点二叉树访问顺序为:20、18、47
- 右子结点也为一颗二叉树,按照根左右的顺序,右子结点二叉树访问顺序为:30、35、27
- 综合上述,上图中二叉树的先序遍历顺序为:35、20、18、47、30、35、27
如下图为先序遍历的一个访问顺序,虚线为访问顺序
代码实现
private void preOrder(Node node) {
if(node != null){
//先访问根结点
System.out.println("编程小马:" + node.data);
//递归遍历左子结点
preOrder(node.left);
//递归遍历右子结点
preOrder(node.right);
}
}
参数node为树的根结点。
先序遍历:先访问根结点,在递归遍历左子结点,最后递归遍历右子结点。
中序遍历
中序遍历的顺序:访问左子结点 => 访问跟结点 => 访问右子结点,即左根右。在访问左子结点或右子结点时,仍按照这个规则继续访问。
对下图进行先序遍历
中序遍历:
- 先访问左子结点,左子结点为一颗二叉树,按照中序遍历规则遍历左子结点的二叉树为:18、20、47
- 再访问根结点35
- 最后访问右子结点,右子结点也是一颗二叉树,按照中序遍历规则遍历右子结点的二叉树为:35、30、27
- 综合上述,上图中二叉树的中序遍历顺序为:18、20、47、35、35、30、27
如下图为先序遍历的一个访问顺序,虚线为访问顺序
代码实现
private void inOrder(Node node) {
if(node != null){
inOrder(node.left);
System.out.println("编程小马:" + node.data);
inOrder(node.right);
}
}
参数node为树的根结点。
中序遍历:先递归遍历左子结点,在访问根结点,最后递归遍历右子结点。
后序遍历
后序遍历的顺序:访问左子结点 => 访问右子结点 => 访问根结点,即左右根。在访问左子结点或右子结点时,仍按照这个规则继续访问。
对下图进行先序遍历
后序遍历:
- 先访问左子结点,左子结点为一颗二叉树,按照中序遍历规则遍历左子结点的二叉树为:18、47、20
- 在访问右子结点,右子结点也是一颗二叉树,按照中序遍历规则遍历右子结点的二叉树为:35、27、30
- 再访问根结点35
- 综合上述,上图中二叉树的中序遍历顺序为:18、47、20、35、27、30、35
如下图为先序遍历的一个访问顺序,虚线为访问顺序
代码实现
private void postOrder(Node node) {
if(node != null){
postOrder(node.left);
postOrder(node.right);
System.out.println("编程小马:" + node.data);
}
}
参数node为树的根结点。
后序遍历:先递归遍历左子结点,在递归遍历右子结点,最后访问根结点。
通过上面的介绍,对二叉树也有了一些了解,可以访问二叉查找数对二叉树更深的学习。