树和二叉树
树(tree)是一个有限集合,在任意一颗非空的树里面
1 有且仅有一个被称为根的根节点(root)
2 当节点的个数n > 1的时候,也就是这个树里面除了这个
根节点以外还有别的节点存在,其余的节点都可以看成是一个小的
树,除了这个根节点以外,其他的这些节点实际上也是一颗树的根
节点,而这些树我们称为子树
也就是每一个节点实际上都是一颗子树的根节点
树的节点包含一个数据元素(树存在的意义实际上就是为了保持数据的)
然后还有指向其他子树的关系(指针去实现的),也就是指向其他子树的指针
一个树节点:1 数据域 2指针域
为什么有链表之后还会出现树这种结构
1 链表是不利于查找
因为我们只能保存这个链表的头或者尾
你如果要找到中间只能遍历
2 但是我们的现实生活中很多时候都是查找优先的
这个时候我们就可以将链表变成一个利于查找的东西不就可以了
这个时候我们就需要对链表进行改革!!!
让它好像二分法,树的这种结构就是为了实现
链表的二分法而出来的
在树里面每一个节点都会有一些分支
这些节点的分之数量称为这个节点的度(degree)
节点的度也可能是为0的,为0的节点我们称为终端节点/叶子节点(leaf)
树里面的有一个概念叫层次,根为第一层,层的孩子为第二层…,树的最大层次为这颗树的高度
二叉树:树的一种形态
节点的度不会超过2,节点下面的每一个节点
我们都称为孩子节点
二叉树至多有2个孩子
被称为左孩子与右孩子
左右孩子的顺序是不能颠倒的
一般:左孩子比右孩子要小
他们的大小区别为
左孩子 < 根 < 右孩子
二叉树的五种形态
1 空树
2 只有一个节点
3 只有一个左孩子
4 只有一个右孩子
5 儿女双全
二叉树的基本性质:
1 二叉树的第i层上面最多有 2 ^ ( i - 1)个节点
2 深度(高度)为k的二叉树最多有 2 ^ k - 1个节点
3 对任何一颗二叉树来说,度为0的节点个数为n0
度为2的节点个数n2
n0 = n2 + 1
满二叉树:深度为k的二叉树有 2 ^ k - 1个节点
这颗树就被称为满二叉树
完全二叉树:
1 去除最后一层是为满二叉树
2 最后一层所有的节点全部都尽左排
左边不能添加任何一个节点了
4 如果对n个节点的完全二叉树从上到下,从左到右
从1开始进行编号,如果有一个节点的编号为i
则:
它的父节点的编号为 i / 2;
它的左子节点(如果有)的编号就是 2 * i;
它的右子节点(如果有)的编号就是 2 * i + 1;
5 具有n个节点的完全二叉树的深度 k
log2n向下取整 + 1
2 ^ (k - 1) - 1 < n <= 2 ^ k - 1
由于都是整数
2 ^ (k - 1) <= n < 2 ^ k
-> k = log2n向下取整 + 1
链式存储
链式结构有指针域,我只需要弄出两个指针就可以实现左右孩子的区分,因此更多的时候我们是采用链式结构去存储二叉树的。
代码实现:
typedef char MyTreeDataType;
typedef struct TreeNode
{
int n;
MyTreeDataType data;
struct TreeNode * lchild;
struct TreeNode * rchild;
}TreeNode;
创建树
//用data生产一个新的节点 单独的孤立的节点
TreeNode * CreateTreeNode(const MyTreeDataType data)
{
TreeNode * pnew = malloc(sizeof(*pnew));
//memset(pnew,0,sizeof(*pnew));
pnew ->lchild = pnew ->rchild = NULL;
pnew ->data = data;
return pnew;
}
//将数据做成节点添加到我们的树里面去
TreeNode *AddTreeNodeForData(TreeNode *root,const MyTreeDataType data)
{
root = AddTreeNodeForNode(root,CreateTreeNode(data));
return root;
}
//直接将节点添加到我们的树里面去 非递归形式添加
TreeNode *AddTreeNodeForNode(TreeNode *root,TreeNode * pnew)
{
//这棵树是否存在是优先考虑的
if(!root)
return pnew;
//找添加位置
TreeNode * p = root;
while(p)
{
if(pnew ->data < p ->data)//小的在左边
{
if(p ->lchild == NULL)
{
p ->lchild = pnew;
return root;
}
p = p ->lchild;
}
else if(pnew ->data > p ->data)//大的在右边
{
if(p ->rchild == NULL)
{
p ->rchild = pnew;
return root;
}
p = p ->rchild;
}
else //相等不能添加 我们可以在节点里面添加一个成员去表示这个节点出现了几次
{
free(pnew);
return root;
}
}
return root;
}
//数据来源 解析str字符串
//返回树的根节点
TreeNode * CreateTree(const char * str)
{
TreeNode * root = NULL;
while(*str)
{
root = AddTreeNodeForData(root,*str);
str++;
}
return root;
}
销毁树
这个树里面的节点都是malloc出来,请不用的时候将其释放
//实现销毁树的函数
static void DestoryTreeTest(TreeNode *root)
{
if(!root)
return;
//实际上就是一个后序遍历
//左右子树全部释放完毕之后才能释放根节点
//左右子树的情况跟我现在释放这棵树是一模一样的
DestoryTreeShixian(root ->lchild);
DestoryTreeShixian(root ->rchild);
root ->lchild = root ->rchild = NULL;//孤立
free(root);
}
//销毁这棵树
//传二级指针 调用完成会将调用它的那个根置空
void DestoryTree(TreeNode **root)
{
printf("开始释放这棵树.....");
if(!root)
{
printf("二级指针都传了一个空,释放了个鸡毛\n");
return;
}
DestoryTreeTest(*root);
*root = NULL;
printf("释放结束!!!\n");
}
前序、中序、后序递归遍历树
添加节点是用一个遍历指针,根据大的找右边小的找左边的原则去添加节点的
而这个做法跟递归不谋而合
因此将我的循环改递归试一试
1 如果是小的就以相同的规则添加到左边去
2 如果是大的就以相同的规则添加到右边去
3 如果相同就停止添加
4 发现NULL的位置就是你添加的位置
依照小的在左边大的在右边这种规则去建立的二叉树我们叫 — 排序二叉树
static void PreOrderShixian(TreeNode *root)
{
//如果你的root不存在 不用访问
if(!root)
return;
//1 打印根节点
printf("%c ",root ->data);
//2 以相同的规则访问左子树
PreOrderShixian(root ->lchild);
//3 以相同的规则访问右子树
PreOrderShixian(root ->rchild);
}
//递归先序
void PreOrder(TreeNode *root)
{
printf("递归先序访问:");
PreOrderShixian(root);
printf("\n");
}
static void MidOrderShixian(TreeNode *root)
{
//如果你的root不存在 不用访问
if(!root)
return;
//1 以相同的规则访问左子树
MidOrderShixian(root ->lchild);
//2 打印根节点
printf("%c ",root ->data);
//3 以相同的规则访问右子树
MidOrderShixian(root ->rchild);
}
//递归中序
void MidOrder(TreeNode *root)
{
printf("递归中序访问:");
MidOrderShixian(root);
printf("\n");
}
static void PostOrderShixian(TreeNode *root)
{
//如果你的root不存在 不用访问
if(!root)
return;
//1 以相同的规则访问左子树
PreOrderShixian(root ->lchild);
//2 以相同的规则访问右子树
PreOrderShixian(root ->rchild);
//3 打印根节点
printf("%c ",root ->data);
}
//递归后序
void PreOrder(TreeNode *root)
{
printf("递归后序访问:");
PostOrderShixian(root);
printf("\n");
}