数据结构---树

定义

  • 从数据结构角度看,树包含n(n≥0)个结点,当n=0时,称为空树;非空树的定义为:有且仅有一个根结点。除根结点外,中的每个结点有且仅有一个前驱结点,但可以有多个后继结点。树中的所有节点可以有零个或多个终端结点。

  • 递归定义
    树是 n(n≥0)个结点的有限集。若 n=0, 称为空树;若 n > 0,则它满足如下两个条件:有且仅有一个特定的称为根的结点;其余结点可分为 m (m≥0) 个互不相交的有限 集 T1, T2, T3, …, Tm, 其中每一个集合本身又是一棵树,并称为根的子树 (SubTree)。

  • 适合于表示具有层次结构的数据

  • 树的逻辑结构:树中任一结点都可以有零个或多个直接后 继结点但至多只能有一个直接前趋结点。

表示方法

  • 属性表示法
  • 嵌套集合(文氏图)表示法
  • 凹入表示法
  • 广义表表示法

基本术语

  • 根结点:非空树中无前驱结点的结点,一个数只有一个根节点。
  • 叶子节点(终端结点 ):度为0,不再延伸出新节点的节点。当一棵树只有一个节点时,根节点也是叶子节点。
  • 祖先节点:根A到节点K的唯一路径上的任意节点,称为节点K的祖先节点。
  • 分支结点:度不为0,又称为非终端结点。
  • 结点的度:结点拥有的子树数。树中节点的最大度数称为树的度。
  • 内部结点:根结点以外的分支结点称为。
  • 堂兄弟节点:双亲在同一层的结点。
  • 结点的子孙:以某结点为根的子树中的任一结点。
  • 树的层次:从根节点开始算起,根节点为第一层,根节点的子树为根节点的第二层,以此类推。
  • 节点的深度:从根节点开始自顶向下逐层累加到该节点时的深度值。
  • 节点的高度:从最底层叶子节点开始自底向上逐层累加到该节点时的高度值。树的高度(深度)是树中节点的最大层数。
  • 有序树:树中结点的各子树从左至右有次序,不能交换。在有序树中,一个节点的子节点按从左到右的顺序出现是有关联的。
  • 无序树:树中结点的各子树无次序。
  • 满m次树:当一棵m次树的第i层有mi-1个结点(i≥1)时,称该层是满的,若一棵m次树的所有叶子结点在同一层,除该层外其余每一层都是满的,称为满m次树。对于n个结点,构造的m次树为满m次树或者接近满m次树,此时树的高度最小。
  • 路径:注重两个节点之间的路径只有这两个节点之间所经过的节点序列构成的。
  • 路径长度:路径上所经过边的个数。
  • 森林:是 m (m≥0) 棵互不相交的树的集合。把根结点删除树就变成了森林。给森林中的各子树加上一个双亲结点,森林就变成了树。 一棵树可以看成是一个特殊的森林。

树的性质

  • 树可以没有节点,在这种情况下把树称为空树。
  • 对于有n个节点的树,边数一定是n-1,且满足连通边数等于顶点数-1的结构一定是一棵树。
  • 树中的结点数等于所有结点的度数加1。
  • 度为m的树中第i层上至多有mi-1个结点,这里应有i≥1。
  • 高度为h的m次树至多有(mh-1)/(m-1)(=m0+m1+m2+…+mh-1 )个结点。
  • 具有n个节点的m叉树的最小高度为logm(n(m-1)+1)向上取整。

树的基本运算

  • 节点
//
struct node{
	typename data;
	int child[maxn];
}Node[maxn];
//
struct node{
	typename data;
	vector<int> child;
}Node[maxn];
  • 新建节点
int index=0;
int newNode(int v){
	Node[index].data=v;
	Node[index].child.clear();
	return index++;
}

寻找满足某种特定关系的结点

  • 插入或删除某个结点
  • 遍历树中每个结点
    树的遍历:按某种方式访问树中的每一个结点且每一个结点只被访问一次。
    先根遍历:若树不空,则先访问根结点,然后依次先根遍历各棵子树。
void preorder(int root){
	printf("%d\n",Node[root].data);
	for(int i=0;i<Node[root].child.size();i++){
		preorder(Node[root].child[i]);
	}
}

后根遍历:若树不空,则先依次后根遍历各棵子树,然后访问根结点。其访问顺序与中序遍历顺序相同。

层次遍历:若树不空,则自上而下自左至右访问树中每个结点。

void layerorder(int root){
	queue<int>q;
	q.push(root);
	while(!q.empty()){
		int front=q.front();
		printf("%d\n",Node[front].data);
		q,pop();
		for(int i=0;i<Node[front].child.size){
			q.push(Node[front].child[i]);
		}
		
	}
}
//若需要对层号进行求解
struct node{
	int layer data;
	vector<int> child;
}Node[maxn];
void layerorder(int root){
	queue<int>q;
	q.push(root);
	Node[root].layer=0;
	while(!q.empty()){
		int front=q.front();
		printf("%d\n",Node[front].data);
		q.pop();
		for(int i=0;i<Node[front].child.size){
			int child=Node[front].child[i];
			Node[child].layer=Node[front].layer+1;
			q.push(child);
		}
		
	}
}

树的存储结构

  • 双亲表示法
    定义结构数组存放树的结点, 每个结点含两个域:数据域:存放结点本身信息。 双亲域:指示本结点的双亲结点在数组中的位置。

    特点:找双亲容易,找孩子难。

typedef struct{
	ElemType data;
	int parent;
}PTNode;
typedef struct{
	PTNode nodes[MAX];
	int n;
}PTree;
  • 孩子表示法(树的链式存储结构)
    节点同构:每个节点的指针个数相等,为树的度。在有 n 个结点、度为 d 的树的 d 叉链表中,有 n×(d-1)+1 个空链域。
    结点不同构:结点的指针个数不相等, 为该结点的度 degree。 节约存储空间,但操作不方便。

  • 孩子链表
    把每个结点的孩子结点排列起来,看成是一个线性表,用单链表存储,则 n 个结点有 n 个孩子链表(叶子的孩子链表为空表)。而 n 个头指针又组成一个线性表,用顺序表(含 n 个元素的结构数组)存储。

    找孩子容易,找双亲难。

  • 孩子兄弟表示法
    用二叉链表作树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子结点和下一个兄弟结点。

    孩子兄弟链表的结构形式与二叉链表完全 相同,但存储结点中指针的含 义不同:二叉链表中结点的左 右指针分别指向该结点的左右 孩子;而孩子兄弟链表结点的 左右指针分别指向它的“长子” 和“大弟”。

typedef struct CSNode{
	ElemType data;
	struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;

二叉树

  • 递归定义为:二叉树或者是一棵空树,或者是一棵由一个根结点和两棵互不相交的,分别称做根结点的左子树和右子树所组成的非空树,左子树和右子树又同样都是一棵二叉树。

  • 二叉树与度为2的树是不同的。度为2的树至少有3个结点,而二叉树的结点数可以为0;度为2的树不区分子树的次序,而二叉树中的每个结点最多有两个孩子结点,且必须要区分左右子树,即使在结点只有一棵子树的情况下也要明确指出该子树是左子树还是右子树。

    二叉树不是树的特殊情况,它们是两个概念。
    二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也要进行区分,说明它是左子树,还是右子树。树当结点只有一个孩子时,就无须区分它是左还是右。(也就是二叉树每个结点位置或者说次序都是固定的,可以是空,但是不可以说它没有位置,而树的结点位置是相对于别的结点来说的,没有别的结点时,它就无所谓左右了),因此二者 是不同的。

  • 五种基本形态
    空二叉树
    根和空的左右子树
    根和左子树
    根和右子树
    根和左右子树

  • 满二叉树
    一棵二叉树中,当第 i 层的结点数为2 ^i - 1^个时,则称此层的结点数是满的,当一棵二叉树的每一层都满时,则称此树为满二叉树。

    除叶子结点以外的其他结点的度皆为2。 叶子全部在最底层。

    编号规则:从根结点开始,自上而下,自左而右。

  • 完全二叉树
    深度为 k 的具有 n 个结点的二叉树,当且仅当其每一个结点都与深度为 k 的满二叉树中编号为 1~ n 的结点一一对应时,称之为完全二叉树。

    叶子只可能分布在层次最大的两层上。

    非空的完全二叉树至多有一个度为1的节点。

    如果完全二叉树的某个结点没有左孩子,则一定没有右孩子。

    对任一结点,如果其右子树的最大层次为 h,则其左子树的最大层次必为h或 h+ 1。

    满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。

二叉树的性质

  • 对任何一棵二叉树 T,如果其叶子数为 n0,度为 2 的结点数为 n2,则 n0 = n2 + 1。

    n = n0 + n1 + n2
    n = B(分支数)+1
    B = n1 + 2n2
    n = B + 1=n1 + 2
    n2+ 1
    n0 + n1 + n2 = n1 + 2*n2+ 1
    n0 = n2 + 1

  • 一棵非空二叉树的第 i 层上最多有 2 ^i - 1^ 个结点 (i ≥1)。

  • 一棵深度为 k 的二叉树至多有 2k-1 个结点(k ≥1)。

  • 对完全二叉树中编号为i的结点(1≤i≤n,n≥1,n为结点数)
    若i≤n/2向下取整,即2i≤n,则编号为i的结点为分支结点,否则为叶子结点。

    若n为奇数,则每个分支结点都既有左孩子结点,也有右孩子结点;若n为偶数,则编号最大的分支结点只有左孩子结点,没有右孩子结点,其余分支结点都有左、右孩子结点。

    若编号为i的结点有左孩子结点,则左孩子结点的编号为2i;若编号为i的结点有右孩子结点,则右孩子结点的编号为(2i+1)。

    除树根结点外,若一个结点的编号为i,则它的双亲结点的编号为i/2向下取整,也就是说,当i为偶数时,其双亲结点的编号为i/2,它是双亲结点的左孩子结点,当i为奇数时,其双亲结点的编号为(i-1)/2,它是双亲结点的右孩子结点。

  • 具有 n 个结点的完全二叉树的深度k为 log2n(向下取整) + 1或log2(n+1)向上取整。

  • 高度为h的完全二叉树最少有2h-1 个结点,最多有2h-1个结点。

  • 一棵含n个结点的非空满二叉树,其叶子结点的个数是(n+1)/2 。

  • 二叉排序树:左子树上的所有节点的关键字均小于根节点的关键字,右子树上的所有节点的关键字均大于根节点的关键字。左右子树又各是一棵二叉排序树。

  • 平衡二叉树:任意节点的左子树和右子树的深度之差不超过1。

二叉树的存储结构

  • 顺序存储结构
    完全二叉树:用一组地址连续的存储单元依次自上而下、自左至右存储结点元素,即将编号为 i 的结点元素存储在一维数组中下标为 i 的分量中(从1开始存储)。然后通过一些方法确定结点在逻辑上的父子和兄弟关系。

    一般二叉树:将其每个结点与完全二叉树上的结点相对照,存储在一维数组的相应分量中。 浪费存储空间。

    最坏情况:深度为 k 的且只有 k 个结点的右单支树需要长度为2k-1 的一维数组。

  • 链式存储结构
    用一个链表来存储一棵二叉树,二叉树中的每个节点用链表的一个链节点来存储。

    在 n 个结点的二叉链表中有 n + 1 个空指针域。

二叉链表

struct node{
	typename date;
	node *lchild;
	node *rchild;
};
//二叉树建树前根节点不存在
node *root=NULL;
//创建节点
node *newNode(int v){
	node *Node=newNode();
	node->date=v;
	Node->lchile=Node->rchild=NULL;
	return Node;
};

三叉链表:多一个指向双亲节点的指针。

二叉树的递归算法

  • 在定义一个过程或函数时出现调用本过程或本函数的成分,称之为递归。若调用自身,称之为直接递归。若过程或函数p调用过程或函数q,而q又调用p,称之为间接递归。

  • 一般地,递归模型由两部分组成,一部分为递归出口,它给出了递归的终止条件,另一部分为递归体,它确定递归求解时的递推关系。

  • 递归算法求解过程的特征是:先将不能直接求解的问题转换成若干个相似的小问题,通过分别求解各子问题,最后获得整个问题的解。当这些子问题不能直接求解时,还可以再将它们转换成若干个更小的子问题,如此反复进行,直到遇到递归出口为止。

  • 递归算法设计一般方法
    对原问题f(s)进行分析,假设出合理的“小问题”f(s’)(与数学归纳法中假设n=k-1时等式成立相似);假设f(s’)是可解的,在此基础上确定f(s)的解,即给出f(s)与f(s’)之间的关系(与数学归纳法中求证n=k时等式成立的过程相似);确定一个特定情况(如f(1)或f(0))的解,由此作为递归出口(与数学归纳法中求证n=1或时n=0式成立相似)。

  • 一般地,对于二叉树b,设f(b)是求解的“大问题”,则f(b->lchild)和f(b->rchild)为“小问题”,假设f(b->lchild)和f(b->rchild)是可求的,在此基础上得出f(b)和f(b->lchild)、f(b->rchild)之间的关系,从而得到递归体,再考虑b=NULL或只有一个结点的特殊情况,从而得到递归出口。

  • 二叉树节点的查找修改

void search(node *root,int x,int newdate){
	if(root==NULL)return;
	if(root->date==x){
		root->date=newdate;
	}
	search(root->lchild,x,newdate);
	search(root->rchild,x,newdate);
}
  • 二叉树节点的查找插入
    二叉树节点的插入位置就是数据域在二叉链表中查找失败的位置
void insert(node *&root,int x,int newdate){//根节点为引用类型 
	//一般来说若在函数中需要新建节点即修改二叉树的结构 需要使用引用
	if(root==NULL){
		root=newNode(x);
	}
	if(x应插入在左子树){
		insert(root->lchild);
	}else{
		insert(root->rchild);
	}
}
  • 二叉树的创建
node *creat(int date[],int n){
	node* root=NULL;
	for(int i=0;i<n;i++){
		insert(root,date[i]);
	}
	return root;
}
  • 建立一棵以括号表示法表示的二叉树
    ‘(’:表示前面刚创建的p结点存在着孩子结点,需将其进栈,以便建立它和其孩子结点的关系。然后开始处理该结点的左孩子,因此置k=1,表示其后创建的结点将作为这个结点(栈顶结点)的左孩子结点;
    ‘)’:表示以栈顶结点为根结点的子树创建完毕,将其退栈;
    ‘,’:表示开始处理栈顶结点的右孩子结点,置k=2;
    其他情况:只能是单个字符,表示要创建一个新结点p,根据k值建立p结点与栈顶结点之间的联系,当k=1时,表示p结点作为栈顶结点的左孩子结点,当k=2时,表示p结点作为栈顶结点的右孩子结点。
void CreateBTree(node* &root,char *str)
{    node*St[MaxSize],*p=NULL;
  int top=-1,k,j=0;
  char ch;
  root=NULL;		//建立的二叉树初始时为空
  ch=str[j];
  while (ch!='\0'){	//str未扫描完时循环
  switch(ch){
		case '(':top++;St[top]=p;k=1; break;	//为左孩子结点
		case ')':top--;break;
		case ',':k=2; break;			//为右孩子结点
		default:
			p=newNode;
			p->data=ch;
			p->lchild=p->rchild=NULL;
			if (root==NULL)			//*p为二叉树的根结点
		  		root=p;
			else{ 				//已建立二叉树根结点
				switch(k) {
		  			case 1:St[top]->lchild=p;break;
		  			case 2:St[top]->rchild=p;break;
		  		}
			}
		}
		j++; ch=str[j];
  }
}

  • 以括号表示法输出二叉树
void DispBTree(node *root) {   
	if (root!=NULL){	
		printf("%c",root->data);
		if (root->lchild!=NULL || root->rchild!=NULL){	
			printf("(");		//有子树时输出'('
	  		DispBTree(root->lchild);	//递归处理左子树
	  		if (root->rchild!=NULL)	//有右子树时输出','
			printf(",");
	  		DispBTree(root->rchild);	//递归处理右子树
	  		printf(")");			//右子树输出完毕,再输出一个')'
		}
  }
}

  • 二叉树的销毁
void Destroy(node *&root){   
	if (root!=NULL){	
		DestroyBTree(root->lchild);
		DestroyBTree(root->rchild);
		free(root);
  }
}
  • 求二叉树高度
int height(node *root) {      
	int lchilddep,rchilddep;
	if (root==NULL) return 0; 		//空树的高度为0
	else{	
		lchilddep=height(root->lchild);	//求左子树的高度为lchilddep
		rchilddep=height(root->rchild);	//求右子树的高度为rchilddep
		return (lchilddep>rchilddep)? (lchilddep+1):(rchilddep+1);
  }
}
  • 求二叉树结点个数
int NodeCount(node* root){	//求二叉树bt的结点个数
	int num1,num2;
  if (root==NULL)	return 0;		//为空树时返回0
	else{	
		num1=NodeCount(root->lchild);	//求左子树结点个数
		num2=NodeCount(root->rchild);	//求右子树结点个数
		return (num1+num2+1);		//返回和加上1
  }
}
  • 求二叉树叶子结点个数运算算法
int LeafCount(node *root){	//求二叉树bt的叶子结点个数
	int num1,num2;
	if (root==NULL)	return 0;	//空树返回0
	else if (bt->lchild==NULL && bt->rchild==NULL) 
	return 1;		//为叶子结点时返回1
  	else{	
  		num1=LeafCount(bt->lchild);	//求左子树叶子结点个数
		num2=LeafCount(bt->rchild); 	//求右子树叶子结点个数
		return (num1+num2);		//返回和
  }
}

二叉树的遍历

  • 顺着某一条搜索路径巡访二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次。 “访问”的含义很广,可以是对结点作各种处理, 但要求这种访问不破坏原来的数据结构。

先序遍历

  • 若二叉树非空,则访问根结点;先序遍历左子树;先序遍历右子树。
  • 递归算法
void preorder(node *root){
	if(root==NULL){
		return ;
	}
	printf("%d\n",root->data);
	preorder(root->lchild);
	preorder(root->right);
}
  • 非递归算法
//1
void preorder(node *root){
	stack<node*>s;
	node *p=root;
	while(s.empty()){
		p=s.pop();
		printf("%d ",p->data);
		if(p->lchild!=NULL){
			s.push(p->lchild);
		}
		if(p->rchild!=NULL){
			s.push(p->rchild);
		}
	}
}
//2
void preorder(node *root){
	stack<node*>s;
	node *p=root;
	while(p||s.empty()){
		if(p){
			printf("%d ",p->data);
			s.push(p);
			p=p->lchild;
		}else{
			p=s.pop();
			p=p->rchild;
		}
	}
}

中序遍历

  • 若二叉树非空,中序遍历左子树;访问根结点;中序遍历右子树。
  • 递归算法
void inorder(node *root){
	if(root==NULL){
		return ;
	}
	inorder(root->lchild);
	printf("%d\n",root->data);
	inorder(root->rclild);
}
  • 非递归算法
void inorder(node *root){
	stack<node*>s;
	node *p=root;
	while(p||s.empty()){
		if(p){
			s.push(p);
			p=p->lchild;
		}else{
			p=s.pop();
			printf("%d ",p->data);
			p=p->rchild;
		}	
	}
}

后序遍历

  • 若二叉树非空,后序遍历左子树;后序遍历右子树;访问根结点;。
  • 递归算法
void postorder(node *root){
	if(root==NULL){
		return ;
	}
	postorder(root->lchild);
	postorder(root->rclild);
	printf("%d\n",root->data);
}
  • 非递归算法
void postorder(node *root) {
    node*p = root, *r = NULL;
    stack<node*> s;
    while (p || !s.empty()) {
        if (p) {
	        s.push(p);
            p = p->lchild;
        }else {
            p = s.top();
            if (p->rchild&& p->rchild!= r)//存在右孩子并且未被访问
                p = p->right;
			}else {
                s.pop();
                printf("%d\n",p->data);
                r = p;//记录最近访问过的节点
                p = NULL;
            }
        }
    }
}

层次遍历

将根节点入队,取队首节点,若该节点有左孩子,将左孩子入队,若该节点有右孩子,将右孩子入队,直到队空。

//若需要计算节点的层次,应在节点的定义中增加一个layer变量
struct node{
	int date;
	int layer;
	node *lchild;
	node *rchild;
};
//层次遍历
void layerorder(node *root){
	queue<node*>q;
	root->layer=1;
	q.push(root);
	while(!q.empty()){
		node *now=q.front();
		q.pop();
		printf("%d ",now->date);
		if(now->lchild!=NULL){
			now->lchild->layer=now->layer+1;
			q.push(now->lchild);
		}
		if(now->rchild!=NULL){
			now->rchild->layer=now->layer+1;
			q.push(now->rchild);
		}
	}
}

通过遍历序列重建二叉树

  • 中序序列可以与先序序列,后序序列,层次序列中的任意一个来构建唯一的二叉树。而后三者的两两搭配或是三个一起都无法构建唯一的二叉树。
//通过先序和中序重建二叉树
node* creat(int prel,int prer,int inl,int inr){
	if(prel>prer){
		return NULL;
	}
	node *root=new node;
	root->data=pre[prel];
	int k;
	for(k=inl;k<=inr;k++){
		if(in[k]==pre[prel])
			break;
	}
	int numleft=k-inl;
	root->lchild=creat(prel+1,prel+numleft,inl,k-1);
	root->rchild=creat(prel+numleft+1,prer,k+1,inr);
	return root;
}
//通过后序和中序重建二叉树
node* creat(int postl,int postr,int inl,int inr){
	if(postl>postr){
		return NULL;
	}
	node *root=new node;
	root->data=post[postr];
	int k;
	for(k=inl;k<=inr;k++){
		if(in[k]==post[postr])
			break;
	}
	int numleft=k-inl;
	root->lchild=creat(postl+1,postl+numleft-1,inl,k-1);
	root->rchild=creat(postl+numleft,postr-1,k+1,inr);
	return root;
}

二叉树的静态实现

  • 节点的定义
struct node{
	typename data;
	int lchild,rchild;
}Node[maxn];
  • 节点的生成
int index=0;
int newNode(int v){
	Node[index].date=v;
	Node[index].lchild=-1;
	Node[index].rchild=-1;
	return index++;
}
  • 查找
void search(int root,int x,int newdata){
	if(root==-1)return;
	if(Node[root].data==x){
		Node[root].data=newdata;
	}
	search(Node[root].lchild,x,newdata);
	search(Node[root].rchild,x,newdata);
}
  • 插入
void insert(int &root,int x){
	if(Node[root]=-1){
		root=newNode(x);
	}
	if(x应该插入在左子树){
		insert(Node[root].lchild,x);
	}else{
		insert(Node[root].rchild,x);
	}
}
  • 建立
int create(int data[],int n){
	int root=-1;
	for(int i=0;i<n;i++){
		insert(root,data[i]);
	}
	return root;
}
  • 先序遍历
void preorder(int root){
	if(root==-1)return ;
	printf("%d\n",Node[root].data);
	preorder(root.lchild);
	preorder(root.rchild);
	
}
  • 中序遍历
void inorder(int root){
	if(root==-1)return ;
	inorder(root.lchild);
	printf("%d\n",Node[root].data);
	inorder(root.rchild);
	
}
  • 后序遍历
void postorder(int root){
	if(root==-1)return ;
	postorder(root.lchild);
	postorder(root.rchild);
	printf("%d\n",Node[root].data);
}
  • 层次遍历
void layerorder(int root){
	queue<int>q;
	q.push(root);
	while(!q.empty()){
		int now=q.front();
		q.pop();
		printf("%d\n",Node[now].data);
		if(Node[now].lchild!=-1)q.push(Node[now].lchild);
		if(Node[now].rchild!=-1)q.push(Node[now].rchild);
	}
}

二叉树与树的转换

  • 森林/树转换成二叉树(兄弟相连留长子)
    加线:在兄弟之间加一连线
    抹线:对每个结点去除其与孩子之间的关系(第一孩子除外)
    旋转:以树的根结点为轴心,顺时针转45
    树转换成的二叉树其右子树一定为空。
  • 将二叉树转换成森林(去掉全部右孩线,孤立二叉再还原。 )
    抹线:将二叉树中根结点与其右孩子连线,及沿右分支搜索到的所有右孩子间连线全部抹掉,使之变成孤立的二叉树。
    还原:将孤立的二叉树还原成树。
  • 当一棵二叉树是由m棵树构成的森林转换而来的,该二叉树的根结点一定有m-1个右下孩子。

并查集

  • 合并:合并两个集合
  • 查找:判断两个元素是否在一个集合
  • 初始化
void init(){
	for(int i=0;i<n;i++){
		father[i]=i;
	}
}
  • 查找
int findFater(int x){
	int a=x;
	while(father[x]!=x){
		x=father[x];
	}
	while(father[a]!=a){
		int z=a;
		a=father[a];
		father[a]=x;
	}
	return x;
}
  • 合并
void Union(int a,int b){
	int fa=findFather(a);
	int fb=findFather(b);
	if(fa!=fb){
		father[fa]=fb;
	}
}

线索二叉树

  • 引入线索二叉树是为了加快查找节点前驱和后继的速度。

结构

ltag lchild date rchild rtag

若无左子树,令lchild指向其前驱节点,若无右子树,令rchild指向其后继节点。两个标志域表明当前指针域表明当前指针域所指的对象是左右节点还以前驱后继。在这里插入图片描述

  • 节点结构
typedef struct ThreadNode{   
	ElemType data;
  struct ThreadNode *lchild,*rchild;
  int ltag,rtag;
} ThreadNode,*ThreadTree;

线索二叉树的构造与销毁

若结点没有左子树,则令其左指针指向它的“前驱”并将左指针类型标志改为 “1”;

若结点没有右子树,则令其右指针指向它的“后继”并将右指针类型标志改为 “1”;

线索化过程就是在遍历过程中修改空指针的过程:
将空的lchild改为结点的直接前驱;将空的rchild改为结点的直接后继。非空指针仍然指向孩子结点(称为“正常情况”)。

//中序遍历二叉线索树
void Thread(ThreadTree *&p,ThreadTree &pre) {
	if (p!=NULL) { 
		Thread(p-> lchild);  
		if (!p-> lchild) { //左子树为空
			p->ltag = 1;   
         	p-> lchild = pre; 
		}else{
			 p->ltag=0;
		}
		if (!pre-> rchild) { 
			pre-> rtag = 1;  
         	pre-> rchild = p; 
		} else{
			 p->rtag=0;
		} 
      	pre = p; 
      	Thread(p->rchild);
 	}   
}
//带头结点 建树
ThreadNode *CreaThread(ThreadNode *root) {
	ThreadNode *head;
    head=(BthNode *)malloc(sizeof(ThreadNode));  
    head->ltag = 0; 
    head->rtag = 1;// 建头结点
    head->rchild = root;
    // 右指针回指
    if (root==NULL) {
    	head -> lchild = head;
        head -> rchild=null;
	}else { 
		head -> lchild = root;     
		pre = head;
		Thread (root);   
        pre-> rchild = head; //处理遍历的最后一个节点
		pre-> Rtag = 1;   
     	head -> rchild = pre; 
     }
     return head;
} 
  • 销毁
void Destroy(ThreadNode *&b){    
	if (b->ltag==0)		//*b有左孩子,释放左子树
	Destroy(b->lchild);
  if (b->rtag==0)		//*b有右孩子,释放右子树
	Destroy(b->rchild);
  free(b);
}
void Destroy(ThreadNode *&tb){      
	Destroy(tb->lchild);	
  free(tb);			//释放头结点
}

线索二叉树的遍历

  • 中序线索二叉树的下第一个节点
ThreadNode *Firstnode(ThreadNode *p){//找最左下节点
	while(p->ltag==0){
		p=p->lchild;
	}
	return p;
}
  • 中序线索二叉树的下p的前驱
    若p->ltag=1,则p -> lchild就是其前驱结点;
    若p->ltag=0,则前驱是左子树的最右下结点。
ThreadNode *Frontnode(ThreadNode *p){
	if(p->ltag==0){
		ThreadNode q=p->lchild;
		while(q->rtag==1){
			q=q->rchild;
		}
		return q;
	}return p->lchild;
	
  • 中序线索二叉树的下p的后继
    若p -> rtag=1,则p -> rchild就是其后继结点;
    若p -> rtag=0,则后继是右子树的最左下结点。
ThreadNode *Nestnode(ThreadNode *p){
	if(p->rtag==0)return Firstnode(p->rchild);
	else return p->rchild;
}
  • 中序线索二叉树的最后一个节点
ThreadNode *Lastnode(ThreadNode *tb){
	return(tb->rchild);
}
  • 不含头结点的中序线索二叉树的中序遍历算法
ThreadNode *Nestnode(ThreadNode *T){
	for(ThreadNode *p=Firstnode(T);p!=NULL;p=Nestnode(p)){
	visit(p);
	}
}
  • 前序
    若p -> ltag=1,则p -> lchild就是其前驱结点;
    若p -> ltag=0,则若p是根,则无前驱;若P是父亲的左儿子或是右儿子但无左兄弟, 前驱是父亲;若p是父亲的右儿子,且有左兄弟,其前驱是父亲左子树上的最右下结点。

    若p -> rtag=1,则p -> rchild就是其后继结点;
    若p -> rtag=0,则若如果p有左子树,则后继是左子树的根,否则是右子树的根;

  • 后序
    若p -> ltag=1,则p -> lchild就是其前驱结点;
    若p -> ltag=0,若p的右子树不空,则前驱是右子树的根,否则是左子树的根;

    若p -> rtag=1,则p ->rchild就是其后继结点;
    若p -> rtag=0,若p是根,则无后继;若p是父亲的右儿子或是左儿子但无右兄弟,后继是父亲;
    若p是父亲的左儿子,且有右兄弟,其后继是右子树上的最左下结点.

二叉排序树(BST)

  • 二叉排序树定义:
    二叉排序树或者是一棵空树;或者是具有如下特性的二叉树:
    若它的左子树不空,则左子树上所有结点的值均小于根结点的值;
    若它的右子树不空,则右子树上所有结点的值均大于根结点的值;
    它的左、右子树也都分别是二叉排序树。
    递归的数据结构。
    对二叉排序树进行中序遍历可以得到一个递增的有序序列。

  • 二叉排序树的查找算法:
    若二叉排序树为空,则查找不成功;否则
    若给定值等于根结点的关键字,则查找成功;
    若给定值小于根结点的关键字,则继续在左子树上进行查找;
    若给定值大于根结点的关键字,则继续在右子树上进行查找。

  • 查找
    在二叉排序树bt中查找关键字为k的结点,找到后返回该结点的指针,找不到返回NULL。

//递归
void *search(node *root,int x){      
	if(root==NULL){
		printf("search fail\n");
		return ;
	}
	if(x==root->data){
		printf("%d\n",root->data);
	}else if(x<root->data){
		search(x->lchild,x);
	}else{
		search(x->rchild,x);
	}
}
//非递归
int *Search(node *root,int x){      
	node *p=root;
  	while (p!=NULL){	
  		if (p->data==x){	
			return p;
		}else if (x<p->data)
			p=p->lchild;
		}else{
	 		p=p->rchild;		
  		}
  		return NULL;		
}

  • 插入
    若二叉排序树为空树,则新插入的结点为根结点;
    若二叉排序树非空,则新插入的结点必为一个新的叶子结点,并且是查找不成功时查找路径上访问的最后一个结点 的左孩子或右孩子结点。
//递归
void insert(node*root,int x){
	if(root==NULL){
		root=newNode(x);
		return;	
	}
	if(x==root->data)return;
	else if(x<root->data)insert(root->lchild,x);
	else insert(root->rchild,x);
}
//非递归
int insert(node *&root,int x){      
	node *f,*p=root;
	while (p!=NULL){	
		if (p->data==x)return 0;
		f=p;
		if (x<p->key)p=p->lchild;		
		else p=p->rchild;		
  }
  p=nweNode(x);		
  if (root==NULL)root=p;
  else if (x<f->key)f->lchild=p;			
  else f->rchild=p;		
  return 1;				
}

  • 建立
    一个无序序列可通过构造二叉排序树而变成一个有序序列。 构造树的过程就是对无序序列进行排序的过程。

    插入的结点均为叶子结点,故无需移动其他结点。相当于在有序序列上插入记录而无需移动其他记录。

    二叉排序树既有类似于折半查找的特性,又采用了链表作存 储结构。

node* creat(int data[],int n){
	node* root=NULL;
	for(int i=0;i<n;i++){
		insert(root,data[i]);
	}
	return root;
}
  • 销毁
void Destroy(node *&root){      
	if (bt!=NULL){	
		Destroy(root->lchild);	
		DestroyBST(root->rchild);	
		free(bt);	
  }
}
  • 寻找权值最大的节点
node *findMax(node *root){
	while(root->rchild!=NULL){
		root=root->rchild;
	}
	return root;
}
  • 寻找权值最小的节点
node *findMin(node *root){
	while(root->lchild!=NULL){
		root=root->lchild;
	}
	return root;
}
  • 删除节点:在删除某个结点之后,仍然保持二叉排序树的特性。

    若当前节点的权值恰为x,说明找到了想要删除的节点,进入删除处理。若当前节点不存在左右孩子,说明是叶节点,直接删除。若当前节点存在左孩子,那么在左子树中寻找节点前驱pre,然后让pre的数值覆盖该节点,直接删除pre。若当前节点存在右孩子,那么在右子树中寻找节点后继next,然后让next的数值覆盖该节点,直接删除next。

    若当前节点的权值小于给定的权值x,则在左子树中递归删除权值为x的节点。

    若当前节点的权值大于给定的权值x,则在右子树中递归删除权值为x的节点。

void deleteNode(node *&root,int x){
	if(root==NULL)return ;
	if(root->data==x){
		if(root->lchild==NULL&&root->rchild==NULL){
			root=NULL;
		}else if(root->lchild!=NULL){
			node *pre=findMAx(root->lchild);
			root->data=pre->data;
			deletNode(root->lchild,pre->data);
		}else{
			node* next=findMin(root->rchild);
			root->data=next->data;
			deletNode(root->rchild,next->data);
		}
	}else if(root->data>x){
		deleteNode(root->lchild,x);
	}else{
		deleteNode(root->rchild,x);
	}
}
  • 查找效率分析
    二叉排序树的平均查找长度去决定于树的高度,最坏为(单链情况)O(n),最好(完全二叉树)O(log2n)。

    当有序表是静态查找表时,宜用顺序表作为其存储结构,而采用二分查找实现其查找操作。

    若有序表是动态查找表,则应选择二叉排序树作为其逻辑结果。

平衡二叉树(AVL)

  • 左、右子树是平衡二叉树;所有结点的左、右子树深度之差的绝对值≤ 1。 即: |左子树深度-右子树深度| ≤ 1

  • 为了方便起见,给每个结点附加一个数字 = 该结点左子树与右子树的深度差。这个数字称为结的平衡因子。这样,可以得到 AVL 树的其它性质(可以证明): 任一结点的平衡因子只能取:-1、0 或 1;如果树中任意一个结点的平衡因子的绝对值大于 1,则这棵二叉树就失去平衡。 对于一棵有 n 个结点的 AVL 树,其深度和 log n 同数量级, ASL 也和 log n 同数量级。

  • 节点结构

struct node{
	int v,height;
	node *lchild,*rchild;
}
  • 定义一个新节点
node *newNode(int v){
	node *Node=new node;
	Node->v=v;
	node->height=1;
	Node->lchild=Node->rchild=0;
	return Node;
}
  • 获得高度
int getHeight(node *root){
	if(root==NULL)return 0;
	return root->height;
}
  • 计算平衡因子
int getBalanceFactor(node*root){
	return getHeight(root->lchild)-getHeight(root->rchild);
}
  • 更新Height
void updataHeight(node *root){
	root->height=max(getHeight(root->lchild),getHeight(root->rchild));
}
  • 查找
void search(node *root,int x){
	if(root==NULL){
		printf("search fail\n");
		return;
	}
	if(x==root->data){
		printf("%d\n",root->data);
	}else if(x<root->data){
		search(root->lchild,x);
	}else {
		search(root->rchild,x);
	}
}
  • 如果在一棵 AVL 树中插入一个新结点后造成失衡,则
    必须重新调整树的结构,使之恢复平衡。 我们称此调整平衡的过程为平衡旋转。

  • 平衡旋转的类别
    只有在根节点到该插入节点的路径上节点才可能发生平衡因子的变化,因此只需要对这条路径上失衡的节点进行调整。可以证明,只要把最靠近插入节点的失衡节点调整到正常,路径上的所有节点都会平衡。

    LL 平衡旋转

void L(node *&root){
	node *temp=root->rchild;
	root->rchild=temp->lchild;
	temp->lchild=root;
	updateHeight(root);
	updateHeight(temp);
	root=temp;
}

RR 平衡旋转

void R(node *&root){
	node *temp=root->lchild;
	root->lchild=temp->rchild;
	temp->rchild=root;
	updateHeight(root);
	updateHeight(temp);
	root=temp;
}

LR 平衡旋转
先对root->lchild进行左旋,再对root进行右旋

RL 平衡旋转
先对root->rchild进行右旋,再对root进行左旋

  • 插入节点
void insert(node*root,int x){
	if(root==NULL){
		root=newNode(x);
		return;	
	}
	if(v<root->v){
		insert(root->lchild,v);
		updataHeight(root);
		if(getBanlaceFactor(root)==2){
			if(getBanlaceFactor(root->lchild)==1){
				R(root);
			}else if(getBanlaceFactor(root->lchild)==-1){
				L(root->lchild);
				R(root);
			}
		}
	}else {
		insert(root->rchild,v);
		updataHeight(root);
		if(getBanlaceFactor(root)==-2){
			if(getBanlaceFactor(root->lchild)==-1){
				L(root);
			}else if(getBanlaceFactor(root->lchild)==1){
				R(root->lchild);
				L(root);
			}
		}
	}
}
  • 建树
node *create(int data[],int n){
	node *root=NULL;
	for(int i=0;i<n;i++){
		insert(root,data[i]);
	}
	return root;
}

哈夫曼树与哈夫曼编码

  • 定义
    路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径。

    结点的路径长度:两结点间路径上的分支数。

    权:将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。

    结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积。

    树的带权路径长度:树中所有叶子结点的带权路径长度之和。

    最优二叉树:带权路径最小的二叉树,即哈夫曼树。

  • 哈夫曼树的构造
    给定n个权值分别为w1…wn的节点,将这n个节点分别作为n棵仅含一个节点的二叉树,构成森林F。构造一个新节点,从F中选取两棵根节点权值最小的树作为新节点的左右子树,并将新节点的权值置位左右子树上根节点的权值之和。从F中删除刚选出的两棵树,同时将新得到的树加入到F中,重复该步骤,直到F中只剩下一棵树为止。

    每个初始节点最终都成为叶节点,且权值越小的节点到根节点的路径长度越大。

    构造过程中,共新建了n-1个节点,因此哈夫曼树中的节点总数为2n-1。

    不存在度唯一的节点。

  • 求最短路径长度

void func(int data[],int n)
	priority_queue<long long,vector<long,long>,greater<long,long> >q;
	long long temp,x,y,ans;
	for(int i=0;i<n;i++){
		q.push(data[i]);
	}
	while(q.size>1){
		x=q.top;
		q.pop();
		y=q.top;
		q.pop();
		q.push(x+y);
		ans+=x+y;
	}
	printf("%d\n",ans);
  • 哈弗曼编码
    使得任意一个字符的编码都不是另一个字符编码的前缀。意义在于不产生混淆,使得解码顺利进行。
发布了30 篇原创文章 · 获赞 1 · 访问量 386

猜你喜欢

转载自blog.csdn.net/weixin_46265246/article/details/105283932