二叉树与树
二叉树
二叉树的类型与性质:
概念:
- 完全二叉树:只有最下面两层的度数会<2,且最下一层结点都在左边。
- 满二叉树:任何结点或是树叶、或是非空子树(只有度为0和2的结点)
- 扩充二叉树:把所有结点度数扩充为2.
- 路径长度:两个结点之间的结点个数。
- 树的高度:树结点的最大层数
性质:
- 非空二叉树i层上至多有2i个结点。(i>=0)
- 高度为k的二叉树最多有2k+1-1个结点。(k>=0)
证明:第k层最多有2^k个结点,k-1层有2^k-1个结点...根据等比数列求和公式可求得:2^k<=Sn<=2^(k+1)-1
- 叶节点有n0个,度为2结点有n2个,则n0=n2+1.
证明:向下看,x个度有着x条边,即 2*n2+1*n1+0*n0 = sn (sn为树的总边数)。向上看,除了根节点外每个结点连着一条边,即 n2+n1+n0 = sn-1.上面两式联立得,n0=n2-1.
- 有n个结点的完全二叉树高度k为[log2n].
证明:由性质2推出的等式:2^k<=n<=2^(k+1)-1同时取对数可得,k<=log2n<k-1,即k=[log2n].
- 完全二叉树的父节点(i-1)/2;左儿子2i+1;右儿子2i+2;(根i=0)
二叉树的周游:
- 深度优先递归算法(先根、中根、后根)
- 深度优先非递归算法(先根、中根、后根)
- 广度优先算法
/*二叉树递归周游*/
void preOrder(BinTree t)//先根
{
if(t!=NULL) return ;
visit(root(t));
preOrder(leftChild(t));
preOrder(rightChild(t));
}
void inOrder(BinTree t)//对称
{
if(t!=NULL) return ;
inOrder(leftChild(t));
visit(root(t));
inOrder(rightChild(t));
}
void postOrder(BinTree t)//后序
{
if(t==NULL) return ;
postOrder(leftChild(t));
postOrder(rightChild(t));
visit(root(t));
}
/*二叉树非递归周游*/
void nPreOrder(BinTree t)
{
Stack s;//栈元素类型是BinTree *
BinTree p;//不能BinTreeNode
s=createEmptyStack();
if(t!=NULL)
{
push(s,p);
}
while(!isEmpty(s))
{
p=top(s);pop(s);
if(p!=NULL)
{
visit(root(p));
push(s,leftChild(p));
push(s,rightChild(p));
}
}
}
void nInOrder(BinTree t)
{
Stack s;
s=createEmptyStack();
BinTree p=t;
if(t==NULL) return ;
do
{
while(p!=NULL)
{
push(s,p);
c=leftChild(p);
}
p=top(s);pop(s);
visit(root(p));//打印节点
rightChild(p);//如果右儿子存在则继续找右子树的最左儿子,如果不存在则回到父节点
}while(p!=NULL||!isEmptyStack(s))
}
void npostOrder(BinTree t)
{
Stack s;
s=createEmptyStack();
BinTree p=t;
while(p!=NULL||!isEmptyStack(s))
{
while(p!=NULL)
{
push(s,p);
p=leftChild(p)?leftChild(p):rightChild(p);//循环倒当前需要处理的结点
}
p=top(s);pop(s);visit(root(p));
if(!isEmptyStack(s)&&leftChild(top(s))==p)
p=rightChild(top(s));//从左子树返回
else
p=NULL;//从右子树返回
}
}
/*广度优先周游二叉树*/
void levelOrder(BinTree t)
{
Queue q=createEmptyQueue();
BinTree p=t;
if(t==NULL) return ;
enQueue(q,p);
while(!isEmptyQueue(q))
{
p=frontQueue(q);deQueue(q);
if(leftChild(p)!=NULL)
enQueue(q,leftChild(p));
if(rightChild(p)!=NULL)
enQueue(q,rightChild(p));
}
}
二叉树的实现:
顺序表示:
把二叉树扩充为完全二叉树,按从上到下、从左到右的方式给结点编号,用连续的顺序存储单元来存储。
优点:方便求父节点、儿子 (根据二叉树性质用数组下标来操作);对于接近完全二叉树的数可以节省存储空间. (数组基本存满)
不好:若空结点过多则扩充时要补充的空间变多,浪费空间。最坏情况,高度为k的二叉树只有k+1个右子,扩充存储却需要2k+1-1个存储空间。
/*顺序二叉树类型定义*/
struct SeqBinTree{
int MAXNUM;//完全二叉树中允许结点的最大个数
int n;//改造成完全二叉树后,结点的实际个数
DataType *nodelist;
};
typedef struct SeqBinTree *PSeqBinTree;//顺序二叉树的指针类型
/*寻亲记*/
int parent_seq(PSeqBinTree t,int p)//找父节点下标
{
if(p<0||p>t->n-1) return -1;
return (p-1)/1;
}
int leftChild_seq(PSeqBinTree t,int p)
{
if(p<0||p>t->n-1) return -1;
return 2*p+1;//可能不存在
}
int rightChild_seq(PSeqBinTree t,int p)
{
if(p<0||p>t->n-1) return -1;
return 2*p+2;//可能不存在
}
链式存储
对于空结点过多的树顺序存储则浪费空间,我们可以采用链式存储。
用两个指针域分别指向左子和右子。
优点:对于空结点过多的树能节省空间。
不好:找父节点麻烦(只能向下走到叶子才能返回到上层),最坏情况跟周游的代价一样。
改进:可以用三叉链表,增加一个指针域指向父节点。但这样又增加空间开销了,抵消优点。
struct BinTreeNode;
typedef struct BinTreeNode * PBinTreeNode;//结点的指针类型
struct BinTreeNode{
Datetype info;
PBinTreeNode llink;
PBinTreeNOde rlink;
};
/*寻亲记*/
PBinTreeNode leftChild_link(PBinTreeNode p)
{
if(p!=NULL)
return p->llink;
return NULL;
}
PBinTreeNode rightChild_link(PBinTreeNode p)
{
if(p!=NULL)
return p->rlink;
return NULL;
}
线索二叉树
在用链式存储时,有不少指针占着空间却不干活(指向null),本着物尽其用的原则,我们可以给指针打上标签,按某种周游顺序(这里用中序)让本是指向null的指针指向按周游顺序的下一个结点。
增加标志域flag,若flag=0,则指针是正常的(指向左子和右子);若flag=1,则指针是线索,指向前驱(左指针)和后继(右指针)。
优点:方便找前驱和后继(不用再周游了)
缺点:写起来累
struct ThrTreeNode;
typedef struct ThrTreeNode * PYHrTreeNode;
struct ThrTreeNode{
DataType info;
PTHrTreeNode llink,rlink;
int ltag,rtag;
};
typedef struct ThrTreeNode * ThrTree;
typedef ThrTree * PThrTree;
/*按对称序线索化二叉树*/
void thread(ThrTree t)
{
PSeqStack s=createEmptyStack(M);//栈元素的类型是ThrTree,M一般取t的高度
ThrTree p=t,pr=NULL;
if(t==NULL)
return
do
{
while(p!=NULL)
{
push_seq(s,p);
p=p->llink;
}
p=top_seq(s);pop_seq(s);
if(pr!=NULL)
{
if(pr->rlink==NULL)
{
pr->rlink=p;
pr->rtag=1;
}
if(p->llink==NULL)
{
p->llink=pr;
p->ltag=1;
}
}
pr=p;p=p->rlink;
}while(!isEmptyStack_seq(s)||p!=NULL)
}
/*按对陈序周游对称序二叉树*/
void nInOrder(ThrTree t)
{
ThrTree p=t;
if(t==NULL) return ;
while(p->llink!=NULL&&p->ltag==0) p=p->llink;
while(p!=NULL)
{
visit(*p);
if(p->rlink!=NULL&&p->rtag==0)
{
p=p->rlink;
while(p->llink!=NULL&&p->ltag==0)
p=p->llink;
}
else
p=p->rlink;
}
}
堆与优先队列:
堆是一颗具有堆序性的完全二叉树。(堆序性:每个非叶子结点都<=其左右儿子,则是小根堆。)当然也有大根堆,根大的就是大根堆。
优先队列:遵守最大(小)元素先出原则的队列,它是用堆来实现的。stl里的优先队列默认是大根堆。
(先挖个坑,关于堆的详细介绍之后补上)
下面是怎么用堆实现优先队列
struct PriorityQueue{
int MAXNUM;
int n;
DataType *pq;
};
typedef struct PriorityQueue * PPriorityQueue;
/*向优先队列中插入一个元素*/
void add_heap(PPriorityQueue papq,DataType x)
{
if(papq->n>=papq->MAXNUM-1)
{
printf("full");
return ;
}
int i;
for(i=papq->n;papq->pq[(i-1)/2]&&i>0;i=(i-1)/2)
papq->pq[i]=papq->pq[(i-1)/2];
papq->pq[i]=x;n++;
return ;
}
/*从优先队列中删除最小元素*/
void removeMin_heap(PPriorityQueue papq)
{
int s;
if(isEmpty_heap(papq))
{
printf("Empty");
return ;
}
s=--papq->n;
papq->pq[0]=papq->pq[s];
sift(papq,s,0);//把完全二叉树从指定结点调整为堆
}
/*把完全二叉树从指定结点调整为堆*/
void sift(PPriorityQueue papq,int size,int p)
{
DataType temp;
temp=papq->pq[p];
int i=p,child=2*i+1;
while(child<size)
{
if(i<size-1&&papq->pq[child]>pq[child+1])//找到左右儿子最小的
child++;
if(temp>papq->pq[child])
{
papq->pq[i]=papq->pq[child];
i=child;
child=2*i+1;
}
else
break;
}
papq->pq[i]=temp;
}
哈夫曼树:
带权外部路径:WPL=∑ wi li(li是路径长度,wi是权值)。
哈夫曼树则是一颗使带权外部路径最小的树。
实现思路:每次选取权值最小的两个结点相连,可以用上面提到的优先队列进行存储与找到权值最小的两个结点。
struct HtNode{
int ww;
int parent,llink,rlink;
};
struct HtTree{
int m;//外部结点的个数
int root;
struct HtNode *ht;//存放2*m-1个结点的数组
};
typedef struct HtTree PHtTree;
/*哈夫曼算法*/
PHtTree huffman(int m,int *w)
{
PHtTree pht;
int i,j,x1,x2,m1,m2;
pht= (PHtTree) malloc(sizeof(struct HtTree));//分配哈夫曼树空间
if(pht==NULL)
{
printf("out of space");
return pht;
}
pht->ht(struct HtNode)malloc(sizeof(struct HtNode)*(2*m-1))//分配ht数组空间
if(pht==NULL)
{
printf("out of space");
return pht;
}
for(i=0;i<2*m-1;i++)//初始化
{
pht->ht[i].llink=-1;pht->ht[i].rlink=-1;pht->ht[i].parent=-1;
if(i<m)
pht->ht[i].ww=w[i];
else
pht->ht[i].ww=-1;
}
for(i=0;i<m-1;i++)
{
m1=MAXINF;m2=MAXINF;//MAXINF为无穷大
x1=-1;x2=-1;
for(j=0;j<m+1;j++)
{
if(pht->ht[j].ww<m1&&pht->ht[j].parent==-1)
{
m2=m1;x2=x1;//先把大的给m2,m1再拿小的
m1=pht->ht[j].ww;x2=j;
}
else if(pht->ht[j].ww<m2&&pht->ht[j].parent==-1)
{
m2=pht->ht[j].ww;
x2=j;
}
}
pht->ht[x1].parent=m+i;pht->ht[x2].parent=m+i;
pht->ht[m+i].ww=m1+m2;
pht->ht[m+i].llink=x1;pht->ht[m+i].rlink=x2;
}
pht->root=2*m-2;
return pht;
}
树与森林
树:
概念:
- 树的度:树中最大度的结点
- 长子:最左的子节点(为了方便,之后称最左结点为长子)
- 次子:长子的右节点
二叉树是度最大为2的树,只有一左一右结点。这里的树不一定是二叉,可能是三叉、四叉…因此会有一个左子结点和几个右子结点。
树的实现:
父指针表示法:
因为每个结点有唯一的父亲,很容易想到二叉树的顺序存储。但由于儿子个数不确定,不能像二叉树通过数组下标操作来找到父亲儿子,因此可以给结点再增加一个记录父节点在数组位置的元素。
优点:存储方便,好找父节点(唯一一个好找父亲的)。
不好:不好找儿子和兄弟
改进:可以按周游顺序存放,若按先根周游,长子在父节点下一个,次子在长子下,易查找 。(若无次子,需要走完整个数组才能确定)。
struct ParTreeNode{
DataType info;
int parent;//父节点位置
};
struct ParTree{
int MAXNUM;
int n;
struct ParTreeNode *nodelist;
};
typedef struct ParTree *PParTree;
//按先根周游次序存放
int rightSibling_partree(PParTree t,int p)//找第一个右兄弟的位置
{
int i;
if(p>=0&&p<t->n)
{
for(int i=p+1;i<t->n;i++)
{
if(t->nodelist[i].parent==t->nodelist[p].parent)
return i;
}
}
return -1;
}
int leftChild_partree(PParTree t,int p)
{
if(t->nodelist[p+1].parent==p)
return p+1;
return -1;
}
子表表示法:
顺序不好实现的肯定要找链表啦,找不到儿子我们就存储儿子。我们可以把整棵树表示成一个结点表,结点表中每个元素又包含一个表,记录这个结点的所有子节点位置。
优点:好找儿子,直接在此结点的儿子域就可以找到。
不好:没有父亲的域找不到父亲,需要遍历才能找到;兄弟也找不到,找兄弟得先找到父亲。
struct EdgeNode{//子表中结点的结构
int nodeposition;
struct EdgeNode *link;
};
struct ChiTreeNode{//结点表中结点的结构
DataType info;
struct EdgeNode *children;
};
struct ChiTree{//树结构
int MAXNUM;
int root;
int n;
struct ChiTreeNode *nodelist;
};
typedef struct ChiTree *PChiTree;
int rightSibling_chitree(PChiTree t,int p)//找右兄弟结点位置
{
struct EdgeNode *v;
for(int i=0;i<t->n;i++)
{
v=t->nodelist[i].children;//先找父亲
while(v!=NULL)
{
if(v->nodeposition==p)//找到自己
{
if(v->link==NULL) return -1;
else return v->link->nodeposition;
}
else
v=v->link;
}
}
return -1;
}
int parent_chitree(PChiTree t,int p)//找到父节点
{
struct EdgeNode *v;
for(int i=0;i<t->n;i++)
{
v=t->nodelist[i].children;
while(v!=NULL)
{
if(v->nodeposition==p)
return i;
else
v=v->link;
}
}
return -1;
}
长子兄弟表示法:
有没有觉得子表表示法太麻烦了,两个表写起来太累,我们可以进行改进一下,类似二叉树的链式存储。有两个指针域,一个指向长子,另一个指向次子。
优点:还是好找儿子。
不好:又找不到父亲了,还是需要周游才能找到。
struct CSNode;
typedef struct CSNode PCSNode;
struct CSNode{
DataType info;
PCSNode lchild;
PCSNode rsibling;
};
typedef struct CSNode *CSTree;
森林:
概念:
- 由零个或多个不相交的树组成的集合。
- 森林周游:逐步按树周游
森林转二叉树(意会就好了):
1.以第一棵树的根节点为根,第一棵树的子树为左子树,其它树的根结点为右子树(与第一棵的根结点相连)
2.这样根结点就有很多儿子了,为了保证二叉,又不能有那么多儿子,所以把右子与根的连线全部去掉,只留下长子。
3.被抛去的右子全部向左依次连条边,这样以原来左兄弟儿子的身份回来树上了。
二叉树转森林把上面逆序操作就可以了