数据结构 第五章 树与二叉树

目录

第五章 树与二叉树

一、树

树的定义和基本术语

树的性质

二、二叉树

1.二叉树的定义和基本术语

基本概念:

几个特殊的二叉树:

2.二叉树的基本性质

二叉树的常考性质

完全二叉树的常考性质

3.二叉树的存储结构

顺序存储

链式存储

初始化:

4.二叉树的遍历

1.先中后序遍历

先序遍历

中序遍历

后序遍历

前序+中序遍历序列

后序+中序遍历序列

层序+中序遍历序列

5.线索二叉树

6.二叉树的线索

中序线索化

先序线索化

后序线索化

7.线索二叉树找前驱/后继

中序线索二叉树

先序线索二叉树

后序线索二叉树

总结

三、树的存储结构

1.双亲表示法(顺序存储):

2.孩子表示法(顺序+链式存储)

3.**孩子兄弟表示法(链式存储)

森林和二叉树的相互转换

四、哈夫曼树(最优二叉树)

带权路径长度

哈夫曼树的定义

哈夫曼树的构造

哈夫曼编码

五、并查集

逻辑结构——“集合”

“并查集”的存储结构——双亲表示法

“并查集”的基本操作

Union操作优化(小树合并到大树)

并查集的进一步优化

Find操作的优化(压缩路径)


第五章 树与二叉树

一、树

树的定义和基本术语

        定义:

        树是n(n>=0)个结点的有限集合,n=0时,称为空树,这是一种特殊情况。在任意一颗非空树中满足:

  1. 有且仅有一个特定的称为的结点。
  2. 当n>1时,其余结点可分为m(m>0)个互不相交的有限集合T1,T2,...,Tm,其中每个集合本身又是一棵树,并且称为根结点的子树

        空树\oslash

        :结点数为0的树

        非空树的特性:

        有且仅有一个根节点

        没有后继的结点称为“叶子节点”(或终端节点)

        有后继的结点称为“分支节点”(或非终端节点)

        除了根结点外,任何一个结点都有且仅有一个前驱

        每个结点可以有0个或多个后继。

基本术语:

结点、树的属性描述:

        两个结点之间的路径:只能从上往下数

        路径的长度:经过的边数

        结点的层次(深度)-- 从上往下数

        结点的高度 -- 从下往上数

        树的高度(深度) -- 总共多少层

        结点的 -- 有多少个孩子(分支)

        树的 -- 各结点的度的最大值

有序树V.S无序树

        有序树 -- 逻辑上看,树中结点的各子树从左至右是有次序的,不能互换

        无序树 -- 逻辑上看,树中结点的各子树从左至右是无次序的,可以互换

树V.S森林

        森林,是m(m>=0)颗互不相交的树的集合 (eg:全中国所有人家的家谱),m可以为0,空森林。

树的性质

常见考点1:结点数 = 总度数+1

        结点的度 -- 结点有几个孩子(分支)

常见考点2:度为m的树、m叉树的区别

常见考点3:度为m的树第i层至多有mi-1个结点(i>=1)

        常见考点4:高度为h的m叉树至多有个结点

        等比数列求和公式:a + aq + aq2 + ... + aqn-1 =

常见考点5:高度为h的m叉树至少有h个结点。

   高度为h、度为m的树至少有h+m-1个结点。

       

常见考点6:具有n个结点的m叉树的最小高度为

        高度最小的情况 -- 所有结点都有m个孩子 

二、二叉树

1.二叉树的定义和基本术语

基本概念:

定义:

        二叉树是n(n>=0)个结点的有限集合:

  1. 或者为空二叉树,即n=0
  2. 或者由一个根结点和两个互不相交的被称为根的左子树右子树组成。左子树和右子树又分别死一颗二叉树。

特点:1.每个结点至多只有两颗子树

           2.左右子树不能颠倒(二叉树是有序树

二叉树的五种状态:

几个特殊的二叉树:

        满二叉树。一颗高度为h,且含有2h-1个结点的二叉树

        特点:

  1. 只有最后一层有叶子结点
  2. 不存在度为1的结点
  3. **按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1;结点i的父节点为[i/2](如果有的话)

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

        (满二叉树是完全二叉树的一种)

        特点:

  1. 只有最后两层可能有叶子结点
  2. 最多只有一个度为1的结点
  3. 同上3)
  4. i<=[n/2]为分支结点,i>[n/2]为叶子结点

        二叉排序树。一颗二叉树或者是空二叉树,或者是具有如下性质的二叉树:

        左子树上所有结点的关键字小于根结点的关键字;

        右子树上所有结点的关键字大于根结点的关键字。

        左子树和右子树又各是一颗二叉排序树。

平衡二叉树。树上任一结点的左子树右子树深度之差不超过1。

2.二叉树的基本性质

二叉树的常考性质

        常见考点1:设非空二叉树中度为0、1和2的结点个数分别为n0、n1和n2,则n0 = n2 + 1 (叶子结点比二分支结点多一个)

        假设树中结点总数为n,则

  1. n = n0 + n1 + n2
  2. n = n1 +2n2 + 1 (树的结点数 = 总度数+1)

     2.-1. = n0 = n2 + 1

        常见考点2:二叉树第i层至多有2i-1个结点(i>=1)

m叉树第i层至多有mi-1个结点(i>=1)

        常见考点3:高度为h的二叉树至多有个2h-1结点(满二叉树)

                高度为h的m叉树至多有个结点

        等比数列求和公式:a + aq + aq2 + ... + aqn-1 =

完全二叉树的常考性质

        常见考点1:具有n个(n>0)结点的完全二叉树的高度h为

        第i个结点所在层次为

        常见考点2:对于完全二叉树,可以由结点数n推出度为0、1和2的结点个数为n0、n1和n2

        完全二叉树最多只有一个度为1的结点,即:

        n1 = 0或1

        n0 = n2 + 1 -> n0 + n2一定是奇数

        若完全二叉树有2k个(偶数)个结点,则必有n1 = 1,n0 = k,n2 = k-1

        若完全二叉树有2k个(偶数)个结点,则必有

n1 = 0,n0 = k,n2 = k-1

3.二叉树的存储结构

顺序存储

二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来

#define MAXSIZE 100
typedef struct TreeNode
{
	int data;//结点中的数据元素
	int isEmpty;//结点是否为空
}TreeNode;

TreeNode t[MAXSIZE];
//初始化时所有结点标记为空
for (int i = 0; i < MAXSIZE; i++)
{
	t[i].isEmpty = 1;
}

几个重要的常考的基本操作:

  1. i的左孩子 -- 2i
  2. i的右孩子 -- 2i+1
  3. i的父节点 --
  4. i所在的层次 --

完全二叉树中共有n个结点,则

  1. 判断i是否有左孩子? -- 2i<=n?
  2. 判断i是否有右孩子? -- 2i+1<=n?
  3. 判断i是否是叶子/分支结点? -- i>

如果是非完全二叉树,要把二叉树的结点编号与完全二叉树对应起来

  1. i的左孩子 -- 2i
  2. i的右孩子 -- 2i+1
  3. i的父节点 --

        非完全二叉树的存储密度不是很高,最坏情况:高度为h且只有h个结点的单支树(所有结点只有右孩子),也至少需要2h-1个存储单元

        结论:二叉树的顺序存储结构,只适合存储完全二叉树

链式存储
typedef struct BiTNode
{
	int data;//数据域
	struct BiTNode* lchild, * rchild;//左、右孩子指针
}BiTNode,*BiTree;

如果没有对应的左、右孩子,就把对应的指针置成NULL

        n个结点的二叉链表共有n+1个空链表

        如果一个二叉树有n个结点,那么就有2n个指针域,除了根结点,n-1个结点的头上都会连有一个指针的,因此在2n个指针域当中就有n-1个是指向NULL的(这些空指针域可以利用起来,构造线索二叉树)

初始化:
//定义一个空树
BiTree root = NULL;
//插入根结点
root = (BiTree)malloc(sizeof(BiTNode));
root->data = 1;
root->lchild = NULL;
root->rchild = NULL;
//插入新结点
BiTNode* p = (BiTNode*)malloc(sizeof(BiTNode));
p->data = 2;
p->lchild = NULL;
p->rchild = NULL;
root->lchild = p;//作为根结点的左孩子

 

        找到指定p结点的左、右孩子,只需要检查左孩子指针或右孩子指针是指向哪的就可以了

        如果想找指定结点p的父结点,只能从根结点开始遍历寻找

(Tips:根据实际需求决定要不要加父结点指针)

//三叉链表--方便找父结点
typedef struct BiTNode
{
	int data;//数据域
	struct BiTNode* lchild, * rchild;//左、右孩子指针
	struct BiTNode* parent;//父结点指针
}BiTNode, * BiTree;

4.二叉树的遍历

        遍历:按照某种次序把所有结点都访问一遍

1.先中后序遍历

        二叉树的递归特性:

  • 要么是个空二叉树
  • 要么就是由“根结点+左子树+右子树”组成的二叉树

        先序遍历:左右(NLR)

        中序(根)遍历:左右(LNR)

        后序(根)遍历:左右(LRN)

手算练习

分支结点逐层展开法

先序遍历

先序遍历(PreOrder)的操作过程如下:

  1. 若二叉树为空,则什么也不做;
  2. 若二叉树非空:
  • 访问根结点:
  • 先序遍历左子树
  • 先序遍历右子树。
    //先序遍历
    void PreOrder(BiTree* T)
    {
    	if (T != NULL)
    	{
    		visit((*T));//访问根结点(打印……)
    		PreOrder((*T)->lchild);//递归遍历左子树
    		PreOrder((*T)->rchild);//递归遍历右子树
    	}
    }

        脑补空结点,从根节点出发,画一条路:

        如果左边还有没走的路,优先往左边走

        走到路的尽头(空结点)就往回走

        如果左边没路了,就往右边走

        如果左、右都没路了,则往上面走

        先序遍历――第一次路过时访问结点(每个节点都会被路过三次)

中序遍历

中序遍历(InOrder)的操作过程如下:

  1. 若二叉树为空,则什么也不做;
  2. 若二叉树非空:
  • 先序遍历左子树
  • 访问根结点:
  • 先序遍历右子树。
void InOrder(BiTree* T)
{
	if (T != NULL)
	{
		InOrder((*T)->lchild);//递归遍历左子树
		visit((*T));//访问根结点(打印……)
		InOrder((*T)->rchild);//递归遍历右子树
	}
}

        脑补空结点,从根节点出发,画一条路:

        如果左边还有没走的路,优先往左边走

        走到路的尽头(空结点)就往回走

        如果左边没路了,就往右边走

        如果左、右都没路了,则往上面走

        先序遍历――第二次路过时访问结点(每个节点都会被路过三次)

后序遍历

同理,把visit(T);放到最后位置即可。

        脑补空结点,从根节点出发,画一条路:

        如果左边还有没走的路,优先往左边走

        走到路的尽头(空结点)就往回走

        如果左边没路了,就往右边走

        如果左、右都没路了,则往上面走

        先序遍历――第三次路过时访问结点(每个节点都会被路过三次)

空间复杂度:O(h)

  1. 层次遍历

        算法思想:

  • 初始化一个辅助队列
  • 根结点入队
  • 若队列非空,则队列结点出队,访问该结点,并将其左,右孩子插入队尾(如果有的话)
  • 重复3直至队列为空

代码实现:

//二叉树结点(链式存储)
typedef struct BiTNode
{
	char data;
	struct BiNode* lchild, * rchild;
}BiTNode,*BiTree;

//链式队列结点
typedef struct LinkNode
{
	BiTNode* data;//存指针,而不是存结点
	struct LinkNode* next;
}LinkNode;

typedef struct
{
	LinkNode* front, * rear;//队头队尾
}LinkQueue;
void LevelOrder(BiTree* T)
{
	LinkQueue Q;
	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);//右孩子入队
		}
	}
}

  1. 由遍历序列构造二叉树

        结论:若只给出一棵二叉树的前/中/后/层 遍历序列中的一种,不能唯一确定一棵二叉树;而如果给出 前序+中序/后序+中序/层序+中序 这三种的一种遍历序列,就可以得到与之对应的唯一的二叉树

前序+中序遍历序列

后序+中序遍历序列

原理和上述相同

层序+中序遍历序列

        结论:前序、后序、层序序列的两两组合无法唯一确定一棵二叉树

5.线索二叉树

        如何找到指定结点p在中序遍历序列中的前驱?

思路:

        从根结点从发,重新进行一次中序遍历,指针q记录当前访问的结点,指针pre记录上一个被访问的结点

  • 当q==p时,pre为前驱
  • 当pre==p时,q为后继

        缺点:找前驱、后继很不方便;遍历操作必须从根开始

指向前驱、后继的指针被称为“线索”

        当把二叉树线索化,变成线索二叉树,我们找指向结点的前驱、后继结点变得更方便,并且遍历也更方便

先序线索二叉树同理

后序线索二叉树亦同理

6.二叉树的线索

中序线索化

        初步建成的二叉树 ltag、rtag = 0

//线索二叉树结点
typedef struct ThreadNode
{
	int data;
	struct ThreadNode* lchild, * rchild;
	int ltag, rtag;
}ThreadNode,*ThreadTree;
//全局变量 pre,指向当前访问结点的前驱
ThreadNode* pre = NULL;
//中序遍历二叉树,一边遍历一边线索化
void InThread(ThreadTree T)
{
	if (T != NULL)
	{
		InThread(T->lchild);//中序遍历左子树
		visit(T);//访问根结点
		InThread(T->rchild);//中序遍历右子树
	}
}
void visit(ThreadNode* q)
{
	if (q->lchild == NULL)//左子树为空,建立前驱线索
	{
		q->lchild = pre;
		q->ltag = 1;
	}
	if (pre!=NULL && pre->rchild == NULL)
	{
		pre->rchild = q;//建立前驱结点的后继线索
		pre->rtag = 1;
	}
		pre = q;
}
//中序线索化二叉树T
void CreateInThread(ThreadTree T)
{
	pre = NULL;
	if (T!= NULL)
	{
		InThread(T);
		if (T = NULL)
		{
			pre->rchild = NULL;
			pre->rtag = 1;
		}
	}
}

 

先序线索化

        同理

//--------先序线索化
void PreThread(ThreadTree T)
{
	if (T != NULL)
	{
		visit(T);//先处理根结点
		PreThread(T->lchild);
		PreThread(T->rchild);
	}
}

 

        这里会进入死循环,要进行修改,在访问左子树时添加条件

//--------先序线索化
void PreThread(ThreadTree T)
{
	if (T != NULL)
	{
		visit(T);//先处理根结点
		if(T->ltag == 0)//lchild不是前驱线索
			PreThread(T->lchild);
		PreThread(T->rchild);
	}
}

后序线索化

        同理,在中序线索化基础上,把visit放在最后,不会出现死循环问题,而且不用处理最后一结点的rchild,rtag

7.线索二叉树找前驱/后继

中序线索二叉树

找后继

在中序线索二叉树中找到指定结点*p的中序后继next

  • 若p->rtag == 1,则next = p->rchild
  • 若p->rtag == 0

next = p的右子树中最左下结点

代码实现:

ThreadNode* Firstnode(ThreadNode* p)
{
	//循环找到最左下结点(不一定是叶子结点)
	while (p->ltag == 0)
	{
		p = p->lchild;
	}
	return p;
}
ThreadNode* NextNode(ThreadNode* p)
{
	//右子树中最左下结点
	if (p->rtag == 0)
	{
		return Fristnode(p->rchild);
	}
	else
	{
		return p->rchild;//rtag == 1直接返回后继线索
	}
}

        对中序线索二叉树进行中序遍历(利用线索实现的非递归算法)

void Inorder(ThreadNode* T)
{
	for (ThreadNode* p = Firstnode(T); p != NULL; p = NextNode(p)
	{
		visit(p);
	}
}

此方法遍历的空间复杂度为O(1)

找前驱:

        在中序线索二叉树中找到指定结点*p的中序前驱pre

  • 若p->ltag == 1,则pre = p->lchild
  • 若p->ltag == 0

        pre = p的左子树中最右下角结点

代码实现:

  

//找到以P为根的子树中,最后一个被中序遍历的结点
ThreadNode* Lastnode(ThreadNode* p)
{
	//循环找到最右下结点(不一定是叶子结点)
	while (p->rtag == 0)
	{
		p = p->rchild;
	}
	return p;
}

//在中序线索二叉树中找到结点p的前驱结点
ThreadNode* PreNode(ThreadNode* p)
{
	//左子树中最右下角结点
	if (p->ltag == 0)
	{
		return Lastnode(p->lchild);
	}
	else
	{
		return p->lchild;//ltag == 1直接返回前驱线索
	}
}

对中序线索二叉树进行逆向中序遍历

void RevInoder(ThreadNode* T)
{
	for (ThreadNode* p = Lastnode(T); p != NULL; p = PreNode(p))
	{
		visit(p);
	}
}

先序线索二叉树

找后继

        在先序线索二叉树中找到指定结点*p的先序后继next

  • 若p->rtag == 1,则next = p->rchild
  • 若p->rtag == 0

ThreadNode* NextNode(ThreadNode* p)
{
	if (p->rtag == 0)//结点的后继线索为空
	{
		if (p->lchild != NULL)//结点左孩子不空返回其左孩子
		{
			return p->lchild;
		}
		else
		{
			return p->rchild;//为空则返回右孩子
		}
	}
	else
	{
		return p->rchild;//不空则返回其后继线索
	}
}

 

找前驱

        在先序线索二叉树中找到指定结点*p的先序前驱pre

  • 若p->ltag == 1,则next = p->lchild
  • 若p->ltag == 0

改用三叉链表可以找到父结点

  • 如果能找到p的父结点,且p是左孩子

    

  • 如果能找到p的父结点,且p是右孩子,其左兄弟为空

    

  • 如果能找到p的父结点,且p是右孩子,其左兄弟非空

  • 如果p是根结点,则p没有先序前驱
后序线索二叉树

找前驱

在后序线索二叉树中找到指定结点*p的后序前驱pre

  • 若p->ltag == 1,则pre = p->lchild
  • 若p->ltag == 0

ThreadNode* NextNode(ThreadNode* p)
{
	if (p->ltag == 0)
	{
		if (p->rchild != NULL)
		{
			return p->rchild;//右孩子不空则返回右孩子
		}
		else
		{
			return p->lchild;//否则返回左孩子
		}
	}
	else
	{
		return p->lchild;//ltag == 1直接返回前驱线索
	}
}

找后继

        在后序线索二叉树中找到指定结点*p的后序后继next

  • 若p->rtag == 1,则next = p->rchild
  • 若p->rtag == 0

    

        改用三叉链表可以找到父结点

  • 如果能找到p的父结点,且p是右孩子

  • 如果能找到p的父结点,且p是左孩子,其右兄弟为空

    

  • 如果能找到p的父结点,且p是左孩子,其右兄弟非空

  • 如果p是根结点,则p没有后序后继

总结

三、树的存储结构

1.双亲表示法(顺序存储):

双亲表示法:每个结点中保存指向双亲的“指针”

#define MAX_TREE_SIZE 100//树中最多结点数
typedef struct//树的结点定义
{
	int data;//数据元素
	int parent;//双亲位置域
}PTNode;

typedef struct//树的类型定义
{
	PTNode nodes[MAX_TREE_SIZE];//双亲表示
	int n;//结点数
}PTree;

增:

  

删:

        方案一:将下标变成-1

        方案二:将底部元素移上去(更忧)

        并且还要修改n值

        如果删除的不是叶子结点

  

2.孩子表示法(顺序+链式存储)

孩子表示法:顺序存储各个结点,每个结点中保存孩子链表头指针

struct CTNode
{
	int child;//孩子结点在数组中的位置
	struct CTNode* next;//下一个孩子
};
typedef struct
{
	int data;
	struct CTNode* firstChild;//第一个孩子
}CTBox;

typedef struct
{
	CTBox nodes[MAX_TREE_SIZE];
	int n, r;//结点数和根的位置
}CTree;

 

3.**孩子兄弟表示法(链式存储)

        孩子兄弟表示法存储的树,从存储视角来看形态上和二叉树类似

        考点:树与二叉树的相互转换。本质就是用孩子兄弟表示法存储树

 

typedef struct CSNode
{
	int data;//数据域
	struct CSNode* fristchild, * nextsibling;//第一个孩子和右兄弟指针
}CSNode,*CSTree;

        优点:可以用我们熟悉的二叉树操作来处理树

森林和二叉树的相互转换

        本质:用二叉链表存储森林

  

        各个树的根结点视为兄弟关系

 

四、哈夫曼树(最优二叉树)

带权路径长度

结点的:有某种现实含义的数值(如:表示结点的重要性等)

结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值乘积

树的带权路径长度:树中所有叶结点的带权路径长度之和(WPL,Weighted Path Length)WPL =        

哈夫曼树的定义

        在含有n个带权叶子结点的二叉树中,其中带权路径长度(WPL) 最小的二叉树称为哈夫曼树,也称最优二叉树

哈夫曼树的构造

        给定n个权值分别为w1,w2,...,wn的结点,构造哈夫曼树的算法描述如下:

  1. 将这n 个结点分别作为n棵仅含一个结点的二叉树,构成森林F
  2. 构造一个新结点,从F中选取两棵结点权值最小的树作为新结点的左右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
  3. F中删除刚才选出的两棵树,同时将新得到的树加入F中。
  4. 重复步骤2)和3),直至F中只剩下一棵树为止。

  1. 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大
  2. 哈夫曼树的结点总数为2n-1
  3. 哈夫曼树中不存在度为1的结点。
  4. 哈夫曼树并不唯一,但WPL必然相同且为最优

哈夫曼编码

        固定长度编码 -- 每个字符用相等长度的二进制位表示

        可变长度编码 -- 允许对不同字符用不等长的二进制位表示

        若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码

        由哈夫曼树得到哈夫曼编码 -- 字符集中的每个字符作为一个叶子结点,各个字符出现的频度作为结点的权值,根据之前介绍的方法构造哈夫曼树

五、并查集

逻辑结构——“集合”

        将各个元素划分为互不相交的子集

        在集合这种逻辑结构之下,两个元素的关系,要么从属于同一个集合,要么就从属于不同的集合

代码表示集合的思路:

        同一子集中的各个元素,组织成一个棵树,用互不相交的树,表示多个集合

如何“”到一个元素到底属于哪个集合?

        ——从指定元素出发,一路向北,找到根结点

如何判断两个元素是否属于同一个集合?

        ——分别到两个元素的根,判断根结点是否相同即可

如何把两个集合“”为一个集合?

——让一棵树成为另一棵树的子树即可

“并查集”的存储结构——双亲表示法

“并查集”的基本操作

        集合的两个基本操作——“”和“

        Find——“查”操作:确定一个指定元素的所属集合

        Union——“并”操作:将两个不相交的集合合并为一个

注:并查集(Disjoint Set)是逻辑结构——集合的一种具体实现,只进行“并”和“查”两种基本操作

//--------并查集
#define SIZE 13
int UFSets[SIZE];//集合元素数组

//初始化并查集
void Initial(int S[])
{
	for (int i = 0; i < SIZE; i++)
	{
		S[i] = -1;
	}
}

//Find“查”操作,找x所属集合,(返回x所属根结点)
int Find(int S[], int x)
{
	//循环寻找x的根
	while (S[x] >= 0)
	{
		x = S[x];
	}
	return x;//根的S[]小于0
}

若结点数为n,则Find最坏时间复杂度:O(n)

//Union“并”操作,将两个集合合并为一个
void Union(int S[], int Root1, int Root2)
{
	//要求Root与Root2是不同的集合
	if (Root1 == Root2)
	{
		return;
	}
	//将根Root2连接到另一根Root1下面
	S[Root2] = Root1;
}

时间复杂度:O(1)

Union操作优化(小树合并到大树)

        让Find的时间复杂度降低的优化思路:在每次Union操作构建树的时候,尽可能让树不长高高

  • 用根结点的绝对值表示一棵树(集合)的结点总数
  • Union操作,让小树合并到大树
//Union “并”操作,小树合并到大树
void Union(int S[], int Root1, int Root2)
{
	//要求Root与Root2是不同的集合
	if (Root1 == Root2)
	{
		return;
	}
	if (S[Root1] < S[Root2])//Root2结点数更少
	{
		S[Root1] += S[Root2];//累加结点总数
		S[Root2] = Root1;//小树合并到大树
	}
	else
	{
		S[Root2] += S[Root1];//累加结点总数
		S[Root1] = Root2;//小树合并到大树
	}
}

 该方法构造的树高不超过

 Union操作优化后,Find操作最坏时间复杂度:O()

并查集的进一步优化

Find操作的优化(压缩路径)

        压缩路径——Find操作,先找到根结点,再将查找路径上所有结点都挂在根结点下

//Find“查”操作优化,先找到根结点,再进行“压缩路径”
int Find(int S[], int x)
{
	int root = x;
	//循环找到根
	while (S[root] >= 0)
		root = S[root];
	//压缩路径
	while (x != root)
	{
		int tmp = S[x];//tmp指向x的父结点
		S[x] = root;//x直接挂到根节点下
		x = tmp;
	}
	return root;//返回根结点编号
}

        每次Find操作,先找根,再“压缩路径”,可使树的高度不超过O(a(n))。a(n)是一个增长很缓慢的函数,对于常见的n值,通常a(n)<=4,因此优化后并查集的Find、Union操作时间开销都很低

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值