链式二叉树结构的实现
1、了解树的遍历方式
之前我们提到树的定义递归式的
所以实现二叉树之前我们先了解一下链式二叉树的遍历方式
前序遍历:将二叉树递归式地按照:根、左子树、右子树执行
中序遍历:将二叉树递归式地按照:左子树、根、右子树执行
后序遍历:将二叉树递归式地按照:左子树、右子树、根执行
以下图为例:
前序遍历:1,2,3,NULL,NULL,NULL,4,5,NULL,NULL,6,NULL,NULL
中序遍历:NULL,3,NULL,2,NULL,1,NULL.5,NULL,4,NULL,6,NULL
后序遍历:NULL,NULL,3,NULL,2,NULL,NULL,5,NULL,NULL,6,4,1
2、构建二叉树
节点结构每个父节点最多有两个孩子节点
typedef char BTDataType;
typedef struct BinaryTreeNode
{
BTDataType val;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
之前只是一个节点,现在还需要通过一个个节点来构建二叉树
有两种构建方式
手动构建
BTNode* BTCreate()
{
BTNode* node1 = BuyNode(1);
BTNode* node2 = BuyNode(2);
BTNode* node3 = BuyNode(3);
BTNode* node4 = BuyNode(4);
BTNode* node5 = BuyNode(5);
BTNode* node6 = BuyNode(6);
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;
return node1;
}
前序遍历构建
用前序把输入的字符串构建成二叉树 "#"代表NULL
输入的字符串需要传它的首元素地址过来,用参数接收
我们还需要一个数组下标来一次次访问数组元素
注意:要传下标的地址过来
因为不只只访问一个元素,下标在函数的递归过程中会不断改变
如果传的不是地址的话,你懂的(形参的改变不会影响实参)
进入函数之后我们要判断数组当前元素是否为空,也就是"#"
为空的话那就不用存了,解引用下标指针然后++,返回NULL就行了
不是空呢,就要扩容了,不然没空间给节点存
扩容完把当前数组元素的值赋给当前节点的值
下标指针继续往后走
由于是前序遍历,按照其特点,上面内容为根部分,下面就是左右子树部分了
一个节点有两个指针连着它的左右孩子节点
那就左边孩子先调用该函数
然后右边孩子在调用该函数
这样层层深入,最后函数底部返回根节点,只要函数能走到最后那就能返回上一层函数
// 通过前序遍历的数组构建二叉树
BTNode* BTCreate(BTDataType* a,int* pi)
{
assert(a);
assert(pi);
//#说明为空返回上一层函数栈帧
if (a[(*pi)] == "#")
{
(*pi)++;
return NULL;
}
//不是空,需要开辟一个节点的空间来存放
BTNode* root = (BTNode*)malloc(sizeof(BTNode));
if (root == NULL)
{
perror("malloc fail");
exit(-1);
}
//把数组的值赋给节点里的值
root->val = a[(*pi)];
(*pi)++;
//往下递归
root->left = BTCreate(a,pi);
root->right = BTCreate(a,pi);
//返回上一层
return root;
}
3、二叉树的销毁
销毁二叉树能从根节点开始销毁吗?自然是不能的,销毁就找不到后面的子孙了
所以应该从叶子节点开始往根节点销毁,那就是最后再销毁根,这和我们上面提到的后序遍历思路是不是一样啊
所以我们采用后序遍历的思路,当然如果节点为空那就不用销毁了直接返回空就行
先走左孩子,再走有孩子,最后释放该节点,归零置空一条龙
void BTDestory(BTNode* root)
{
if (root == NULL)
{
return;
}
//走到这说明根节点非空
//后序递归,从叶子销毁到根
BTDestory(root->left);
BTDestory(root->right);
free(root);
root->val = 0;
root->left = NULL;
root->right = NULL;
}
4、二叉树节点个数
下面我写一下常见的错误思路
int TreeSize(BTnode* root)
{
//递归需要保证过程中size会保存
static int size =0;
//节点为空返回0
if(root==NULL)
{
return 0;
}
else
{
//节点非空
++size;
}
TreeSize(root->left);
TreeSize(root->left);
return size;
}
缺点:static在静态区,只会初始化一次,多次调用会累加
就是这个函数只能用一次,第二次它就废了
优化:将size改为全局变量
int size = 0;
int TreeSize(BTnode* root)
{
//节点为空返回0
if(root==NULL)
{
return 0;
}
else
{
//节点非空
++size;
}
TreeSize(root->left);
TreeSize(root->left);
return size;
}
这种也有缺点,每次调用该函数都要把size置为0,而且不能多线程调用该函数
最优的解法是用分治的思路
让下属去管下属的下属,然后让他们一层层汇报上来(后序的思路)
节点为空返回0
非空那就先左孩子进入函数,进入函数时该节点(左孩子)为空返回0
然后有孩子进入函数,节点(右孩子)为空返回0
如果左右孩子为空但是节点本身不为空那就返回1
int BTSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
//左子树+右子树+根节点本身
return BTSize(root->left) + BTSize(root->right) + 1;
}
5、二叉树叶子节点个数
叶子节点的特点是什么?左右节点都为空
如果节点为空那就不用找了直接返回0
那就要以左右节点都为空的为条件找节点
如果节点非空且左右孩子也非空,那就继续往下走直到走到叶子节点或空节点
// 二叉树叶子节点个数
int BTLeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
//根节点非空 只有根节点(左右节点为空)
if (root->left == NULL && root->right == NULL)
{
return 1;
}
return BTLeafSize(root->left) + BTLeafSize(root->right);
}
6、求第k层节点的个数
不知道k是多少,所以我们需要一个新的参数来存放k的值,k代表着第k层
换个思路总共有x层,从根开始,每往下走一层k就减1,当k减到1时就代表走到第k层(前序思路)
节点为空时直接返回0;当前节点k=1时就返回1
当前树的第k层=左子树的第k-1层+右子树的第k-1层
int BTLevelKSize(BTNode* root, int k)
{
assert(k > 0);
if (root == NULL)
{
return 0;
}
//当k等于1时说明正在第k层
if (k == 1)
{
return 1;
}
//不在k层往下走,然后k-1
return BTLevelKSize(root->left, k - 1) + BTLevelKSize(root->right, k - 1);
}
7、二叉树查找值为x的节点
需要一个新的参数x,用来判断节点的值是否和值x一样
思路:还是前序遍历的思路
根节点为空直接返回,节点的值如果等于x,返回该节点(注意:返回值是指针要用指针接收)
创建一个新的指针变量用来存放值为x的节点的地址
左边找到了(指针不指向空),就返回该指针变量,找不到就到右边去找
右边也找不到就返回空
BTNode* BTFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
//节点非空 根
if (root->val == x)
{
return root;
}
//创建指针
BTNode* ret = NULL;
//左
ret = BTFind(root->left, x);
if (ret != NULL)
{
return ret;
}
//右
ret = BTFind(root->right, x);
if (ret != NULL)
{
return ret;
}
//找不到
return NULL;
}
8、前、中、后序遍历
思路前面讲过了
讲讲别的
递归的思路(二叉树)
把大问题分为子问题,返回条件(最小规模子问题)
(DFS)深度优先遍历:前序遍历 不严格来说:前、中、后序都算深度优先遍历
(BFS)广度优先遍历:层序遍历 一般用队列来配合实现
// 二叉树前序遍历
void PrevOrder(BTNode* root)
{
if (root==NULL)
{
return;
}
printf("%d ", root->val);
PrevOrder(root->left);
PrevOrder(root->right);
}
// 二叉树中序遍历
void InOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
InOrder(root->left);
printf("%d ", root->val);
InOrder(root->right);
}
// 二叉树后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->val);
}
9、二叉树的层序遍历
层序遍历顾名思义就是一层一层地往下遍历
我们可以使用队列的结构来辅助完成这一目标,队列的特点是先进先出
通过这个特点我们可以先往队列里入根节点,在根节点出队列之前用一个指针变量来获取队列的队头(根节点)用来打印,入完趁着根节点还没被删除,如果其左、右孩子非空的话,利用其存放指向其左、右孩子节点的指针去将其左、右孩子拉入队列中,最后删除队头
后面同理,边入队列边出队列直到队列为空(没有节点可入)时,遍历结束
void LevelOrder(BTNode* root)
{
DL q;
DLInit(&q);
if (root)
DLPush(&q, root);
while (!DLEmpty(&q))
{
BTNode* front = DLFront(&q);
printf("%d ", front->val);
if (front->left)
DLpush(&q, front->left);
if (front->right)
DLpush(&q, front->right);
DLPop(&q);
}
printf("\n");
DLDestroy(&q);
}
10、判断二叉树是否为完全二叉树
二叉树的特点,非空节点是连续的,就是完全二叉树
由于需要判断有无空节点,需要用队列录入NULL节点,思路是层序遍历的思路
层序遍历一层层入数据,一边入一边出,当队头为空时就条出循环;
这时遇到空节点了,但是不知道是正常情况的空节点(二叉树走到最后面全是空节点)
还是说遇到"断点"了,非空节点中插了个空节点
接下来二叉树继续往后遍历,如果说还有非空节点说明是非空节点中插了个空节点,返回假
如果遍历完还没遇到非空节点说明是走到最后面,返回真
bool BinaryTreeComplete(BTNode* root)
{
DL q;
DLInit(&q);
if (root)
DLPush(&q, root);
while (!DLEmpty(&q))
{
BTNode* front = DLFront(&q);
if (front == NULL)
break;
DLpush(&q, front->left);
DLpush(&q, front->right);
DLPop(&q);
}
//走到这已经遇到空节点
while (!DLEmpty(&q))
{
BTNode* front = DLFront(&q);
DLPop(&q);
if (front != NULL)
{
return false;
DLDestroy(&q);
}
DLDestroy(&q);
}
return true;
DLDestroy(&q);
}