之前都是介绍的逻辑结构中的线性结构,今天介绍一种非线性结构——树结构。
一、树的基本认识以及相关概念
1,树的结构
树是具有N(N>=0)个节点的有限集。树中可以没有任何节点(空树),也可以只有一个根节点(如上图左侧),也可以有多个节点(如上图右侧)。
2,节点、度、叶子节点
如上图所示,A、B、C、D等就是节点,节点中包含了数据,也包含了各种指向关系。
度,指的是节点所拥有的子树个数。比如,A节点的度是3,B节点的度是2,D节点的度是3。
叶子节点,又称为终端节点,指的是度为0的节点。比如,上图中的叶子节点是K、J、F、G、M、I、J。
非叶子节点,又称为非终端节点,指的是度不为0的节点。比如,上图中的非终端节点是A、B、C、D、E、H。
3,双亲结点、兄弟节点
在树中,双亲结点指的是一个节点。比如,B节点的双亲结点是A节点,E节点的双亲结点是B节点。
兄弟节点指的是,同属于一个双亲结点的子节点。比如,E、F互相称为兄弟节点,H、I、J互相称为兄弟节点。
4,节点的高度、深度、层数
节点的高度,指的是该节点到叶子节点的最长路径(边数)。
节点的深度,指的是根节点到该节点所经历的边的个数。
节点的层数,一棵树的根节点为第一层,往下依次递增。
树的高度,指的是根节点的高度。
A节点的高度是3,B节点的高度是2,C节点的高度是1,H节点的高度是1,I节点的高度是0。
A节点的深度是0,B节点的深度是1,C节点的深度是1,H节点的深度是2,I节点的深度是2。
二、二叉树的基本认识以及相关概念
1,基本认识
二叉树上的每个节点最多只有两个子节点,也就是说,节点的度 <= 2。
二叉树的子树有左右之分的,左子树和右子树都有其特殊的意义,不能随意颠倒。如下图所示,A节点下面只有一个B节点,但是树1的B节点是左节点,树2的B节点是右节点,因此树1和树2是两个不同的树。
2,满二叉树、完全二叉树
在一棵二叉树里面,如果所有的非叶子节点都具备左右两个节点,并且所有的叶子节点都在同一层,这样的二叉树称为满二叉树。如下图所示:
一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。
如下图所示,就是一个完全二叉树:
如下图所示,少了一个节点10,那么它就不是完全二叉树:
如下图所示,少了一个节点6、7,也不是完全二叉树:
如下图所示,少了节点10、11,所以也不是完全二叉树:
三、二叉树的顺序存储
我们是使用数组来实现二叉树的线性存储,各个节点信息依次有序存入数组的对应位置。
对于一个一般的二叉树,顺序存储的方式是会造成很大的空间浪费的。如下图所示,该树中只有四个节点,但是却需要开辟15个空间。
一般而言,对于完全二叉树,我们才会考虑去使用顺序存储的方式,因为顺序存储相对于链式存储要简单,而完全二叉树又不会造成很大的空间浪费。
而对于一般的二叉树,最好是使用链式存储的方式。
1,二叉树的初始化
核心思路:将每一个节点遍历置空
代码如下:
代码语言:javascript复制#include <stdio.h>
#include "stdio.h"
#include "stdlib.h"
#include "math.h"
#include "time.h"
#include <stdbool.h>
#define Tree_Size 100 // 二叉树的大小
// 操作的状态
typedef enum : int {
Success,
Error,
} Status;
// 空节点
#define Nil 0
// 节点类型
typedef int ElementType;
// 定义二叉树类型
typedef ElementType BinaryTree[Tree_Size];
// 1,二叉树的初始化(初始化为空树)
Status initBinaryTree(BinaryTree binaryTree) {
for (int i = 0; i < Tree_Size; i ) {
// 将二叉树的所有节点都置空
binaryTree[i] = Nil;
}
return Success;
}
2,二叉树的创建
核心思路:
(1)遍历二叉树所有节点,将输入的初始节点值依次赋值到指定位置,余下的位置均置空
(2)在遍历插入的时候,如果一个节点非空,但是其双亲节点为空,此时需要报错
代码如下:
代码语言:javascript复制// 2,二叉树的创建
Status createBinaryTree(BinaryTree tree, ElementType *nodeValues, int nodeValuesLength) {
for (int i = 0; i < Tree_Size; i ) {
// 当前节点的值(超出输入数组的范围之后直接取Nil值)
ElementType currentNodeValue = i < nodeValuesLength ? nodeValues[i] : Nil;
// 双亲结点为空的时候,不可赋有效值
int parentsIndex = (i - 1) / 2;
if (currentNodeValue != Nil && tree[parentsIndex] == Nil) {
printf("输入的节点值有误,在父节点为空的情况下,子节点竟然有值!n");
return Error;
}
// 给节点赋值(能通过输入的数组)
tree[i] = currentNodeValue;
}
return Success;
}
3,二叉树的清空
核心思路如下:
1,遍历二叉树的每一个节点,然后给每个节点置空
2,二叉树的清空逻辑和二叉树的初始化逻辑都是一样的,因此可以直接复用
代码如下:
代码语言:javascript复制// 3,二叉树的清空
/*
二叉树的清空跟二叉树的初始化逻辑是完全一样的,都是将各个节点置空。
此时只需要重新定义一个函数来复用原来初始化函数的逻辑即可
*/
#define initBinaryTree clearBinaryTree
4,二叉树的判空
核心思路:如果二叉树的根节点为空,那么该二叉树就是一个空树。
代码如下:
代码语言:javascript复制// 4,二叉树判空
/*
只要根节点为空,那么该二叉树就是一个空树
*/
bool isBinaryTreeEmpty(BinaryTree tree) {
return tree[0] == Nil;
}
5,二叉树的深度
核心思路如下:
(1)二叉树的深度就是该树中所有节点的深度最大的那一个;
(2)因此,找到最后一个节点,然后计算其深度即可
(3)函数powl(2, n)表示2^n
代码如下:
代码语言:javascript复制// 5,获取二叉树深度
/*
二叉树的深度是就是该数里面所有节点的深度最大的那一个
所以找到最后那个非空节点,然后计算其深度即可
*/
int depthOfBinaryTree(BinaryTree tree) {
// 找到最后那个非空节点的下标
int finalNodeIndex;
for (finalNodeIndex = Tree_Size - 1; finalNodeIndex >= 0; finalNodeIndex--) {
if (tree[finalNodeIndex] != Nil) {
break;
}
}
// 计算深度
int depth = 0; // 声明一个临时变量记录深度
while (powl(2, depth) - 1 <= finalNodeIndex) { // powl函数的作用是计算次幂
depth ;
}
return depth;
}
6,获取指定位置上面的节点值
核心思路如下:
(1)通过两个元素来只是某个节点的位置
①level,层
②order,在当前层中的序号
代码如下:
代码语言:javascript复制// 6,获取指定位置上面的节点值
/*
level:节点的层
order:节点在所在层上的顺序值
*/
ElementType elementOfPosition(BinaryTree tree, int level, int order) {
return tree[(int)powl(2, level-1) order - 2];
}
7,获取根节点的值
核心思路:
所谓根节点,指的就是排在首位的节点
代码如下:
代码语言:javascript复制// 7,获取根节点的值
ElementType roorNode(BinaryTree tree) {
return tree[0];
}
8,给指定位置的节点赋值
核心思路如下:
(1)首先根据层序找到对应的位置
(2)然后找到该位置的双亲结点
(3)如果双亲结点为空,并且当前节点值为非空,则报错
(4)如果当前节点值为空,并且其子节点非空,则报错
代码如下:
代码语言:javascript复制// 8,给指定位置的节点赋值
Status setValue(BinaryTree tree, int level, int order, ElementType element) {
// 找到节点的位置坐标
int index = (int)powl(2, level - 1) order - 2;
// 如果该值非空但是双亲节点为空,则报错
if (element != Nil && tree[(index - 1) / 2] == Nil) {
return Error;
}
// 如果该值为空,但其子节点非空,则报错
if (element == Nil && (tree[2*index 1]!=Nil || tree[2*index 2]!=Nil)) {
return Error;
}
// 赋值
tree[index] = element;
return Success;
}
9,获取某节点的双亲结点
核心思路如下:
(1)如果是空树,则报错
(2)如果当前节点是根节点或者未找到当前节点,则报错
代码如下:
代码语言:javascript复制// 9,获取某节点的双亲结点
ElementType parentNode(BinaryTree tree, ElementType node) {
// 如果是空树,则直接返回Nil
if (tree[0] == Nil) {
return Nil;
}
// 遍历找到对应节点
int index;
for (index = 0; index < Tree_Size; index ) {
if (tree[index] == node) {
break;
}
}
// 如果给定的节点不存在,则返回Nil
if (index == Tree_Size) {
return Nil;
}
// 找到双亲节点
int parentsNodeIndex = (index - 1) / 2;
return tree[parentsNodeIndex];
}
10,获取节点的左孩子
核心思路:
(1)如果是空树,则报错
(2)找到对应节点,如果没找到该节点或者节点为空,则报错
(3)需要判断子节点的底标是否超限,如果超限则报错
代码如下:
代码语言:javascript复制// 10,获取节点的左孩子
ElementType leftChild(BinaryTree tree, ElementType element) {
// 如果给定节点为空
if (element == Nil) {
return Nil;
}
// 获取到当前节点
int index;
for (index = 0; index < Tree_Size; index ) {
if (tree[index] == element) {
break;
}
}
// 如果指定节点不存在
if (index == Tree_Size) {
return Nil;
}
// 获取子节点
int leftChildIndex = index*2 1;
if (leftChildIndex < Tree_Size) {
return tree[leftChildIndex];
} else {
return Nil;
}
}
11,获取节点的右孩子
核心思路:
(1)判断当前节点是否为空,如果为空则报错
(2)寻找当前节点,如果未找到则报错
(3)计算右子节点坐标,如果超限则报错
代码如下:
代码语言:javascript复制// 11,获取节点的右孩子
ElementType rightChild(BinaryTree tree, ElementType element) {
// 如果是空树
if (tree[0] == Nil) {
return Nil;
}
// 如果当前节点为空
if (element == Nil) {
return Nil;
}
// 寻找当前节点
int index;
for (index = 0; index < Tree_Size; index ) {
if (tree[index] == element) {
break;
}
}
// 如果对应节点不存在
if (index == Tree_Size) {
return Nil;
}
// 寻找右节点
int rightIndex = index * 2 2;
if (rightIndex < Tree_Size) {
return tree[rightIndex];
} else {
return Nil;
}
}
12,获取节点的左兄弟
核心思路如下:
(1)如果节点为空,则直接报错
(2)寻找当前节点,如果当前节点未找到,则报错
(3)如果当前节点是根节点,则报错
(4)如果当前节点是左节点,则报错
代码如下:
代码语言:javascript复制// 12,获取节点的左兄弟
ElementType leftBrother(BinaryTree tree, ElementType element) {
// 如果是空树
if (tree[0] == Nil) {
return Nil;
}
// 如果该节点为空
if (element == Nil) {
return Nil;
}
// 寻找对应节点
int index;
for (index = 0; index < Tree_Size; index ) {
if (tree[index] == element) {
break;;
}
}
// 如果对应节点不存在
if (index == Tree_Size) {
return Nil;
}
// 如果是根节点
if (index == 0) {
return Nil;
}
// 如果是左节点
if (index % 2 == 1) {
return Nil;
}
// 获取节点的左兄弟
return tree[index - 1];
}
13,获取节点的右兄弟
核心思路:
(1)如果是空树,则报错
(2)如果节点为空,则报错
(3)自第二层开始遍历树的每一个节点,如果匹配成功了,并且是左节点,并且右兄弟底标未超限,则返回右兄弟
代码如下:
代码语言:javascript复制// 13,获取节点的右兄弟
ElementType rightBrother(BinaryTree tree, ElementType element) {
// 如果是空树,或者给定元素为空
if (tree[0] == Nil || element == Nil) {
return Nil;
}
// 寻找对应元素
for (int index = 1; index < Tree_Size; index ) {
if (tree[index] == element && index % 2 == 1) {
return tree[index 1];
}
}
return Nil;
}
四、顺序二叉树的遍历
二叉树的遍历是一个很常见的问题。二叉树的遍历方式主要有前序遍历、中序遍历、后序遍历和层序遍历四种。
前序、中序和后序,指的都是双亲结点被访问的次序。如果在遍历过程中,双亲结点先于它的子节点被访问,则为前序遍历;父节点被访问的次序位于左右孩子节点中间,则为中序遍历;访问完左右子孩子节点之后再访问双亲结点,则为后序遍历。不管是前序、中序还是后序遍历,左右孩子节点的相对访问次序是不变的,都是先访问左子节点,再访问右子节点。
层序遍历是按照自上而下、自左到右的次序来访问二叉树的每个节点。
如下图所示:
1,层序遍历
核心思路:
(1)二叉树的顺序存储就是按照完全二叉树的形式自上而下、自左到右来存的,因此层序遍历就直接按照节点数组的顺序依次进行遍历即可
(2)遍历的时候注意筛选空节点
代码如下:
代码语言:javascript复制// 打印节点信息
void printNode(ElementType element) {
printf("%d ", element);
}
// 1,层序遍历
void levelOrderTraverse(BinaryTree tree) {
int index;
for (index = 0; index < Tree_Size; index ) {
if (tree[index] != Nil) {
printNode(tree[index]);
}
}
printf("n");
}
2,前序遍历
核心思路:先找到双亲结点,然后找到左节点,然后找到右节点
代码如下:
代码语言:javascript复制// 2,前序遍历
void preTraverse(BinaryTree tree, int nodeIndex) {
// 打印当前节点
printNode(tree[nodeIndex]);
// 处理左子节点
if (2*nodeIndex 1 < Tree_Size && tree[2*nodeIndex 1] != Nil) {
preTraverse(tree, 2*nodeIndex 1);
}
// 处理右子节点
if (2*nodeIndex 2 < Tree_Size && tree[2*nodeIndex 2] != Nil) {
preTraverse(tree, 2*nodeIndex 2);
}
}
void preorderTraverse(BinaryTree tree) {
// 如果是空树
if (tree[0] == Nil) {
printf("该二叉树为空");
return;
}
preTraverse(tree, 0);
}
3,中序遍历
核心思路:先找到左子节点,然后双亲结点,然后右子节点
代码如下:
代码语言:javascript复制// 3,中序遍历
void inTraverse(BinaryTree tree, int index) {
// 处理左子节点
if (2*index 1 < Tree_Size && tree[2*index 1] != Nil) {
inTraverse(tree, 2*index 1);
}
// 打印当前节点
printNode(tree[index]);
// 处理右子节点
if (2*index 2 < Tree_Size && tree[2*index 2] != Nil) {
inTraverse(tree, 2*index 2);
}
}
void inorderTraverse(BinaryTree tree) {
// 如果是空树
if (tree[0] == Nil) {
printf("该二叉树为空树");
}
inTraverse(tree, 0);
}
4,后序遍历
核心思路如下:先找到左子节点,然后找到右子节点,最后双亲结点
代码如下:
代码语言:javascript复制// 4,后序遍历
void postTraverse(BinaryTree tree, int index) {
// 处理左子节点
if (2*index 1 < Tree_Size && tree[2*index 1] != Nil) {
postTraverse(tree, 2*index 1);
}
// 处理右子节点
if (2*index 2 < Tree_Size && tree[2*index 2] != Nil) {
postTraverse(tree, 2*index 2);
}
// 打印当前节点
printNode(tree[index]);
}
void postorderTraverse(BinaryTree tree) {
// 如果是空树
if (tree[0] == Nil) {
printf("该二叉树是空树");
}
postTraverse(tree, 0);
}
五、二叉树的链式存储
上面讲了二叉树的顺序存储,其最大的弊端就是浪费空间,所以我们一般是用它来实现完全二叉树;如果是一般的二叉树的话,我们基本都是通过链式存储的方式来实现的。
二叉树的链式结构设计如下:
代码语言:javascript复制#include <stdio.h>
#include "stdio.h"
#include "stdlib.h"
#include "math.h"
#include "time.h"
#include <stdbool.h>
// 操作的状态
typedef enum : int {
Success,
Error,
} Status;
// 节点值类型
typedef char ElementType;
#define Nil ' ' // 定义空值
// 节点类型
typedef struct BinaryTreeNode {
ElementType data; // 数值域
struct BinaryTreeNode *leftChildNode; // 左子节点
struct BinaryTreeNode *rightChildNode; // 右子节点
} BinaryTreeNode;
// 二叉树类型
typedef struct BinaryTreeNode * BinaryTree;
1,二叉树的初始化
思路如下:将根节点指向NUll即可。
代码如下:
代码语言:javascript复制// 1,二叉树的初始化
/*
初始化二叉树的时候,只需要将二叉树指针指向NULL即可,不需要进行其他节点的销毁工作
*/
Status initBinaryTree(BinaryTree *tree) {
*tree = NULL;
return Success;
}
2,二叉树的创建
思路如下:
(1)创建二叉树的时候需要输入一个字符串,该字符串是前序排列的,并且字符#代表是一个空节点
(2)由于字符串是前序排列的,所以通过前序遍历的方式对二叉树各个节点进行递归创建
代码如下:
代码语言:javascript复制// 2,二叉树的创建
/*
(1)空节点的值赋为”#“
(2)按照前序依次对各个节点进行递归赋值
*/
char *originalString = "abcdefghijklmn";
int stringIndex = 0;
Status createBinaryTree(BinaryTree *tree) {
// 递归结束的条件之一——字符串已经遍历完了
if (stringIndex >= strlen(originalString)) {
return Success;
}
ElementType currentChar = originalString[stringIndex ];
if (currentChar == '#') {
// #表示空节点
*tree = NULL;
} else {
// 新建节点
*tree = malloc(sizeof(BinaryTreeNode));
(*tree)->data = currentChar; // 给当前节点赋值
createBinaryTree(&(*tree)->leftChildNode); // 左子节点
createBinaryTree(&(*tree)->rightChildNode); // 右子节点
}
return Success;
}
3,二叉树的销毁
思路如下:
(1)递归遍历,依次寻找左右子节点
(2)最后销毁节点的内存空间,并且将指针置空
(3)递归的结束条件是当前遍历到的节点为空
代码如下:
代码语言:javascript复制// 3,二叉树的销毁
/*
(1)递归操作各个子树
*/
Status destroyBinaryTree(BinaryTree *tree) {
// 递归结束条件
if (!(*tree)) {
return Success;
}
// 销毁左子树
destroyBinaryTree(&(*tree)->leftChildNode);
// 销毁右子树
destroyBinaryTree(&(*tree)->rightChildNode);
// 销毁当前节点
free(*tree);
*tree = NULL;
return Success;
}
4,二叉树的判空
思路如下:
当根节点为NUll的时候,该树为空。
代码如下:
代码语言:javascript复制// 4,二叉树的判空
bool isBinaryTreeEmpty(BinaryTree tree) {
return tree == NULL;
}
5,二叉树的深度
思路如下:
(1)采用递归的思想
(2)一个节点的深度分为左子树的深度和右子树的深度,取较大值,然后加1,最后返回
(3)递归退出的条件是,当前遍历到的节点为空
代码如下:
代码语言:javascript复制// 5,二叉树的深度
/*
(1)递归实现
*/
int binaryTreeDepth(BinaryTree tree) {
// 如果是空树
if (!tree) {
return 0;
}
int leftChildDepth, rightChildDepth; // 声明左右子树的深度
// 左子树深度取值
leftChildDepth = tree->leftChildNode ? binaryTreeDepth(tree->leftChildNode) : 0;
// 右子树深度取值
rightChildDepth = tree->rightChildNode ? binaryTreeDepth(tree->rightChildNode) : 0;
// 返回较大的
return leftChildDepth > rightChildDepth ? leftChildDepth 1 : rightChildDepth 1;
}
6,二叉树的根节点的值
思路:当二叉树的根节点为空的时候返回Nil,根节点不为空的时候直接返回根节点的值。
代码如下:
代码语言:javascript复制// 6,二叉树的根节点的值
ElementType rootNodeValue(BinaryTree tree) {
return tree ? tree->data : Nil;
}
六、链式二叉树的遍历
1,前序遍历
顺序:双亲结点->左子节点->右子节点
思路:
(1)使用递归
(2)如果当前节点为空,则直接返回,结束递归
(3)如果当前节点不为空,则打印当前节点信息,并依次递归处理左右子节点
代码如下:
代码语言:javascript复制// 1,前序遍历
void preorderTraverse(BinaryTree tree) {
// 递归退出的条件
if (!tree) {
return;
}
// 打印当前节点
printf("%c", tree->data);
// 处理左子节点
preorderTraverse(tree->leftChildNode);
// 处理右子节点
preorderTraverse(tree->rightChildNode);
}
2,中序遍历
顺序:左子节点->双亲结点->右子节点
思路:
(1)使用递归
(2)如果当前节点为空,则直接返回,结束递归
(3)如果当前节点不为空,则先递归处理左侧子节点,然后打印当前节点信息,最后递归处理右子节点
代码如下:
代码语言:javascript复制// 2,中序遍历
void inorderTraverse(BinaryTree tree) {
// 递归结束的条件
if (!tree) {
return;
}
// 处理左子节点
inorderTraverse(tree->leftChildNode);
// 打印当前节点
printf("%c", tree->data);
// 处理右子节点
inorderTraverse(tree->rightChildNode);
}
3,后序遍历
顺序:左子节点->右子节点->双亲结点
思路:
(1)使用递归
(2)如果当前节点为空,则直接返回,结束递归
(3)如果当前节点不为空,则先递归处理左、右侧子节点,最后打印当前节点信息
代码如下:
代码语言:javascript复制// 3,后序遍历
void postorderTraverse(BinaryTree tree) {
// 递归退出的条件
if (!tree) {
return;
}
// 处理左子节点
postorderTraverse(tree->leftChildNode);
// 处理右子节点
postorderTraverse(tree->rightChildNode);
// 打印当前节点
printf("%c", tree->data);
}
以上。