数据结构与算法系列二十五(树的存储与访问)

170 阅读6分钟

1.引子

上一篇我们分享了:节点、高度、深度、层等树的基本概念,你都还记得吗?接下来我们要分享的是树的存储,与树的访问

你可以先尝试思考一下,在实际应用中,我们应该如何去存储一棵树,用什么样的数据结构?

你还可以先尝试思考一下,在实际应用中,我们该如何去访问一棵树,比如说访问某个节点,用什么样的算法?

#考考你:
1.你知道实际应用中,用什么数据结构存储树吗
2.你知道都有哪些树的访问方式吗

2.案例

2.1.树的存储方式

如果现在有一堆数据,我们需要按照树的结构来组织这些数据,你会怎么做呢?我们可以先思考一下树的特点:树是由一个一个节点组织而成的

为了简化问题的理解,以二叉树为例,关于二叉树,我们先用一句话描述一下:二叉树是指树中的每一个节点,最多只有左、右两个子节点的树。关于二叉树的更多内容,我们将在下一篇中分享。为了直观理解,我们先看一个图:

image.png

#图一:
1.图一树中的每一个节点,最多都只有左、右两个子节点
2.这样的树,我们称为二叉树
​
#图二:
1.图二树中的节点,有些有三个子节点(甚至还可以有更多子节点)
2.这样的树,我们称为多叉树
3.图二是一棵三叉树

2.1.1.链式存储法

假如现在有一棵二叉树,比如上图图一中的树。我们该如何存储呢?该用什么样的数据结构呢?

这里所谓链式存储法,你能联系到什么吗?没错,我们在前边已经非常熟悉的基本数据结构:链表

确定了数据结构,该如何设计树中的节点呢?以二叉树为例,我们说每一个节点至少需要存储:节点数据、左子节点指针、右子节点指针

到这里,我们基本上确定了通过链式存储法,存储二叉树。看一个图,你应该就明白了:

image.png

#链式存储法:
1.每个节点,有三个字段
2.数据、左子节点指针、右子节点指针
3.从根节点,通过左右子节点指针,将整棵树串联起来

2.1.2.顺序存储法

通过链式存储法存储树。我们会发现树的结构其实非常清晰,可以很清晰的看到:父节点、以及左右子节点。但是链式存储法有它瑕疵的一面,比较消耗空间,每个节点需要存储:节点数据、左子节点指针、右子节点指针

如果你们家不是地主,没有太多的余粮,那就比较尴尬了,对吧。

这个时候,我们可以想想别的办法。假如有这样一棵树,它满足:

  • 是一棵二叉树
  • 叶子节点都在最底下两层
  • 最后一层的子节点,都靠左排列
  • 除了最后一层,其它层的子节点个数,都达到最大(都有两个子节点)

等等,描述的是什么鬼,感觉好抽象有没有。没关系,我们看一个图,你就明白了:

image.png

图上的树,是一棵比较特殊的树,我们称为:完全二叉树。关于什么是完全二叉树,我们放到下一篇中分享,你暂时只需要知道它满足上面提到的四个特点即可。

好了,前奏铺垫得差不多了,假如有这样一棵完全二叉树。除了链式存储法,你还能想到其它更好的存储方式吗

如果你还是没有想起来,我们不妨提示一下:顺序存储。说到顺序,你一定想到什么了,对吧?没错,通过:数组来存储通过数组存储的方式,即我们说的顺序存储法。还是看一个图,你应该就能明白了:

image.png

#顺序存储法
1.为了方便操作,根节点,存储在下标i=1的位置
2.左子节点存储的下标位置:2*i
3.右子节点存储的下标位置:2*i + 1
4.以此类推

顺序存储法:可以解决地主家没有余粮的问题。每一个节点都只需要存储该节点数据即可,通过数组下标可以很方便的表达左、右子节点关系。可以节省存储空间

但是,我们需要注意,通过顺序存储法存储树,对树本身是有要求的,即树必须满足一棵完全二叉树。要不然,也存在浪费存储空间。关于这点,我就不展开了,你可以稍微思考一下。

2.2.二叉树的访问方式

关于二叉树的访问方式,我们先尝试思考一下:通过什么样的方式,能够完整的访问一棵树的所有节点呢? 比如说,从根节点开始,如何能够访问到树的所有节点

如果大学的时候,你没有逃课:数据结构与算法的课程的话,你一定对:前序遍历、中序遍历、后序遍历印象深刻,对吧。尽管你当时不一定懂,事实上我当时也不是很明白,但是考试的时候还是能把答案选对。

那么到底什么是前序遍历、中序遍历、后序遍历呢?我们分别来看一下:

#前序遍历
1.对于树中的任意节点,先打印该节点
2.然后再打印它的左子树
3.最后打印它的右子树
​
#中序遍历
1.对于树中的任意节点,先打印它的左子树
2.然后再打印该节点
3.最后打印它的右子树
​
#后序遍历
1.对于树中的任意节点,先打印它的左子树
2.然后再打印它的右子树
3.最后打印该节点本身
​
#如果你觉得文字描述有点抽象,那么对照以下示例图,你肯定就能明白了:

示例图:

image.png

2.2.1.前序遍历

/**
* 二叉树前序遍历:伪代码
* 1.对于树中的任意节点,先打印该节点
* 2.然后再打印它的左子树
* 3.最后打印它的右子树
* @param root
*/
public void preOrder(Node root){
   if(root == null){ return;}
   // 1.访问节点本身
   System.out.println(root);
        
   // 2.递归访问左子树
   preOrder(root.left);
        
    // 3.递归访问右子树
    preOrder(root.right);
        
}

2.2.2.中序遍历

/**
* 二叉树中序遍历:伪代码
* 1.对于树中的任意节点,先打印它的左子树
* 2.然后再打印该节点
* 3.最后打印它的右子树
* @param root
*/
public void inOrder(Node root){
   if(root == null){ return;}
    
   // 1.递归访问左子树
   inOrder(root.left);
    
   // 2.访问节点本身
   System.out.println(root);
        
   // 3.递归访问右子树
   inOrder(root.right);
        
}

2.2.3.后序遍历

/**
* 二叉树后序遍历:伪代码
* 1.对于树中的任意节点,先打印它的左子树
* 2.然后再打印它的右子树
* 3.最后打印它本身
* @param root
*/
public void postOrder(Node root){
   if(root == null){ return;}
    
   // 1.递归访问左子树
   postOrder(root.left);
    
   // 2.递归访问右子树
   postOrder(root.right);
    
   // 3.访问节点本身
   System.out.println(root);
     
}