在数据结构与算法第6讲和第7讲中,博主带着大家学习了树与二叉树的基本概念及相关特性,并带着大家详细分析了由完全二叉树衍生出的堆和堆排序及二叉树的顺序存储等相关知识,前两章的链接如下所示。今天这一讲将会为大家介绍二叉树的另一种存储方式,即链式存储结构。下面继续跟着博主的步伐一起学习吧!!!
数据结构与算法初阶6:树与二叉树基础知识精讲_King_lm_Guard的博客-CSDN博客https://blog.csdn.net/King_lm_Guard/article/details/125885126数据结构与算法初阶7:基于二叉树的堆和堆排序知识精讲_King_lm_Guard的博客-CSDN博客https://blog.csdn.net/King_lm_Guard/article/details/125906250博主建议大家先阅读前两节的内容,然后再跟着博主学习这一讲!!!
目录
1、二叉树的回顾
一颗二叉树是结点的一个有限集合,该集合包含两种情况:
1、或者为空;
2、由一个根结点加上两颗别称为左子树和右子树的二叉树组成。
从上图中可以看出:
1、二叉树不存在度大于2的结点;
2、二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。
注意:对于任意的二叉树都是由以下几种情况复合而成的:
从以上的分析中可以看出:二叉树定义是递归式的,因此后面关于二叉树的相关分析方式等都是基于此概念出发的。在这里给读者们预提醒以下。
2、二叉树的创建方式
因为目前是首次接触到二叉树,所以博主采用手动创建的方式为大家开展后面的工作,如果学到高阶二叉树了,就不需要这种方式了,望读者知悉。
2.1 二叉树结构体定义
从这里开始关于二叉树的分析是基于链式存储结构实现的,所以我们需要定义基于链表思想的结构体。链表相关知识直通车:
数据结构与算法篇初阶3:线性表—链表相关知识点讲解_King_lm_Guard的博客-CSDN博客https://blog.csdn.net/King_lm_Guard/article/details/125655336数据结构与算法初阶4:链表—带头双向循环链表知识讲解_King_lm_Guard的博客-CSDN博客https://blog.csdn.net/King_lm_Guard/article/details/125685649
在第6讲中,我们知道,关于二叉树的表达方式,我们通常采用的是孩子兄弟表示法,即左孩子右兄弟表示法。也就是说,我们需要定义的结构体包含三部分:1、存储当前结点的数据;
2、定义指向当前结点的左孩子的指针;
3、定义指向当前结点的右孩子的指针;
二叉树的逻辑表现形式如下图所示,定义的结构体内容同样如下:
//创建二叉树结构体 typedef int BTDataType; typedef struct BinaryTreeNode { struct BinaryTreeNode* left; struct BinaryTreeNode* right; BTDataType data; }BTNode;
2.2 结点创建
根据上述定义的结构体,手动创建6个结点,结点数据存储的逻辑示意图如下所示:
创建的程序如下所示:
#include<stdio.h> #include<stdlib.h> #include<assert.h> #include<stdbool.h> //创建二叉树结构体 typedef int BTDataType; typedef struct BinaryTreeNode { struct BinaryTreeNode* left; struct BinaryTreeNode* right; BTDataType data; }BTNode; //创建二叉树节点,初始化函数 BTNode* BuyNode(BTDataType x) { //动态开辟空间 BTNode* node = (BTNode*)malloc(sizeof(BTNode)); assert(node); if (node == NULL) { printf("malloc fail\n"); exit(-1); } node->data = x; node->left = NULL; node->right = NULL; return node; } //结合初始化二叉树节点函数,手动创建二叉树 BTNode* CreatBinaryTree() { BTNode* node1 = BuyNode(1); BTNode* node2 = BuyNode(2); BTNode* node3 = BuyNode(3); BTNode* node4 = BuyNode(4); BTNode* node5 = BuyNode(5); BTNode* node6 = BuyNode(6); node1->left = node2; node1->right = node4; node2->left = node3; node2->right = node5; node4->left = node6; //此时的node1表示根节点; return node1; }
3、二叉树的遍历方式
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
二叉树遍历方式(递归式遍历)分为三种:
1. 前序遍历(Preorder Traversal或称先序遍历)——访问根结点的操作发生在遍历其左右子树之前:即先访问根结点,然后左子树,最后右子树。
2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间):即先访问左子树,然后根结点,最后访问右子树。
3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后:即先访问左子树,然后右子树,最后左子树。
注:下面分析的遍历方式均需要第2讲创建的含有6个结点的二叉树,为了节省篇幅,就不重复把前面的程序内容拷贝在下面的分析中了,读者如果要使用,则直接将程序拷贝在一起就可以使用了。
3.1 前序遍历
为了方便大家查看,当遍历到空NULL的时候,打印“#”。
//前序遍历:根/左子树/右子树 void PreOrder(BTNode* root) { if (root == NULL) { printf("# "); return; } printf("%d ", root->data); PreOrder(root->left); PreOrder(root->right); }
3.2 中序遍历
//中序遍历:左子数/根/右子树 void InOrder(BTNode* root) { if (root == NULL) { printf("# "); return; } InOrder(root->left); printf("%d ", root->data); InOrder(root->right); }
3.3 后序遍历
//后序遍历:左子树/右子树/根 void PostOrder(BTNode* root) { if (root == NULL) { printf("# "); return; } PostOrder(root->left); PostOrder(root->right); printf("%d ", root->data); }
4、层序遍历
4.1 什么是层序遍历?
在第3讲中,我们分析了二叉树的前序遍历、中序遍历、后序遍历,其实除此以外,我们还可以对二叉树进行层序遍历,其又叫广度优先遍历。
在二叉树中的理解就是从根结点所在的起始层出发,首先访问第一层树的根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
4.2 具体实现思路
实现层序遍历,需要用到我们前面所学的栈和队列一讲中的队列知识。链接如下:数据结构与算法初阶5:栈和队列知识精讲_King_lm_Guard的博客-CSDN博客https://blog.csdn.net/King_lm_Guard/article/details/125738989 我们知道队列具有先进先出的特点,所以根据此特点,当我们把根结点入队列以后,然后在从队列中将该结点取出时,把该结点的左孩子和右孩子入队列,然后再出左孩子结点的同时将左孩子结点对应的左孩子和右孩子入队列,然后再取出根结点的右孩子结点,取出时将右孩子结点对应的左孩子和右孩子入队列,循环执行此过程,这样就可以保证我们能够实现层序遍历了。下面我们一起尝试用代码实现吧。
4.2.1 队列.h文件
#pragma once #include<stdio.h> #include<assert.h> #include<stdlib.h> #include<stdbool.h> //注意这里添加前置声明 struct BinaryTreeNode ; typedef struct BinaryTreeNode* QDataType; typedef struct QueueNode { struct QueueNode*next; QDataType data; }Qnode; typedef struct Queue { Qnode*head; Qnode*tail; }Queue; //初始化队列 void QueueInit(Queue*pq); //清空队列 void QueueDestory(Queue*pq); //入数据 void QueuePush(Queue*pq, QDataType x); //将首元素移除队列,队列总数减1; void QueuePop(Queue*pq); //取队列头元素 QDataType QueueFront(Queue*pq); //判断队列是否为空 bool QueueEmpty(Queue*pq);
4.2.2 队列.c文件
#define _CRT_SECURE_NO_WARNINGS #include"Queue.h" //初始化队列 void QueueInit(Queue*pq) { assert(pq); pq->head = pq->tail = NULL; } //清空队列 void QueueDestory(Queue*pq) { assert(pq); Qnode*cur = pq->head; while (cur) { Qnode*next = cur->next; free(cur); cur = next; } pq->head = pq->tail = NULL; } //入数据 void QueuePush(Queue*pq, QDataType x) { assert(pq); Qnode*newnode = (Qnode*)malloc(sizeof(Qnode)); if (newnode == NULL) { printf("malloc fail\n"); exit(-1); } newnode->data = x; newnode->next = NULL; if (pq->tail == NULL) { pq->head = pq->tail = newnode; } else { pq->tail->next = newnode; //tail一直标记最后一个元素的位置 pq->tail = newnode; } } void QueuePop(Queue*pq) { assert(pq); assert(!QueueEmpty(pq)); if (pq->head->next == NULL) { free(pq->head); pq->head = pq->tail = NULL; } else { Qnode*next = pq->head->next; free(pq->head); pq->head = next; } } //取队列头元素 QDataType QueueFront(Queue*pq) { assert(pq); assert(!QueueEmpty(pq)); return pq->head->data; } //判断队列是否为空 bool QueueEmpty(Queue*pq) { assert(pq); return pq->head == NULL; }
4.1.3 二叉树层序遍历main文件
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include"Queue.h" //创建二叉树结构体 typedef int BTDataType; typedef struct BinaryTreeNode { struct BinaryTreeNode* left; struct BinaryTreeNode* right; BTDataType data; }BTNode; //创建二叉树节点,初始化函数 BTNode* BuyNode(BTDataType x) { //动态开辟空间 BTNode* node = (BTNode*)malloc(sizeof(BTNode)); assert(node); if (node == NULL) { printf("malloc fail\n"); exit(-1); } node->data = x; node->left = NULL; node->right = NULL; return node; } //结合初始化二叉树节点函数,手动创建二叉树 BTNode* CreatBinaryTree() { BTNode* node1 = BuyNode(1); BTNode* node2 = BuyNode(2); BTNode* node3 = BuyNode(3); BTNode* node4 = BuyNode(4); BTNode* node5 = BuyNode(5); BTNode* node6 = BuyNode(6); node1->left = node2; node1->right = node4; node2->left = node3; node2->right = node5; node4->left = node6; //此时的node1表示根节点; return node1; } //层序遍历的方法:利用队列的先进先出,根结点入队列,然后再取队头数据,此时将根节点取出,然后将根结点的下一层结点带入队列中 void LevelOrder(BTNode*root) { Queue q; QueueInit(&q); //root不为空,则先放进队列 if (root) { //入队列 QueuePush(&q, root); } while (!QueueEmpty(&q)) { //取队头数据然后删除队列中的存储值 BTNode*front = QueueFront(&q); printf("%d ", front->data); QueuePop(&q); //从队列中每次出一个带下一层的结点 //入根结点下一层的左结点,这里是通过前面创建的front结点记录根结点的位置, //然后通过front找到根结点的左孩子结点和右孩子结点 //入根结点下一层的左结点 if (front->left) { QueuePush(&q, front->left); } //入根结点下一层的右结点 if (front->right) { QueuePush(&q, front->right); } } printf("\n"); //最后这里记得销毁开辟的队列 QueueDestory(&q); } int main() { BTNode*root = CreatBinaryTree(); //层序遍历 LevelOrder(root); return 0; }
4.1.4 重视前置申明的理解
上图中红色方框部分是我调用队列文件时添加的前置声明。
在回答为什么这样操作之前,我先确认常规的东西:
1、我们正常实现了头文件的包含,即(#include “Queue.h”);
2、我们也正常将data的数据类型定义为:typedef BinaryTreeNode* QDataType(因为我们在队列中存储的结点类型就是结构体指针类型)。
下面开始解释:
当我们正常实现这样的操作以后,直接运行程序我们会发现出现下面图片所提供的错误,这是因为,虽然我们包含了头文件,但在头文件中,找不到我们定义的结构体typedef BinaryTreeNode,所以就会报错,此时我们需要在头文件中添加一个前置声明:struct BinaryTreeNode,此时程序就可以被正常运行。下面提供错误示范、不正规示范和正规示范的比较。
4.1.5 层序遍历函数分析
5、结语
今天这一讲为大家详细介绍了二叉树的遍历及层序遍历的方法。相信阅读了博主的文章的小伙伴们一定会有所收获的。关于二叉树的运用及层序遍历方法的运用将在后面为大家带来详细讲解,制作不易,欢迎大家点赞、关注、收藏、支持!!!