树的概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因 为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
有一个特殊的结点,称为根结点,根结点没有前驱结点
除根结点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
因此,树是递归定义(套娃,大问题分解成多个小问题,直到不用分解)的。任何一棵树都是有[根+N棵子树(N>=0)]构成,例如:下图A的子树就由B...,C...D...三棵子树构成。B的子树有E...,F。当只有根没有子树的时候就不会再分了。
注意:树形结构中,子树之间不能有交集,否则就不是树形结构
树的相关概念
结点的度:一个结点含有的子树的个数称为该结点的度; 如上图:A的为6
叶结点或终端结点:度为0的结点称为叶结点; 如上图:B、C、H、I...等结点为叶结点
非终端结点或分支结点:度不为0的结点; 如上图:D、E、F、G...等结点为分支结点
双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点; 如上图:A是B的父结点
孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点; 如上图:B是A的孩子结点
兄弟结点:具有相同父结点的结点互称为兄弟结点; 如上图:B、C是兄弟结点
树的度:一棵树中,最大的结点的度称为树的度; 如上图:树的度为6
结点的层次:从根开始定义起,根为第1层(也可以为第0层,没有特别情况以1为准),根的子结点为第2层,以此类推;
树的高度或深度:树中结点的最大层次; 如上图:树的高度为4
堂兄弟结点:双亲在同一层的结点互为堂兄弟;如上图:H、I互为兄弟结点
结点的祖先:从根到该结点所经分支上的所有结点;如上图:A是所有结点的祖先
子孙:以某结点为根的子树中任一结点都称为该结点的子孙。如上图:所有结点都是A的子孙
森林:由m(m>0)棵互不相交的树的集合称为森林;(多棵树)
二叉树的性质
1. 若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有 2^(i-1)个结点.
2. 若规定根结点的层数为1,则深度为h的二叉树的最大结点数是 2^h-1.
3. 对任何一棵二叉树, 如果度为0其叶结点个数为n0 , 度为2的分支结点个数为n1 ,则有 n0=n1 +1
4. 若规定根结点的层数为1,具有n个结点的满二叉树的深度,h= log2(n+1).
5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从0开始编号,则对 于序号为i的结点有:
1. 若i>0,i位置结点的双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点
2. 若2i+1,左孩子序号:2i+1,2i+1>=n否则无左孩子
3. 若2i+2,右孩子序号:2i+2, 2i+2>=n否则无右孩子
树结构体的创建
左孩子右兄弟法
struct treenode
{
int val;
struct treenode* leftchild;
struct treenode* rightbrother;
};
这个方法就是,A右边没有兄弟,所以他的btother没有用到,然后将A的child指向下面左边的B,然后B的brother指针指向右边的兄弟C,将B的child指向下面最左边的D,然后D的brether指向右边的E,再将E的brother指向右边的兄弟F,总体逻辑就是这样。
无论一个父亲结点有多少个结点,child始终指向从左边开始的第一个孩子。
二叉树
树的度最大被限定为2。
特殊的二叉树:
1.满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是2^k - 1,则它就是满二叉树。假设这棵树有N个结点,那么它的层数为log(N+1);
2. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K 的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树,也就是前k-1层都是满的,最后一层不满,且最后一层从左到右都是连续的,那它就是完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
二叉树的存储
1.用数组存储二叉树
当用数组存储完全二叉树和满二叉树时,可以用下标表示父子关系。
假设父亲在数组中的位置为:i,那么左孩子在数组中的下标为:2*i+1,右孩子在数组中的下标为2*i+2。
假设孩子在数组中的下标为j,那么父亲在数组中的下标为(j-1)/2。
非完全二叉树可以用数组来存储,但是不合适,会造成很多空间浪费,数组存储带有局限性。
2.链式存储二叉树
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是 链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所 在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面课程 学到高阶数据结构如红黑树等会用到三叉链。
二叉树的遍历
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的结点进行相应的操作,并且每个结点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
前序:根,左子树,右子树
中序:左子树,根,右子树
后序:左子树,右子树,根
层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根结点所在层数为1,层序遍历就是从所在二叉树的根结点出发,首先访问第一层的树根结点,然后从左到右访问第2层 上的结点,接着是第三层的结点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
二叉树的实现
二叉树的创建
typedef struct treenode
{
elem val;
struct treenode* left;
struct treenode* right;
}tree;
二叉树最大的度为2,所以我们在这里只需要创建两个指针就可以保存整棵二叉树。
二叉树结点的创建
tree* BuyNode(elem x)
{
tree* new = (tree*)malloc(sizeof(tree));
if (new == NULL)
{
perror("malloc");
return 0;
}
new->val = x;
new->left = new->right = NULL;
return new;
}
在创建结点时,我们先用malloc创建一块空间,随后赋值,再将它的左右两个指针置空就行。
二叉树的遍历
1.递归前序遍历
void preorder(tree* node)//递归前序遍历
{
if (node == NULL)
{
printf("N ");
return;//return在递归中不会全部结束,会回到上一层中
}
printf("%d", node->val);
preorder(node->left);
preorder(node->right);
}
前序遍历图解
这里我们使用递归对二叉树实现前序遍历,这里的N表示二叉树的结点为空,二叉树的遍历同时也会遍历二叉树中的NULL结点。这里我们首先访问二叉树的根节点,然后访问二叉树的左子树,由于前序遍历是先根节点,所以在访问左子树是会先访问左子树的根,注意不要和中序,后序混淆。访问左子树到NULL结点为止,在递归函数中,return并不会退出函数,而是返回调用函数中。
2.递归中序遍历
void inorder(tree* node)//中序遍历
{
if (node == NULL)
{
printf("N ");
return;
}
inorder(node->left);
printf("%d", node->val);
inorder(node->right);
}
3.递归后序遍历
void traorder(tree* ps)//后序遍历
{
if (ps == NULL)
{
printf("N ");
return;
}
traorder(ps->left);
traorder(ps->right);
printf("%d", ps->val);
}
中序遍历及后序遍历的整体思路和前序遍历差不多,只需要改变递归函数的位置即可。
4.层序遍历
void levelorder(tree* ps)//层序遍历
{
que duilie;
ini(&duilie);
if (ps)
{
push(&duilie, ps);
}
while (!emp(&duilie))
{
tree* new = front(&duilie);
pop(&duilie);
printf("%d", new->val);
if (new->left)
{
push(&duilie, new->left);
}
if (new->right)
{
push(&duilie, new->right);
}
}
qdel(&duilie);
}
层序遍历较为复杂,在实现层序遍历的过程中,我们需要使用队列的函数:
1.que 队列结构体,2.front 取头结点,3.push 插入结点,4.pop 删除,5.qdel 销毁 ,6.ini 初始化
首先我们创建一个队列结构体,并将它初始化,如果树不为空,那就将根节点放进队列中,
随后我们使用while循环,循环结束的条件时队列为空,在循环中我们创建一个树来接收队头结点,然后将队头结点给删除,然后打印队头结点值,这里可能有同学会产生疑问,Pop的时候new为什么不会变为野指针,那是因为当你调用pop(&duilie)
时,你只是从队列中移除了对这个节点的引用,但new
指针仍然指向那个节点的内存地址。
随后将根节点的左右子树入队列,当有结点出队列时,它的左右子树同时也会入队列,保证了层序遍历能够打印二叉树全部内容。
最后将队列进行销毁避免内存泄漏。
二叉树结点个数
int size(tree* ps)//结点个数
{
if (ps == NULL)
{
return 0;
}
return size(ps->left) + size(ps->right)+1;
}
二叉树叶子结点个数
int leafsize(tree* ps)//叶子结点个数
{
if (ps==NULL)
{
return 0;
}
if (ps->left == NULL && ps->right == NULL)
{
return 1;
}
return leafsize(ps->left) + leafsize(ps->right);
}
叶子结点就是度为0的结点,所以存在左右子树为NULL的结点返回1即可
树的高度
int hight(tree* ps)//树的高度
{
if (ps == NULL)
{
return 0;
}
int left = hight(ps->left);
int right = hight(ps->right);
return left > right ? left + 1 : right + 1;
}
这里我们使用left和right将左右两颗子树的高度存起来,然后使用三元操作符返回最大的高度加上根节点的高度。
第k层数据个数
int ksize(tree* ps,int k)//第k层数据个数
{
if (ps == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return ksize(ps->left,k-1) + ksize(ps->right,k-1);
}
查找数据
tree* find(tree* ps, elem k)//查找数据
{
if (ps == NULL)
{
return NULL;
}
if (ps->val == k)
{
return ps;
}
tree* p1=find(ps->left, k);
if (p1)
{
return p1;
}
tree* p2=find(ps->right, k);
if (p2)
{
return p2;
}
return NULL;
}
这里创建p1和p2的目的是为了在找到数据时避免函数还在不断递归,return在递归中的作用是将数据返回上一次的调用之中。并不是结束运行 ,两个if用来判断有没有找到数据,在找到数据是就会不断的返回上一次的调用之中。
判断一个树是否为完全二叉树
bool BinaryTreeComplete(tree* ps)//判断是否为完全二叉树
{
que duilie;
ini(&duilie);
if (ps)
{
push(&duilie, ps);
}
while (!emp(&duilie))
{
tree* new = front(&duilie);
pop(&duilie);
if (new == NULL)
{
break;
}
push(&duilie, new->left);
push(&duilie, new->right);//不需要判断new的left和right是否为空,都录进去
}
while (!emp(&duilie))
{
tree* new = front(&duilie);
pop(&duilie);
if (new != NULL)
{
qdel(&duilie);
return false;
}
}
qdel(&duilie);
return true;
}
判断完全二叉树时也要调用队列函数。可参考层序遍历的介绍。
判断完全二叉树使用层序遍历即可,不过它遇到NULL时也入队列,当遇到空时,如果后面遇到非空就不是完全二叉树。
当第一个while
循环结束时,有两种情况:
-
如果这是一个完全二叉树,那么所有的节点都已经按照层序遍历的顺序入队了。当遇到第一个
NULL
节点时,它表示的是最后一层的末端,即之后不应该有任何子节点。因此,队列中剩余的都应该是NULL
节点。 -
如果这不是一个完全二叉树,那么在遇到第一个
NULL
节点之后,如果队列中还有非NULL
的节点,这意味着树的结构在之前的某个点已经不连续了,即在某个非叶子节点缺少了子节点。
二叉树的销毁
void treedel(tree* ps)//销毁
{
if (ps == NULL)
{
return;
}
treedel(ps->left);
treedel(ps->right);
free(ps);
}
只需递归销毁左右子树,最后再销毁根节点即可。
源码
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int elem;
typedef struct treenode
{
elem val;
struct treenode* left;
struct treenode* right;
}tree;
tree* BuyNode(elem x)
{
tree* new = (tree*)malloc(sizeof(tree));
if (new == NULL)
{
perror("malloc");
return 0;
}
new->val = x;
new->left = new->right = NULL;
return new;
}
tree* CreatBinaryTree()
{
tree* node1 = BuyNode(1);
tree* node2 = BuyNode(2);
tree* node3 = BuyNode(3);
tree* node4 = BuyNode(4);
tree* node5 = BuyNode(5);
tree* node6 = BuyNode(6);
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;
return node1;
}
void preorder(tree* node)//递归前序遍历
{
if (node == NULL)
{
printf("N ");
return;//return在递归中不会全部结束,会回到上一层中
}
printf("%d", node->val);
preorder(node->left);
preorder(node->right);
}
void inorder(tree* node)//中序遍历
{
if (node == NULL)
{
printf("N ");
return;
}
inorder(node->left);
printf("%d", node->val);
inorder(node->right);
}
void traorder(tree* ps)//后序遍历
{
if (ps == NULL)
{
printf("N ");
return;
}
traorder(ps->left);
traorder(ps->right);
printf("%d", ps->val);
}
int size(tree* ps)//结点个数
{
if (ps == NULL)
{
return 0;
}
return size(ps->left) + size(ps->right)+1;
}
int leafsize(tree* ps)//叶子结点个数
{
if (ps==NULL)
{
return 0;
}
if (ps->left == NULL && ps->right == NULL)
{
return 1;
}
return leafsize(ps->left) + leafsize(ps->right);
}
int hight(tree* ps)//树的高度
{
if (ps == NULL)
{
return 0;
}
int left = hight(ps->left);
int right = hight(ps->right);
return left > right ? left + 1 : right + 1;
}
int ksize(tree* ps,int k)//第k层数据个数
{
if (ps == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return ksize(ps->left,k-1) + ksize(ps->right,k-1);
}
tree* find(tree* ps, elem k)//查找数据
{
if (ps == NULL)
{
return NULL;
}
if (ps->val == k)
{
return ps;
}
tree* p1=find(ps->left, k);
if (p1)
{
return p1;
}
tree* p2=find(ps->right, k);
if (p2)
{
return p2;
}
return NULL;
}
//0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
typedef tree* qelem;
typedef struct node//使用单链表实现刚刚好,不需要尾删
{
struct node* next;
qelem val;
}nn;
typedef struct que
{
nn* ph;//头节点
nn* pt;//尾节点
int size;
}que;
void ini(que* ps)//初始化
{
assert(ps);
ps->ph = ps->pt = NULL;
ps->size = 0;
}
void qdel(que* ps)//删除
{
assert(ps);
nn* new = ps->ph;
while (new)
{
nn* del = new->next;
free(new);
new = del;
}
ps->ph = ps->pt = NULL;
ps->size = 0;
}
void push(que* ps, qelem x)//队尾插入
{
assert(ps);
nn* new = (nn*)malloc(sizeof(nn));
if (new == NULL)
{
perror("push new");
return;
}
new->next = NULL;
new->val = x;
if (ps->pt == NULL)//如果队列没有结点
{
ps->ph = ps->pt = new;
}
else//尾插
{
ps->pt->next = new;
ps->pt = new;
}
ps->size++;
}
void pop(que* ps)//队头删除
{
assert(ps);
if (ps->ph->next == NULL)//只有一个链表
{
free(ps->ph);
ps->ph = ps->pt = NULL;
}
else//多个结点
{
nn* new = ps->ph->next;
free(ps->ph);
ps->ph = new;
}
ps->size--;
}
qelem front(que* ps)//取头结点
{
assert(ps);
assert(ps->ph);
return ps->ph->val;
}
qelem back(que* ps)//取尾结点
{
assert(ps);
assert(ps->pt);
return ps->pt->val;
}
//int size(que* ps)//求数据个数
//{
// assert(ps);
// return ps->size;
//}
bool emp(que* ps)//判空
{
assert(ps);
return ps->size == 0;
}
//000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
void levelorder(tree* ps)//层序遍历
{
que duilie;
ini(&duilie);
if (ps)
{
push(&duilie, ps);
}
while (!emp(&duilie))
{
tree* new = front(&duilie);
pop(&duilie);
printf("%d", new->val);
if (new->left)
{
push(&duilie, new->left);
}
if (new->right)
{
push(&duilie, new->right);
}
}
qdel(&duilie);
}
bool BinaryTreeComplete(tree* ps)//判断是否为完全二叉树
{
que duilie;
ini(&duilie);
if (ps)
{
push(&duilie, ps);
}
while (!emp(&duilie))
{
tree* new = front(&duilie);
pop(&duilie);
if (new == NULL)
{
break;
}
push(&duilie, new->left);
push(&duilie, new->right);//不需要判断new的left和right是否为空,都录进去
}
while (!emp(&duilie))
{
tree* new = front(&duilie);
pop(&duilie);
if (new != NULL)
{
qdel(&duilie);
return false;
}//此时就将
}
qdel(&duilie);
return true;
}
void treedel(tree* ps)//销毁
{
if (ps == NULL)
{
return;
}
treedel(ps->left);
treedel(ps->right);
free(ps);
}