树与二叉树二叉树链式结构及实现--万字详解介绍
Posted Sherry的成长之路
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树与二叉树二叉树链式结构及实现--万字详解介绍相关的知识,希望对你有一定的参考价值。
📝个人主页:@Sherry的成长之路
🏠学习社区:Sherry的成长之路(个人社区)
📖专栏链接:数据结构
🎯长路漫漫浩浩,万事皆有期待
文章目录
一、链式存储:
在之前的博客中,我们说过,二叉树的存储结构一般可以简单地分为顺序存储结构与链式存储结构,今天我们将要进行研究的,就是其实中的链式存储结构。
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。
链式存储结构又可以分为二叉链与三叉链:
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
struct BinTreeNode* left; // 指向当前节点左孩子
struct BinTreeNode* right; // 指向当前节点右孩子
BTDataType data; // 当前节点值域
// 三叉链
struct BinaryTreeNode
struct BinTreeNode* parent; // 指向当前节点的双亲
struct BinTreeNode* left; // 指向当前节点左孩子
struct BinTreeNode* right; // 指向当前节点右孩子
BTDataType data; // 当前节点值域
;
我们今天要研究的,是其中的二叉链部分,而三叉链的部分将来在研究红黑树时详细学习。普通二叉树的增删查改复杂且没有意义,所以我们并不打算学习它的增删查改,主要是学习它的结构。
二、链式结构的遍历:
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是指:按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。同时,遍历也是二叉树上最重要的运算之一,是二叉树上进行其它运算的基础。
1.前序、中序与后序遍历:
按照规则,二叉树的遍历有:前序、中序与后序的递归结构遍历,其规则如下:
前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之间。
后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
// 二叉树前序遍历
void PreOrder(BTNode* root);
// 二叉树中序遍历
void InOrder(BTNode* root);
// 二叉树后序遍历
void PostOrder(BTNode* root);
由于被访问的结点必是某子树的根,所以 N(Node)、L(Left subtree)和 R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR 和 LRN 分别又称为先根遍历、中根遍历和后根遍历。
1.1 概念选择题
利用已知限有条件构建二叉树
1.某完全二叉树按层次输出(同一层从左到右)的序列为 ABCDEFGH 。该完全二叉树的前序序列为( )
A. ABDHECFG
B. ABCDEFGH
C. HDBEAFCG
D. HDEBFGCA
- 分析
所以选择 A 选项
2.二叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK; 中序遍历:HFIEJKG; 则二叉树根结点为( )
A. E
B. F
C. G
D. H
- 分析
根据这棵树的先序遍厉、中序遍厉可以重建这棵树。但其实这里并不需要重建,因为先序遍厉是从根开始的。
所以选择 A 选项
3.设—课二叉树的中序遍历序列: badce,后序遍历序列: bdeca, 则二叉树前序遍历序列为( )
A. adbce
B. decab
C. debac
D. abcde
- 分析
根据这棵树的后序遍厉、中序遍厉可以重建这棵树。
所以选择 D 选项
某二叉树的后序遍历序列与中序遍历序列相同,均为 ABCDEF,
4.某二叉树的后序遍历序列与中序遍历序列相同,均为 ABCDEF,则按层次输出 (同一层从左到右) 的序列为( )
A. FEDCBA
B. CBAFED
C. DEFCBA
D. ABCDEF
- 分析
显然这道题作为选择题来说一眼就能知道答案:根据它的后序遍厉知道根是 F
所以选择 A 选项
总结
:想要构建或还原一棵树,需要知道1.前序遍历序列+中序遍历序列 2.中序遍历序列+后序遍历序列 //前序+后序是无法还原的,因为不能判断左右子树的顺序
2.层序遍历:
除了最常用的先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为 1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第 2 层上的节点,接着是第三层的节点,以此类推。而这种自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
3.DFS(深度)与BFS(广度)
深度和广度,其实指的是:
1.深度优先遍厉:二叉树的前序遍历、中序遍历、后序遍历->一般用递归
注意
有些说法只认同前序遍历,看具体如何定义
2. 广度优先遍厉:二叉树的层序遍历->一般用队列
eg.扫雷 DFS->八路遍历 , BFS->一圈一圈往外出
三、各接口功能实现:
1.创建二叉树结构:
首先创建一个二叉树节点的结构体类型,然后通过根节点对这个二叉树进行操作
typedef char BDataType;
typedef struct BinaryTreeNode
struct BinaryTreeNode* left; // 指向当前节点左孩子
struct BinaryTreeNode* right; // 指向当前节点右孩子
BDataType data; // 当前节点值域
BNode;
2.创建二叉树节点:
节点的创建只需要动态开辟一个空间,用于存放我们节点的值,再将左右指针置空,并返回创建好的节点的地址
BNode* BuyNode(BDataType x)
BNode* node = (BNode*)malloc(sizeof(BNode));
if (node == NULL)
printf("malloc Error!\\n");
return;
node->data = x;
node->left = NULL;
node->right = NULL;
return node;
3.前序遍历:
执行操作前需进行非空判断,防止对空指针进行操作。
对于前序遍历的操作原理,我们可以结合这张示意图来理解:
这个接口的实现方式(访问顺序)为:先访问根节点,即当前节点的值,接着递归访问左子树,最后递归访问右子树。使用递归实现整个二叉树的遍历,而不使用循环语句。
void PrevOrder(BNode* root)
if (root == NULL)
printf("PrevOrder Error!\\n");
return;
printf("%c ", root->data); // 访问当前节点的值
PrevOrder(root->left); // 先递归访问当前节点的左子树
PrevOrder(root->right); // 再递归访问当中前节点的右子树
4.中序遍历:
执行操作前需进行非空判断,防止对空指针进行操作。
同样我们结合操作原理示意图来理解:
这个接口的实现方式(访问顺序)为:先递归访问左树,再访问节点自身,最后递归访问右树。中序遍历同样使用递归实现整个二叉树的遍历,而不使用循环语句。
void InOrder(BNode* root)
if (root == NULL)
printf("InOrder Error!\\n");
return;
InOrder(root->left); // 递归访问当前节点的左树
printf("%c ", root->data); // 访问当前节点的值
InOrder(root->right); // 最后递归访问当前节点的右树
5.后序遍历:
执行操作前需进行非空判断,防止对空指针进行操作。
后序遍历操作原理示意图:
后序遍历接口的实现方式(访问顺序)为:先递归访问左子树,再递归访问右子树,最后访问节点自身。后续遍历也使用递归实现整个二叉树的遍历,而不使用循环语句。
void PostOrder(BNode* root)
if (root == NULL)
printf("PostQrder Error!\\n");
return;
PostOrder(root->left); // 先递归访问左子树
PostOrder(root->right); // 再递归访问右子树
printf("%c ", root->data); // 最后访问当前节点的值
6.层序遍历:
执行操作前需进行非空判断,防止对空指针进行操作。
层序遍历操作原理示意图:
层序遍历就是一层一层的遍历,在链式储存中,我们一般借助队列来实现层序遍历。
利用的是队列的先进先出的性质。先让根入队,然后出队头数据,再让队头数据的左右孩子入队。每从队头删除掉一个元素,就让这个元素的两个孩子入队,直到队列为空为止。
首先创建队列,并对队列进行初始化。接着让二叉树的根入队
注意
记得修改队列元素的类型 typedef BinaryTreeNode* QDataType; //结构体指针。
判断队列是否为空,如果队列为空,说明遍历已经结束,应当换行并销毁队列。若队列不为空,就将队头的节点拷贝出来,然后删除队头节点,把拷贝的队头节点数据进行打印,最后让拷贝接节点的左右孩子先后入队。如果孩子没有子节点,相当于使空 NULL 入队,并不影响访问结果。
void TreeLevelOrder(BNode* root)
Q q;
QInit(&q);//初始化队列
if (root)//不是空树
QPush(&q, root);//push root
while (!QEmpty(&q))//队列不为空
BNode* front = QFront(&q);//取对头数据
QPop(&q);//这里只是pop了队列的头节点,但值被局部变量front保存下来了
//树的节点也依然存在
printf("%c ", front->data);//所以可以打印这个值
if (front->left)//不为空,入左树
QPush(&q, front->left);
if (front->right)//不为空,入右树
QPush(&q, front->right);
printf("\\n");
QDestroy(&q);//及时销毁
7.二叉树节点个数:
执行操作前需进行非空判断,防止对空指针进行操作。
对于二叉树节点数量的统计,采用的方式是任意选择一种遍历顺序(只依照遍历顺序,不访问节点),遍历整个树结构,每找到一个节点让计数变量加一即可。
思路1 :使用前序 /中序 /后序遍历,全局变量记录
但是以下代码有 Bug :如果采用前序重复遍历 2 次
主要问题出在全局变量上,这里只需要在第 2 次遍历时置 0 即可(size=0)
学习了以后的知识,会发现这种操作还有线程安全的问题
思路 2:函数使用带返回值的方式,其内部的递归本质上是一个后序遍厉
//不能直接用size 线程安全问题
void TreeSize(BNode* root, int* size)
if (root == NULL)
printf("TreeSize Get Error!\\n");
return;
(*size)++;
TreeSize(root->left, size);
TreeSize(root->right, size);
//分治-分而治之
//其实这种就是递归的思想,在现实生活中也经常使用到
//比如 1 位校长要统计学校的人数,他不可能亲自去挨个数
//一般是通过院长、系主任、辅导员、班长、舍长的层层反馈才得到结果的
int BinaryTreeSize(BNode* root)
return root==NULL ? 0:TreeSize(root->left)+TreeSize(root->right)+1;
8.求树的高度/深度
核心思想 :当前树的深度 = max (左子树的深度,右子树的深度) + 1
//二叉树的深度/高度
int BinaryTreeDepth(BTNode* root)
if (root == NULL)
return 0;
int leftDepth = BinaryTreeDepth(root->left);
int rightDepth = BinaryTreeDepth(root->right);
return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
9.第 K 层节点个数:
执行操作前需进行非空判断,防止对空指针进行操作。
若根节点为空,则节点的个数为0。如果我们要计算第 K 层的元素(节点)个数,首先从根节点开始统计,假设我们每向下一层 K 就减 1,那么当 K = 1 时,表示我们来到了第 K 层,然后计算 K = 1 时的节点个数返回值相加的结果即可。
核心思想 :求当前树的第 k 层 = 左子树的第 k - 1 层 + 右子树的第 k - 1 层 (当 k = 1 时,说明此层就是目标层)
int TreeKLevelSize(BNode* root, int k)
assert(k>0);
if (root == NULL)
printf("TreeKLevelSize Get Error!\\n");
return 0;
if (k == 1)
return 1;
int leftk=TreeKLevelSize(root->left, k - 1);
//要及时存储递归后的值,不然忘了又要算一遍,浪费时间
int rightk=TreeKLevelSize(root->right, k - 1);
return leftk + rightk;
8.叶节点个数:
执行操作前需进行非空判断,防止对空指针进行操作。
叶节点就是度为0的节点,即没有子树,我们同样使用递归进行统计。
根节点进入函数后,应当首先判断根节点是否为叶节点,如果一个节点的左子树和右子树同时为空,说明这是一个叶节点;如果不是,其左子树的叶节点和右子树的叶节点之和就是当前节点以下的所以叶节点,形成递归。
核心思想 :以 left 和 right 为标志,如果都为 NULL,则累加
int TreeLeafSize(BNode* root)
if (root == NULL)
printf("TreeLeafSize Get Error!\\n");
return 0;
else
return (root->left) == NULL && (root->right) == NULL ? 1 : TreeLeafSize(root->left) + TreeLeafSize(root->right);
10.查找值为x的节点:
执行操作前需进行非空判断,防止对空指针进行操作。
若节点为空,就返回空。若节点的值等于要查找的值,就返回该节点。
若节点不为空,但节点的值不是我们要查找的值,就查找节点的左子树,如果查找的结果不为空,就返回该节点。若左子树的查找结果为空,就以同样的方式处理右子树。如果都找不到,就返回空。
核心思想 :
1、先判断是不是当前节点,是就返回,不是就继续找;
2、先去左树找,找到就返回
3、左树没找到,再找右树
BNode* TreeFind(BNode* root, BDataType x)
if (root == NULL)
return NULL;
if (root->data == x)
return root;
BNode* lret = TreeFind(root->left, x);
if (lret)
return lret;
BNode* rret = TreeFind(root->right, x);
if (rret)
return rret;
return NULL;
//简化版本
BNode* TreeFind(BNode* root, BDataType x)
if (root == NULL)
return NULL;
if (root->data == x)
return root;
BNode* lret = TreeFind(root->left, x);
if (lret)
return lret;
returnTreeFind(root->right, x);
11.完全二叉树判断:
执行操作前需进行非空判断,防止对空指针进行操作。
判断是否为完全二叉树需要用到层序遍历的思想。
若一个二叉树不是完全二叉树时,那么当我们对它进行层序遍历时,其中的部分节点就会是 NULL,于是我们可以通过这一点来判断一个二叉树是否为完全二叉树。
前半部分与二叉树的层序遍历一样,建队列,根入队,队列不为空,进入while循环,在循环中删队头节点,然后让该节点的左右孩子入队。
注意
这里循环停止的条件还要加上一个即堆顶的元素为空。
在跳出循环后存在两种情况,第一种是队列已空,节点之间没有空,表明是完全二叉树,返回true;而第二种情况是队列不为空,但在访问队头节点时访问到了 NULL,这时我们需要再次进行循环,若队列不为空,就进入循环逐个查找并删除队头的节点,若发现不为空的节点,说明节点间有 NULL 相隔,即该二叉树不是完全二叉树,返回false。
步骤:
给一个辅助队列。
如果 root 非空,则将 root 入队。
然后给定循环队列为空则停止,取队头元素,并出队头;这时将取出元素的左右子树都放入,一旦出元素出到 NULL 这时结束循环。
完全二叉树是连续的,一旦出现 NULL ,那么后面的元素都应该是空。如果空指针后还有非空元素,那么一定不是完全二叉树。
这时继续出队列,如果出队列过程中遇到非空元素,则销毁队列返回假;否则不断出队列元素。
如果循环结束还没有返回,说明后面都是空指针,这时销毁队列,返回真。
bool BinaryTreeComplete(BTNode* root)
// 使用层序遍历思想
Q q;
QueueInit(&q);
// 如果非空,则入队列
if (root)
QueuePush(&q, root);
while (!QueueEmpty(&q))
BTNode* front = QueueFront(&q);
QueuePop(&q);
// 一旦出队列到空,就出去判断
if (front == NULL)
break;
else
QueuePush(&q, front->left);
QueuePush(&q, front->right);
while (!QueueEmpty(&q))
BTNode* front = QueueFront(&q);
if (front != NULL)
QueueDestroy(&q);
return false;
else
QueuePop(&q);
QueueDestroy(&q);
return true;
12.二叉树销毁:
执行操作前需进行非空判断,防止对空指针进行操作。
销毁二叉树需要把二叉树的每个节点都销毁,故采用后序遍历的顺序进行销毁。
注意:节点里存放的是左右孩子的指针,若我们在传参时仅传递节点的指针类型,则函数中的左右孩子地址就是一份临时拷贝,将导致无法对每个节点的指针进行置空,故我们在销毁二叉树时,函数参数应当传递二级指针。
先销毁左树,再销毁右树,最后销毁根
注意
pproot是t的拷贝,应该传二级指针(或C++引用取别名)
void BinaryTreeDestory(BNode** pproot)//*& C++引用取别名
if (*pproot == NULL)
printf("BinaryTreeDestroy Error!\\n");
return NULL;
BinaryTreeDestory(&(*pproot)->left);
BinaryTreeDestory(&(*pproot)->right);
free(*pproot);
pproot = NULL;
四、链式存储结构完整代码:
1.Heap.h:
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//队列(为层序遍历做准备):
typedef BinaryTreeNode* QDataType;//结构体指针
typedef struct QueueNode
QDataType data;
struct QNode* next;
QNode;
typedef struct Queue
QNode* head;
QNode* tail;
Q;
void QInit(Q* p); //初始化队列
void QPush(Q* p, QDataType x); //入队
void QPop(Q* p); //出队
QDataType QFront(Q* p); //查看队头
bool QEmpty(Q* p); //查看队列容量
void QDestroy(Q* p); //队列的销毁
//二叉树的链式结构:
typedef char BDataType;
typedef struct BinaryTreeNode
struct BinaryTreeNode* left; // 指向当前节点左孩子
struct BinaryTreeNode* right; // 指向当前节点右孩子
BDataType data; // 当前节点值域
BNode;
BNode* BuyNode(BDataType x); //二叉树节点创建
void PrevOrder(BNode* root); // 前序遍历
void InOrder(BNode* root); // 中序遍历
void PostOrder(BNode* root); // 后序遍历
void TreeLevelOrder(BNode* root); //层序遍历
void TreeSize(BNode* root, int* size); // 统计二叉树节点个数
int BinaryTreeSize(BTNode* root);// 二叉树节点个数
int BinaryTreeDepth(BTNode* root)//二叉树深度/高度
int TreeLeafSize(BNode* root); // 计算二叉树叶节点个数
int TreeKLevelSize(BNode* root, int k)以上是关于树与二叉树二叉树链式结构及实现--万字详解介绍的主要内容,如果未能解决你的问题,请参考以下文章