之前我们已经学习了二叉树的顺序存储结构,但是二叉树的顺序结构只适合存储完全二叉树,因为完全二叉树是连续的,如果存储非完全二叉树时树会造成空间的浪费。本期我们我们要进行的是链式二叉树的学习。
目录
链式二叉树结构
今后,只要学习了一种数据结构是链式存储的,那么它的定义就是与链表类似的;同理,如果一种数据结构是顺序存储的,那么它的定义就是与顺序表类似的。因此,本次学习的二叉树是链式结构存储的,所以它的结构应该与链表类似。
//二叉树的每个元素都是一个字符
typedef char BTDataType;
//定义二叉树的数据结构
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
BTDataType Data;
}BTNode;
链式二叉树的相关操作
创建二叉树节点
BTNode* BuyNode(BTDataType x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
if (node == NULL )
{
printf("malloc fail\n");
exit(-1);
}
node->Data = x;
node->left = node->right = NULL;
return node;
创建一个二叉树
BTNode* CreatBinaryTree()
{
BTNode* nodeA = BuyNode('A');
BTNode* nodeB = BuyNode('B');
BTNode* nodeC = BuyNode('C');
BTNode* nodeD = BuyNode('D');
BTNode* nodeE = BuyNode('E');
BTNode* nodeF = BuyNode('F');
BTNode* nodeG = BuyNode('G');
nodeA->left = nodeB;
nodeA->right = nodeC;
nodeB->left = nodeD;
nodeC->left = nodeE;
nodeC->right = nodeF;
nodeF->right = nodeG;
return nodeA;
}
二叉树的创建其实并不是这样创建的,我们是为了方便展示才这样创建的二叉树,详细正确的建立二叉树的方式后续我们会继续讲解。
所建立的二叉树图示如下:(后续我们所有的遍历和操作全部基于此树)
二叉树的遍历
在为大家讲解二叉树遍历之前,先为大家讲述一个我们在编程过程中常用的思想。“分治思想”,何为分治,字面上的意思就是“分而治之”,就是把一个复杂的问题分成两个或者更多的相同或者相似的问题,再把子问题分成更小的子问题......直到最后子问题可以可以简单的直接求解,原问题的解即子问题的解的合并。其实之前我们已经接触过了分治思想,分治思想说白了就是递归。
说了这么多,可能还是无法理解,就比如我们要统计全国的人数,一个个统计当然是可行的,但是要付出的代价是很大的,此时我们就可以去求出每个省的人数,要求出每个省的人数,就要先统计每个省中每个市的人数,要统计每个市的人数,得统计每个区的人数,要想统计每个区的人数,就得统计每个镇的人数,要想统计每个镇的人数就得先统计每个小区和乡村的人数,要想统计每个小区或者每个乡村的人数,就得求每户的人数,此时每户的人数就是一个最小的单元就不需要再往下求了,将每户的人数求出之后,依次返回上报,最终将全省的人数相加就可以求出全国的人数。这样的方法比原来一个个统计人数的效率要高很多。
二叉树的前序遍历
分治思想如何进行二叉树的遍历呢?
我们要遍历一个二叉树,可以把它理解为访问根节点,遍历左子树,遍历右子树,以上三个顺序随着遍历方式而改变。
前序遍历思想:先访问根节点,再遍历左子树,再遍历右子树。
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%c ", root->Data);
PreOrder(root->left);
PreOrder(root->right);
}
前序遍历结果:A,B,D,NULL,NULL,NULL,C,E,NULL,NULL,F,NULL,G,NULL,NULL
二叉树的中序遍历
中序遍历思想:先中序遍历左子树,再访问根节点,再中序遍历右子树
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%c ", root->Data);
InOrder(root->left);
InOrder(root->right);
}
中序遍历结果:NULL,D,NULL,B,NULL,A,NULL,E,NULL,C,NULL,F,NULL,G,NULL
二叉树的后续遍历
后序遍历思想:先后序遍历左子树,再后序遍历右子树,再访问根节点
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;//函数的递归调用,所以函数必须得有一个返回值,不然就会导致栈溢出,因为递归调用实在栈中进行的。
}
PostOrder(root->left);
PostOrder(root->right);
printf("%c ", root->Data);
}
后续遍历结果:NULL,NULL,D,NULL,B,NULL,NULL,E,NULL,NULL,NULL,G,F,C,A
二叉树的层序遍历
层序遍历思想:从第一层开始,逐个遍历节点
void BinaryTreeLevelOrder(BTNode* root)
{
//当二叉树树为空树时,层序遍历直接返回即可
if (root == NULL)
return;
//当二叉树不为空时,我们采用的思想就是,先将根节点入队列,然后将根节点出队列并打印,然后再入根节点的左孩子和右孩子,这样依次往后执行,就可以实现二叉树的层序遍历
Queue q;
//队列的初始化
QueueInit(&q);
//先将根节点入队列
QueuePush(&q, root);
//当队列不为空时
while (!QueueEmpty(&q))
{
//取队头的元素
BTNode* front = QueueFront(&q);
//将队头的元素删除
QueuePop(&q);
//打印二叉树的元素
printf("%c ", front->Data);
//当根节点的左孩子和右孩子存在时,让根节点的左孩子和右孩子依次入队列
if (front->left)
QueuePush(&q, front->left);
if (front->right)
QueuePush(&q, front->right);
}
//为了防止内存泄漏,我们必须进行队列的销毁
QueueDestroy(&q);
}
层序遍历结果:A,B,C,D,NULL,E,F,NULL,NULL,NULL,NULL,G
判断是否是完全二叉树
判断是否是完全二叉树:我们知道完全二叉树是连续存储的,我们怎样去判断一颗二叉树是不是完全二叉树呢?我们可以用层序遍历的方法去判断,当我们层序遍历得到一个节点的节点指针为空时,则只需要判断后续遍历过程中是否遇到了一个节点的节点指针不为空,如果遇到了,那么它就不是完全二叉树,否则就是完全二叉树。
bool BinaryTreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
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);
QueuePop(&q);
if (front)
{
return false;
}
}
QueueDestroy(&q);
return true;
}
统计二叉树节点的个数
int BinaryTreeSize(BTNode* root)
{
//如果二叉树是个空树,那么节点的个数就是0
if (root == NULL)
{
return 0;
}
//如果二叉树不是空树,但是根节点没有左孩子和右孩子,那么节点的个数是1
if (root->left==NULL&&root->right==NULL)
{
return 1;
}
//如果二叉树不是空树并且根节点存在左孩子和右孩子,那么节点的个数就是1+左右子树的节点的个数
return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
统计二叉树叶子节点的个数
int BinaryTreeLeafSize(BTNode* root)
{
//如果二叉树为空,那么叶子结点的个数为0
if (root == NULL)
{
return 0;
}
//如果二叉树不为空,但是根节点无左孩子和右孩子,那么叶子节点的个数为1
if (root->left == NULL && root->right == NULL)
{
return 1;
}
//如果二叉树不为空,并且根节点存在左孩子和右孩子,那么叶子节点的个数就为左子树的叶子节点的个数 加 右子树节点的个数
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
统计二叉树第k层节点的个数
int BinaryTreeLevelKSize(BTNode* root,int k)
{
//如果二叉树为空树,第k层节点的个数为0
if (root == NULL)
{
return 0;
}
//如果k==1,第1层节点的个数是1
if (k == 1)
{
return 1;
}
//二叉树不为空且k!=1二叉树的第k层节点的个数=二叉树左子树第k-1层节点的个数+二叉树右子树第k-1层节点的个数
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
计算二叉树的高度
int BinaryTreeDepth(BTNode* root)
{
//如果是个空树,高度为0
if (root == NULL)
{
return 0;
}
//如果不是空树,但是其根节点无左孩子和右孩子,那么二叉树的高度为1
if (root->left == NULL && root->right == NULL)
{
return 1;
}
//如果不是空树且根节点的左孩子和右孩子都存在,树的高度就为左子树和右子树中高度较高的子树的高度+1
int RDepth = BinaryTreeDepth(root->left);
//这里用了两个变量存储左子树和右子树的高度的原因是为了防止额外的递归,因为比较时要递归求值,去最终值时也要递归求值
int LDepth = BinaryTreeDepth(root->right);
return(RDepth > LDepth ? RDepth : LDepth) + 1;
}
查找二叉树中值为x的节点
BTNode* BinaryTreeFind(BTNode* root, char x)
{
//如果二叉树为空,则返回空指针
if (root == NULL)
{
return NULL;
}
//如果根节点的值为x,则返回根节点的指针
if (root->Data == x)
{
return root;
}
//如果二叉树不为空,且根节点的值不为x,在根节点的左子树和右子树中查找值为x的节点
BTNode* left = BinaryTreeFind(root->left, x);
if (left)
return left;
BTNode* right = BinaryTreeFind(root->right, x);
if (right)
return right;
//如果在左子树和右子树中还是没有找到值为x的节点,那么就返回空指针
return NULL;
二叉树的销毁
void BinaryTreeDestory(BTNode** root)
{
if (*root == NULL)
{
return;
}
BinaryTreeDestory(&((*root)->left));
BinaryTreeDestory(&((*root)->right));
free(*root);
}
我们不难发现,不论是求二叉树节点的个数,还是求二叉树的高度,还是销毁二叉树,我们采用的都是分治思想,在排除了一些特例之后,我们就将剩余的情况全部用分治思想全部解决。
以上便是本期链式二叉树的全部内容,一些进阶性的问题我们将在后期为大家进一步讲解。
好了本期的内容到这里就结束了!^_^