数据结构与算法之树(一)二叉树概念及遍历方式(图文并茂)

数据结构与算法之树

数据结构与算法之树(一)二叉树概念及遍历方式(图文并茂)

数据结构与算法之树(二)二叉查找树

数据结构与算法之树(三)AVL树

数据结构与算法之树(四)红黑树

数据结构与算法之树(一)二叉树概念及遍历方式(图文并茂)

一、树的基本概念

首先我们看一看树长什么样

在这里插入图片描述

以上都是我画的树,其中每个元素我们称之为节点,用线连着相邻两个元素的关系我们称为父子关系

我们以下面这副图来讲解树中的各种概念

在这里插入图片描述

在上述这副图中,我们可以说A节点B节点父节点B节点A节点子节点

因为B、C、D三个节点它们的父节点都是A节点,所以称它们为兄弟节点

我们把没有父节点的节点称为根节点,如A节点就是一个根节点

我们把没有子节点的节点称为叶子节点或叶节点,如E、F、G节点

关于树的概念还有高度深度

高度:该节点到叶节点经历的边数

深度:该节点到根节点经历的边数

层:该节点的深度+1

树的高度:根节点的高度

上面这几个概念可能不好理解,看一下下面这个例子就好理解了

在这里插入图片描述

到这里你应该搞懂了树的众多概念了,我将其归纳如下,看你是否还能回忆起来

1.节点

2.父子关系

3.子节点

4.父节点

5.兄弟节点

6.叶子节点或叶节点

7.高度

8.深度

9.层

二、二叉树的定义

2.1 二叉树的定义

二叉树是一种特殊的树,所谓二叉树,其和树的区别是每个节点最多有两个子节点,如下图所示

在这里插入图片描述

又有两种特殊的二叉树:满二叉树完全二叉树

2.2 满二叉树

满二叉树:所有的叶子节节点都在最底层,除叶子节点外,每个节点都有左右两个子节点

如下图就是一棵满二叉树

在这里插入图片描述

2.3 完全二叉树

完全二叉树:从上到下,从左到右,节点是连续的

下图就是一棵完全二叉树

在这里插入图片描述

上述这棵树,你按从上到下,从左到右的顺序看,确实它的节点是连续的,如下图就不是完全二叉树

在这里插入图片描述

上图不是完全二叉树,因为7和9之间少了一个节点

下面再回顾一下,关于二叉树这些定义,你理解了吗?

1.二叉树与树的区别

2.满二叉树的定义

3.完全二叉树的定义

三、二叉树的存储方式

二叉树是一种定义,关于其在内存中具体存储的方式有一下两种方式

1.基于指针的链式存储法

2.基于数组的顺序存储法

3.1 链式存储

我们先看一下直观的链式存储,这种存储方式的每个节点有三个字段,一个存储数据,一个为左子节点指针,一个为右子节点指针,如下定义

TreeNode {
    T data; //数据
    TreeNode* left; //指向左子节点
    TreeNode* right; //指向右子节点
};

 
 
  • 1
  • 2
  • 3
  • 4
  • 5

我们只要存储树根,然后通过各个节点的左右子节点指针,就可以连成一棵树,如下所示

在这里插入图片描述

3.2 顺序存储

我们可以把一棵树的根节点编号为0,然后从上到下,从左到右,对每个节点进行编号,如下图所示

在这里插入图片描述

从上面这棵树,我们可以得到规律

左子节点的编号 = 父节点编号*2 + 1

右子节点的编号 = 父节点编号*2 + 2

父节点的编号 = (子节点编号-1)/ 2

到这里你可以停下来,根据上述的规则自己推导一下

如果我们将上述每个节点的编号对应数组的下标,然后将其存储到数组中,各个节点之间的父子关系我们完全可以通过上述的规律推导出来,这就是基于数组的顺序存储

在这里插入图片描述

上图是一颗完全二叉树,所以编完号之后,我们的数组是没有空缺项的,如果对于下面这棵树来编号,那么它的顺序存储会是怎样?

根据我们的编号规则,即使节点为NULL,也要为它编号,因为只有这样子,父子节点才会满足上述规律

在这里插入图片描述

NULL节点在数组中也是占用一项的,只是用于占位并不使用,所以其顺序存储如下

在这里插入图片描述

可以看到下标为5和8的数组元素为空

所以到这里我们可以看到顺序存储的缺点,就是如果当一棵树不是完全二叉树的时候,那么可能会存在许多空缺节点,使用顺序存储会造成内存空间的浪费

四、二叉树的遍历

二叉树的遍历分为前序遍历中序遍历后续遍历,它们的遍历顺序定义如下

前序遍历:当前节点、左子树、右子树

中序遍历:左子树、当前节点、右子树

后序遍历:左子树、右子树、当前节点

我们可以发现,如果当前节点的访问顺序在最前,那么就是前序遍历当前节点的访问顺序在中间,那么就是中序遍历当前节点的访问顺序在最后,那么就是后续遍历

下面我将超详细地演示前序遍历,这可能是你见过最详细地二叉树遍历教程了

首先准备一颗树,其中没有颜色的节点表示为空,我将其画出来是为了演示

在这里插入图片描述

然后再准备一个三角形

在这里插入图片描述

这个三角形是用来辅助我们观察,现在定义这样一个规则,三角形的定点表示我们关注节点(A节点),左下角表示左子树(B节点),右下角表示右子树(C节点)

二叉树的遍历其实是一个递归的过程,下面我将按照递归调用过程的步骤一步一步讲解前序遍历

4.1 前序遍历

前序遍历的顺序是先访问关注节点,再访问左子树,再访问右子树,请牢牢记住这个规则

前序遍历的结果我这里先给出

A B D E C F

 
 
  • 1

OK,现在开始

1.首先我们的关注节点肯定从根节点开始

在这里插入图片描述

2.根据前序遍历的规则,我们会先访问关注节点(A节点),然后再访问左子树(B节点),此时关注节点就会跳转到B节点

在这里插入图片描述

3.根据前序遍历的规则,我们继续访问当前关注节点(B节点),然后再访问左子树(D节点),此时关注节点跳转到D节点

在这里插入图片描述

4.我们继续访问当前节点(D节点),然后再访问左子树,此时发现左子树为空,那么就继续放回到D节点

在这里插入图片描述

5.在D节点的时候,当前关注节点(D节点)已访问过,左子树也已经访问过,那么根据前序遍历的规则,就会访问右子树,可是现在发现右子树为空,那么关注节点就会继续返回到D节点

在这里插入图片描述

6.此时关注节点还是D节点,左子树访问过,右子树访问过,那么关注节点就会按原路返回到B节点

在这里插入图片描述

7.现在关注节点为B节点,左子树已经访问过,根据前序遍历的规则,现在访问右子树,关注节点跳转到E节点

在这里插入图片描述

8.现在关注节点为E节点,访问E节点,然后继续访问左子树,可是发现左子树为空,那么关注节点就会继续返回到E节点

在这里插入图片描述

9.此时关注节点已经访问过,左子树访问过,根据前序遍历的规则,就会继续访问右子树,此时发现右子树为空,那么就会继续返回到E节点

在这里插入图片描述

10.此时关注节点为E节点,当前关注节点已经访问过,左子树访问过,右子树访问过,那么关注节点就会原路放回到B节点

在这里插入图片描述

11.现在的关注节点为B节点,关注节点访问过,左子树访问过,右子树访问过,那么关注节点就会原路返回到A节点

在这里插入图片描述

12.此时关注节点为A节点,关注节点已经访问过,左子树访问过,根据前序遍历的规则,此时会关注节点会跳转到右子树(C节点),关注节点跳转到C节点

在这里插入图片描述

13.此时关注节点为C节点,会先访问关注节点(C节点),然后访问左子树,发现左子树为空,那么关注节点就会继续返回到C节点

在这里插入图片描述

14.现在关注节点为C节点,关注节点访问过,左子树访问过,现在访问右子树,关注节点跳转到F节点

在这里插入图片描述

15.现在关注节点为F节点,先访问关注节点(F节点),然后访问左子树,发现左子树为空,那么关注节点就继续返回到F节点

在这里插入图片描述

16.此时关注节点为F节点,关注节点已访问,左子树已访问,现在访问右子树,发现右子树为空,所以继续返回到F节点

在这里插入图片描述

17.现在关注节点是F节点,关注节点已访问,左子树已访问,右子树已访问,那么关注节点就原路返回到C节点

在这里插入图片描述

18.现在关注节点为C节点,关注节点已访问,左子树已访问,右子树已访问,那么关注节点就原路返回到A节点,此时整个前序遍历结束

在这里插入图片描述

二叉树的遍历实际上就是一个递归的过程,上述的前序遍历的代码如下

void preOrder(Node* root)
{
	if (root == null)
		return;
print root // 此处为伪代码,表示打印 root 节点
preOrder(root->left); //左子树
preOrder(root->right); //右子树

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

可以看到非常的简单,其本质就是递归自动地帮我们不断地压栈和出栈

如果你仔细观察,你会发现每个节点都会被访问3次,这就表明,在不断地压栈和出栈过程中,一个节点其对应的栈高度在整个过程中会到达三次

关于中序遍历和后续遍历,我这里会给出答案,不会再从头到尾推一边,第一觉得你自己应该去推一边,第二这实在太累人了

4.2 中序遍历

中序遍历的顺序:左子树、关注节点、右子树

中序遍历的结果

D B E A C F

 
 
  • 1

遍历的过程

在这里插入图片描述

中序遍历的递归代码

void inOrder(Node* root)
{
	if (root == null)
		return;
preOrder(root->left); //左子树
print root // 此处为伪代码,表示打印 root 节点
preOrder(root->right); //右子树

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

4.3 后续遍历

后续遍历的顺序:左子树、右子树、关注节点

后续遍历的结果

D B E A C F

 
 
  • 1

遍历的过程

在这里插入图片描述

后续遍历的递归代码

void postOrder(Node* root)
{
	if (root == null)
		return;
preOrder(root->left); //左子树
preOrder(root->right); //右子树
print root // 此处为伪代码,表示打印 root 节点

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

到这里你可能又会发现,三种遍历方式它们的路径都相同,只是访问节点的时机不同而已

是的,我们从三种遍历方式的代码种也可以看出,它们递归调用顺序都是一样的,只是节点的访问时间不同,所以就出现了这种情况

本篇文章到这里就结束了,适当总结一下,本文讲解的树的众多概念,讲解了什么是二叉树,什么是满二叉树,什么是完全二叉树,二叉树的存储方式,二叉树的三种遍历方式,详细剖析了前序遍历的过程

                                </div><div id="content_views" class="markdown_views prism-atom-one-light">
                <!-- flowchart 箭头图标 勿删 -->
                <svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
                    <path stroke-linecap="round" d="M5,0 0,2.5 5,5z" id="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path>
                </svg>
                                        <p><strong>数据结构与算法之树</strong></p>

数据结构与算法之树(一)二叉树概念及遍历方式(图文并茂)

数据结构与算法之树(二)二叉查找树

数据结构与算法之树(三)AVL树

数据结构与算法之树(四)红黑树

数据结构与算法之树(一)二叉树概念及遍历方式(图文并茂)

一、树的基本概念

首先我们看一看树长什么样

在这里插入图片描述

以上都是我画的树,其中每个元素我们称之为节点,用线连着相邻两个元素的关系我们称为父子关系

我们以下面这副图来讲解树中的各种概念

在这里插入图片描述

在上述这副图中,我们可以说A节点B节点父节点B节点A节点子节点

因为B、C、D三个节点它们的父节点都是A节点,所以称它们为兄弟节点

我们把没有父节点的节点称为根节点,如A节点就是一个根节点

我们把没有子节点的节点称为叶子节点或叶节点,如E、F、G节点

关于树的概念还有高度深度

高度:该节点到叶节点经历的边数

深度:该节点到根节点经历的边数

层:该节点的深度+1

树的高度:根节点的高度

上面这几个概念可能不好理解,看一下下面这个例子就好理解了

在这里插入图片描述

到这里你应该搞懂了树的众多概念了,我将其归纳如下,看你是否还能回忆起来

1.节点

2.父子关系

3.子节点

4.父节点

5.兄弟节点

6.叶子节点或叶节点

7.高度

8.深度

9.层

二、二叉树的定义

2.1 二叉树的定义

二叉树是一种特殊的树,所谓二叉树,其和树的区别是每个节点最多有两个子节点,如下图所示

在这里插入图片描述

又有两种特殊的二叉树:满二叉树完全二叉树

2.2 满二叉树

满二叉树:所有的叶子节节点都在最底层,除叶子节点外,每个节点都有左右两个子节点

如下图就是一棵满二叉树

在这里插入图片描述

2.3 完全二叉树

完全二叉树:从上到下,从左到右,节点是连续的

下图就是一棵完全二叉树

在这里插入图片描述

上述这棵树,你按从上到下,从左到右的顺序看,确实它的节点是连续的,如下图就不是完全二叉树

在这里插入图片描述

上图不是完全二叉树,因为7和9之间少了一个节点

下面再回顾一下,关于二叉树这些定义,你理解了吗?

1.二叉树与树的区别

2.满二叉树的定义

3.完全二叉树的定义

三、二叉树的存储方式

二叉树是一种定义,关于其在内存中具体存储的方式有一下两种方式

1.基于指针的链式存储法

2.基于数组的顺序存储法

3.1 链式存储

我们先看一下直观的链式存储,这种存储方式的每个节点有三个字段,一个存储数据,一个为左子节点指针,一个为右子节点指针,如下定义

TreeNode {
    T data; //数据
    TreeNode* left; //指向左子节点
    TreeNode* right; //指向右子节点
};

 
 
  • 1
  • 2
  • 3
  • 4
  • 5

我们只要存储树根,然后通过各个节点的左右子节点指针,就可以连成一棵树,如下所示

在这里插入图片描述

3.2 顺序存储

我们可以把一棵树的根节点编号为0,然后从上到下,从左到右,对每个节点进行编号,如下图所示

在这里插入图片描述

从上面这棵树,我们可以得到规律

左子节点的编号 = 父节点编号*2 + 1

右子节点的编号 = 父节点编号*2 + 2

父节点的编号 = (子节点编号-1)/ 2

到这里你可以停下来,根据上述的规则自己推导一下

如果我们将上述每个节点的编号对应数组的下标,然后将其存储到数组中,各个节点之间的父子关系我们完全可以通过上述的规律推导出来,这就是基于数组的顺序存储

在这里插入图片描述

上图是一颗完全二叉树,所以编完号之后,我们的数组是没有空缺项的,如果对于下面这棵树来编号,那么它的顺序存储会是怎样?

根据我们的编号规则,即使节点为NULL,也要为它编号,因为只有这样子,父子节点才会满足上述规律

在这里插入图片描述

NULL节点在数组中也是占用一项的,只是用于占位并不使用,所以其顺序存储如下

在这里插入图片描述

可以看到下标为5和8的数组元素为空

所以到这里我们可以看到顺序存储的缺点,就是如果当一棵树不是完全二叉树的时候,那么可能会存在许多空缺节点,使用顺序存储会造成内存空间的浪费

四、二叉树的遍历

二叉树的遍历分为前序遍历中序遍历后续遍历,它们的遍历顺序定义如下

前序遍历:当前节点、左子树、右子树

中序遍历:左子树、当前节点、右子树

后序遍历:左子树、右子树、当前节点

我们可以发现,如果当前节点的访问顺序在最前,那么就是前序遍历当前节点的访问顺序在中间,那么就是中序遍历当前节点的访问顺序在最后,那么就是后续遍历

下面我将超详细地演示前序遍历,这可能是你见过最详细地二叉树遍历教程了

首先准备一颗树,其中没有颜色的节点表示为空,我将其画出来是为了演示

在这里插入图片描述

然后再准备一个三角形

在这里插入图片描述

这个三角形是用来辅助我们观察,现在定义这样一个规则,三角形的定点表示我们关注节点(A节点),左下角表示左子树(B节点),右下角表示右子树(C节点)

二叉树的遍历其实是一个递归的过程,下面我将按照递归调用过程的步骤一步一步讲解前序遍历

4.1 前序遍历

前序遍历的顺序是先访问关注节点,再访问左子树,再访问右子树,请牢牢记住这个规则

前序遍历的结果我这里先给出

A B D E C F

 
 
  • 1

OK,现在开始

1.首先我们的关注节点肯定从根节点开始

在这里插入图片描述

2.根据前序遍历的规则,我们会先访问关注节点(A节点),然后再访问左子树(B节点),此时关注节点就会跳转到B节点

在这里插入图片描述

3.根据前序遍历的规则,我们继续访问当前关注节点(B节点),然后再访问左子树(D节点),此时关注节点跳转到D节点

在这里插入图片描述

4.我们继续访问当前节点(D节点),然后再访问左子树,此时发现左子树为空,那么就继续放回到D节点

在这里插入图片描述

5.在D节点的时候,当前关注节点(D节点)已访问过,左子树也已经访问过,那么根据前序遍历的规则,就会访问右子树,可是现在发现右子树为空,那么关注节点就会继续返回到D节点

在这里插入图片描述

6.此时关注节点还是D节点,左子树访问过,右子树访问过,那么关注节点就会按原路返回到B节点

在这里插入图片描述

7.现在关注节点为B节点,左子树已经访问过,根据前序遍历的规则,现在访问右子树,关注节点跳转到E节点

在这里插入图片描述

8.现在关注节点为E节点,访问E节点,然后继续访问左子树,可是发现左子树为空,那么关注节点就会继续返回到E节点

在这里插入图片描述

9.此时关注节点已经访问过,左子树访问过,根据前序遍历的规则,就会继续访问右子树,此时发现右子树为空,那么就会继续返回到E节点

在这里插入图片描述

10.此时关注节点为E节点,当前关注节点已经访问过,左子树访问过,右子树访问过,那么关注节点就会原路放回到B节点

在这里插入图片描述

11.现在的关注节点为B节点,关注节点访问过,左子树访问过,右子树访问过,那么关注节点就会原路返回到A节点

在这里插入图片描述

12.此时关注节点为A节点,关注节点已经访问过,左子树访问过,根据前序遍历的规则,此时会关注节点会跳转到右子树(C节点),关注节点跳转到C节点

在这里插入图片描述

13.此时关注节点为C节点,会先访问关注节点(C节点),然后访问左子树,发现左子树为空,那么关注节点就会继续返回到C节点

在这里插入图片描述

14.现在关注节点为C节点,关注节点访问过,左子树访问过,现在访问右子树,关注节点跳转到F节点

在这里插入图片描述

15.现在关注节点为F节点,先访问关注节点(F节点),然后访问左子树,发现左子树为空,那么关注节点就继续返回到F节点

在这里插入图片描述

16.此时关注节点为F节点,关注节点已访问,左子树已访问,现在访问右子树,发现右子树为空,所以继续返回到F节点

在这里插入图片描述

17.现在关注节点是F节点,关注节点已访问,左子树已访问,右子树已访问,那么关注节点就原路返回到C节点

在这里插入图片描述

18.现在关注节点为C节点,关注节点已访问,左子树已访问,右子树已访问,那么关注节点就原路返回到A节点,此时整个前序遍历结束

在这里插入图片描述

二叉树的遍历实际上就是一个递归的过程,上述的前序遍历的代码如下

void preOrder(Node* root)
{
	if (root == null)
		return;
print root // 此处为伪代码,表示打印 root 节点
preOrder(root-&gt;left); //左子树
preOrder(root-&gt;right); //右子树

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

可以看到非常的简单,其本质就是递归自动地帮我们不断地压栈和出栈

如果你仔细观察,你会发现每个节点都会被访问3次,这就表明,在不断地压栈和出栈过程中,一个节点其对应的栈高度在整个过程中会到达三次

关于中序遍历和后续遍历,我这里会给出答案,不会再从头到尾推一边,第一觉得你自己应该去推一边,第二这实在太累人了

4.2 中序遍历

中序遍历的顺序:左子树、关注节点、右子树

中序遍历的结果

D B E A C F

 
 
  • 1

遍历的过程

在这里插入图片描述

中序遍历的递归代码

void inOrder(Node* root)
{
	if (root == null)
		return;
preOrder(root-&gt;left); //左子树
print root // 此处为伪代码,表示打印 root 节点
preOrder(root-&gt;right); //右子树

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

4.3 后续遍历

后续遍历的顺序:左子树、右子树、关注节点

后续遍历的结果

D B E A C F

 
 
  • 1

遍历的过程

在这里插入图片描述

后续遍历的递归代码

void postOrder(Node* root)
{
	if (root == null)
		return;
preOrder(root-&gt;left); //左子树
preOrder(root-&gt;right); //右子树
print root // 此处为伪代码,表示打印 root 节点

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

到这里你可能又会发现,三种遍历方式它们的路径都相同,只是访问节点的时机不同而已

是的,我们从三种遍历方式的代码种也可以看出,它们递归调用顺序都是一样的,只是节点的访问时间不同,所以就出现了这种情况

本篇文章到这里就结束了,适当总结一下,本文讲解的树的众多概念,讲解了什么是二叉树,什么是满二叉树,什么是完全二叉树,二叉树的存储方式,二叉树的三种遍历方式,详细剖析了前序遍历的过程

                                </div>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值