二叉树的链式结构及实现
普通二叉树的增删查改没有什么价值,因为用来存放数据太复杂了。它的价值在于
- 在他的基础之上增加一些性质,才有意义,比如
- 搜索二叉树,左子树的值都比父节点小,右子树的值逗比父节点大。最多可以查找高度次。->平衡搜索二叉树、AVLTree、红黑树 -> B树
- huffman tree
不关注增删查改,关注递归遍历结构。
学习这个是为了后面学习更有用的树打基础,同时很多oj题目结构普通二叉树。
前序、中序以及后序遍历
学习二叉树的结构,最简单的方式就是遍历。二叉树遍历 (Traversal)时按照某种待定的规则,依次堆二叉树中的节点进行相应的操作,并且每个节点只操作一次。
二叉树可以分为根、左子树和右子树。走到空就中止。
二叉树的遍历分为:前序/中序/后序的递归结构遍历。
前序遍历(preorder Traversal) – 访问跟节点的操作发生在遍历左右子树之前。
根 -> 左子树 -> 右子树
也就是说,遍历顺序为: A->B->D ->NULL(left) -> NULL(right) -> NULL(B下面) -> C -> E -> NULL(left) ->NULL(right)->F->NULL(left)->NULL(right)
前期学习应该把空写出来,表示清楚的遍历顺序,但是在打印的时候由于是空,所以不会打印出来。
中序遍历(Inorder Traversal) – 访问根节点的操作发生在遍历其左右子树中间。
左子树 -> 根 -> 右子树
也就是访问A先访问A的左子树B,访问B先访问B的左子树D,访问D先访问D的左子树NULL,所以是:
NULL(left) -> D ->NULL(right) -> B -> NULL(right) -> A -> NULL(left) -> E -> NULL(right) -> C -> NULL(left) -> F -> NULL(right)
后序遍历(postorder Traversal)
左子树 -> 右子树 -> 根
NULL(left) -> NULL(right) -> D -> NULL(right) -> B ->NULL(left) -> NULL( right) -> E -> NULL(left) -> NULL(right) -> F -> C -> A
实现遍历
分置思想,大事化小小事化了
前序遍历
void PreOrder(BTNode *root)
{
if(root == NULL)
{
printf("NULL ");
return;
}
printf("%c ",root->data);
PreOrder(root->left);
PreOrder(root->right);
}
本质就是遇到空或者调用完成后就会回到调用的地方,然后继续进行递归。
中序遍历
void InOrder(BTNode *root)
{
if(!root)
{
printf("NULL ");
return;
}
PreOrder(root->left);
printf("%c ", root->data);
PreOrder(root->right);
}
遇到了不为空的节点,先走这个节点的左子树,左子树不为空,再找左子树,直到为空,打印,然后往回走。只是因为顺序不同,打印的时机不同。
后序遍历
void PostOrder(BTNode *root)
{
if (!root)
{
printf("NULL ");
return;
}
PreOrder(root->left);
PreOrder(root->right);
printf("%c ", root->data);
}
二叉树oj题目
求二叉树节点的个数
如果我们用一遍遍历。在此我们用前序遍历写一下。
错误:
int BinaryTreeSize(BTNode *root)
{
int num = 0;
if (!root)
{
printf("NULL ");
return;
}
num++;//这样是不对的。结合栈帧的知识,每次递归都是建立栈帧,出函数后会销毁
//用静态的和用全局的都会出现多次调用存在问题。(不会销毁)
BinaryTreeSize(root->left);
BinaryTreeSize(root->right);
}
我们可以在main函数里创建一个变量,然后传变量的地址进去就可以了。
void BinaryTreeSize(BTNode *root, int *pn)
{
if (!root)
{
return;
}
++(*pn);
BinaryTreeSize(root->left, pn);
BinaryTreeSize(root->right, pn);
}
调用多次也不会出现问题。还有一种方式,返回值可以帮助我们解决:
int BinaryTreeSize(BTNode *root)
{
return root == NULL ? 0 : BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
求叶子节点的个数
//求叶子节点的个数
int BinaryTreeLeafSize(BTNode* root)
{
if(!root)
return 0;
if(root->left == NULL && root->right == NULL)
return 1;
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
求第k层的节点个数
求树的第四层,可以求它的左子树的第三层加上右子树的第三层。也就是:
BinaryTreeLevelKSize(root,k) = BinaryTreeLevelKSize(root->left,k-1) + BinaryTreeLevelKSize(root->right,k-1);
所以我们又可以进行递归了。
int BinaryTreeLevelKSize(BTNode* root,int k)
{
assert(k > 0);
if(!root)
return 0;
if(k==1)
return 1;
//若root不为空,k也不等于1,那么说明第k层还在root的子树里面
//转换成求左右子树的第k-1层的节点数量
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
二叉树的深度/高度
我们一般默认空树的高度是0。
都采用分置的思想,就是递归的思想,我们想求当前树的高度,就是去求左子树高度和右子树高度里取较大值然后加1。
int BinaryTreeDepth(BTNode *root)
{
if(!root)
return 0;
//求较大值加1
return 1 + (BinaryTreeDepth(root->left) > BinaryTreeDepth(root->right) ? BinaryTreeDepth(root->left) : BinaryTreeDepth(root->right));
}
但是这并非最优解(严谨说会造成严重的浪费),如果深度较大会导致溢出。因为每次都需要重复计算左子树和右子树的深度。我们如果先保存一下,就会防止浪费。
int BinaryTreeDepth(BTNode *root)
{
//空树给0
if(!root)
return 0;
int leftDepth = BinaryTreeDepth(root->left);
int rightDepth = BinaryTreeDepth(root->right);
//求较大值加1
return 1 + (leftDepth > rightDepth ? leftDepth: rightDepth);
}
二叉树查找值为x的节点
在二叉树里面都必须要先判断root的空情况,如果不空, 再去进行判断。如果root->data存在而且不等于x,我们需要去左子树和右子树里面去找,如果左子树返回值不为空,那么说明找到了,直接返回这个左子树里面查找的返回值就可以。然后再进行右子树的查找。如果都没找到,返回空即可。
BTNode* BinaryTreeFind(BTNode* root,BTDataType x)
{
if(!root)
return NULL;
if (root->data == x)
return root;
BTNode *leftNode = BinaryTreeFind(root->left, x);
if (leftNode)
return leftNode;
BTNode *rightNode = BinaryTreeFind(root->right, x);
if(rightNode)
return rightNode;
return NULL;
}
单值二叉树
如果二叉树的每个节点都具有相同的值,那么该二叉树就是单值二叉树。只有给定的树是单值二叉树时,才返回true,否则false。
bool isUnivalTree(BTNode* root)
{
if(!root)
return true;
if(root->left && root->left->data != root->data)
return false;
if(root->rigth && root->right->data != root->data)
return false;
return isUnivalTree(root->left) && isUnivalTree(root->right);
}
二叉树前序遍历并且把遍历过的值存储到数组中
数组a是我们malloc出来的,但是并不知道malloc多少内存,所以我们先写一个函数来计算树的节点个数。
int TreeSize(BTNode* root)
{
return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}
要求前序遍历并储存值,我们可以先写一个前序遍历。其中pi是i的地址,i是数组的下标,用来将值储存在数组中。
int* _preorderTraversal(BTNode* root,int* a,int*pi)
{
if(!root)
{
return;
}
a[(*pi)++] = root->data;
_preorderTraversal(root->left,a,pi);
_preorderTraversal(root->right, a,pi);
}
接下来我们只需要在要求的函数内部调用这个函数即可。注意一点,returnSize在我们的编写代码过程中并未用到,这是一个输出型参数,是因为我们在写OJ题目中,提交代码之后,服务器并不知道到底return的数组的size是多少,我们应该告诉服务器是多少。也就是在结束代码之前将returnSize的值修改成正确值。
int* preorderTraversal(BTNode* root,int* returnSize)
{
int size = TreeSize(root);
int *a = (int *)malloc(sizeof(int) * size);
//应该检查,oj题目可以不用。
int i = 0;
_preorderTraversal(root, a, &i);
*returnSize = size;
return a;
}
在这里,很多小伙伴会犯这样的错误,写成如下的形式
int* _preorderTraversal(BTNode* root,int* a,int i)
{
if(!root)
{
return;
}
a[(i)++] = root->data;
_preorderTraversal(root->left,a,i);
_preorderTraversal(root->right, a,i);
}
注意,这种情况下,结合我们之前学习的栈帧的知识,可以知道,每次递归都会创建新的函数栈帧,i是在新的函数栈帧中对于上一层递归中i的临时拷贝,出栈就会销毁。所以这个i并不能传递出本次递归,也就是对i的++是无效的。我们在这里应该传址调用,才能准确修改i的值。
相同的树
给两个二叉树的根节点p和q,编写一个函数来检查这两棵树是否相同。如果两棵二叉树的每个节点都相同,则视为相同。要求:
- 形状相同
- 值相同
如果解题,我们一般先考虑根相等与否,如果根相等,再去比较左子树相不相等,再去比较右子树相不相等。
bool isSameTree(BTNode* p,BTNode*q)
{
if(!p && !q)
return true;
if(!p || !q)
return false;
if (p->data != q->data)
return false;
//&&是走完左边如果相等再走右边,如果左边值为0,则右边根本就不需要判断了。
return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}
判断他的时间复杂度。考虑他的思想,每个数都比较一遍,这就是经典的O(N)。
对称二叉树/镜像二叉树
给定一个二叉树,检查它是否是镜像对称的。
可以这样做:先比较根,根都不相等那就必然不镜像对称。然后拿左的左树和右的右树比,拿左的右树根右的左树去比。
所以我们可以考虑一个子函数,用来比较根的左树和右树的镜像情况。先考虑好,传过去两个节点有以下几种情况:
- 两个节点都为空,符合镜像关系
- 两个节点有一个为空,不符合镜像关系
- 两个节点都不为空,而且不等,不符合镜像关系
- 两个节点都不为空,而且相等,符合镜像关系,我们需要继续递归。
bool _isSymmertric(BTNode *root1,BTNode* root2)
{
if(!root1 && !root2)
return true;
if(!root1 || !root2)
return false;
if(root1->data != root2->data)
return false;
return _isSymmertric(root1->left, root2->right) && _isSymmertric(root1->right, root2->left);
}
写好子函数之后,主函数就好写了。
bool isSymmertric(BTNode *root)
{
if(!root)
return true;
return _isSymmertric(root->left, root->right);
}
另一棵树的子树
给你两棵二叉树root和subRoot。检验root中是否包含和subRoot具有相同结构和节点值的子树。如果存在,返回true;否则,返回false。
二叉树tree的一棵子树包括trree的某个节点和这个节点的所有后代节点。tree也可以看作它自身的一棵子树。
思路就是拿root的每一棵子树来比较subroot。如何拿每一棵子树?遍历
bool isSubTree(BTNode* root,BTNode* subRoot)
{
if(!root)
return false;
if(isSameTree(root,subRoot))
{
return ture;
}
return isSubTree(root->left, subRoot) || isSubTree(root->right, subRoot);
}
最好情况下的时间复杂度是O(N)查找一个就找到了,因为isSameTree需要遍历,为N。
最坏情况下的时间复杂度是O(N2),每个子树都比但是都最后没有通过。
层序遍历
把二叉树按照一层从左到右的顺序进行遍历。
思路就是用队列,先入根,出根的时候入根的左右孩子,左孩子出的时候入它的左右孩子。直到队列为空停止,遍历结束。
void BinaryTreeLevelOrder(BTNode *root)
{
if (!root)
return;
Queue q;
QueueInit(&q);
//队列得放结构体指针才可以。否则找不到左右孩子
while (QueueEmpty(&q))
{
BTNode *front = QueueFront(&q);
QueuePop(&q);
printf("%d ", front->data);
if (front->left)
{
QueuePush(&q, front->left);
}
if (front->right)
{
QueuePush(&q, front->right);
}
}
printf("\n");
}
这样我们就写好了层序遍历。用层序遍历我们就可以很好的判断二叉树是否为完全二叉树了。因为完全二叉树是除最后一层外全满,最后一排可能不满而且必须连续。我们层序遍历来判断即可。
判断是否为完全二叉树
之前的层序遍历是为空就不进了,现在我们空也进去,空出来的时候判断队列是否为空,队列为空则是,队列不为空则说明不连续,非完全二叉树。
int BinaryTreeComplete(BTNode* root)
{
if (!root)
return;
Queue q;
QueueInit(&q);
//队列得放结构体指针才可以。否则找不到左右孩子
while (QueueEmpty(&q))
{
BTNode *front = QueueFront(&q);
QueuePop(&q);
if(!front)
{
break;
}
else
{
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
}
BTNode* front = QueueFront(&q);
// while (!front)
// {
// //
// QueuePop(&q);
// front = QueueFront(&q);
// }
// if (front)
// {QueueDestroy(&q);
// return 0;}
// return 1;
while(!QueueEmpty(&q))
{
BTNode *front = QueueFront(&q);
QueuePop(&q);
if(front)
{
QueueDestroy(&q);
return 0;
}
}
QueueDestroy(&q);
return 1;
}
二叉树遍历
编写一个横序,读入用户输入的一串先序遍历字符串,根据此字符串建立一个二叉树(以指针方式存储)。例如如下的先序遍历字符串:ABC##DE#G##F### 其中#表示的是空格,空格字符代表空树。建立起此二叉树以后,再对二叉树进行中序遍历,输出遍历结果。
IO型题目:
#include<stdio.h>
#include<stdlib.h>
struct TreeNode
{
struct TreeNode* left;
struct TreeNode* right;
char val;
}
strcut TreeNode* CreateTree(char* str, int* pi)
{
if(str[*pi]=='#')
{return NULL;
(*pi)++;}
struct TreeNode* root = malloc(sizeof(struct TreeNode));
root->val = str[(*pi)++];
root->left = CreateTree(str,pi);
root->right = CreateTree(str,pi);
}
void InOrder(struct TreeNode* root)
{
if(!root)
return;
InOrder(root->left);
printf("%c ",root->left);
InOrder(root->right);
}
int main()
{
char str[100];
scanf("%s",str);
//多行输入
//while(scanf("%s",str)!=EOF)
int i = 0;
struct TreeNode* root = CreateTree(str,&i);
InOrder(root);
return 0;
}
二叉树销毁
传二级指针可以直接置空,传一级指针也可以,只需要让调用者置空。就像用free一样。
void BinaryTreeDestroy(BTNode* root)
{
if(!root)
return;
BinaryTreeDestroy(root->left);
BinaryTreeDestroy(root->left);
free(root);
}