数据结构之二叉树(《算法笔记》)

初识二叉树(binary tree)

与度为2的树相区别,二叉树有左右之分,是有序的。

递归定义

递归式:对当前结点的左子树和右子树分别递归。
递归边界:当前结点为空时到达“死胡同”。

存储结构——二叉链表

&1 结点结构

struct node{
	typename data;  //数据域
	node* lchild;  //指向左子树根结点
	node* rchild;  //指向右子树根结点
};

&2 新建结点

空树——根结点不存在

node* root = NULL;

新建结点👇

//生成一个新结点,x为结点数据域
node* newNode(int x){
	node* p = new node;  //为新结点申请空间 
	p->data = x;  //结点赋值 
	p->lchild = p->rchild = NULL;  //初始状态下没有左右孩子 
	return p;   //返回新建结点的地址 
} 

【注】新建结点后,左右孩子暂空,不可忘了设置。

基本操作

&1 查找+修改

用了二叉树的递归定义

void search(node* root,int x, int newdata){
	if ( root==NULL ){ //空树(空子树),递归边界
		return; 
	}
	if( root->data==x ){
		root->data = newdata;
	}
	search(root->lchild, x, newdata);  //递归式:往左子树搜索
	search(root->rchild, x, newdata);  //递归式:往右子树搜索
} 

&2 插入

在递归查找过程中,根据二叉树的性质来选择左子树或右子树找到一棵子树进行递归,最后到达空树(“死胡同”)的地方就是查找失败的地方,即二叉树结点插入的位置。

(只能给出一个模板👇)

//将在二叉树中插入一个数据域为x的新结点
void insert(node* &root, int x){
	if ( root==NULL ){  //空树,插入位置,递归边界
		root = newNode(x);  //根结点指向新建结点
		return;
	}
	if ( 由二叉树的性质,x应该插在左子树 ){
		insert(root->lchild, x);  //往左子树搜索,递归式
	}
	else{  //由二叉树的性质,x应该插在右子树
		insert(root->rchild, x);  //往右子树搜索,递归式
	}
}

【注】①整体逻辑与查找差不离,都是向左右子树递归,树空时为递归边界。
关键点: 参数中,根结点指针root传的是 引用 ,使新建结点能真正接到二叉树上去。

&3 创建二叉树

其实就是插入结点的过程,一般将需要插入的数据存储在数组中进行 insert 插入,也可以边输入边 insert插入。

STEP1 创建空根结点 root。
STEP2 用循环将数据 insert 入二叉树中,最后返回根结点。

node* create(vector<int> data){
	node* root = NULL;  //新建空根结点 root
	for ( int i=0; i<data.size(); i++ ){
		insert(root, data[i]);  //插入以二叉树为根的二叉树中 
	} 
	return root;  //返回二叉树的根结点 
} 

二叉树的遍历

前中后序遍历一般用DFS(递归实现),层序遍历一般用BFS(队列实现),复习传送门👉算法提高之搜索专题(《算法笔记》)

前中后序遍历

&1 先序遍历

①递归式:由先序遍历的定义得到——根结点→左子树→右子树。。
②递归边界:空树(子树为空),到达“死胡同”。

void preorder(node* root){
	if ( root==NULL ){
		return;  //递归边界:空树 
	}
	//访问根结点root,例如输出其数据域
	cout << root->data << endl;
	//访问左子树
	preorder(root->lchild);
	//访问右子树
	preorder(root->rchild); 
}

【先序序列的性质】序列的第一个一定是根结点。

&2 中序遍历

void inorder(node* root){
	if ( root==NULL ){
		return;  //递归边界:空树 
	}
	//访问左子树
	preorder(root->lchild);
	//访问根结点root,例如输出其数据域
	cout << root->data << endl;
	//访问右子树
	preorder(root->rchild); 
}

【中序序列的性质】由于中序遍历总把根结点放在左子树和右子树的中间,因此,只要知道根结点,就可以通过根结点在中序遍历学列中的位置区分出左子树和右子树

&3 后序遍历

void inorder(node* root){
	if ( root==NULL ){
		return;  //递归边界:空树 
	}
	//访问左子树
	preorder(root->lchild);
	//访问右子树
	preorder(root->rchild);
	//访问根结点root,例如输出其数据域
	cout << root->data << endl; 
}

【先序序列的性质】序列的最后一个一定是根结点。

层序遍历

&1 初识层序遍历

(复习)对比 BFS基本模板 👉传送门:算法提高之搜索专题(《算法笔记》)

void LayerOrder(node* root){  //这里给的是二叉树的二叉链表
	queue<node*> q;  //注意:队列存储的是地址
	q.push(root);  //根结点入队
	while( !q.empty() ){
		node* now = q.front();  //取出队首元素
		
		//访问队首元素,例如输出其数据域
		cout << now->data << endl;

		q.pop();  //用完且保存好了,即可弹出
		
		//让队首元素的左右孩子入队(如果有的话) 
		if ( now->lchild!=NULL )
			q.push(now->lchild);  //注意要有非空判断 
		if ( now->rchild!=NULL )
			q.push(now->rchild);  //注意要有非空判断
	}
}

【重要】这里队列中的元素是 node* 型而不是 node 型,在数据结构之线性结构(《算法笔记》)中的 STL容器queue 提过,入队只是制造副本,故存放地址

&2 增加:计算每个结点所在层次

①在结点结构中添加一个记录层次的 layer 变量。

struct node{
	int data;
	int layer;  //层次,一般编号从1开始
	node* lchild;
	node* rchild;
};

②每个结点入队前都修改其层次信息。

void LayerOrder(node* root){
	queue<node*> q;  //注意:队列存储的是地址
	root->layer = 1;  //根结点层号为1
	q.push(root);  //根结点入队
	while( !q.empty() ){
		node* now = q.front();  //取出队首元素
		
		//访问队首元素,例如输出其数据域
		cout << now->data << endl;

		q.pop();  //用完且保存好了,即可弹出
		
		//让队首元素的左右孩子入队(如果有的话) 
		if ( now->lchild!=NULL ){
			now->lchild->layer = now->layer + 1;  //左孩子层号是当前层号+1
			q.push(now->lchild);  //注意要有非空判断 
		}
		if ( now->rchild!=NULL ){
			now->rchild->layer = now->layer + 1;  //右孩子层号是当前层号+1
			q.push(now->rchild);  //注意要有非空判断
		}
	}
}

&3 给定先序和中序遍历序列,重建二叉树⭐⭐【重要】

重建:建立新结点,而后根据二叉树的递归定义,将新结点与二叉树链接起来(二叉链表)。

重点 ① 找到根结点中序序列中的下标;
② 分出该根结点下左右子树在两序列中的区间。

递归式: ①往左子树递归,返回左子树根结点地址给root的左孩子指针;
②往右子树递归,返回右子树根结点地址给root的右孩子指针。
递归边界: 先序序列的区间小于0。

//当前先序序列区间为[prel, prer],中序序列区间为[inl, inr],返回根结点地址
node* recreate(int prel, int prer, int inl, int inr){  //参数为两序列的区间边界信息 
	if ( prel>prer )
		return NULL;  //先序序列长度小于0时返回——递归边界
	
	node* root = new node;  //新建结点,存放重建的二叉树的根结点
	root->data = pre[prel];  //赋值,先序序列第一个结点为根结点
	
	//在中序序列中找到in[k]==pre[prel]的结点下标
	int k;
	for ( k=inl; k<=inr; k++ ){
		if ( in[k]==pre[prel] )
			break;
	} 
	
	//左子树的先序区间为[prel+1, prel+k-inl],中序区间为[inl, k-1]
	//返回左子树的根结点地址,赋值给root的左指针
	root->lchild = recreate(prel+1, prel+k-inl, inl, k-1);  
	
	//右子树的先序区间为[prel+k-inl+1, prer],中序区间为[k+1, inr]
	//返回右子树的根结点地址,赋值给root的右指针
	root->rchild = recreate(prel+k-inl+1, prer, k+1, inr);   
	
	return root;  //返回根结点地址。 
} 

二叉树的静态实现——静态二叉链表⭐【实用】

静态初步

&1 结点定义

将左右孩子指针用 int 型代替,用来标识左右子树的根结点在数组中的下标。

const int maxn = 100;
struct node{
	typename data;
	int lchild, rchild;
}Node[maxn];

&2 新建结点

静态指定数组下标即可;用 -1 表示“空”。

int index = 0;
int newNode(int x){
	Node[index].data = x;
	Node[index].lchild = -1;  //以-1表示“空” 
	Node[index].rchild = -1;
	return index++;  //返回新结点在数组中的下标,并且数组指针index后移一位 
}

基本操作

&1 查找+修改

①其实就是先根遍历
②递归边界仍是空树,只是这里用下标为-1标识“空”。

//查找+修改,root为根结点在数组中的下标
void search(int root, int x, int newdata){
	if ( root==-1 ){  //用-1来代替NULL 
		return;  //空树,递归边界(“死胡同”) 
	}
	
	//先根遍历 
	//访问根:若找到x,改成newdata 
	if ( Node[root].data==x )
		Node[root].data = newdata;
	//遍历左子树(递归式)
	search(Node[root].lchild, x, newdata);
	//遍历右子树(递归式)
	search(Node[root].rchild, x, newdata);
} 

&2 插入

根据情况,看往左还是往右递归。

//插入,root为根结点在数组中的下标
void insert(int &root, int x){  //【注意】需要修改地址(下标值) 
	if ( root==-1 ){  //空树,即插入位置(递归边界)
		root = newNode(x);  //给root赋予新的结点在数组中的下标 
		return;
	}
	if ( 由二叉树性质,x应该插在左子树 )
		insert(Node[root].lchild, x);  //递归式
	else
		insert(Node[root].rchild, x);
} 

&3 建立二叉树

//二叉树的建立,函数返回根结点root的下标
int create(int data[], int n){
	int root = -1;  //新建根结点(初始为空树)
	for ( int i=0; i<n; i++ )
		insert(root, data[i]);  //给空结点找一个数组位置并赋值
	return root;  //返回二叉树的根结点的下标

遍历

&1 先/中/后序遍历

递归实现,DFS思想

//先序遍历 
void preorder(int root){
	if ( root==-1 )
		return;  //空树,递归边界
	//访问根结点,例如输出数据域
	cout << Node[root].data;
	//访问左子树
	preorder(Node[root].lchild);
	//访问右子树
	preorder(Node[root].rchild); 
}
//中序遍历 
void inorder(int root){
	if ( root==-1 )
		return;  //空树,递归边界
	//访问左子树
	preorder(Node[root].lchild);
	//访问根结点,例如输出数据域
	cout << Node[root].data;
	//访问右子树
	preorder(Node[root].rchild); 
}
//后序遍历 
void postorder(int root){
	if ( root==-1 )
		return;  //空树,递归边界
	//访问左子树
	preorder(Node[root].lchild);
	//访问右子树
	preorder(Node[root].rchild); 
	//访问根结点,例如输出数据域
	cout << Node[root].data;
}

&2 层序遍历

队列实现,BFS思想

void LayerOrder(int root){
	queue<int> q;  //此处队列存放结点下标
	q.push(root);  //将根结点地址入队
	while( !q.empty() ){
		int now = q.front();  //取出队首元素
		q.pop();  //出队
		printf("%d ", Node[now].data);  //访问队首元素
		if ( Node[now].lchild!=-1 )
			q.push(Node[now].lchild);  //左子树非空
		if ( Node[now].rchild!=-1 )
			q.push(Node[now].rchild);  //右子树非空 
	} 
}

二叉查找树(BST)

二叉查找树(Binary Search Tree,BST),也称为二叉排序树、二叉搜索树。

递归定义

递归式:就数据域而言,左子树 根结点 右子树。
递归边界:空树(“死胡同”)。

性质

BST的中序遍历序列是一个 非递减 序列。

基本操作

&1 查找

由BST的性质决定了——可以只选择其中一棵子树进行遍历——查找将会是从树根到查找结点的一条路径

最坏复杂度: O(h),h为BST的高度。

①递归边界:当前根结点root为空(失败结点),说明查找失败,“死胡同”。
②递归式:根据待查找值x与当前根结点的数据域的大小关系,看往左子树还是右子树查找(递归)。

//查找二叉查找树中数据域为x的结点
void search(node* root, int x){
	if ( root==NULL ){  //递归边界:空树(失败结点) 
		cout << "search failed" << endl;
		return;
	}
	//分情况
	if ( root->data==x )
		cout << root->data;
	else if ( root->data>x )  //x比根结点小,往左子树找 
		search(root->lchild, x);
	else
		search(root->rchild, x);  //x比根结点大,往右子树找 
} 

&2 插入

(1)递归边界:①查找失败的位置,也即需插入结点的位置;②查找成功,无需插入。
(2)递归式:同查找操作。
(3)最坏复杂度:O(h),h为BST的高度。

//在二叉树中插入一个数据域为x的新结点
void insert(node* &root, int x){  //插入操作对链表进行了修改,需要传引用
	//递归边界1 
	if ( root==NULL ){  //失败结点,也即插入位置
		root = newNode(x);  //新建结点赋予空结点(失败结点)
		return; 
	} 
	//递归边界2
	if ( root->data==x )
		return;  //查找成功,无需插入
	else if ( root->data>x ){  //插入位置在左子树
		insert(root->lchild, x); 
	} 
	else
		insert(root->rchild, x);  //插入位置在右子树
} 

&3 建立

node* create(int data[], int n){
	node* root = NULL;  //新建根结点root
	for ( int i=0; i<n; i++ ){
		insert(root, data[i]);  //用insert函数建树 
	} 
	return root;  //返回根结点 
}

【注】同一组相同的数字, 若插入它们的顺序不同,最后生成的BST也可能不同。

&4 删除

【关键】 ① 找到待删除结点前驱结点(比结点权值小的最大结点)——从左子树根结点开始不断沿 rchild 往下直到 rchild为NULL;
或② 找到待删除结点的后继结点(比结点权值大的最小结点)——从右子树根结点开始不断沿 lchild 往下直到 lchild为NULL。

👇找最大/小权值结点——辅助找前驱/后继

//寻找以root为根结点的树中的最大权值结点
node* findMax(node* root){
	while( root->rchild!=NULL ){
		root = root->rchild;  //不断往右 
	}
	return root;
} 
//寻找以root为根结点的树中的最小权值结点
node* findMin(node* root){
	while( root->rchild!=NULL ){
		root = root->lchild;  //不断往左 
	}
	return root;
}

👇总是优先删除前驱/后继结点,就把问题转换为在左子树/右子树中删除结点,如此递归下去,直到递归到一个叶子结点,就可以直接删除了。

//删除以root为根结点的树中权值为x的结点
void deleteNode(node* &root, int x){
	if ( root==NULL ) 
		return;  //不存在x 
	//外层if选择在于递归寻找待删除的结点;内层if选择在于递归删除结点 
	if  ( root->data==x ){  //找到欲删除结点
		if ( root->lchild==NULL && root->rchild==NULL )
			root = NULL;  //叶子结点,把root地址设为NULL,即其父结点的孩子(该结点)为NULL
		else if ( root->lchild!=NULL ){  //左子树非空时 
			node* pre = findMax(root->lchild);  //找到root前驱
			root->data = pre->data;  //用前驱覆盖root
			deleteNode(root->lchild, pre->data);   //递归:删除顶替上来的前驱结点pre 
		} 
		else{  //右子树非空时
			 node* next = findMin(root->rchild);  //找到root的后继
			 root->data = next->data;  //用后继覆盖root
			 deleteNode(root->rchild, next->data);  //递归:删除顶替上来的后继结点next 
		}
	}
	else if ( root->data>x ){
		deleteNode(root->lchild, x);  //往左子树中查找欲删除的x 
	}
	else
		deleteNode(root->rchild, x);  //往右子树中查找欲删除的x 
} 

【注意】 总是优先删除前驱/后继结点容易导致树的左右子树高度极端不平衡。

  1. 解决思路1:每次交替删除前驱或后继。
  2. 解决思路2:记录子树高度,总是优先在高度较高的一棵子树里删除结点。

平衡二叉树(AVL树)

AVL树 仍然是一棵 二叉查找树,只是在 BST 的基础是增加了 “平衡” 的要求:

  1. 保持每个结点的 平衡因子 的绝对值不超过 1 (平衡因子:左子树与右子树高度之差)。
  2. 使树的高度在每次插入元素后仍能保持 O(logn) 的级别,以达到使用 二叉查找树优化数据查询 的目的(查询操作保持 O(logn) 的时间复杂度)。

AVL树的定义

&1 结点结构

加入一个变量height用来记录以当前结点为根结点的子树的高度。

struct node{
	int data, height;  //height为当前子树高度
	node *lchild, *rchild; 
};

&2 新建结点

新建结点初始高度为 1

//新建一个权值为x的结点,返回该结点地址 
node* newNode(int x){
	node* p = new node;  //申请新的空间
	p->data = x;  //赋值
	p->height = 1;  //结点高度初始为1
	p->lchild = p->rchild = NULL;  //初始状态下没有孩子
	return p;  //返回新建结点地址 
}

&3 获取结点root所在子树的当前高度

int getHeight(node* root){
	if ( root==NULL )
		return 0;  //空结点高度为0
	return root->height; 
}

&4 计算平衡因子

平衡因子无法通过平衡因子计算,而是通过子树高度求得。

int getBalanceFactor(node* root){
	//左子树高度减右子树高度
	return getHeight(root->lchild) - getHeight(root->rchild); 
}

&5 更新结点的高度

void updateHeight(node* root){
	//取左右孩子高度中更大的那个再+1
	root->height = max(getHeight(root->lchild), getHeight(root->rchild)) + 1; 
}

基本操作

&1 查找

同二叉查找树。

&2 插入⭐【难点】

先看插入后的主要调整操作——左旋和右旋。

左旋(Left Rotation)

👇实现过程(对root左下旋)
摘自《算法笔记》,侵删
👇搬个代码

void L(node* &root){  //修改需传引用 
	node* tmp = root->rchild;  //root指向A结点,tmp指向B结点
	root->rchild = tmp->lchild;  //步骤1:B的左子树接入A的右子树 
	tmp->lchild = root;  //步骤2:B的左子树接到A
	updateHeight(root);  //更新结点A的高度
	updateHeight(tmp);  //更新结点B的高度
	root = tmp;  //步骤3:B完成左旋,做根结点 
}

右旋(Right Rotation)

👇实现过程(对root右下旋)
在这里插入图片描述
👇搬个代码

void R(node* &root){  //修改需传引用
	node* tmp = root->lchild;   //tmp指向结点root的左孩子(让tmp做根结点) 
	root->lchild = tmp->rchild;  //步骤1:把tmp的右孩子挂到root的左边
	tmp->rchild = root;  //步骤2:让root做tmp的右孩子
	updateHeight(root);  //更新结点 
	updateHeight(tmp);
	root = tmp;  //步骤3:让tmp做根结点
}

往平衡二叉树中插入一个结点时,可能会有结点的平衡因子的绝对值大于 1(只可能是 2 或 -2),那么就需要进行调整,且只有从根结点到该插入结点的路径上的结点才可能发生平衡因子的变化。
而可证,只要把 最靠近插入结点的失衡结点 调整到正常,路径上的所有结点就会平衡

👇插入后的四种树型及调整示意图(BF表示平衡因子)

在这里插入图片描述

树型判定条件调整方法
LLBF(root) = 2, BF(root->lchild) = 1对 root 右下旋

在这里插入图片描述

树型判定条件调整方法
LRBF(root) = 2, BF(root->lchild) =-1先对 root->lchild 左下旋,再对 root 右下旋

在这里插入图片描述(图中,A的平衡因子应为-2)

树型判定条件调整方法
RRBF(root) =-2, BF(root->rchild) =-1对 root 左下旋

在这里插入图片描述(图中,A的平衡因子应为-2)

树型判定条件调整方法
RLBF(root) =-2, BF(root->rchild = 1先对 root->rchild 右下旋,再对 root 左下旋

👇基于二叉查找树的插入操作

void insert(node* &root, int x){
	if ( root==NULL ){  //到达空结点,即插入的位置 
		 root = newNode(x);
		 return;
	}
	if ( root->data>x ){  //x比根结点的权值小 
		insert(root->lchild, x);  //往左子树插入
		//调整
		updateHeight(root);  //更新树高
		if ( getBalanceFactor(root)==2 ){
			if ( getBalanceFactor(root->lchild)==1 )
				R(root);  //LL型,让root右下旋
			else if ( getBalanceFactor(root->lchild)==-1 ){ //LR型 
				L(root->lchild);  //让root->lchild左下旋
				R(root);  //让root右下旋 
			}
		} 
	}
	else{   //x比根结点的权值大 
		insert(root->rchild, x);  //往右子树插入
		//调整
		updateHeight(root);   //更新树高
		if ( getBalanceFactor(root)==-2 ){
			if ( getBalanceFactor(root->rchild)==-1 )
				L(root);   //RR型,让root左下旋
			else if ( getBalanceFactor(root->rchild)==1 ){  //RL型 
				R(root->rchild);   //让root->rchild右下旋
				L(root);   //让root左下旋 
			}
		}
	}
}

&3 建立AVL树

基于上面的插入操作,AVL树的建立只需依次插入n个结点即可。

node* create(int data[], int n){
	node* root = NULL;  //空结点
	for ( int i=0; i<n; i++ ){
		insert(root, data[i]);
	}
	return root;
}

堆(Heap)

堆是一棵完全二叉树;一般用于优先队列的实现,而优先队列默认情况下使用的是大顶堆(每个结点的值都不小于其左右孩子结点的值。

完全二叉树(Complete Binary Tree)⭐⭐⭐【常忘用】

用数组保存完全二叉树时

  1. 结点编号从1开始时,若有结点编号为 i ,则其左孩子的编号为 2i右孩子的编号为 2i+1
  2. 数组中元素存放的顺序——恰好为该完全二叉树的 层序遍历序列
  3. 结点(下标记为index)是叶结点—— 该结点的左子结点编号大于结点总数 index*2 > n(不需要判断右子结点,是因为若完全二叉树的某结点没有左孩子则该结点必没有右子结点)。
  4. 结点(下标记为index)是空结点—— 该结点下标大于结点总数 index>n
  5. 叶子结点个数为 ⌈ n 2 \frac{n}{2} 2n⌉;数组下标在 [1, ⌊ n 2 \frac{n}{2} 2n⌋ ] 范围内的结点都是非叶子结点

堆的结点存储

使用数组来存储完全二叉树,这样,结点就按层序存储于数组中。

const in maxn = 100;
int heap[maxn], n = 10;  //heap为堆,n为元素个数

//也可以用vector动态数组
int n = 10;
vector<int> v(n+1);  //数组下标可以从1开始

基本操作

&1 建堆

给定一个初始序列,怎样把它建成一个堆呢?

从数组最后一个元素开始,(按完全二叉树)从下往上,从右往左,调整结点位置使其符合堆的定义——会发现每次调整都是把结点不断从上往下调整的过程(“下坠”)。

循环 调整结点V:
(1)循环条件:① 结点V存在孩子结点(while条件);② 结点V的孩子结点的权值都比其小(break条件) 。
(2)循环式:不断与其左右孩子(若有)比较,并及时交换。

时间复杂度为 **O(logn)*

//对heap数组在[low, high]范围进行向下调整
//其中low为欲调整结点的数组下标,high一般为堆的最后一个元素的数组下标 
void downAdjust(int low, int high){
	int i = low, j = i*2;  //i为欲调整结点,j为其左孩子 
	while( j<=high ){  //存在孩子结点
		//如果右孩子存在,且右孩子的值大于左孩子
		if ( j+1<=high && heap[j+1]>heap[j] )
			j++;  //让j存储右孩子下标
		//如果孩子中最大的权值比欲调整结点 i 的大
		if ( heap[j]>heap[i] ){
			swap(heap[j], heap[i]);  //交换最大权值的孩子与欲调整的结点i
			i = j;  //保持i为欲调整结点
			j = i*2;  //保持j为欲调整结点的左孩子 
		} 
		else
			break;  //若孩子的权值均小于欲调整结点i的,调整结束 
	}
}

swap函数

【注】上述代码中用到了cpp自带的 swap函数,可以用于交换任意类型的变量,参数传的是引用,在c++11下,包含于 utility 头文件下(但是好像不需要特别去include)。
——————————————————————————朴素的分界线(/▽\)

继续说建堆,从⌊ n 2 \frac{n}{2} 2n⌋号位置开始倒着枚举结点——倒着枚举可以使每次调整完一个结点后的当前子树是一个合格的堆。

时间复杂度为 O(n)

//建堆
void createHeap(){
	for ( int i=n/2; i>=1; i-- ){
		downAdjust(i, n);
	}
} 

&2 删除最大元素(即堆顶元素)

👉只需要最后一个元素覆盖堆顶元素(并让元素个数-1),然后对根结点进行调整即可。

时间复杂度为 O(logn)

void deleteTop(){
	heap[1] = heap[n--];  //用最后一个元素覆盖堆顶元素,并让元素个数-1
	downAdjust(1, n);  //向下调整堆顶元素 
}

&3 插入新元素

可以把新元素放在最后一个结点后面,然后进行 向上调整

循环 调整结点V:
(1)循环条件:① 未到达堆顶(while条件);② 结点V的父结点的权值较大(break条件) 。
(2)循环式:不断与其父结点(若有)比较,并及时交换。

时间复杂度为 O(logn)

//对heap数组在[low, high]范围进行向上调整
//其中low一般设置为 1,high表示欲调整结点的数组下标
void upAdjust(int low, int high){
	int i = high, j = i/2;  //i为欲调整结点,j为其父结点
	while( j>=low ){  //父结点在[low, high]范围内
		//父结点权值小于欲调整结点 i 的权值
		if ( heap[j]<heap[i] ){
			swap(heap[j], heap[i]);  //交换父结点和欲调整结点
			i = j;  //保持 i 为欲调整结点
			j = i / 2;  //保持 j 为 i 的父亲 
		} 
		else
			break;  //父结点比欲调整结点 i 的权值大,调整结束 
	} 
} 

在此基础上易得插入操作的代码👇

//添加元素x
void insert(int x){
	heap[++n] = x;  //让元素个数+1,并将数组最后一个位置给x
	upAdjust(1, n);  //向上调整新加入的结点x 
} 

堆排序

使用堆结构对一个序列进行排序,以递增排序为例。

  1. 建堆:对给定序列建堆。
  2. 倒着枚举(类似删除操作):堆顶元素必然是最大的。
    ① 每次循环将当前堆顶元素换到堆的末尾,即数组末尾是递增序列(数组成为部分有序)。
    ② 对当前堆( [1, i-1] )的堆顶元素进行向下调整
void heapSort(){
	createHeap();  //建堆
	for ( int i=n; i>1; i-- ){  //倒着枚举,直到堆中只有一个元素
		swap(heap[i], heap[1]);  //交换heap[i]与堆顶元素
		downAdjust(1, i-1);  //调整堆顶 
	} 
}

并查集

并查集是一种维护集合的数据结构,支持以下两种操作:

  1. 合并:合并两个集合;
  2. 查找:判断两个元素是否在同一个集合。

并查集的 实现 ——数组

int father[N];

father[i]表示元素 i 的父结点,父结点本身也是这个集合内的元素。

根结点:同一个集合只有一个根结点,且将其作为所属集合的标识

father[i] = i;  //元素 i 是该集合的根结点

基本操作

并查集的使用需要先初始化father数组,然后再根据需要进行查找或合并操作。

&1 初始化
一开始每个元素都是独立的一个集合,因此所有father[i]都为 i 。

for ( inti=1; i<=N; i++ ){
	father[i]= i;
}

&2 查找
对给定的结点寻找其根结点——反复寻找父结点(向上)直到找到根结点

  • 递推法
int findFather(int x){
	while( x!=father[x] )
		x = father[x];  //非根结点则获得自己的父结点
	return x;
}
  • 递归法
int findFather(int x){
	if ( x==father[x] ) return x;
	else findfather(father[x]);
}

&3 合并
把两个集合合并成一个集合。
👉题目中一般给出两个元素,要求把这两个元素所在的集合合并:① 先判断这两个元素是否属于不同集合;② 若不同集合,一般把其中一个集合的根结点的“父指针”指向另一个集合的根结点(根结点的父指针指向自己)。

  1. 对于给定的两个元素a、b,判断它们是否属于同一集合——通过 查找函数 判断其根结点是否相同
  2. 合并两个集合——已获得根结点 faA 与 faB,就可以令 father[faA] = faB。
void Union(int a, int b){
	int faA = findFather(a);
	int faB = findFather(b);
	if ( faA!=faB )
		father[faA] = faB;  //若不属于同一个集合则合并
}

性质

同一个集合中一定不会产生环,即 并查集产生的每一个集合都是一棵树

路径压缩

之前的查找函数当元素数量很多且形成一条链时,效率非常低下。
路径压缩,即优化并查集查找效率——让当前查询结点的路径上的所有结点的 父指针直接指向根结点;复杂度为 O(1)

  • 按原先的写法先获得 x 的根结点 r。

  • 重新从 x 开始到根结点,把路径上经过的所有结点的父亲全部改为根结点 r。

  • 递推法

int findFather(int x){
	int a = x;  //由于x在下面会变成根结点,所有先保存一下
	while( x!=father[x] )
		x = father[x];
	//此时,x存的是根结点;下面把路径上所有结点的father都改成根结点
	while( a!=father[a] ){
		int z = a;  //因为a要被father[a]覆盖,故先保存a,以修改father[a]
		a = father[a];  //a回溯父结点
		father[z] = x;  //将原先的结点a的父亲改为根结点
	}
	return x;  //返回根结点
}
  • 递归法
int findFather(int x){
	if ( x==father[x] ) return x;  //找到根结点
	else{
		int F = findFather(father[x]);  //递归找到father[x]的根结点F
		father[x] = F;   //让x的父指针直接指向根结点
		return F;  //返回根结点F
	}
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值