1.二叉树链式结构及实现
上篇我们讲到,完全二叉树采用顺序存储,并讲到其应用堆。这一篇我们来讲讲二叉树的链式存储。
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是 链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所 在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们讲解的是二叉链。三叉链会多一个指针指向父亲节点。
typedef int DataType;
typedef struct BiTreeNode
{
DataType data;
struct BiTreeNode* lchild;
struct BiTreeNode* rchild;
}BiTreeNode;
1.1.创建二叉树
在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能进行其相关的基本操作。我们可以手动快速创建一棵简单的二叉树。
void test2()
{
BiTreeNode* node1 = BuyTreeNode(1);
BiTreeNode* node2 = BuyTreeNode(2);
BiTreeNode* node3 = BuyTreeNode(4);
BiTreeNode* node4 = BuyTreeNode(3);
BiTreeNode* node5 = BuyTreeNode(5);
BiTreeNode* node6 = BuyTreeNode(6);
node1->lchild = node2;
node1->rchild = node3;
node2->lchild = node4;
node3->lchild = node5;
node3->rchild = node6;
}
创建二叉树如左图
二叉树的概念:二叉树是: 1. 空树 2. 非空:根节点,根节点的左子树、根节点的右子树组成的。
二叉树的增删改并没有太大意义,所以我们主要看普通二叉树的遍历,理解递归的思想。
1..2二叉树的遍历
1.2.1 前序、中序以及后序遍历
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉 树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历 是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:
1. 前序遍历(Preorder Traversal亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。
//树的前序遍历
void PreOrder(BiTreeNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%d ", root->data);
PreOrder(root->lchild);
PreOrder(root->rchild);
}
//树的中序遍历
void InOrder(BiTreeNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->lchild);
printf("%d ", root->data);
InOrder(root->rchild);
}
//树的后序遍历
void PostOrder(BiTreeNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
PostOrder(root->lchild);
PostOrder(root->rchild);
printf("%d ", root->data);
}
在这里我们要注意递归的使用:以中序遍历为例:
1.一般二叉树的递归都包含三个部分来处理,当前节点("root")处理,当前root可以分为不同的情况下进入分别,遍历左右子树再进行处理.
2.中序遍历中,若当前访问节点(节点1)存在左孩子节点(2),则会调用自己先访问左孩子节点(2),而左孩子节点发现自己也有左孩子节点(3),则递归进入以3为root的调用之中,结果发现3无左孩子,直接打印NULL,再return,返回之后执行 打印3,之后再到3的右孩子节点之中,也是NULL,打印NULL返回,这样以3为root的调用结束,即为2为root的左孩子调用结束,则打印2,再执行2的右孩子调用。
3.二叉树的问题之中,一般的起始判断条件是root为空的判断以及处理,这可以防止空指针异常。
1.2.2 层序遍历
层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在 层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层 上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
// 树的层次遍历 非递归的
void LevelOrder(BiTreeNode* root)
{
Queue q;
QueueInit(&q);
//如果根不为空,就把这个根节点入进入
if (root)
QueuePush(&q, root);
// 因为队列只能取出队头或者队尾元素,所以,每次把它的左右孩子入队列之后需要 pop掉这个节点,下次再取下一个队头节点
while (!QueueEmpty(&q))
{
BiTreeNode* front = QueueFront(&q);
printf("%d ", front->data);
if (front->lchild)
QueuePush(&q, front->lchild);
if (front->rchild)
QueuePush(&q, front->rchild);
QueuePop(&q);
}
QueueDestroy(&q);
}
1.树的层序遍历需要队列,且是非递归的遍历,先将根节点入队列
2.因为要将队列的孩子入队列,为了可以得到其孩子,队列中保存树节点指针而不是单纯的int值
3.队列只能在一边进行插入,另一边删除,且只能获取两边的元素,所以需要push后pop掉队头元素,也使得子节点的孩子节点可以入队。
1.2.3 节点个数以及高度和查找
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
以上四个接口函数,一个个分析。
首先是求二叉树的节点个数,因为它的返回值是int,如果遍历到的当前节点是NULL,返回0,如果当前节点不为空,则返回1+左子树的节点个数+右子树的节点个数,这就实现了不断遍历。若采用局部静态变量,由于count只初始化一次,所以下次调用该函数就会出现问题,采用全局变量计数也是同样的问题。
int TreeSize(BiTreeNode* root)
{
//static count = 0; 这样也可以,静态变量只初始化一次,第二次调用就无法初始化为0
if (root == NULL)
{
return 0;
}
return 1+ TreeSize(root->lchild)+TreeSize(root->rchild);
}
第二个是求叶子节点个数。
如果遍历到的当前节点是NULL,返回0,如果当前节点是叶子节点,不往后遍历,直接返回1,
而如果既不是叶子节点,也不为空,则继续往左右分别遍历,返回左右叶子节点数量之和。
int TreeLeefSize(BiTreeNode* root)
{
if (root == NULL)
{
return 0;
}
if (root->lchild == NULL && root->rchild == NULL)
{
return 1;
}
return TreeLeefSize(root->lchild)+ TreeLeefSize(root->rchild);
}
第三个是求第k层节点个数。
如果k==1时,证明该节点是第k层节点,返回1即可,如果不是,则访问其孩子,同时层数减1.
int TreekLevelSize(BiTreeNode*root,int k)
{
assert(k > 0);
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return TreekLevelSize(root->lchild, k - 1) + TreekLevelSize(root->rchild, k - 1); //不断向深处遍历,k不断减小,如果刚好是第k层,返回1
}
最后一个是 二叉树查找值为x的节点
先判断是否为空,为空直接返回NULL,如果当前节点就是x节点,直接返回,上述情况如果都不满足,就判断左子树是否有x节点,如果左边有x节点,它的返回值节点一直上传,之后会将这个值进行返回,之后无需查找右边。
BiTreeNode* FindNode(BiTreeNode*root,DataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
if (FindNode(root->lchild, x)) //需要判断是否有x节点的返回值,如果左边有x节点,返回即可,这个节点一直上传,无需查找右边
{
//每一层判断,若其不为空,返回给上一层 不断下来到 “3”时,返回给上一层的判断之中 ,之后上一层判断也不为空,继续再给上一层,
return FindNode(root->lchild, x);
}
else {
return FindNode(root->rchild, x);
}
}