【本节目标】
- 树概念及结构。
- 二叉树概念及结构。
- 二叉树常见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, ©_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。