考研时个人整理的,参考王道、严蔚敏等数据结构。
上一章——栈和队列:https://blog.csdn.net/mooe1011/article/details/88089325
1. 树的概念
- 结点的孩子个数为度,最大度数称为树的度
- 第i层最多有结点数:二叉树为2i-1;m叉树有mi-1个结点(i>=1)
- 高度h最多有结点数:二叉树有2h-1(即满二叉树);m叉树有(mh-1)/(m-1)
- 平衡二叉树第一层N1=1,第二层N2=2,h层Nh=Nh-1+Nh-2+1
- n个结点最小高度:(完全)二叉树为⌊log2n⌋+1或者为⌈log2(n+1)⌉(满二叉树为log2(n+1));m叉树为⌈logm(n(m-1)+1)⌉
- 总结点数=n0+n1+n2+n3+…=度总数(总分支数)+1=1+1* n1+2* n2+3* n3…
- 叶子结点数:二叉树n0=1+n2;m叉树n0=1+n2+2* n3+…+(m-1)nm
- n个结点有 ( 2 n ) ! ( n + 1 ) ! n ! \dfrac{(2n)!}{(n+1)!n!} (n+1)!n!(2n)!种不同的二叉树(前序入栈,中序出栈)
2. 二叉树
- 二叉树可为空,度为2的树至少有3个结点;并且,度为2的有序树某个结点如果只有一个孩子结点,无须分左右次序
- 完全二叉树
- 若i<=⌊n/2⌋,则结点i为分支结点,左孩子编号为2i,如果i<=n/2-1,则右孩子编号为2i+1;否则为叶子结点
- 非根结点的双亲为⌊i/2⌋,i为偶数时,它是双亲的左孩子;i为奇数时,双亲为(i-1)/2,它是右孩子
- 若n为奇数,则分支结点都有左右孩子;为偶数时,则分支结点编号最大的结点为n/2,只有左孩子
- 叶子结点只能在最大两层上出现,排列该层最左边的位置上,如果有度为1的结点,只有一个,并且是左孩子
- 所在层次为⌊log2i⌋+1
- 一般来说根结点i为1,注意有些为0
- 二叉排序树,关键字:左子树<根结点<右子树;插入需比较次数为log2n
- 平衡二叉树,树上任一结点的左子树和右子树的深度差不超过1
- 存储结构——顺序和链式
- 二叉链表叶结点有2个空指针,度为1的有1个空指针,总共2n0+n1;总结点数=n0+n1+n2,又有n2=n0-1,所以n个结点含有n+1个空链域
- 结构
typedef struct BiTNode{
int data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
3. 二叉树的遍历和线索二叉树
- 先序PreOrder
- 先序和后序序列正好相反,即根左右和根右左相等(后序相反),则每层只有一个结点,其高度等于结点数
中序后序类似,visit(T)位置不同
void PreOrder(BiTree T){
if (T!=NULL){
visit(T); 或者 printf("%c",T->data); //%c即为按字符输出
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
- 中序InOrder
- 最后一个结点如果是叶子结点,则一定是右孩子,并且一定是先序遍历的最后一个结点;如果是非叶子结点,则一定有左子树
- 某结点有左孩子,则前驱是左子树最右下的结点;有右孩子,则后继是右子树最左下结点
- abcdMefgh,M为根结点,则a、e是最左下结点,d、h是最右下的结点
- 后序PostOrder
- 用后序可以记录路径,从祖先结点开始,到访问该结点时,栈顶到栈底正好是该结点从双亲开始直到祖先的所有结点
- 结点是双亲的右孩子或者唯一左孩子时,后继为双亲
- 时间复杂度均为O(n)
- 中序/后序非递归
void InOrder(BiTree T){
InitStack(S);
BiTree p=T;
while(p||!IsEmpty(s)){
if(p){
Push(S,p);
p=p->lchild;
} else {
Pop(S,p);
visit(p-data);
p=p->rchild;
}
}
}
void PostOrder(BiTree T){
InitStack(S);
BiTree p=T;
r=NULL; //记录最近访问过的结点
while(p||!IsEmpty(S)){
if(p){
push(S,p);
p=p->lchild;
} else { //遍历完左子树
GetTop(S,p);
if(p->rchild&&p->rchild!=r){ //右子树未访问过
p=p->rchild;
push(S,p);
p=p->lchild;
} else {
pop(S,p);
visit(p->data);
r=p; //记录访问过的结点
p=NULL;
}
}
}
}
- 层次遍历
void LevelOrder(BiTree T){
InitQueue(Q);
BiTree p;
EnQueue(Q,T); //根结点入队
while(!IsEmpty(Q)){
DeQueue(Q,p);
visit(p);
if(p->lchild!=NULL)
EnQueue(Q,p->lchild);
if(p->rchild!=NULL)
ENQueue(Q,p->rchild);
}
}
- 线索二叉树
- ltag 为0表示lchild指示左孩子; 为1指示前驱
- rtag 为0表示rchild指示右孩子; 为1指示后继
- 若左子树为空,先序线索化的空链域有2个(根结点左标示域和最后一个叶结点无后继)
- 先序查找后继简单,后序查找前驱简单,仅叶子结点指示前驱、后继,其他结点都需要知道双亲结点
- 线索二叉树是链表结构,是一种物理结构
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag;
}ThreadNode,*ThreadTree;
- 线索二叉树的构造(中序)
void InThread(ThreadTree &p,ThreadTree &pre){
if(p!=NULL){
InThread(p->lchild,pre);
if(p->lchild==NULL){
p->lchild=pre;
p->ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL){
pre->rchild=p;
pre->rtag=1;
}
pre=p;
InThread(p->rchild,pre);
}
}
void CreatInThread(ThreadTree T){
ThreadTree pre=NULL;
if(T!=NULL){
InThread(T,pre);
pre->rchild=NULL;
pre->rtag=1;
}
}
- 线索二叉树的遍历
求中序第一个结点
ThreadNode *Firstnode(ThreadNode *p){
while(p->ltag==0) p=p->lchild;
return p;
}
求下一个结点
ThreadNode *Nextnode(ThreadNode *p){
if(p->rtag==0)return Firstnode(p->rchild); //递归求该点右子树最左下的结点
else return p->rchild; //这个rchild是后继结点
}
void Inorder(ThreadNode *T){
for(ThreadNode *p=Firstnode(T);p!=NULL;p=Nextnode(p))
visit(p);
}
4. 树和森林
- 存储结构
- 双亲表示法:求孩子结点时需要遍历整个结构
#define Max_Tree_Size 100
typedef struct{
int data;
int parent;
}PTNode;
typedef struct{
PTNode nodes [Max_Tree_Size];
int n;
}PTree;
- 孩子双亲表示法
typedef struct CSNode{
int data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
- 树、森林转换二叉树
- 树转换规则:每个结点左指针指向第一个孩子结点,右指针指向相邻兄弟结点(左孩子右兄弟),由于根没有兄弟,由树转换成二叉树没有右子树
- 森林转换规则:森林每棵树都转换成二叉树,第一棵树作为根结点,第二棵树转换为二叉树的右子树,第三棵为右子树的右子树,以此类推
- 森林转换成二叉树最后一棵树的右指针域为空,每个非叶子结点的最后一个孩子的右指针域也为空
- 二叉树转换森林:根及其左子树作为第一棵树的二叉树形式,右子树作为第二棵二叉树,直到产生最后一棵没有右子树的二叉树为止,二叉树转换成树或森林是唯一的
- 树的先根遍历与二叉树的先序遍历相同,后根遍历与二叉树的中序遍历相同,因此一棵树的先根遍历和后根遍历能唯一确定一棵树
5. 树与二叉树的应用
1. 二叉排列树(二叉查找树)
- 左子树结点值<根结点值<右子树结点值,对其中序遍历,可得到递增的有序序列
- 删除:
- 如果是叶子结点,直接删除
- 如果只有一棵子树,则直接成为删除结点父结点的子树
- 如果有左右子树,则令该点直接后继(或前驱)代替,然后该直接后继(或前驱)的位置转换为情况1、2、3
- 高度为H的二叉排列树,插入删除的时间都是O(H),特别地,倾斜单支树效率最低。如果是平衡二叉树,平均查找长度达到O(log2n)
非递归查找法
BSTNode *BST_Search(BiTree T,int key,BSTNode *&p){
p=NULL;
while(T!=NULL&&key!=T->data){
p=T;
if(key<T->data) T=T->lchild;
else T=T->rchild;
}
return T;
}
插入(新结点一定是叶子结点)
int BST_Insert(BiTree &T,KeyType k){
if(T==NULL){
T=(BiTree)malloc(sizeof(BSTNode));
T->key=k;
T->lchild=T-rchild=NULL;
return 1;
} else if(k==T->key)
return 0;
else if(k<T->key)
return BST_Insert(T->lchild,k);
else
return BST_Insert(T->rchild,k);
}
构造
void Creat_BST(BiTree &T,KeyType str[],int n){
T=NULL;
int i=0;
while(i<n){
BST_Insert(T,str[i]);
i++;
}
}
2. 哈夫曼树
- 从根结点到任意结点经过的边数(路径长度),与该结点的权值的乘积称为带权路径长度。而所有叶子结点带权路径长度之和称为该树的带权路径长度(WPL)。而WPL最小的称为哈夫曼树或最优二叉树
- 构造:
- 将N个权值为w1,w2,w3…的结点分别作为只含一个结点的二叉树,构成森林F
- 从F中选取两棵根结点权值最小的树作为新结点的左、右子树,新结点的权值为子树根结点之和
- 将新树加入F中,并删除刚选的两棵树
- 重复2)3),直到F中只剩一棵树为止
- 特点
- 初始结点都成为叶子结点,并且权值越小的到根结点的路径长度越大
- N个叶结点(码字数)共新建了N-1个结点(N2=N-1),故结点总数为2N-1
- 每次构造都选择2棵树作为子树,因此不存在度为1的结点
- 子树左右顺序任意,因此哈夫曼树不唯一
- 度为m的哈夫曼树只有度为0和m的点
如果您看了这篇文章觉得有用的话,请随意打赏,您的支持将鼓励我继续创作,谢谢!