4. 二叉树链式结构的实现
首先,我们先要了解一下二叉树的遍历顺序有哪些:
通过了解二叉树的遍历顺序,我们不难看出要实现二叉树的遍历需要用到递归,而使用递归我们就要思考以下两点:
- 子问题
- 结束条件(最小子问题)
在这里,空树就是不可再分割的子问题。
我们先手搓一棵树,方便我们一会进行验证:
代码语言:javascript复制#include <stdio.h>
#include <stdlib.h>
typedef struct BinTreeNode
{
struct BinTreeNode* left;
struct BinTreeNode* right;
int val;
}BTNode;
BTNode* BuyBTNode(int val)
{
BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
if (NULL == newnode)
{
perror("malloc fail");
return NULL;
}
newnode->val = val;
newnode->left = NULL;
newnode->right = NULL;
return newnode;
}
//手搓一棵树
BTNode* CreateTree()
{
BTNode* n1 = BuyBTNode(1);
BTNode* n2 = BuyBTNode(2);
BTNode* n3 = BuyBTNode(3);
BTNode* n4 = BuyBTNode(4);
BTNode* n5 = BuyBTNode(5);
BTNode* n6 = BuyBTNode(6);
n1->left = n2;
n1->right = n4;
n2->left = n3;
n4->left = n5;
n4->right = n6;
return n1;
}
int main()
{
BTNode* root = CreateTree();
return 0;
}
- 前序
void PreOrder(BTNode* root)
{
if (NULL == root)
{
printf("N ");
return;
}
printf("%d ", root->val);
PreOrder(root->left);
PreOrder(root->right);
}
- 中序
void InOrder(BTNode* root)
{
if (NULL == root)
{
printf("N ");
return;
}
InOrder(root->left);
printf("%d ", root->val);
InOrder(root->right);
}
- 后序
void PostOrder(BTNode* root)
{
if (NULL == root)
{
printf("N ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->val);
}
- 节点个数 最容易想到的就是用计数的方法来实现:
int TreeSize(BTNode* root)
{
static int size = 0;
if (NULL == root)
{
return 0;
}
else
{
size;
}
TreeSize(root->left);
TreeSize(root->right);
return size;
}
但是这样写会出现问题:
代码语言:javascript复制int main()
{
BTNode* root = CreateTree();
printf("%dn", TreeSize(root));
printf("%dn", TreeSize(root));
printf("%dn", TreeSize(root));
return 0;
}
多次调用时它的个数就会出错,这是因为第一次调用完之后第二次再调用时size的值并不为0,但是size又是在函数里面的,在函数外又不能把它变为0,所以我们可以这样修改:
代码语言:javascript复制//static int size = 0;
int size = 0;
int TreeSize(BTNode* root)
{
if (NULL == root)
{
return 0;
}
else
{
size;
}
TreeSize(root->left);
TreeSize(root->right);
return size;
}
int main()
{
BTNode* root = CreateTree();
printf("%dn", TreeSize(root));
size = 0;
printf("%dn", TreeSize(root));
size = 0;
printf("%dn", TreeSize(root));
return 0;
}
但是因为创建的是全局变量,这样写会有线程安全的风险,比如说这个函数给多个线程用,它们之间互相会影响(这里先了解一下,之后学了多线程就可以理解了)
如果一定要用计数的方式来实现,可以这样写:
代码语言:javascript复制void TreeSize(BTNode* root, int* psize)
{
if (NULL == root)
{
return;
}
else
{
(*psize);
}
TreeSize(root->left, psize);
TreeSize(root->right, psize);
}
那么还有没有其他更好的方法呢?
还是递归,递归本质上就是分而治之。
我们举一个形象的例子:
代码语言:javascript复制int TreeSize(BTNode* root)
{
return NULL == root ? 0 : TreeSize(root->left) TreeSize(root->right) 1;
}
int main()
{
BTNode* root = CreateTree();
printf("%dn", TreeSize(root));
printf("%dn", TreeSize(root));
printf("%dn", TreeSize(root));
return 0;
}
这种写法可以看作是后序,因为先算出左子树的节点个数,再算出右子树的节点个数,最后加上根就可以算出根所在的这棵树的节点个数。(代码中的 1 不管放在最前面,还是中间还是最后面都不会影响它是后序,因为我们必须要先算出左右子树的个数才能算出总的节点个数;如果把 1 放在中间就认为是中序,那么就变成了只要算出左子树的节点个数,再加上根就可以算出总的节点个数了,这显然是不对的;所以判断前中后序不能简单的看代码,而是要了解它的核心逻辑,它是如何达到这个结果的)
- 树的高度
int TreeHeight(BTNode* root)
{
if (NULL == root)
{
return 0;
}
int leftHeight = TreeHeight(root->left);
int rightHeight = TreeHeight(root->right);
return leftHeight > rightHeight ? leftHeight 1 : rightHeight 1;
}
如果我们不记录左右子树的高度,而是直接把递归写到return里,也是对的,但是它的时间复杂度会变得很大:
- 第k层的节点个数
int TreeKLevel(BTNode* root, int k)
{
assert(k > 0);
if (NULL == root)
{
return 0;
}
if (1 == k)
{
return 1;
}
//不等于空,且k > 1说明第k层的节点在子树里面,转换成子问题求解
return TreeKLevel(root->left, k - 1) TreeKLevel(root->right, k - 1);
}
- 查找x所在的节点
用递归写很容易会写出这样错误的代码:
代码语言:javascript复制BTNode* TreeFind(BTNode* root, int x)
{
if (NULL == root)
{
return NULL;
}
if (root->val == x)
{
return root;
}
TreeFind(root->left, x);
TreeFind(root->right, x);
}
通过画递归展开图我们就可以很容易发现问题:有一些递归函数是没有返回值的。
那我们这样修改:
代码语言:javascript复制BTNode* TreeFind(BTNode* root, int x)
{
if (NULL == root)
{
return NULL;
}
if (root->val == x)
{
return root;
}
return TreeFind(root->left, x) || TreeFind(root->right, x);
}
这样写也是不对的,如果是实现查找x在不在二叉树中,那么可以用这种写法:
代码语言:javascript复制bool TreeFind(BTNode* root, int x)
{
if (NULL == root)
{
return false;
}
if (root->val == x)
{
return true;
}
return TreeFind(root->left, x) || TreeFind(root->right, x);
}
正确写法:
代码语言:javascript复制BTNode* TreeFind(BTNode* root, int x)
{
if (NULL == root)
{
return NULL;
}
if (root->val == x)
{
return root;
}
BTNode* ret1 = TreeFind(root->left, x);
if (ret1)
{
return ret1;
}
BTNode* ret2 = TreeFind(root->right, x);
if (ret2)
{
return ret2;
}
return NULL;
}
当然,也可以简化一下:
代码语言:javascript复制BTNode* TreeFind(BTNode* root, int x)
{
if (NULL == root)
{
return NULL;
}
if (root->val == x)
{
return root;
}
BTNode* ret1 = TreeFind(root->left, x);
if (ret1)
{
return ret1;
}
return TreeFind(root->right, x);
}
第一种写法:为空树就返回空;找到了就返回当前节点;找不到就先找左子树,找到了返回节点;找不到就再找右子树,找到了返回节点;左右子树都找不到就返回空
第二种写法:为空树就返回空;找到了就返回当前节点;找不到就先找左子树,找到了返回节点;找不到就直接返回右子树(因为右子树中如果找到了就返回节点,找不到就返回空,而左右子树都没找到的时候确实应该返回空,所以这样写没有问题 )
5. 二叉树基础oj练习
- 检查两颗树是否相同
bool isSameTree(struct TreeNode* p, struct TreeNode* q)
{
//根
//左子树
//右子树
//都为空
if (p == NULL && q == NULL)
{
return true;
}
//其中一个为空
if (p == NULL || q == NULL)
{
return false;
}
if (p->val != q->val)
{
return false;
}
return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}
- 单值二叉树
子问题:根(根的值和它的孩子的值进行比较)、左子树、右子树
结束条件(最小子问题):空树
代码语言:javascript复制bool isUnivalTree(struct TreeNode* root)
{
if (NULL == root)
{
return true;
}
if (root->left && root->left->val != root->val)
{
return false;
}
if (root->right && root->right->val != root->val)
{
return false;
}
return isUnivalTree(root->left) && isUnivalTree(root->right);
}
- 对称二叉树
这道题其实就是检查两棵树是否相同的变形:将左子树和左子树比,右子树和右子树比改成了左子树和右子树比,右子树和左子树比。
代码语言:javascript复制bool _isSymmetric(struct TreeNode* root1, struct TreeNode* root2)
{
//根比较
//左子树和右子树比较
//右子树和左子树比较
//都为空
if (root1 == NULL && root2 == NULL)
{
return true;
}
//一个为空 另一个不为空
if (root1 == NULL || root2 == NULL)
{
return false;
}
if (root1->val != root2->val)
{
return false;
}
return _isSymmetric(root1->left, root2->right) && _isSymmetric(root1->right, root2->left);
}
bool isSymmetric(struct TreeNode* root)
{
return _isSymmetric(root->left, root->right);
}
- 二叉树的前序遍历
这道题的第二个参数有些小伙伴可能会觉得有些奇怪,其实这个参数传的是数组大小的一个指针;因为我们最后要返回一个数组,后台为了方便测试,就统一规定了返回数组的时候就要返回数组的大小。
法一:像顺序表一样,空间不够了就扩容
法二:先把树的节点个数算出来,再malloc数组
第一次写的时候可能会出现这样的问题:
代码语言:javascript复制int TreeSize(struct TreeNode* root)
{
return NULL == root ? 0 : TreeSize(root->left) TreeSize(root->right) 1;
}
void preorder(struct TreeNode* root, int* a, int i)
{
if (NULL == root)
{
return;
}
a[i ] = root->val;
preorder(root->left, a, i);
preorder(root->right, a, i);
}
int* preorderTraversal(struct TreeNode* root, int* returnSize)
{
*returnSize = TreeSize(root);
int* a = (int*)malloc(sizeof(int) * (*returnSize));
preorder(root, a, 0);
return a;
}
这样写只能过一部分测试用例,原因在于:函数在递归的时候开辟了很多块栈帧,一个栈帧中的i 是不会影响另一个栈帧中的i的,所以当递归返回的时候上一个递归函数中的i并没有被 ,就会导致数组中存的数据被覆盖掉(画递归展开图可以很直观的看出来)
正确代码:
代码语言:javascript复制int TreeSize(struct TreeNode* root)
{
return NULL == root ? 0 : TreeSize(root->left) TreeSize(root->right) 1;
}
void preorder(struct TreeNode* root, int* a, int* pi)
{
if (NULL == root)
{
return;
}
a[(*pi) ] = root->val;
preorder(root->left, a, pi);
preorder(root->right, a, pi);
}
int* preorderTraversal(struct TreeNode* root, int* returnSize)
{
*returnSize = TreeSize(root);
int* a = (int*)malloc(sizeof(int) * (*returnSize));
int i = 0;
preorder(root, a, &i);
return a;
}
- 另一颗树的子树
思路:跟原树的每一棵子树进行比较,看是不是相同(比较两棵树是不是相同前面的题已经写过,可以直接复用前面的代码)
代码语言:javascript复制bool isSameTree(struct TreeNode* p, struct TreeNode* q)
{
//根
//左子树
//右子树
//都为空
if (p == NULL && q == NULL)
{
return true;
}
//其中一个为空
if (p == NULL || q == NULL)
{
return false;
}
if (p->val != q->val)
{
return false;
}
return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}
//查找 树的比较
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot)
{
if (NULL == root)
{
return false;
}
if (root->val == subRoot->val && isSameTree(root, subRoot))
{
return true;
}
return isSubtree(root->left, subRoot) || isSubtree(root->right, subRoot);
}
- 二叉树的构建及遍历
#include <stdio.h>
#include <stdlib.h>
typedef struct BinTreeNode
{
struct BinTreeNode* left;
struct BinTreeNode* right;
char val;
}BTNode;
BTNode* CreateTree(char* a, int* pi)
{
if ('#' == a[(*pi)])
{
(*pi) ;
return NULL;
}
BTNode* root = (BTNode*)malloc(sizeof(BTNode));
root->val = a[(*pi) ];
root->left = CreateTree(a, pi);
root->right = CreateTree(a, pi);
return root;
}
void InOrder(BTNode* root)
{
if (NULL == root)
{
return;
}
InOrder(root->left);
printf("%c ", root->val);
InOrder(root->right);
}
int main()
{
char a[100];
scanf("%s", a);
int i = 0;
BTNode* root = CreateTree(a, &i);
InOrder(root);
return 0;
}
补充:
- 二叉树的销毁:
用后序比较好(前序也可以,但是比较麻烦,要在销毁根之前把它保存起来)
代码语言:javascript复制void TreeDestroy(BTNode* root)
{
if (NULL == root)
{
return;
}
TreeDestroy(root->left);
TreeDestroy(root->right);
free(root);
}
- 层序遍历
//Queue.h
#include <stdio.h>
#include <stdbool.h>
#include <assert.h>
#include <stdlib.h>
typedef struct BinTreeNode* QDataType;
typedef struct QueueNode
{
QDataType val;
struct QueueNode* next;
}QNode;
typedef struct Queue
{
QNode* phead;
QNode* ptail;
int size;
}Queue;
void QueueInit(Queue* pq);
void QueueDestroy(Queue* pq);
//入队列
void QueuePush(Queue* pq, QDataType x);
//出队列
void QueuePop(Queue* pq);
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
bool QueueEmpty(Queue* pq);
int QueueSize(Queue* pq);
代码语言:javascript复制//Test.c
typedef struct BinTreeNode
{
struct BinTreeNode* left;
struct BinTreeNode* right;
int val;
}BTNode;
#include "Queue.h"
void TreeLevelOrder(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 (front->left)
{
QueuePush(&q, front->left);
}
if (front->right)
{
QueuePush(&q, front->right);
}
}
printf("n");
QueueDestroy(&q);
}
注: 队列具体的实现由于之前讲过,所以这里就不展示出来了
如果想要把空也打印出来,可以这样写:
代码语言:javascript复制void TreeLevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front)
{
printf("%d ", front->val);
//带入下一层
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
else
{
printf("N ");
}
}
printf("n");
QueueDestroy(&q);
}
- 判断二叉树是否是完全二叉树
bool TreeIsComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
//遇到空以后就跳出后续判断
if (NULL == front)
{
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;
}
补充:
二叉树的性质:
- 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1)个结点
- 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h - 1
- 对任何一棵二叉树, 如果度为0的叶结点个数为n0,度为2的分支结点个数为n2,则有 n0=n2 + 1
- 若规定根节点的层数为1,具有n个结点的满二叉树的深度 h= log2(n 1) (ps:log2(n 1) 是log以2为底,n 1为对数)
- 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
- 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
- 若2i 1<n,左孩子序号:2i 1;2i 1>=n,无左孩子
- 若2i 2<n,右孩子序号:2i 2;2i 2>=n,无右孩子
选择题:
- 某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为(B) A 不存在这样的二叉树 B 200 C 198 D 199
- 下列数据结构中,不适合采用顺序存储结构的是(A) A 非完全二叉树 B 堆 C 队列 D 栈
- 在具有 2n 个结点的完全二叉树中,叶子结点个数为(A) A n B n 1 C n-1 D n/2
- 一个具有767个节点的完全二叉树,其叶子节点个数为(B) A 383 B 384 C 385 D 386
- .一棵完全二叉树的节点数位为531个,那么这棵树的高度为(B) A 11 B 10 C 8 D 12