前言
各位读者朋友们大家好,上期我们讲了数据结构的堆以及建堆算法,这期我们主要来讲一下数据结构的二叉树,这部分内容对于C语言的函数递归有比较多的应用,因此难度也比较大,我们一一来讲解。
目录
一. 树的基本概念
在讲二叉树之前我们先讲一下树的基本概念:树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
- 节点的度: 一个节点含有的子树的个数;如上图A节点的度为3。
- 叶节点或终端节点: 度为0的节点;如上图的J、K、L。
- 非终端节点或分支节点: 度不为0的节点;如上图的B、C 、D等。
- 父节点或双亲节点:若一个节点有子节点,那么该节点就称为其子节点的父亲节点;如上图的A、B、G等。
- 子节点:根节点的子节点;上图除A节点外都是子节点。
- 兄弟节点:具有相同的父亲节点的节点;如上图中的E和F、H和I是兄弟节点。
- 树的度:一棵树中,最大的结点的度称为树的度 ,上图中树的度是3。
- 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推。
- 树的高度或深度:树中节点的最大层次,;上图中树的高度为4。
二. 树的表示方法
因为树是一种非线性的数据结构,所以存储表示起来很复杂,既要存值域,又要存节点与节点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法。
左孩子右兄弟表示法:
typedef int DataType;
struct Node
{
struct Node* firstChild1; // 第一个孩子结点
struct Node* pNextBrother; // 指向其下一个兄弟结点
DataType data; // 结点中的数据域
};
三. 二叉树的概念及结构
二叉树是一种非线性数据结构,是一种一个父亲节点最多有两个子节点的树。
在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树。
特殊的二叉树:
- 满二叉树:一个二叉树,如果每一层的节点个数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为k,且节点的个数是2k-1,则这个二叉树就是满二叉树。
- 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
四. 二叉树的性质
- 1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2i-1个节点。
- 2. 若规定根节点的层数为1,则深度为h的二叉树最多有2h-1个节点。
- 3. 对于任何一棵二叉树,如果度为0的叶节点个数为n0,度为2的分支节点个数为n2,则n0 = n2+1。
- 4. 若规定根节点的层数是1,具有n个结点的满二叉树深度,h=log2(n+1)。
五. 二叉树的基础操作
5.1二叉树的创建
我们定义二叉树的结构:
typedef int BTDataType;
typedef struct BTreeNode
{
BTDataType val;
struct BTreeNode* left;
struct BTreeNode* right;
}BTNode;
现在我们手搓这棵二叉树
BTNode* BuyNode(BTDataType x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
if (node == NULL)
{
perror("malloc fail");
exit(1);
}
node->val = x;
node->left = node->right = NULL;
return node;
}
BTNode* BTCreat()
{
BTNode* n1 = BuyNode(1);
BTNode* n2 = BuyNode(2);
BTNode* n3 = BuyNode(3);
BTNode* n4 = BuyNode(4);
BTNode* n5 = BuyNode(5);
BTNode* n6 = BuyNode(6);
n1->left = n2;
n1->right = n4;
n2->left = n3;
n4->left = n5;
n4->right = n6;
return n1;
}
5.2 二叉树的遍历
5.2.1 前序遍历
前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
void PrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
printf("%d ", root->val);
PrevOrder(root->left);
PrevOrder(root->right);
}
我们看一下递归展开图来理解一下:
5.2.2 中序遍历
中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
InOrder(root->left);
printf("%d ", root->val);
InOrder(root->right);
}
我们看一下递归展开图来理解一下:
5.2.3 后序遍历
后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
void AfterOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
AfterOrder(root->left);
AfterOrder(root->right);
printf("%d ", root->val);
}
我们看一下递归展开图来理解一下:
5.3 求二叉树结点的个数
int TreeSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return 1 + TreeSize(root->left) + TreeSize(root->right);
}
这里的思路跟前序遍历差不多,1代表每棵树的根节点,然后再访问左子树和右子树,最后将左右子树的节点求和即可。
5.4 求二叉树叶子结点的个数
int TreeLeftSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
if (root->left == NULL && root->right == NULL)
{
return 1;
}
return TreeLeftSize(root->left) + TreeLeftSize(root->right);
}
5.5 求二叉树的高度
要求二叉树的高度即求左右子树的高度然后取最大值再加上根节点即可。
我们先看一下代码:
int TreeDepth(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int left = TreeDepth(root->left);//提高效率
int right = TreeDepth(root->right);
return left > right ? left + 1 : right + 1;
}
借助递归展开图来理解一下:
5.6 求二叉树第K层结点的个数
在二叉树中,第一层只有一个节点,我们要求第K层的节点个数,我们可以将第K层的每一个节点看作第一层的节点然后再将这些节点的个数加起来即可
int TreeKSize(BTNode* root,int k)
{
if (root == NULL)
{
return 0;
}
if (k == 0)
{
return 0;
}
if (k == 1)
{
return 1;
}
return TreeKSize(root->left, k - 1) + TreeKSize(root->right, k - 1);
}
5.7 在二叉树中找值为X的节点
我们在二叉树中找值为X的节点并能将其返回,思路就是先遍历左子树,如果左子树找到的话就直接返回节点的地址,如果找不到就遍历右子树,找到的话就返回对应节点的地址,如果左右子树都没找到就返回NULL,当树是空树的情况下也返回NULL。
BTNode* TreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->val == x)
{
return root;
}
BTNode* left = NULL;
left = TreeFind(root->left, x);
if (left)
{
return left;
}
BTNode* right = NULL;
right = TreeFind(root->right, x);
if (right)
{
return right;
}
//没找到
return NULL;
}
5.8 二叉树的销毁
对于二叉树的销毁,我们可以采用后序遍历那样的思路来实现,我们将二叉树分为左子树、右子树和根,先销毁左子树和右子树最后销毁根,把根放在最后销毁是为了便于通过根来找到后面的节点。
void BTDestory(BTNode* root)
{
if (root == NULL)
{
return;
}
BTDestory(root->left);
BTDestory(root->right);
free(root);
}
六. 二叉树的层序遍历
层序遍历(level-order traversal)从二叉树的顶部到底部逐层遍历二叉树,并在每一层按照从左到右的顺序访问节点。
层序遍历本质上属于广度优先遍历(breadth-first traversal),也称广度优先搜索(breadth-first search, BFS),它体现了一种“一圈一圈向外扩展”的逐层遍历方式。
层序遍历的实现要借助队列来实现。在根节点出队列的同时,其对应的子节点入队列。
void TreeLeverOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)//非空入队列
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);//取队列的头元素
QueuePop(&q);
printf("%d ", front->val);
if (root->left)
QueuePush(&q, front->left);//非空左子树入队列
if (root->right)
QueuePush(&q, front->right);//非空右子树入队列
}
}
七. 判断是不是完全二叉树
这里我们的思路是将二叉树的根节点入队列,在节点出队列的同时不管子节点是否为空都将其子节点代入到队列中,当我们出到第一个空节点时,看队列中是否还有非空节点,如果有就不是完全二叉树,反之则是。
这里不必担心有非空节点入不进去的情况,在第一个非空节点出队列之前所有影响判断的节点都已经入队列了,即使有非空节点没有入队列也不会影响判断结果。
bool TreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if(root)
QueuePush(&q, root);
while (!QueueEmpty(&q))//队列出到空
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
//队列出到第一个空节点
if(front == NULL)
break;
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
//有非空节点
if (front)
{
QueueDestroy(&q);
return false;
}
}
QueueDestroy(&q);
return true;
}
结语
这期我们就讲完了二叉树的基本内容,希望对大家有所帮助,欢迎大家批评指正!