目录
链式二叉树
链式二叉树的增删查改没有什么价值,对于其来说,注重的是结构
总而言之,贯彻一种思想:每一棵都可以分成根+左右子树,包括左右子树也看出一棵树,其可分为根+左右子树,直至空树(NULL)不可再分。光剩一个结点也是可以分成根+两个空树。
也就是说,二叉树的每一个结点其实都是根节点!!!是其子树的根节点
这里还有就是递归,函数栈帧的概念。每“递”一次,就会产生一个新的函数栈帧;每“归”一次,就会销毁一个函数栈帧
二叉树结点的定义
//定义二叉树结点
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
四种遍历方法
链式二叉树的四种遍历方法分别为:前序遍历、中序遍历、后序遍历、层序遍历
前序遍历
先根、再左子树、最后右子树
每一棵左子树或右子树都要将其看作一棵独立的树,然后用先根、再左子树、最后右子树这样的逻辑来看
以这棵二叉树为例,那么这棵树的遍历序列应该为什么呢?
我们把这棵二叉树完善一下,应该是这样的:
再按照前序遍历的话,所得序列为:
尽管在实际序列中没有这些NULL,但这种带NULL的序列可以更深刻的反映出遍历的过程
代码:
//前序遍历
void PrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%d ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
中序遍历
先左子树,再根,最后右子树
代码:
//中序遍历
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
后序遍历
先左子树,再右子树,最后根
代码:
//后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
层序遍历
从上到下,从左往右,一层一层遍历
层序遍历与上面三种遍历不同,层序遍历不是递归遍历
代码思路:借助队列实现。先将整个二叉树的根节点入队。然后开始循环,队列每出一个节点,就将该节点的左右非空孩子入队列,当队列为空时循环结束。至此,层序遍历结束
void LevelOrder(BTNode* root)
{
//先创建一个队列
Queue q;
QueueInit(&q);
//先将根节点入队列
if (root)
QueuePush(&q, root);
//只要队列不为空
while (!QueueEmpty(&q))
{
//根节点打印并出队
BTNode* front = QueueFront(&q);
printf("%d ", front->data);
QueuePop(&q);
//左右孩子入队列
if (front->left)
QueuePush(&q, front->left);
if (front->right)
QueuePush(&q, front->right);
}
printf("\n");
QueueDestroy(&q);
}
注意,由于进出队列的是二叉树结点指针,所以队列相关的代码需改动一下。即将原本的typedef int QDataType
改为typedef struct BinaryTreeNode* QDataType;
进阶-每一层打印一行
实现思路:定义一个遍历用来记录每一层的结点数。每次出队都是出一层,当这一层出完,下一层也全进队列了,如此循环。
void ProLevelOrder(BTNode* root)
{
//先创建一个队列
Queue q;
QueueInit(&q);
//先将根节点入队列
if (root)
QueuePush(&q, root);
int levelsize = 1; //记录当前层结点个数
//只要队列不为空
while (!QueueEmpty(&q))
{
while (levelsize--)
{
//根节点打印并出队
BTNode* front = QueueFront(&q);
printf("%d ", front->data);
QueuePop(&q);
//左右孩子入队列
if (front->left)
QueuePush(&q, front->left);
if (front->right)
QueuePush(&q, front->right);
}
levelsize = QueueSize(&q);
printf("\n");
}
printf("\n");
QueueDestroy(&q);
}
由于,队列中每次只会存一层的结点,所以我们可以直接用QueueSize来获取每一层的结点数
求二叉树结点个数
方法一
通常思路是:遍历整个二叉树,遇到结点就++
按照上述思路,以前序遍历为例,代码如下:
int TreeSize1(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int size=0;
size++;
TreeSize1(root->left);
TreeSize1(root->right);
return size;
}
只要结点不是NULL,那么size就++
但上述代码是有问题的。因为这里的size是其各自函数栈帧中的局部变量。所以上述代码并不能得到正确结果
显然,这个结果与实际的二叉树结点个数为:6,是不相符的
针对上述原因,我们想着将size给弄到全局区或静态区是否就能解决问题呢?我们先试着移到静态区,即static int size=0;
问题貌似是解决了。可当我们多次调用求二叉树结点这一函数时,又出现问题了
这是因为静态变量只会在第一次执行代码时初始化一次,后面将不再执行初始化代码。
静态变量也行不通,再用全局变量试试
int size=0;
void TreeSize1(BTNode* root)
{
if (root == NULL)
{
return;
}
size++;
TreeSize1(root->left);
TreeSize1(root->right);
}
这时,问题就全部解决了。
方法二
实际上,刚刚的方法并不好。我们接下来用递归的思想来求解这个问题
int TreeSize2(BTNode* root)
{
return root==NULL ? 0 :
TreeSize2(root->left)+TreeSize2(root->right)+1
}
从递归的角度来看:我们将整个树分成根+左右子树,要求二叉树的结点个数就是求左子树的结点个数+右子树的结点个数+自身这一根结点
下面是逻辑图:
求二叉树叶子节点的个数
这里和上面求二叉树结点个数的思路一样,只不过多了一些限制条件而已。
//求二叉树叶子节点的个数
int TreeLeafSize(BTNode* root)
{
if (root == NULL)
return 0;
if (root->left == NULL && root->right == NULL)
return 1;
else
return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
求二叉树的高度
二叉树高度等于左右子树中较高的树的高度+1
//求树的高度
int TreeHeight(BTNode* root)
{
if (root == NULL)
return 0;
int leftHeight=TreeHeight(root->left);
int rightHeight=TreeHeight(root->right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
//求树的高度 int TreeHeight(BTNode* root) { if (root == NULL) return 0; return TreeHeight(root->left) > TreeHeight(root->right) ? TreeHeight(root->left) + 1 : TreeHeight(root->right) + 1; }
这段代码是我一开始写的。但这段代码存在很大的问题。因为
TreeHeight(root->left) > TreeHeight(root->right)
这里比较过后并没有将左右子树的高度进行保存,所以导致后面TreeHeight(root->left) + 1
或TreeHeight(root->right) + 1
时,需要重新递归获得左子树或右子树的高度,从而导致了严重的内存浪费
求二叉树第k层(k≥1)节点个数
二叉树的第k层=左子树的第k-1层+右子树的第k-1层
//求二叉树第k层结点数
int TreeKLevelSize(BTNode* root, int k)
{
if (root == NULL)
return 0;
if (k == 1)
return 1;
else
return TreeKLevelSize(root->left, k - 1) + TreeKLevelSize(root->right, k - 1);
}
二叉树中查找值为x的结点
思路:先查根节点,如果不是那就分别查左右子树,这样递归下去。如果最终查不到就返回NULL
BTNode* TreeFind(BTNode* root, BTDataType x)
{
if(root==NULL)
return NULL;
//先查根节点
if(root->data==x)
return root;
//再分别查左右子树
BTNode retleft=TreeFind(root->left,x);
if(retleft)
return retleft;
BTNode retright=TreeFind(root->right,x);
if(retright)
return retright;
//左右子树也查不到,说明该树中无值x
return NULL;
}
判断二叉树是否为完全二叉树
思路:层序遍历二叉树,当队列第一次出到NULL时则停止。此时开始判断:如果后面出队列的全是NULL,则是完全二叉树;但凡出队列出到一个非空,则不是完全二叉树
上述思路的理论支撑:
![]()
这种情况就不是完全二叉树,因为第一次出完NULL后,又出了5
这种情况就是完全二叉树了,第一次出完NULL后,出的全是NULL
//判断二叉树是否为完全二叉树
//思路:层序遍历一遍。当第一次出到NULL后,开始判断:如果后面出队的全是NULL,则是完全二叉树;但凡后面出队有非空,则不是完全二叉树
bool TreeComplete(BTNode* root)
{
//新建一个队列
Queue q;
QueueInit(&q);
//先将根节点入队
if (root)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
//第一次出到NULL,退出循环,开始判断
if (front == NULL)
{
break;
}
else
{
//左右孩子都入队,不管是不是NULL
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
}
//开始判断
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
//出现非空,则返回false
if (front)
{
QueueDestroy(&q);
return false;
}
}
//走到这说明,这是一棵完全二叉树,则返回true
QueueDestroy(&q);
return true;
}
二叉树的销毁
二叉树由于是链表实现,所以应该从最末端开始往前销毁。销毁时要注意最后释放根节点,如果先释放根节点就找不到它的左右孩子了。因此我们采用后序销毁的办法
//二叉树的销毁
//思路:利用后序遍历的思路逐步销毁,即先释放左子树,再释放右子树,再释放根
void TreeDestroy(BTNode* root)
{
if (root == NULL)
return;
TreeDestroy(root->left);
TreeDestroy(root->right);
free(root);
}
需要注意的是,由于这里传的是一级指针,所以再调用完销毁函数时,我们还需对实参进行置空
如果传的二级指针,就可以在上述代码的最后来个
*root=NULL
。但由于上面的代码是一级指针,形参的改变不影响实参,所以需要我们自己在外部置空一下