点亮你的数据结构知识:通晓二叉树是必须的

文章目录

  • 树的概念
    • 树在实际中的运用
  • 二叉树
    • 二叉树的概念
    • 特殊的二叉树
    • 二叉树的性质
    • 二叉树的存储方式
    • 二叉树链式结构的实现
      • 二叉树的遍历方式
      • 二叉树的基本操作
        • 二叉树前序遍历
        • 二叉树中序遍历
        • 二叉树后序遍历
        • 二叉树节点个数
        • 叶子节点的个数
        • 二叉树的高度
        • 二叉树第k层结点个数
        • 二叉树的层序遍历

树的概念

  • 树是一种非线性的数据结构,他是一种以树形结构进行组织的数据结构,它由一组节点和连接这些节点的边构成。树最顶部的节点称为根节点,每个非根节点都有且仅有一个父节点,但可以有多个子节点。每个节点的子节点可以有任意个数,
    点亮你的数据结构知识:通晓二叉树是必须的_第1张图片

  • 树的相关概念
    点亮你的数据结构知识:通晓二叉树是必须的_第2张图片
    概念:树+人类亲缘关系描述 (这些相关概念是比较重要的因此我们需要记性理解记忆,我们借租人类亲缘关系来记忆更加得心应手)

  • 节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6
  • 叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I…等节点为叶节点
  • 非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G…等节点为分支节点
  • 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点
  • 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点
  • 兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点
  • 树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6
  • 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推
  • 树的高度或深度:树中节点的最大层次; 如上图:树的高度为4
  • 堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点
  • 节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
  • 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
  • 森林:由m(m>0)棵互不相交的树的集合称为森林
  • 注意:树形结构中,子树之间不能有交集,否则就不是树形结构点亮你的数据结构知识:通晓二叉树是必须的_第3张图片

树在实际中的运用

  • 作为一种基本的数据结构,树结构在计算机科学中扮演着非常重要的角色,被广泛应用于各种领域。下面,让我们来看一些关于数据结构树如何在实际中使用的例子.
  1. 搜索引擎中的应用

大家在使用搜索引擎的时候,肯定会注意到在输入关键词之后,搜索引擎会迅速返回数以百万计的结果。搜索引擎看似简单的背后,搜集整理海量数据并进行高效检索的能力,建立在树结构的基础上。通过构建一棵巨大的树结构,每个节点代表一个网页,搜索引擎就能够快速地将用户的关键词与树中的每个节点匹配,找到最符合要求的结果并迅速返回给用户。
当然,搜索引擎的树并不是普通的树,而是以倒排索引为基础建立的B树或B+树。但是,不管是什么类型的树,都非常强大且高效!

  1. 文件系统

文件系统是我们计算机中使用的组织和存储文件的一种方式。它通常被表示成一个层次结构,也就是一个树。在树形结构中,每个目录都是一个节点,而文件则是这个节点的子节点。通过树形结构,我们可以快速方便地找到需要的文件,提高了文件系统的效率。此外,许多操作系统也使用树来管理进程和内存。

  1. 计算机网络中的应用

计算机网络中的路由器是非常重要的设备,它的主要任务是将数据报文从源地址转发到目的地址。而为了快速准确地定位目的地址,路由器就需要使用到一种名为“路由表”的数据结构,而路由表的底层数据结构正是二叉树。

  • 所以本章我们主要讲解二叉树.

二叉树

二叉树的概念

  • 二叉树是一种由节点和连接构成的数据结构。它由一个根节点开始,它没有父节点,但可能有左子节点和右子节点。每个节点最多可以有两个子节点,分别称为左子节点和右子节点。如果一个节点没有子节点,则称之为叶子节点。
  • 一棵二叉树是结点的一个有限集合,该集合:
  1. 或者为空
  2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成
    点亮你的数据结构知识:通晓二叉树是必须的_第4张图片
    从上图可以看出:
  • 二叉树不存在度大于2的结点
  • 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

注意:对于任意的二叉树都是由以下几种情况复合而成的:
点亮你的数据结构知识:通晓二叉树是必须的_第5张图片


特殊的二叉树

  • 满二叉树

-满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。如图所示:点亮你的数据结构知识:通晓二叉树是必须的_第6张图片

  • 这棵二叉树为满二叉树,也可以说深度为k,有2^k-1个节点的二叉树。
  • 完全二叉树

完全二叉树: 完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。 对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。

大家要自己看完全二叉树的定义,很多人对完全二叉树其实不是真正的懂了。

  • 一个典型的例子点亮你的数据结构知识:通晓二叉树是必须的_第7张图片
    相信不少人以为最后一个二叉树是不是完全二叉树都中招了。

二叉树的性质

  1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1) 个结点.

推导过程: (如有看不懂可以跳过推导过程)

假设一颗满二又树高度是h
总节点的个数:
2~0 + 2’1 + 22…+2(h-1) = N
2^(h-1) = N

  1. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是 2^h - 1

推导过程: (如有看不懂可以跳过推导过程)

首先,深度为1的二叉树只有一个根节点,即只有一个结点。
进一步考虑,深度为2的满二叉树则有一个根节点以及两个子节点,共三个结点。深度为3的满二叉树则在深度为2的树的每个节点下面添加两个子节点,使得节点的数量翻倍,即共有7个节点(1+2+4)。
我们可以发现,每个深度为h的满二叉树的结点数,等于前面所有深度的结点数之和,再加上1(根节点)。即:
结点数 = 1 + 2 + 4 + … + 2^h - 1 等于 2^ 0 + 2^1 + 2^2 + … 2^h - 1
因为2^ 0 + 2^1 + 2^2 + … 2^h - 1是一个等比数列,公比为2,所以可以使用等比数列的求和公式:
结点数 = (1 - 2^h)/(1-2) = 2^h - 1
因此,深度为h的满二叉树的结点数是2^h - 1

  1. 对任何一棵二叉树,如果度为0其叶结点个数为 no,度为2的分支结点个数为 n2. 则有n0 = n2+1.
  2. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h= log2^n + 1

推导过程: (如有看不懂可以跳过推导过程)

假设树的高度是h
假设一颗满二又树高度是h
总节点的个数:
2’0 + 2’1 + 2 '2…+2~(h-1) = N
2~(h-1) = N
h= log2^n + 1

  1. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有
  1. 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
  2. 若2i+1=n否则无左孩子
  3. 若2i+2=n否则无右孩子

二叉树的存储方式

  • 二叉树可以链式存储,也可以顺序存储。
    那么链式存储方式就用指针, 顺序存储的方式就是用数组。
    顾名思义就是顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在各个地址的节点串联一起。

  • 链式存储

二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址.点亮你的数据结构知识:通晓二叉树是必须的_第8张图片 链式存储是大家很熟悉的一种方式,那么我们来看看如何顺序存储呢?

  • 顺序存储

顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆后面的章节会专门讲解。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。点亮你的数据结构知识:通晓二叉树是必须的_第9张图片

  • 如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。

但是用链式表示的二叉树,更有利于我们理解,所以一般我们都是用链式存储二叉树。
所以大家要了解,用数组依然可以表示二叉树. 所以本章节先讲链式存储二叉树。

  • 链式存储结构
// 定义二叉树中存储的数据类型为int
typedef int BTDataType;

// 定义二叉树结点类型
typedef struct BinaryTreeNode
{
    BTDataType data;                    // 节点的数据
    struct BinaryTreeNode* left;        // 指向左子树节点
    struct BinaryTreeNode* right;       // 指向右子树节点
}BTNode;                                // 将该结构体类型命名为BTNode

大家会发现二叉树的定义 和链表是差不多的,相对于链表 ,二叉树的节点里多了一个指针, 有两个指针,指向左右孩子。


二叉树链式结构的实现

在学习二叉树的基本操作前,需要先搭建一棵二叉树,才能学习其相关的基本操作。为了降低大家学习成本,此处我们暴力建立一棵简单的二叉树,快速进入二叉树操作学习。等后面我们在研究二叉树真正的创建方式。

BTNode* CreateBinaryTree()
{
    // 创建根节点1
    BTNode* root = (BTNode*)malloc(sizeof(BTNode));
    root->data = 1;

    // 创建左子树2
    BTNode* node2 = (BTNode*)malloc(sizeof(BTNode));
    node2->data = 2;
    root->left = node2;

    // 创建右子树4
    BTNode* node4 = (BTNode*)malloc(sizeof(BTNode));
    node4->data = 4;
    root->right = node4;

    // 创建2的左子节点3
    BTNode* node3 = (BTNode*)malloc(sizeof(BTNode));
    node3->data = 3;
    node2->left = node3;
    node2->right = NULL;

    // 创建2的右子节点5
    BTNode* node5 = (BTNode*)malloc(sizeof(BTNode));
    node5->data = 5;
    node4->left = NULL;
    node4->right = node5;
    
    node5->left = NULL;
    node5->right = NULL;
    node3->left = NULL;
    node3->right = NULL;

    return root;  // 返回根节点
}

点亮你的数据结构知识:通晓二叉树是必须的_第10张图片
该二叉树是这样子的.


二叉树的遍历方式

学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。

  • 关于二叉树的遍历方式,要知道二叉树遍历的基本方式都有哪些。
    我这里把二叉树的几种遍历方式列出来,大家就可以一一串起来了。

  • 二叉树主要有两种遍历方式

  1. 深度优先遍历:先往深走,遇到叶子节点再往回走。
  2. 广度优先遍历:一层一层的去遍历。

  • 深度优先遍历
    前序遍历(递归法,迭代法)
    中序遍历(递归法,迭代法)
    后序遍历(递归法,迭代法)

递归法:

  1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
  2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
  3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
  • 广度优先遍历
    层次遍历(迭代法)

层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层
上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。

  • 在深度优先遍历中:有三个遍历顺序,前中后序遍历, 有的朋友在这里总分不清这三个顺序,经常搞混,我这里教大家一个技巧。

这里前中后,其实指的就是中间节点的遍历顺序,只要大家记住 前中后序指的就是中间节点的位置就可以了。

看如下中间节点的顺序,就可以发现,中间节点的顺序就是所谓的遍历方式

  • 前序遍历:中左右
  • 中序遍历:左中右
  • 后序遍历:左右中

大家可以对着如下图,看看自己理解的前后中序有没有问题。
点亮你的数据结构知识:通晓二叉树是必须的_第11张图片

  • 而广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。
    点亮你的数据结构知识:通晓二叉树是必须的_第12张图片

二叉树的基本操作

  • 其实这些都是二叉树基本的操作,如有不懂画上递归展开图和代码调式即可一目了然.

二叉树前序遍历

  • 二叉树前序遍历是一种深度优先遍历算法,也叫先序遍历。具体过程如下:

    1 . 访问根节点。

    2 . 遍历左子树。

    3 . 遍历右子树。

void PrevOrder(BTNode* root) // 二叉树先序遍历,接收一个二叉树根节点指针
{
	if (root == NULL) // 终止条件:如果当前节点为空,则输出"N",并返回上一层递归
	{
		printf("N ");
		return;
	}

	printf("%d ", root->data); // 输出当前节点的值

	PrevOrder(root->left); // 递归遍历左子树
	PrevOrder(root->right); // 递归遍历右子树
}

二叉树中序遍历

  • 中序遍历跟前序差不多,只是访问顺序变了
    1 . 遍历左子树。

    2 . 访问根节点。

    3 . 遍历右子树。

void InOrder(BTNode* root) // 二叉树中序遍历,接收一个二叉树根节点指针
{
	if (root == NULL) //终止条件: 如果当前节点为空,则输出"N",并返回上一层递归
	{
		printf("N ");
		return;
	}

	InOrder(root->left); //递归遍历左子树
	printf("%d ", root->data); //输出当前节点的值
	InOrder(root->right); //递归遍历右子树
}

二叉树后序遍历

1 . 遍历左子树。

2 . 遍历右子树。

3 . 访问根节点。
void PostOrder(BTNode* root) // 二叉树后序遍历,接收一个二叉树根节点指针
{
	if (root == NULL) // 终止条件:如果当前节点为空,则输出"N",并返回上一层递归
	{
		printf("N ");
		return;
	}
	
    PostOrder(root->left); // 递归遍历左子树
    PostOrder(root->right); // 递归遍历右子树
    printf("%d ", root->data); // 输出当前节点的值
}

二叉树节点个数

  • 采用后续遍历,先访问他的左右子树然后回到跟节点+1返回,这样就可以把字点个数返回.
    如果当前节点为空(即二叉树为空),返回0。
    递归计算左子树的节点数量,并赋值给leftSize。
    递归计算右子树的节点数量,并赋值给rightSize。
    返回左子树和右子树节点数量之和再加1(1表示根节点)。
int TreeSize(BTNode* root)
{
    // 如果二叉树为空,返回0
    if (root == NULL)
    {
        return 0;
    }

    // 递归计算左子树和右子树的节点数量
    int leftSize = TreeSize(root->left);
    int rightSize = TreeSize(root->right);

    // 返回左子树节点数量、右子树节点数量和根节点数量之和 再加1
    return leftSize + rightSize + 1; 
}

叶子节点的个数

  • 大体逻辑已写再注释.
int BTreeLeafSize(BTNode* root) // 二叉树叶子节点个数,接收一个二叉树根节点指针
{
	if (root == NULL) // 终止条件:如果当前节点为空,则叶子节点个数为 0,返回 0
	{
		return 0;
	}

	if (root->left == NULL && root->right == NULL) // 如果当前节点左右子树都为空,则说明它是一个叶子节点,返回 1
	{
		return 1;
	}

	// 递归计算其左右子树中叶子节点的个数,将它们相加即为二叉树中叶子节点的个数
	return BTreeLeafSize(root->left) + BTreeLeafSize(root->right);
}

二叉树的高度

  • 大体逻辑已写再代码逻辑了,还是那句号,自己画上递归展开图更能了解递归与代码的意义.
int BTreeHeight(BTNode* root) // 二叉树高度,接收一个二叉树根节点指针
{
	if (root == NULL) // 终止条件:如果当前节点为空,则高度为 0,返回 0
		return 0;

	// 递归计算其左右子树的高度,将左右子树中高度较大的值加 1 即为该二叉树的高度
	int leftHeight = BTreeHeight(root->left);
	int rightHeight = BTreeHeight(root->right);
	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

二叉树第k层结点个数

  • 在递归过程中,我们需要判断当前节点的情况。如果当前节点为空,则说明已经遍历到了二叉树的底部,可以直接返回 0。如果 k 的值为 1,则说明当前节点就是第 k 层节点,此时第 k 层节点个数为 1,可以直接返回 1
int BTreeLevelKSize(BTNode* root, int k) // 计算二叉树第 k 层节点个数,接收一个二叉树根节点指针和层数 k
{
	assert(k > 0); // 判断 k 是否大于 0,如果小于等于 0,表示该树不存在,因为树跟节点就是第一层

	if (root == NULL) // 终止条件:如果当前节点为空,返回 0
		return 0;

	if (k == 1) // 如果 k 为 1,则说明当前节点为第 k 层节点,返回 1
		return 1;

	// 递归地计算当前节点的左子树中第 k - 1 层节点的个数和右子树中第 k - 1 层节点的个数,
	//将它们相加即为当前二叉树中第 k 层节点的个数。
	return BTreeLevelKSize(root->left, k - 1) + BTreeLevelKSize(root->right, k - 1);
}

二叉树的层序遍历

  • 层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。
  • 假设二叉树是这样的
    点亮你的数据结构知识:通晓二叉树是必须的_第13张图片
    点亮你的数据结构知识:通晓二叉树是必须的_第14张图片
  • 利用队列实现了二叉树的层序遍历,首先将根节点入队,然后循环取出队首元素,输出其值,并将其左右子树入队。这样可以保证每一层的节点都按从左到右的顺序输出,实现了层序遍历的效果。这样就实现了层序从左到右遍历二叉树。
void LevelOrder(BTNode* root)
{
    Queue q;
    QueueInit(&q); // 初始化队列

    if (root)
        QueuePush(&q, root); // 将根节点入队

    while (!QueueEmpty(&q)) // 当队列不为空时循环
    {
        BTNode* front = QueueFront(&q); // 取出队首元素
        QueuePop(&q); // 将队首元素出队

        printf("%d ", front->data); // 输出队首元素的值

        if(front->left)
            QueuePush(&q, front->left); // 如果左子树不为空,将左子树入队

        if (front->right)
            QueuePush(&q, front->right); // 如果右子树不为空,将右子树入队
    }

    printf("\n");

    QueueDestroy(&q); // 销毁队列
}
  • 递归法 (较复杂如看不懂,只需掌握上面那种方法即可)
// 定义二维数组类型
typedef struct {
    int **data; // 指向二维数组的指针
    int size; // 二维数组的行数
    int *colSize; // 二维数组每行的列数
} Result;

// 定义递归函数
void order(struct TreeNode* cur, Result* result, int depth)
{
    if (cur == NULL) return;
    if (result->size == depth) { // 如果当前行还没有被创建,就创建一个新的行
        result->data[depth] = (int *)malloc(sizeof(int) * 1000); // 每行最多存放 1000 个节点的值
        result->colSize[depth] = 0; // 初始化该行的节点数为 0
        result->size++; // 行数加 1
    }
    result->data[depth][result->colSize[depth]] = cur->val; // 将当前节点的值存放在当前行的末尾
    result->colSize[depth]++; // 该行的节点数加 1
    order(cur->left, result, depth + 1); // 递归遍历左子树
    order(cur->right, result, depth + 1); // 递归遍历右子树
}

// 层序遍历
int **levelOrder(struct TreeNode *root, int *returnSize, int **returnColumnSizes) {
    Result result;
    result.data = (int **)malloc(sizeof(int *) * 1000); // 初始化二维数组
    result.size = 0; // 初始化行数为 0
    result.colSize = (int *)malloc(sizeof(int) * 1000); // 初始化每行的节点数为 0
    order(root, &result, 0); // 从根节点开始递归遍历二叉树
    *returnSize = result.size; // 将行数赋值给返回值中的 returnSize
    *returnColumnSizes = result.colSize; // 将每行的节点数赋值给返回值中的 returnColumnSizes
    return result.data; // 返回存放节点值的二维数组
}

你可能感兴趣的:(数据结构,数据结构,c语言,二叉树,算法)