数据结构 二叉树与树

二叉树

二叉树的类型与性质:

概念:

  • 完全二叉树:只有最下面两层的度数会<2,且最下一层结点都在左边。
  • 满二叉树:任何结点或是树叶、或是非空子树(只有度为0和2的结点)
  • 扩充二叉树:把所有结点度数扩充为2.
  • 路径长度:两个结点之间的结点个数。
  • 树的高度:树结点的最大层数

性质:

  1. 非空二叉树i层上至多有2i个结点。(i>=0)
  2. 高度为k的二叉树最多有2k+1-1个结点。(k>=0)
    证明:第k层最多有2^k个结点,k-1层有2^k-1个结点...根据等比数列求和公式可求得:2^k<=Sn<=2^(k+1)-1
  3. 叶节点有n0个,度为2结点有n2个,则n0=n2+1.
    证明:向下看,x个度有着x条边,即 2*n2+1*n1+0*n0 = sn (sn为树的总边数)。向上看,除了根节点外每个结点连着一条边,即 n2+n1+n0 = sn-1.上面两式联立得,n0=n2-1.
  4. 有n个结点的完全二叉树高度k为[log2n].
    证明:由性质2推出的等式:2^k<=n<=2^(k+1)-1同时取对数可得,k<=log2n<k-1,即k=[log2n].
  5. 完全二叉树的父节点(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. 森林周游:逐步按树周游

森林转二叉树(意会就好了):

1.以第一棵树的根节点为根,第一棵树的子树为左子树,其它树的根结点为右子树(与第一棵的根结点相连)
2.这样根结点就有很多儿子了,为了保证二叉,又不能有那么多儿子,所以把右子与根的连线全部去掉,只留下长子。
3.被抛去的右子全部向左依次连条边,这样以原来左兄弟儿子的身份回来树上了。
二叉树转森林把上面逆序操作就可以了

猜你喜欢

转载自blog.csdn.net/xiaolan7777777/article/details/105674818
今日推荐