数据结构与算法(C语言版)P8---树、二叉树、森林

【本节目标】

  • 树概念及结构。
  • 二叉树概念及结构。
  • 二叉树常见OJ题练习。

1、树概念及结构

1.1、树的概念

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一颗倒挂的树,也就是说它是根朝上、而叶朝下的。

  • 有一个特殊的结点,称为根结点,根节点没有前驱节点。
  • 除根节点外,其余节点被分为成M(M>0)个互不相交的集合T1、T2、…、Tm,其中每个集合Ti(1<=i<=m)又是一棵结构与树类似的子树。每颗子树的根节点有且只有一个前驱,可以有0个或多个后继。
  • 因此树是递归定义的。

在这里插入图片描述

在这里插入图片描述

概念:

  • 没有父节点的节点称为根节点。
  • 没有子节点的节点称为叶节点。

在这里插入图片描述

  • 子树是不相交的。
  • 除了根节点外,每个节点有且仅有一个父节点。
  • 一棵N个节点的树有N-1条边。

下面来说一下树的常见概念:以下图为例
在这里插入图片描述

结点的度:一个节点含有的子树的个数称为该节点的度。如上图:A节点的度为6。

叶(子)结点或终端结点:度为0的节点成额为叶节点。如上图:B、C、H、I…等节点为叶节点。

非终端结点或分支结点:度不为0的节点。如上图:D、E、F、G…等节点为分支节点。

双亲结点或父结点:若一个节点含有子节点,则这个节点称为其子节点的父节点。如上图:A是B的父节点。

孩子结点或子结点:一个节点含有的子树的根节点称为该节点的子节点。如上图:B是A的子节点。

兄弟结点:具有相同父节点互称为兄弟节点。如上图:B、C是兄弟节点。

树的度:一棵树中,最大节点的度称为树的度。如上图:树的度为6。

结点的层次:从根开始定义起,根为第一层,根的子节点为第2层,以此类推。

树的高度或深度:树中节点的最大层次。如上图:树的高度为4。

结点的祖先:从根到该结点所经分支上的所有节点。如上图:A是所有节点的祖先。A、E、J是Q的祖先。

子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙。I、J、P、Q是E的子孙。

森林:由m(m>0)棵树互不相交的多棵树的集合称为森林。(数据结构中的学习并查集本质就是森林)。

树一定是森林,单森林不一定是树。

1.2、树的表示

树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦,实际中树有很多种表示方法,如:双亲表示法,孩子表示法,孩子兄弟表示法等等。我们这里就简单的了解其中最常用的__孩子兄弟表示法__。

typedef  int DataType;
struct Node
{struct Node* _firstChild1;           //第一个孩子节点struct Node* _pNextBrother;         //指向其下一个兄弟节点DataType _data;                    //节点中的数据域
};

在这里插入图片描述

1.3、树在实际中的运用(表示文件系统的目录树结构)

在这里插入图片描述

2、二叉树概念及结构

为什么需要二叉树呢?

首先二叉树有以下特点:

  • 二叉树的结构最简单,规律性最强。
  • 可以证明,所有树都能转为唯一对应的二叉树,不失一般性。

普通树(多叉树)若不转化为二叉树,则运算很难实现。

二叉树在树结构的应用中起这非常重要的作用,因为对二叉树的许多操作算法简单,而任何树都可以与二叉树相互转换,这样就解决了树的存储结构及其运算中存在的复杂性。

2.1、概念

一棵二叉树是节点的一个有限集合,该集合或者为空,或者由一个根节点加上两棵称为左子树和右子树的二叉树组成。

二叉树的特点:

​ 1、每个节点最多由两颗子树,即二叉树不存在度大于2的节点。

​ 2、二叉树的子树有左右之分,其子树的次序不能颠倒。

在这里插入图片描述

2.2、二叉树的性质

性质一:在二叉树的第i层上至多有2^(i-1)个结点(i>=1)。第i层上至少有1个结点。

性质二:深度为k的二叉树至多有2^k - 1个结点(k>=1)。深度为k时至少有k个结点。

性质三:对任何一棵二叉树,如果度为0的叶节点个数为n0,度为2的分支节点个数为n2,则有n0 = n2+1。

如下图演示:

在这里插入图片描述

如上图所示:度为0的节点个数有8个(n0)。度为2的节点有7个(n2),所以n0 = n2+1。

我们看个图:
在这里插入图片描述

如上图所示:度为0的节点有两个(n0):F、E。度为2的节点有1个(n2):A。

所以:n0 = n2+1。

性质四:若规定根节点的层数为1,具有n个节点的满二叉树的深度h为h=log2 N(N是总结点个数)。

2.3、特殊的二叉树

2.3.1、满二叉树

满二叉树:一个二叉树,如果每一层的节点数都达到最大值,则这个二叉树就是满

二叉树。也就是说,如果一个二叉树的层数为k,且节点总数示(2^k)-1,则它就是满二叉树。

【性质】:满二叉树中度为1的节点最多为1个。度为1的个数要么为0要么为1。

所以满二叉树可以使用数组进行存储。

在这里插入图片描述

2.3.2、完全二叉树

完全二叉树:完全二叉树示效率很高的数据结构,完全二叉树是由满二叉树引出来的。对于深度为k的,由n个节点的二叉树,当且仅当其每一个节点都与深度为k的满二叉树中编号从1至n的节点一一对应时称为完全二叉树。要注意的是__满二叉树是一种特殊的完全二叉树__。

换句话说,完全二叉树就是:假设树的高度为h。

  • 前h-1层都是满的。最后一层可以全满,也可以不满。
  • 如果最后一层不满,要求最后一层结点从左向右都是连续的。

关于完全二叉树的性质:

性质一:具有n个结点的完全二叉树的深度为[log2 N] + 1。

N代表完全二叉树的结点总数。

[x]:称作x的底,表示不大于x的最大整数。加入x=3.14,那[x]=3。

例题:如下图,求完全二叉树的深度

在这里插入图片描述

可以看到此完全二叉树的结点总数为12,那直接套公式:[log2 N] + 1,log2 N约等于3.x,所以[log2 N]的结果为3,然后再加1,最终结果为4。所以此完全二叉树的深度为4。

性质二:探讨双亲节点和子节点的关系。

  • 如果i=1,则结点i是二叉树的根,无双亲,如果i>1,则其双亲是结点[i/2]。

  • 如果双亲节点编号是i,那么此双亲结点的左节点编号为2i,右节点编号是2i+1。

2.4、二叉树的存储结构

二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。链式结构又分:二叉链,三叉链。

在这里插入图片描述

2.4.1、顺序存储:

顺序结构存储就是使用__数组来存储__,一般使用数组只适合表示完全二叉树(包含满二叉树),因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆在后面的章节会专门讲解。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。

完全二叉树存储:

在这里插入图片描述

非完全二叉树存储:

在这里插入图片描述

例题:二叉树结点数值采用顺序存储结构,如图所示。画出二叉树结构

在这里插入图片描述

解题思路:画出满二叉树的图,按照序号一次填入。

在这里插入图片描述

2.4.2、链式存储

二叉树的链式存储结构是指:用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。通常的方法是链表中的每一个节点由三个域组成,数据域和左右指针域,左右指针分别用来给出该节点左孩子和右孩子所在的链接点的存储地址。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面课程学到高阶数据结构,如:红黑树等会用到三叉链。

在这里插入图片描述

在这里插入图片描述

//二叉链
struct Node
{struct Node* _firstChild1;           //指向当前节点的左孩子struct Node* _pNextBrother;         //指向当前节点的右孩子DataType _data;                    //节点中的数据域
};//三叉链
struct BinaryTreeNode
{struct BinTreeNode* pParent;     //指向当前节点的双亲struct BinTreeNode* pLeft;       //指向当前节点的左孩子struct BinTreeNode* pRight;      //指向当前节点的右孩子int data;                    //节点中的数据域
};

在n个结点的二叉链表中,有n+1个空指针域。

3、二叉树的一些操作

首先我们在看待二叉树时,应该是这样看待:任何一颗二叉树有三个部分:

  • 根节点
  • 左子树
  • 右子树

在这里插入图片描述

下面我们将要使用的算法是:

​ 分治算法:分而治之,把大问题分成类似子问题,子问题再分为子问题。知道子问题不在可分割。

3.1、二叉树链式结构的遍历

所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个节点均作一次且只做一次访问。访问节点所做的操作依赖于具体的应用问题。遍历是二叉树上最重要的运算之一,是二叉树上进行其它运算之基础。

在这里插入图片描述

__前序/中序/后序的递归结构遍历:__是根据访问节点操作发生位置而命名的。

​ 1、NLR:前序遍历(Preorder Traversal称为先序遍历)——访问根节点的操作发生在遍历其左右子树之 前。

​ 2、LNR:中序遍历(Inorder Traversal)——访问根节点的操作发生在遍历其左右子树之中(间)。

​ 3、LRN:后序遍历(Postorder Travedsal)——访问根节点的操作发生在遍历其左右子树之后。

由于被访问的节点必是某子树的根,所以__N(Node)、L(Left subtree)和R(Right subtree)又可解释为:根、根的左子树和根的右子树。__NLR、LNR、LRN分别又称为:先根遍历、中根遍、后根遍历。

前序,中序,后序遍历又叫做深度优先遍历。

下面我们以图示,来说明__前序遍历、中序遍历、后序遍历__

在这里插入图片描述

前序遍历(先根):访问顺序:A—>B—>D NULL NULL—>E NULL NULL—>C NULL NULL。

  • 先放问A,然后访问A的左子树,也就是P1部分。
  • P1部分,先访问B,然后访问B的左子树,也就是D部分,由于D的左子树和右子树都为NULL。所以B的左子树访问结束。之后再访问B的右子树,也就是E部分,由于E的左子树和右子树都为NULL。所以B的右子树访问结束。
  • 拿到这个时候A的左子树访问完毕,接着访问A的右子树,也就是P2部分。右C的左子树和右子树都为NULL。
  • 所以整个二叉树访问完毕。

中序(中根):左子树 根 右子树

访问顺序:NULL D NULL—>B—>NULL E NULL—>A—>NULL C NULL。

简化顺序:D B E A C。

后序(后根):左子树 右子树 根

访问顺序:NULL NULL D—>NULL NULL E—>B —>NULL NULL C—>A。

简化顺序:D E B C A。

代码实现:

#include <stdio.h>
#include <stdlib.h>typedef int BTDataType;typedef struct BinaryTreeNode
{struct BinaryTreeNode* left;struct BinaryTreeNode* right;char data;
}BTNode;//前序
void PrevOrder(BTNode* root)
{//判断根节点是否为空,为空直接返回if (root == NULL){return;}printf("%c ", root->data);PrevOrder(root->left);PrevOrder(root->right);}//中序
void InOrder(BTNode* root)
{if (root == NULL)return;InOrder(root->left);printf("%c ", root->data);InOrder(root->right);
}//后序
void PostOrder(BTNode* root)
{if (root == NULL)return;PostOrder(root->left);PostOrder(root->right);printf("%c ", root->data);
}int main()
{BTNode* A = (BTNode*)malloc(sizeof(BTNode));A->data = 'A';A->left = NULL;A->right = NULL;BTNode* B = (BTNode*)malloc(sizeof(BTNode));B->data = 'B';B->left = NULL;B->right = NULL;BTNode* C = (BTNode*)malloc(sizeof(BTNode));C->data = 'C';C->left = NULL;C->right = NULL;BTNode* D = (BTNode*)malloc(sizeof(BTNode));D->data = 'D';D->left = NULL;D->right = NULL;BTNode* E = (BTNode*)malloc(sizeof(BTNode));E->data = 'E';E->left = NULL;E->right = NULL;A->left = B;A->right = C;B->left = D;B->right = E;printf("前序:");PrevOrder(A);printf("\n");printf("中序:");InOrder(A);printf("\n");printf("后序:");PostOrder(A);printf("\n");return 0;
}

输出:

在这里插入图片描述

  • 时间复杂度:O(n) //每个结点只访问一次。
  • 空间复杂度:O(n) //栈占用的最大辅助空间。

3.2、通过使用遍历统计的方法来计算二叉树节点个数

void TreeSize(BTNode* root, int* psize)
{if (root == NULL){return;}else{++(*psize);}TreeSize(root->left,psize);TreeSize(root->right,psize);
}int main()
{//计算以A为根节点的二叉树节点个数int Asize = 0;TreeSize(A, &Asize);printf("以A为根节点的节点个数为:%d\n", Asize);//计算以B为根节点的二叉树节点个数int Bsize = 0;TreeSize(B, &Bsize);printf("以B为根节点的节点个数为:%d\n", Bsize);
}

输出:

在这里插入图片描述

3.3、通过分治的思路来计算二叉树节点个数

int TreeSize(BTNode* root)
{return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}printf("以A为根节点的节点个数为:%d\n", TreeSize(A));
printf("以B为根节点的节点个数为:%d\n", TreeSize(B));

输出:

在这里插入图片描述

分析:如下图:

在这里插入图片描述

3.4、通过遍历统计的方法计算二叉树中叶子节点的个数

void TreeLeafSize(BTNode* root, int* psize)
{if (root->left == NULL && root->right == NULL){++(*psize);}else{TreeLeafSize(root->left, psize);TreeLeafSize(root->right, psize);}
}int main()
{int a = 0;TreeSize(A, &a);printf("%d\n",a);
}

3.5、通过分治的思路来计算二叉树中叶子节点的个数

int TreeLeafSize(BTNode* root)
{if (root == NULL)return 0;if (root->left == NULL && root->right == NULL)return 1;return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}int main()
{printf("%d\n", TreeLeafSize(A));
}

3.6、复制二叉树(递归)

核心思路:

  • 如果是空树,递归结束。
  • 否则,申请新节点空间,复制根节点
    • 递归复制左子树。
    • 递归复制右子树。

代码实现:

#include <stdio.h>
#include <stdlib.h>typedef struct BinaryTreeNode
{struct BinaryTreeNode* left;struct BinaryTreeNode* right;char data;
}BTNode;//暴力创建二叉树
BTNode* CreateTree()
{BTNode* A = (BTNode*)malloc(sizeof(BTNode));A->data = 'A';A->left = NULL;A->right = NULL;BTNode* B = (BTNode*)malloc(sizeof(BTNode));B->data = 'B';B->left = NULL;B->right = NULL;BTNode* C = (BTNode*)malloc(sizeof(BTNode));C->data = 'C';C->left = NULL;C->right = NULL;BTNode* D = (BTNode*)malloc(sizeof(BTNode));D->data = 'D';D->left = NULL;D->right = NULL;BTNode* E = (BTNode*)malloc(sizeof(BTNode));E->data = 'E';E->left = NULL;E->right = NULL;A->left = B;A->right = C;B->left = D;B->right = E;return A;
}//复制二叉树
void CopyTree(BTNode* root,BTNode** copy_root)
{if (root == NULL){//如果主二叉树为空,那就将副二叉树指控,也就是说不复制了。*copy_root = NULL;return 0;}else{*copy_root = (BTNode*)malloc(sizeof(BTNode));(*copy_root)->data = root->data;CopyTree(root->left, &(*copy_root)->left);CopyTree(root->right, &(*copy_root)->right);}
}//中序遍历
void InOrder(BTNode* root)
{if (root == NULL){return;}InOrder(root->left);printf("%c ", root->data);InOrder(root->right);
}//后序遍历
void PostOrder(BTNode* root)
{if (root == NULL){return;}PostOrder(root->left);PostOrder(root->right);printf("%c ", root->data);
}int main()
{BTNode* root = CreateTree();BTNode* copy_root;CopyTree(root, &copy_root);printf("中序遍历:");InOrder(copy_root);printf("\n");printf("后序遍历:");PostOrder(copy_root);return 0;
}

3.7、二叉树的深度

核心思想:

  • 如果是空树,则深度为0。
  • 否则,递归计算左子树的深度记为m,递归计算右子树的深度记为n,二叉树的深度则为m与n的较大者加1。

代码展示:

int maxDepth(struct TreeNode* root){if (root == NULL){return 0;}int leftDepth = maxDepth(root->left);int rightDepth = maxDepth(root->right);return leftDepth > rightDepth ? leftDepth+1 : rightDepth+1;
}

3.8、二叉树的销毁

这里直接说结论:使用后序遍历进行销毁

核心代码实现:

//二叉树的销毁
void DestroyTree(struct TreeNode* root)
{if (root == NULL){return;}DestroyTree(root->left);DestroyTree(root->right);free(root);root = NULL;
}

4、二叉树层序遍历的实现(非递归实现)

4.1、利用队列实现层序遍历

上面我们进行二叉树的遍历都是用递归的方法(前序,中序,后序),又叫深度优先遍历。

那可不可以使用非递归的方法来遍历二叉树呢?可以!!!(在上面我们也写了2个案例)。

此方法叫做:层序遍历,广度优先遍历。

这里借助__队列实现(先进先出)。__

层序遍历的作用是将二叉树,从上到下,从左到右依次遍历。如下图遍历的结果是A->B->C->D->E->F->G->H。

这种方法的核心思路就是:上一层带下一层。

具体实现方法,如下图:

在这里插入图片描述

1、首先有个队列,先把节点A放进去。

在这里插入图片描述

2、取出A节点,注意:重点来了。我们说核心思路就是:上一层带下一层。因为A连接下一层的B,C节点。所以把A取出来之后,先把B,C节点放进队列中去。

在这里插入图片描述

3、然后取出节点B,由于B连接的下层有:D,E节点。所以在取出B节点之后,先把D,E节点放进队列中。

在这里插入图片描述

4、然后将C结点取出,由于C左右子树为F,G结点。所以在取出C结点后,在把F,G进栈,如下图:

在这里插入图片描述

就这样以此类推,实现效果。

这里简化代码量,使用一个简单的二叉树(如下),和上面的原理一样,就是少创建几个二叉树结点。

在这里插入图片描述

4.2、代码全放在一个原文件中

代码实现:这里的队列使用前面所学写的队列。并且所有代码都在一个源文件中:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>//typedef struct BinaryTreeNode* QDataType;typedef struct BinaryTreeNode
{struct BinaryTreeNode* left;struct BinaryTreeNode* right;char data;
}BTNode;typedef BTNode* QDataType;typedef struct QueueNode
{struct QueueNode* next;QDataType data;
}QNode;typedef struct Queue
{QNode* head;QNode* tail;
}Queue;void QueueInit(Queue* pq);
void QueueDestroy(Queue* pq);
void QueuePush(Queue* pq, QDataType x);
void QueuePop(Queue* pq);     //在对头删除数据
QDataType QueueFront(Queue* pq);    //取对头的数据
QDataType QueueBack(Queue* pq);    //取对尾的数据
int QueueSize(Queue* pq);          //计算队列中数据个数
bool QueueEmpty(Queue* pq);void QueueInit(Queue* pq)
{assert(pq);pq->head = NULL;pq->tail = NULL;
}QNode* BuyQueueNode(QDataType x)
{QNode* newnode = (QNode*)malloc(sizeof(QNode));if (newnode == NULL){printf("malloc fail\n");exit(-1);}newnode->data = x;newnode->next = NULL;return newnode;
}void QueueDestroy(Queue* pq)
{assert(pq);QNode* cur = pq->head;while (cur != NULL){QNode* next = cur->next;free(cur);cur = next;}pq->head = pq->tail = NULL;
}void QueuePush(Queue* pq, QDataType x)  //插入数据,其实就是尾插
{assert(pq);QNode* newnode = BuyQueueNode(x);if (pq->head == NULL){pq->head = pq->tail = newnode;}else{pq->tail->next = newnode;pq->tail = newnode;}
}void QueuePop(Queue* pq)     //在对头删除数据
{assert(pq);//防止pq->head == NULL,而导致程序崩溃。assert(!QueueEmpty(pq));QNode* next = pq->head->next;free(pq->head);pq->head = next;if (pq->head == NULL){pq->tail = NULL;}
}QDataType QueueFront(Queue* pq)    //取对头的数据
{assert(pq);assert(!QueueEmpty(pq));return pq->head->data;
}QDataType QueueBack(Queue* pq)    //取对尾的数据 
{assert(pq);assert(!QueueEmpty(pq));return pq->tail->data;
}
int QueueSize(Queue* pq)          //计算队列中有多少的数据
{int count = 0;QNode* cur = pq->head;while (cur != NULL){count++;cur = cur->next;}return count;
}bool QueueEmpty(Queue* pq)
{assert(pq);return pq->head == NULL;
}void LevelOrder(BTNode* root)
{Queue qq;QueueInit(&qq);if (root){QueuePush(&qq, root);}while (!QueueEmpty(&qq)){BTNode* front = QueueFront(&qq);QueuePop(&qq);printf("%c ", front->data);if (front->left){QueuePush(&qq, front->left);}if (front->right){QueuePush(&qq, front->right);}}printf("\n");QueueDestroy(&qq);
}int main()
{BTNode* A = (BTNode*)malloc(sizeof(BTNode));A->data = 'A';A->left = NULL;A->right = NULL;BTNode* B = (BTNode*)malloc(sizeof(BTNode));B->data = 'B';B->left = NULL;B->right = NULL;BTNode* C = (BTNode*)malloc(sizeof(BTNode));C->data = 'C';C->left = NULL;C->right = NULL;BTNode* D = (BTNode*)malloc(sizeof(BTNode));D->data = 'D';D->left = NULL;D->right = NULL;BTNode* E = (BTNode*)malloc(sizeof(BTNode));E->data = 'E';E->left = NULL;E->right = NULL;A->left = B;A->right = C;B->left = D;B->right = E;LevelOrder(A);
}

4.3、代码分布放

queuqe.h

#pragma once#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>typedef struct BinaryTreeNode
{struct BinaryTreeNode* left;struct BinaryTreeNode* right;char data;
}BTNode;typedef BTNode* QDataType;typedef struct QueueNode
{QDataType data;struct QueueNode* Next;
}QNode;typedef struct Queue
{struct QueueNode* head;struct QueueNode* tail;
}Queue;//队列初始化
void QueueInit(Queue* pq);//销毁
void QueueDestroy(Queue* pq);//扩容
QNode* BuyQueueNode(QDataType x);//对尾插入数据
void QueuePush(Queue* pq, QDataType x);//队头删除数据
void QueuePop(Queue* pq);//取队尾数据
QDataType QueueBack(Queue* pq);//取对头数据
QDataType QueueFront(Queue* pq);//统计队列元素个数
int QueueSize(Queue* pq);//判断队列是否为空
bool QueueEmpty(Queue* pq);

queue.c

#include "queue.h"//队列初始化
void QueueInit(Queue* pq)
{assert(pq);pq->head = NULL;pq->tail = NULL;
}//销毁
void QueueDestroy(Queue* pq)
{assert(pq);QNode* cur = pq->head;while (cur){QNode* next = cur->Next;free(cur);cur = next;}pq->head = pq->tail = NULL;
}//扩容
QNode* BuyQueueNode(QDataType x)
{QNode* newnode = (QNode*)malloc(sizeof(QNode));if (newnode == NULL){printf("malloc fail\n");exit(-1);}newnode->data = x;newnode->Next = NULL;return newnode;
}//对尾插入数据
void QueuePush(Queue* pq, QDataType x)
{QNode* newnode = BuyQueueNode(x);if (pq->head == NULL){pq->head = pq->tail = newnode;}else{pq->tail->Next = newnode;pq->tail = newnode;}
}//队头删除数据
void QueuePop(Queue* pq)
{assert(pq);assert(!QueueEmpty(pq));QNode* next = pq->head->Next;free(pq->head);pq->head = next;if (pq->head == NULL){pq->tail = NULL;}
}//取队尾数据
QDataType QueueBack(Queue* pq)
{assert(pq);assert(!QueueEmpty(pq));return pq->tail->data;
}//取对头数据
QDataType QueueFront(Queue* pq)
{assert(pq);assert(!QueueEmpty(pq));return pq->head->data;
}//统计队列元素个数
int QueueSize(Queue* pq)
{assert(pq);assert(!QueueEmpty(pq));int count = 0;QNode* cur = pq->head;while (cur){count++;cur = cur->Next;}return count;
}//判断队列是否为空
bool QueueEmpty(Queue* pq)
{assert(pq);return pq->head == NULL;
}

test.c

#include "queue.h"void LevelOrder(BTNode* root)
{Queue qq;QueueInit(&qq);if (root){QueuePush(&qq, root);}while (!QueueEmpty(&qq)){BTNode* front = QueueFront(&qq);QueuePop(&qq);printf("%c ", front->data);if (front->left){QueuePush(&qq, front->left);}if (front->right){QueuePush(&qq, front->right);}}printf("\n");QueueDestroy(&qq);
}int main()
{BTNode* A = (BTNode*)malloc(sizeof(BTNode));A->data = 'A';A->left = NULL;A->right = NULL;BTNode* B = (BTNode*)malloc(sizeof(BTNode));B->data = 'B';B->left = NULL;B->right = NULL;BTNode* C = (BTNode*)malloc(sizeof(BTNode));C->data = 'C';C->left = NULL;C->right = NULL;BTNode* D = (BTNode*)malloc(sizeof(BTNode));D->data = 'D';D->left = NULL;D->right = NULL;BTNode* E = (BTNode*)malloc(sizeof(BTNode));E->data = 'E';E->left = NULL;E->right = NULL;A->left = B;A->right = C;B->left = D;B->right = E;LevelOrder(A);return 0;
}

5、二叉树的建立

按先序遍历序列建立二叉树的二叉链表。

例,已知先序序列为:ABCDEGF。

核心思想:

  • 从键盘输入二叉树的结点信息,建立二叉树的存储结构。
  • 在建立二叉树的过程中按照二叉树先序方式建立。

如果单单给出一个先序,也许会有多种接表结构,就比如:ABCDEGF。会有下面两种(不仅限于这两种):

在这里插入图片描述

那我们到底想要建立那种结构呢?换句话说如果我想建立第一种二叉树呢?其实也很简单,我们将结点左右子树的NULL的地方在表示出来就行了。这里就用#标识NULL吧。那就需要按照下列顺序读入字符:ABC##DE#G##F###。

我们在建立二叉树之后,在输出中序和后序的结果来验证。

中序:C B E G D F A。

后序:C G E F D B A。

知道了实现思想,下面来看看代码实现:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>typedef struct BinaryTreeNode
{struct BinaryTreeNode* left;struct BinaryTreeNode* right;char data;
}BTNode;//创建二叉树
void CreateTree(BTNode** root)
{char ch;scanf("%c", &ch);if (ch == '#'){*root = NULL;}else{*root = (BTNode*)malloc(sizeof(BTNode));if (*root == NULL){printf("malloc fail\n");exit(-1);}(*root)->data = ch;//那这里对应的也需要传地址。CreateTree(&(*root)->left);CreateTree(&(*root)->right);}
}//中序遍历
void InOrder(BTNode* root)
{if (root == NULL){return;}InOrder(root->left);printf("%c ", root->data);InOrder(root->right);
}//后序遍历
void PostOrder(BTNode* root)
{if (root == NULL){return;}PostOrder(root->left);PostOrder(root->right);printf("%c ", root->data);
}int main()
{BTNode* T;CreateTree(&T); //注意这里需要传递结构体指针的地址,所以是个二级指针。printf("中序遍历:");InOrder(T);printf("\n");printf("后序遍历:");PostOrder(T);return 0;
}

输出:

在这里插入图片描述

6、线索二叉树

为什么要研究线索二叉树?

当用二叉链表作为二叉树的存储结构时,可以很方便的找到某个结点的左右孩子;但一般情况下,无法直接找到该节点在某种遍历序列中的前驱和后继结点。

那如何寻找特定遍历序列中二叉树结点和前驱和后继?

解决方法:

  • 通过遍历寻找-------费时间。
  • 每个结点再增设前驱、后继指针域------增加了存储负担。
  • 利用二叉链表中的空指针域(本章研究)。

【结论】:具有n个结点的二叉链表中,有n+1个指针域为空。

结论剖析:具有n个结点的二叉链表中,一共有2n个指针域;因为n个结点中有n-1个孩子,即2n个指针域中,有n-1个用来指示结点的左右孩子,其余n+1个指针域为空。

利用二叉链表中的空指针域:

​ 如果某个结点的左孩子为空,则将空的左孩子指针域改为__指向其前驱__;如果某结点的右孩子为空,则将空的 右孩子指针域改为__指向其后继。__

这种__改变指向的指针称为“线索”。__

那加上线索的二叉树称为__线索二叉树(Threaded Binary Tree)。__

对二叉树按某种遍历次序使其变为线索二叉树的过程叫__线索化。__

那如何实现线索化呢?如下二叉树,其中序遍历:CBEGDFA。

在这里插入图片描述

注意,在强调一遍此二叉树的中序遍历为:__C B E G D F A。__下面我们要根据此中序遍历进行线索化。

如下图,链接结构:

在这里插入图片描述

但是要注意:不是所有的二叉树线索化都是看中序遍历的顺序。而是要求什么样的遍历就按照什么样的遍历来。

为了区分lrchid和rchild指针到底是指向孩子指针,还是指向前驱或者后继的指针,对二叉链表每个结点增设两个标志域ltag和rtag,并约定:

  • ltag = 0;lchild指向该结点的左孩子。
  • ltag = 1;lchild指向该结点的前驱。
  • rtag = 0;rchild指向该结点的右孩子。
  • rtag = 1;rchild指向该结点的后继。

这样,二叉树结点的结构为:

在这里插入图片描述

结构实现,如下:

typedef struct BinaryTreeNode
{struct BinaryTreeNode* left;struct BinaryTreeNode* right;int ltag;int rtag;char data;
}BTNode;

下面我们再来看个二叉树:要求先序线索二叉树。

先序序列:A B C D E。

在这里插入图片描述

那线索化的结果就如下:

在这里插入图片描述

练习:

画出以下二叉树对应的中序线索二叉树。

该二叉树中序遍历结果为:H D I B E A F C G。

在这里插入图片描述

可以看到H没有前驱,G没有后继。那H的左子树和G的右子树结点就置空吗?

可以置空。但我们还可以利用起来。

为了避免悬空态,增设一个头结点。这个头结点顾名思义就在根节点A的头上。

增设一个头结点:

头结点中的ltag=0;lchild指向根节点。

头结点中的rtag=1;rchild指向遍历序列中最后一个结点。

然后再将上图二叉树的H、G结点置空的域都指向结点A。

这样以来:

遍历序列中第一个结点的lchild域和最后一个结点的rchild域都指向头结点。

如下图:

在这里插入图片描述

7、搜索二叉树

实际上我们单纯的学习二叉树没有太多的用处。学习二叉树主要是用于搜索二叉树的。如下图:

在这里插入图片描述

任何一棵树,左子树都比根要小,右子树都比根要大。

搜索中查找一个数,最多查找高度次。

时间复杂度:O(N)。

8、树和森林

首先我们先来回顾一下什么是树,什么是森林。

树:

  • 树是n(n>=0)个结点的有限集。若n=0,称为空树。
  • 若n>0:
    • 有且仅有一个特定的称为根(root)的结点。
    • 其余结点可分为m(m>=0)个互不相交的有限集T1,T2,T3,…,Tm。

在这里插入图片描述

__森林:__是m(m>=0)棵互不相交的树的集合。

在这里插入图片描述

8.1、树的存储结构

8.1.1、双亲表示法

实现:定义结构数组,存放树的的结点,每个结点含两个域。

数据域:存放结点本身信息。

双亲域:指示本结点的双亲结点在数组中的位置。

这样听起来有点抽象,我们来说个示例,给如下数组,写出树的结构。

在这里插入图片描述

那根据上面所描述的规则,我们就可以写出此树的结构了,如下:

在这里插入图片描述

特点:找双亲容易,找孩子难。

C语言的类型描述:

typedef struct PTNode
{Type data;int parent;       //双亲位置域
}PTNode;

树的结构:

#define MAX_TREE_SIZE 100
typedef struct
{PTNode node[MAX_TREE_SIZE];int r,n;     //根节点的位置和结点个数。
}PTree;

8.1.2、孩子链表

把每个结点的孩子结点排列起来,看成是一个线性表,用单链表存储,则n个结点有n个孩子链表(叶子结点的孩子链表为空)。而n个头指针又组成一个线性表,用顺序表(含n个元素的结构数组)存储。

听起来依然抽象,我们来看示例:

在这里插入图片描述

孩子结点结构:

typedef struct CTNode
{int child;       //用来存放单链表结点中child结点的下标值struct CTNode* Next;       //指向下一个结点
}*ChildPtr;

双亲结点结构:

typedef struct
{Type data;        //用来存放结点的值ChildPtr firstchild;          //用来存放第一个孩子结点的指针。
}CTBox;

树结构:

typedef struct
{CTBox nodes[MAX_TREE_SIZE];int n,t;         //结点树和根节点位置。
}CTree;

特点:找孩子容易,找双亲难。

8.1.3、孩子兄弟表示法

孩子兄弟表示法又名:二叉树表示法或叉链表表示法。

实现:用二叉链表作树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子结点和下一个兄弟结点。

结构描述:

typedef struct CSNode
{Type data;struct CSNode *firstchild,*nextbother;     //firstchild指向其第一个孩子结点,*nextbother指向下一个兄弟结点
}CSNode,*CSTree;

下面来看个示例:

在这里插入图片描述

上面补充:B的兄弟结点有两个,A,C。但为什么B的右指针域不指向A,而指向C呢?那是因为,我们强调是找__下一个兄弟结点__,A是B的上一个兄弟节点,C才是B结点的下一个兄弟节点,所以B的右指针域指向结点C。

现在如果想找到结点C的路径是这样的:根据根节点R的firstchild指针域找到结点A,然后根据A结点的nextbother指针域找到B,最后在根据B结点的nextbother指针域找到C即可。

8.2、树与二叉树的转换

将树转化为二叉树进行处理,利用二叉树的算法来实现对树的操作。

那如何操作呢?其实可以发现一个对应关系:

由于树和二叉树都可以用二叉链表做存储结构,则以二叉链表作媒介可以导出树与二叉树之间的一个对应关系。

那是如何对应的呢?如下:

【说明一下这里树的存储结构采用孩子兄弟法。】

在这里插入图片描述

下面来详细说明树和二叉树的转换。

8.2.1、树转换为二叉树:

  • 加线:在树的原始结构中兄弟结点直接每有联系,但转为二叉树前,需要将兄弟结点之间加线。

    在这里插入图片描述

  • 抹线:对每个结点,除了其左孩子外,去除其与其余孩子之间的关系。

如下图,A有三个孩子结点B C E,现在只将A和其左子树,也就是B链接,不和C E链接了。又因为在第一步中兄弟结点之间加线了,所以B又和C链接了。所以树转换为二叉树了。

在这里插入图片描述

  • 旋转:以树的根节点为轴心,将整数顺时针转45°。

总结为一句口诀:兄弟相连留长子。

练习:将树转换为二叉树

在这里插入图片描述

(1)兄弟结点之间连线:

在这里插入图片描述

(2)除了其左孩子外,去除其与其余孩子之间的关系:

在这里插入图片描述

(3)以树的根节点为轴心,将整数顺时针转45°:

在这里插入图片描述

这样就完成了树转换为二叉树的过程。

8.2.2、二叉树转换为树

核心步骤:

  • 加线:若p结点是双亲结点的左孩子,则将p的右孩子,右孩子的右孩子…沿分支找到的所有右孩子,都与p的双亲用线连起来。
  • 抹线:抹掉原二叉树中双亲与右孩子之间的连线。这里有个简单的规律:上一步加多少条线,那这一步就会去掉多少条线。
  • 调整:将结点按层次排序,形成树结构。

这个过程就是上面树转为二叉树的逆操作。

口诀:左孩右右连双亲,去掉原来右孩线。

练习:将二叉树转换为树。

在这里插入图片描述

(1)加线:

在这里插入图片描述

(2)抹线,抹掉原二叉树中双亲与右孩子之间的连线。(上一步加了5条线,那这一步需要去除5条线)

在这里插入图片描述

(3)调整:同一层次的结点给调整到同一行。

在这里插入图片描述

这样就完成了二叉树转换为树的过程。

8.3、森林与二叉树的转换

8.3.1、森林转换为二叉树

核心步骤:

  • 将各棵树分别转换成二叉树。
  • 将每棵树的根节点用线相连。
  • 以第一颗树根结点为二叉树的根,再以根节点为轴心,顺时针旋转,构成二叉树型结构。

口诀:树变二叉根相连。

练习:将森林转换为二叉树

在这里插入图片描述

(1)将各棵树分别转换成二叉树。(这里不在具体介绍树转换为二叉树的过程了)

在这里插入图片描述

(2)将每棵树的根节点用线相连。

在这里插入图片描述

(3)旋转

在这里插入图片描述

8.3.2、二叉树转换为森林

核心步骤:

  • 抹线:将二叉树中根节点与其右孩子连线,及沿右分支搜索到的所有右孩子间连线全部抹掉,使之变成孤立的二叉树。
  • 还原:将孤立的二叉树还原成树。

口诀:去掉全部右孩线,孤立二叉再还原。

练习:将二叉树转换为森林

在这里插入图片描述

(1)去掉全部右孩线。

在这里插入图片描述

(3)还原,将每个二叉树变为树(这里不在具体介绍树二叉转换为树的过程了)

在这里插入图片描述

8.4、树和森林的遍历

8.4.1、树的遍历

前面学习到二叉树有四种遍历方式:先序、中序、后序,层序遍历。

而树的遍历有三种方式(没有中序遍历)。

1、先根(次序)遍历:

​ 若树不为空,则先访问根节点,然后依次先根遍历各棵子树。

2、后跟(次序)遍历:

​ 若树不为空,则先依次后根遍历各棵子树,然后访问根节点。

3、按层次遍历:

​ 若树不为空,则自上而下自左至右访问树中每个结点。

下面给出一个树(如下),我们来写出先根遍历、后跟遍历、层次遍历的顺序。

在这里插入图片描述

先根遍历:A B C D E。

后跟遍历:B D C E A 。

层序遍历:A B C E D。

8.4.2、森林的遍历

将森林看作由三部分构成:

  • 森林中第一颗树的根节点。
  • 森林中第一颗树的子树森林。
  • 森林中其它树构成的森林。

在这里插入图片描述

森林的遍历方式也有三种:(根据访问森林中第一部分的顺序而区分)

  • 先序遍历(先访问第一部分)。
  • 中序遍历(先访问第一棵树的子树森林,再访问第一部分,最后访问其它树构成的森林)。
  • 后序遍历(最后访问第一部分)。

先序遍历:

若森林不空,则:

  • 访问森林中第一棵树的根节点。
  • 先序遍历森林中第一颗树的子树森林。
  • 先序遍历森林中(除第一棵树之外)其余树构成的森林。

即:依次从左至右对森林中的每一棵树进行先根遍历。

中序遍历:

若森林不空,则:

  • 中序遍历森林中第一棵树的子树森林。
  • 访问森林中第一棵树的根节点。
  • 中序遍历森林中(除第一棵树之外)其余树构成的森林。

即:依次从左至右对森林中的每一棵树进行后根遍历。

练习:给一个森林,如下图,进行森林的遍历

在这里插入图片描述

先序遍历的结果:A B C D E F G H I J。

先序遍历的过程分析:

(1)首先分为三部分:

在这里插入图片描述

(2)遍历第一部分,得到A结点。再访问第二部分,第二部分又是个森林,那B C D 结点又是森林中的每个树,那就按照树的遍历方法来,B是B这个树的根结点,遍历B树的根节点,那就得到了B结点。那同理得到C结点,D结点。至此第二部分遍历完毕。

(3)最后再访问第三部分,第三部分又可以分为三部分(如下):

在这里插入图片描述

然后访问第一部分,得到E结点。再访问第二部分,F是个子树森林,遍历此子树森林,得到此子树森林的根节点F。最后再访问第三部分。然后还需要再分为三部分…,这里不在细分,直接写结果了,最后得到了G H I J结点。

所以最终得到此森林的__先序遍历:A B C D E F G H I J。__

练习:基于上面问题,写出森林的中序遍历

访问顺序:先访问第二部分,在访问第一部分,最后访问第三部分。

在这里插入图片描述

中序遍历结果:B C D A F E H J I G。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/89899.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

泽众APM性能监控软件

泽众Application Performance Management&#xff08;简称APM&#xff09;是一款专业的性能监控工具&#xff0c;可以对全链路如Web服务器、应用服务器、数据库服务器等进行实时监控&#xff0c;并以图表化的形式直观地呈现监控数据&#xff0c;为系统性能优化和定位问题提供准…

Caddy Web服务器深度解析与对比:Caddy vs. Nginx vs. Apache

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

基于SpringBoot的大学生就业招聘系统的设计与实现

目录 前言 一、技术栈 二、系统功能介绍 求职信息管理 首页 招聘信息管理 岗位申请管理 岗位分类 企业管理 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着信息互联网信息的飞速发展&#xff0c;大学生就业成为一个难题&#xff0c;好多公司都舍不…

最新AI写作系统ChatGPT源码/支持GPT4.0+GPT联网提问/支持ai绘画Midjourney+Prompt应用+MJ以图生图+思维导图生成

一、智能创作系统 SparkAi创作系统是基于国外很火的ChatGPT进行开发的Ai智能问答系统。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭建部署AI创作ChatGPT&#xff1f;小编这里写一个详细图文教程吧&…

使用光纤激光切割机等激光切割设备时的一些小诀窍

光纤激光切割机极大地提高了钣金加工行业切割效果和生产效率。然而在我们对客户的回访调查中&#xff0c;发现客户普遍存在着对光纤激光切割机设备的保养维护意识不足的问题&#xff0c;这严重影响了设备的正常使用和使用寿命。 虽然激光切割机有日常的保养&#xff0c;但是也需…

jvm垃圾收集算法

简介 由于《分代收集理论》和不同垃圾收集算法&#xff0c;Java堆应该被划分为不同区域&#xff0c;一般至少会把Java堆划分为新生代&#xff08;Young Generation&#xff09;和老年代&#xff08;Old Generation&#xff09;两个区域。 垃圾收集器可以只回收其中某一个或者…

力扣每日一题(+日常水几道题)

每日一题1333. 餐厅过滤器 - 力扣&#xff08;LeetCode&#xff09; 简单的按规则排序,去除几个不满足的条件然后排序返回即可 #include<algorithm> class Solution { public:vector<int> filterRestaurants(vector<vector<int>>& restaurants, …

侯捷 C++ STL标准库和泛型编程 —— 1 STL概述 + 2 OOPvsGP

现在开始更新侯捷的STL的部分了&#xff01;&#xff01;&#xff01; 完整版本会在全部更新完之后就整合发出 或者也可以直接去我的个人网站上查看 关于STL这部分&#xff0c;原课程将其分为了四部分&#xff0c;我做笔记时&#xff0c;会将其整合&#xff0c;使其更具有整体性…

OpenGLES:绘制一个颜色渐变的圆

一.概述 今天使用OpenGLES实现一个圆心是玫红色&#xff0c;向圆周渐变成蓝色的圆。 本篇博文的内容也是后续绘制3D图形的基础。 实现过程中&#xff0c;需要重点关注的点是&#xff1a;如何使用数学公式求得图形的顶点&#xff0c;以及加载颜色值。 废话不多说&#xff0c…

【ROS 2】-2 话题通信

所有内容请看&#xff1a; 博客学习目录_Howe_xixi的博客-CSDN博客https://blog.csdn.net/weixin_44362628/article/details/126020573?spm1001.2014.3001.5502飞书原文链接&#xff1a; Docs

LeetCode算法题---第3天

注:大佬解答来自LeetCode官方题解 121.买卖股票的最佳时期 1.题目 2.个人解答 function maxProfit(prices) {//更新最低价格和最大利润let minPrice prices[0];let maxProfit 0;for (let i 1; i < prices.length; i) {// 如果当前价格比最低价格还低&#xff0c;更新最…

情满中秋᛫欢度国庆 | 联诚发与你共度佳节!

转眼九月份又走到尽头 国庆和中秋正好撞了个满怀 随风飘扬的国旗与满街飘香的月饼 国泰民安与阖家团圆 这是大家与小家最美好的祈愿 当中秋遇上国庆&#xff0c;当团圆遇上国诞 双节来临之际 为庆祝传统佳节与祖国生日 也为感谢联诚发每位员工的辛勤付出 9月28日下午 …

如何礼貌委婉地拒绝上级领导的加班要求?

案例&#xff1a;领导发消息问我今天晚上能否加班完成一项工作&#xff0c;但我已经和一个重要的朋友约好了今晚一起吃饭&#xff0c;我该如何礼貌委婉地拒绝上级领导的加班要求&#xff0c;并且不让上级领导对我产生不好的印象呢? 回复&#xff1a;当面临类似情况时&#xf…

从零开始之了解电机及其控制(11)实现空间矢量调制

广泛地说&#xff0c;空间矢量调制只是将电压矢量以及磁场矢量在空间中调制到任意角度&#xff0c;通常同时最大限度地利用整个电压范围。 其他空间矢量调制模式确实存在&#xff0c;并且根据您最关心的内容&#xff0c;它们可能值得研究。 如何实际执行这种所谓的交替反向序列…

通过http发送post请求的三种Content-Type分析

通过okhttp向服务端发起post网络请求&#xff0c;可以通过Content-Type设置发送请求数据的格式。 常用到的三种&#xff1a; 1&#xff09;application/x-www-form-urlencoded; charsetutf-8 2&#xff09;application/json; charsetutf-8 3&#xff09;multipart/form-dat…

HTML5中使用video标签

参考链接 <videocontrolscontrolslist"nodownload noplaybackrate"disablePictureInPicture"true"disableRemotePlayback"true"src"https://www.runoob.com/try/demo_source/movie.mp4"></video>隐藏下载&#xff1a;nod…

ios证书类型及其作用说明

ios证书类型及其作用说明 很多刚开始接触iOS证书的开发者可能不是很了解iOS证书的类型功能和概念。下面对iOS证书的几个方面进行介绍。 apple开发账号分类&#xff1a; 免费账号&#xff1a; 无需支付费用给apple&#xff0c;使用个人信息注册的账号 可以开发测试安装&…

No133.精选前端面试题,享受每天的挑战和学习

🤍 前端开发工程师(主业)、技术博主(副业)、已过CET6 🍨 阿珊和她的猫_CSDN个人主页 🕠 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 🍚 蓝桥云课签约作者、已在蓝桥云课上架的前后端实战课程《Vue.js 和 Egg.js 开发企业级健康管理项目》、《带你从入…

AUTOSAR RTE介绍(更新版230925)

RTE是什么 AUTOSAR RTE(Run Time Environment)实现了AUTOSAR系统中的虚拟功能总线(VFB),提供了SWC(Software Component)之间的访问接口和SWC对于BSW资源的访问接口。RTE为SWC中的Runnable提供与其他SWC或者BSW模块通信的接口,RTE将Runnable映射到OS Task中,并且管理Runna…

关于vcruntime140.dll丢失如何修复,电脑多种修复vcruntime140.dll丢失方法

在使用某些软件或执行某些代码时&#xff0c;可能会遇到“找不到 vcruntime140.dll&#xff0c;无法继续执行代码”的错误提示。这通常意味着你的计算机上缺少 Visual C Redistributable for Visual Studio 2015 的运行时库&#xff0c;或者该库的版本不正确。 三种解决方法解决…