数据结构和算法(8):搜索树(二叉搜索树和AVL树)

news2025/1/13 11:46:18

查找

所谓的查找或搜索,指从一组数据对象中找出符合特定条件者,这是构建算法的一种基本而重要的操作。其中的数据对象,统一地表示和实现为 词条(entry) 的形式;不同词条之间,依照各自的 关键码(key) 彼此区分。

循关键码访问: 查找的过程与结果,仅仅取决于目标对象的关键码。

词条

template <typename K, typename V> struct Entry { //词条模板类
	K key; V value; //关键码、数值
	Entry ( K k = K(), V v = V() ) : key ( k ), value ( v ) {}; //默认构造函数
	Entry ( Entry<K, V> const& e ) : key ( e.key ), value ( e.value ) {}; //基于克隆的构造函数
	bool operator< ( Entry<K, V> const& e ) { return key < e.key; } //比较器:小于
	bool operator> ( Entry<K, V> const& e ) { return key > e.key; } //比较器:大于
	bool operator== ( Entry<K, V> const& e ) { return key == e.key; } //判等器:等于
	bool operator!= ( Entry<K, V> const& e ) { return key != e.key; } //判等器:不等于
}; //得益于比较器和判等器,从此往后,不必严格区分词条及其对应的关键码

词条对象拥有成员变量keyvalue。前者作为特征,是词条之间比对和比较的依据;后者为实际的数据。

任意词条之间可相互比较大小,是有序向量得以定义,以及二分查找算法赖以成立的基本前提

二叉搜索树

二叉搜索树中,处处都满足顺序性:任一 节点r的 左(右)子树中 ,所有节点( 若存在)均不大于(不小于)r
假定所有节点互不相等 ,回避边界情况:任一 节点r 的 左(右)子树中 ,所有 节点( 若 存在)均小于(大于)r
在实际应用中,对相等元素的禁止既不自然也不必要。

包含不同的 n n n 个节点可以构成的不同二叉搜索树有 C a t a l a n ( n ) = ( 2 n ) ! / [ ( n + 1 ) ! n ! ] Catalan(n) = (2n)!/[(n+1)!n!] Catalan(n)=(2n)!/[(n+1)!n!] 棵。

任何 一棵二叉树是二叉搜索树,当且仅当其中序遍历序列单调非降。
在这里插入图片描述

BST 模板类

#include "../BinTree/BinTree.h"	//引入BinTree

template <typename T> class BST : public BinTree<T>{ //由BinTree派生BST模板类
protected:
	BinNodePosi(T)_hot; //“命中”节点的父亲
	BinNodePosi(T) connect34 (//按照“3+4”结构,联接3个节点及四棵子树
		BinNodePosi(T)BinNodePosi(T)BinNodePosi(T),
		BinNodePosi(T)BinNodePosi(T)BinNodePosi(T)BinNodePosi(T));
	BinNodePosi(T) rotateAt ( BinNodePosi(T) x );//对x及其父亲、祖父做统一旋转调整
public: //基本接口︰以virtual修饰,强制要求所有派生类(BST变种)根据各自的规则对其重写
	virtual BinNodePosi(T) &search ( const T&e );//查找
	virtual BinNodePosi(T) insert ( const T&e );//插入
	virtual bool remove ( const T&e );//删除
};

BST:查找

采用了减而治之的思路与策略,其执行过程可描述为:从树根出发 ,逐步地缩小查找范围,直到发现目标(成功)或缩小至空树(失败)

查找过程中,一旦发现当前节点为NULL,即说明查找范围已经缩小至空,查找失败;否则,视关键码比较结果,向左(更小)或向右(更大)深入,或者报告成功(相等)。

template <typename T> //在以v为根的(AVL、SPLAY、rbTree等)BST子树中查找关键码e
static BinNodePosi(T) & searchIn ( BinNodePosi(T) & v, const T& e, BinNodePosi(T) & hot ) {
	if ( !v || ( e == v->data ) ) return v; //逑弻基:在节点v(或假想的通配节点)处命中
	hot = v; //一般情况:先记下当前节点,然后再
	return searchIn ( ( ( e < v->data ) ? v->lc : v->rc ), e, hot ); //深入一层,递归查找
} //返回时,返回值指向命中节点(或假想的通配哨兵),hot指向其父亲(退化时为初始值NULL)
template <typename T> BinNodePosi(T) & BST<T>::search ( const T& e ) //在BST中查找关键码e
{ return searchIn ( _root, e, _hot = NULL ); } //返回目标节点位置的引用,以便后续插入、删除操作

若查找成功,则searchIn()以及search()的返回值都将如图(a)所示,指向一个关键码为e且真实存在的节点;若查找失败,则返回值的数值虽然为NULL,但是它作为引用将如图(b)所示,指向最后一次试图转向的空节点。
在这里插入图片描述
在调用searchIn()算法之前,search()接口首先将内部变量_hot初始化为NULL,然后作为引用型参数hot传递给searchIn()。在整个查找的过程中,hot变量始终指向当前节点的父亲。因此在算法返回时,按照如上定义,_hot亦将统一指向“命中节点”的父亲。

_hot节点是否拥有另一个孩子,与查找成功与否无关。查找成功时,节点e可能是叶子,也可能是内部节点;查找失败时,假想的哨兵e等效于叶节点,但可能有兄弟。

复杂度
总体所需时间应线性正比于查找路径的长度,或最终返回节点的深度:最好为O(1),最坏为O(n).
若要控制单次查找在最坏情况下的运行时间,须从控制二叉搜索树的高度入手。

BST:插入

为了在二叉搜索树中插入一个节点,首先需要利用查找算法search()确定插入的位置及方向,然后才能将新节点作为叶子插入。

template <typename T> BinNodePosi(T) BST<T>::insert ( const T& e ) { //将关键码插入BST树中
	BinNodePosi(T) & x = search ( e ); if ( x ) return x; //确认目标不存在(留意对_hot癿设置)
	x = new BinNode<T> ( e, _hot ); //创建新节点x:以e为关键码,以_hot为父
	_size++; //更新全树规模
	updateHeightAbove ( x ); //更新x及其历代祖先的高度
	return x; //新插入的节点,必为叶子
} //无论e是否存在于原树中,返回时总有x->data == e

首先调用search()查找e。若返回位置x非空,则说明已有雷同节点,插入操作失败。否则,x必是_hot节点的某一空孩子,于是创建这个孩子并存入e。此后,更新全树的规模记录,并调用updateHeightAbove()更新x及其历代祖先的高度。
按照以上实现方式,无论插入操作成功与否,都会返回一个非空位置,且该处的节点与拟插入的节点相等。如此可以确保一致性,以简化后续的操作。

复杂度
节点插入操作所需的时间,主要消耗于对算法search()updateHeightAbove()的调用。后者与前者一样,在每一层次至多涉及一个节点,仅消耗O(1)时间,故其时间复杂度也同样取决于新节点的深度,在最坏情况下不超过全树的高度。

BST:删除

为从二叉搜索树中删除节点,首先也需要调用算法BST::search(),判断目标节点是否的确存在于树中。若存在,则需返回其位置,然后方能相应地具体实施删除操作。

单分支

在这里插入图片描述
若欲删除节点69,需首先通过search(69)定位待删除节点(69)。因该节点的右子树为空,故只需如图(b)所示,将其替换为左孩子(64),则拓扑意义上的节点删除即告完成。
当然,为保持二叉搜索树作为数据结构的完整性和一致性,还需更新全树的规模记录,释放被摘除的节点(69),并自下而上地逐个更新替代节点(64)历代祖先的高度。注意,首个需要更新高度的祖先(58),恰好由_hot指示。

双分支

设拟再删除二度节点36。如图(b)所示,首先调用BinNode::succ()算法,找到该节点的直接后继(40)。然后,只需如图©所示交换二者的数据项,则可将后继节点等效地视作待删除的目标节点。不难验证,该后继节点必无左孩子,从而相当于转化为此前相对简单的情况。于是最后可如图(d)所示,将新的目标节点(36)替换为其右孩子(46)。
请注意,在中途互换数据项之后,这一局部如图( c )所示曾经一度并不满足顺序性。但这并不要紧–不难验证,在按照上述方法完成整个删除操作之后,全树的顺序性必然又将恢复。
同样地,除了更新全树规模记录和释放被摘除节点,此时也要更新一系列祖先节点的高度。不难验证,此时首个需要更新高度的祖先(53),依然恰好由_hot指示。

remove() 删除关键码

1 template <typename T> bool BST<T>::remove ( const T& e ) { //从BST树中删除关键码e
2 BinNodePosi(T) & x = search ( e ); if ( !x ) return false; //确荏目标存在(留意_hot癿设置)
3 removeAt ( x, _hot ); _size--; //实施删除
4 updateHeightAbove ( _hot ); //更新_hot及其历代祖先的高度
5 return true;
6 } //删除成功与否,由返回值指示

首先调用search()查找e。若返回位置x为空,则说明树中不含目标节点,故删除操作随即可以失败返回。否则,调用removeAt()删除目标节点x。同样,此后还需更新全树的规模,并调用函数updateHeightAbove(_hot),更新被删除节点历代祖先的高度。

/*******************************************容*********容************************容***********
* BST节点删除算法︰删除位置x所指的节点(全局静态模板函数,适用于AVL、Splay、RedBlack等各种BST )
*目标x在此前经查找定位,并确认非NULL,故必删除成功;与searchIn不同,调用之前不必将hot置空
*返回值指向实际被删除节点的接替者,hot指向实际被删除节点的父亲——二者均有可能是NULL
*************************************牢*容**牢****水*容容容***水水水容容容*容容容容容容容容容容容容容容容容容字米林容容容容容**/
template <typename T>
static BinNodePosi(T) removeAt ( BinNodePosi(T) & x,BinNodePosi(T) & hot ) {
	BinNodePosi(T) w = x;	//实际被摘除的节点,初值同x
	BinNodePosi(T) succ = NULL;//实际被删除节点的接替者
	if (!HasLChild ( *x ) )	//若*x的左子树为空,则可
		succ = x = x->rc; //直接将*x替换为其右子树
	else if ( ! HasRChild (*x ) )	//若右子树为空,则可
		succ = x = x->lc; //对称地处理——注意∶此时succ != NULL
	else {	//若左右子树均存在,则选择x的直接后继作为实际被摘除节点,为此需要
		w = w->succ(); //(在右子树中)找到*x的直接后继*w
		swap ( x->data,w->data );//交换*x和*w的数据元素
		BinNodePosi(T) u = w->parent;
		( ( u == x ) ? u->rc : u->lc ) = succ = w->rc;//隔离节点*w
		}
	hot = w->parent;//记录实际被删除节点的父亲
	if ( succ ) succ->parent = hot; //并将被删除节点的接替者与hot相联
	release ( w->data ); release ( w ); return succ;//释放被摘除节点,返回接替者
}

复杂度
删除操作所需的时间,主要消耗于对search()succ()updateHeightAbove()的调用。在树中的任一高度,它们至多消耗O(1)时间。故总体的渐进时间复杂度,亦不超过全树的高度。

平衡

search()insert()remove()等主要接口的运行时间,均线性正比于二叉搜索树的高度。
在最坏情况下,二叉搜索树可能彻底地退化为列表,此时的查找效率甚至会降至O(n),线性正比于数据集的规模。
因此,需要控制树高。

任意的n个互异关键码,都可以构成n!种全排列。若各排列作为输入序列的概率均等,则只要将它们各自所生成二叉搜索树的平均查找长度进行平均,即可在一定程度上反映二叉搜索树的平均查找性能。可以证明,在这一随机意义下,二叉搜索树的平均高度为O(logn)
假定n个互异节点同时给定,然后在遵守顺序性的前提下,随机确定它们之间的拓扑联接。如此,称二叉搜索树由这组节点“随机组成”。n个互异节点组成的二叉搜索树,总共可能有(2n)!/n!/(n + 1)!。若这些树出现的概率均等,则通过对其高度做平均可知 ,平均查找长度为 O ( n ) O(\sqrt n) O(n )

理想平衡与适度平衡

既然二叉搜索树的性能主要取决于高度,故在节点数目固定的前提下,应该尽可能降低高度。相应地,应尽可能使兄弟子树的高度彼此接近,即全树尽可能地平衡。
包含n个节点的二叉树,高度不可能小于 [ log ⁡ 2 n ] [\log_2n] [log2n]。若高度恰好为 [ log ⁡ 2 n ] [\log_2n] [log2n],则称作理想平衡树

在渐进意义下适当放松标准之后的平衡性,称作适度平衡

等价交换

若两颗二叉搜索树的中序遍历序列相同,则称它们彼此等价;反之亦然。
虽然等价二叉搜索树中各节点的垂直高度可能有所不同,但水平次序完全一致。

平衡二叉搜索树的适度平衡性,都是通过对树中每一局部增加某种限制条件来保证的。具有以下局部性:
1)经过单次动态修改操作后,至多只有O(1) 处局部不再满足限制条件
2)总可在O(log n)时间内,使这O(1)处局部(以至全树)重新满足限制条件。
意味着刚刚失去平衡的二叉搜索树,必然可以迅速转换为一棵等价的平衡二叉搜索树。等价二叉搜索树之间的上述转换过程,也称作等价变换

旋转调整

最基本的修复手段,就是通过围绕特定节点的旋转,实现等价前提下的局部拓扑调整。

在这里插入图片描述
zig 旋转(顺时针旋转):将X和v作为c的左子树、右孩子,Y和Z分别作为v的左、右子树。

在这里插入图片描述
zag旋转(逆时针旋转):将v和Z作为c的左孩子、右子树,X和Y分别作为v的左、右子树。

zig和zag旋转均属局部操作,均属于等价操作,仅涉及常数个节点及其之间的联接关系,故均可在常数时间内完成。就与树相关的指标而言,经一次 zig 或 zag 旋转之后,节点v的深度加一,节点c的深度减一;这一局部子树(乃至全树)的高度可能发生变化,但上、下幅度均不超过一层。

AVL树

通过合理设定适度平衡的标准,并借助等价变换,AVL树(AVL tree)可以实现近乎理想的平衡
在渐进意义下,AVL树可始终将其高度控制在O(logn)以内,从而保证每次查找、插入或删除操作,均可在O(logn)的时间内完成。

AVL树:重平衡

任一节点v平衡因子定义为“其左、右子树的高度差”,即:balFac(v) = height(lc(v)) - height(rc(v))
所谓AVL树,即平衡因子受限的二叉搜索树—其中各节点平衡因子的绝对值均不超过1

AVL 模板类

#include "../BST/BST.h”	//基于BST实现AVL树
template <typename T> class AVL : public BST<T>{ //由BST派生AVL树模板类
public:
	BinNodePosi(T) insert ( const T& e );	//插入(重写)
	bool remove ( const T& e );	//删除(重写)
//BST::search()等其余接口可直接沿用
};

#define Balanced(x) ( stature( (x).lc ) == stature( (x).rc ) )//理想平衡条件
#define BalFac(x) ( stature( (x).lc ) - stature( (x).rc ) )//平衡因子
#define AvlBalanced(x)( ( -2<BalFac(x) ) && ( BalFac(x)<2 ) )//AVL平衡条件

在完全二叉树中各节点的平衡因子非01,故完全二叉树必是AVL树。

结论:高度为h的AVL树至少包含fib(h + 3) - 1个节点(数学归纳所得)
包含n个节点的AVL树的高度应为O(logn)

失衡与重平衡

AVL 树经过插入、删除等动态修改操作之后,节点的高度可能发生变化,以致于不再满足AVL树的条件。
因节点x的插入或删除而暂时失衡的节点,构成失衡节点集,记作UT(x)。请注意,若x为被摘除的节点,则UT(x)仅含单个节点;但若x为被引入的节点,则UT(x)可能包含多个节点。

AVL树:插入

新引入节点x后,UT(x)中的节点都是x的祖先,且高度不低于x的祖父。以下,将其中的最深者记作g(x)。在xg(x)之间的通路上,设pg(x)的孩子,vp的孩子。注意,既然g(x)不低于x的祖父,则p必是x的真祖先。
g(x)是因x的引入而失衡,则pv的高度均不会低于其各自的兄弟。

借助如下代码所示的宏tallerChild(),即可反过来由g(x)找到pv

/**************************************************************************************
*在左、右孩子中取更高者
*在AVL平衡调整前,借此确定重构方案
*************************************************************************************/
#define tallerChild(x)( \
	stature( (x)->lc ) > stature( (x)->rc ) ?(x)->lc : (/*左高*/	\
	stature( (x)->lc ) < stature( (x)->rc ) ?(x)->rc : (/*右高*/	\
	IsLChild(* (x)) ? (x)->lc : (x)->rc	/*等高︰与父亲x同侧者(zIg-zIg或zAg-zAg )优先*/	\
) \
) \

单旋

在这里插入图片描述
做逆时针旋转zag(g(x)),得到如图(b)所示的另一棵等价二叉搜索树。

双旋

在这里插入图片描述
节点插入后通过连续的两次旋转操作使AVL树重新平衡
此时,可先做顺时针旋转zig(p),得到如图(b)所示的一棵等价二叉搜索树。再做逆时针旋转zag(g(x)),得到如图( c )所示的另一棵等价二叉搜索树。

实现

template <typename T> BinNodePosi(T) AVL<T>::insert ( const T& e ) {	//将关键码e插入AVL树中
	BinNodePosi(T) & x = search ( e ); if ( x ) return x;//确认目标节点不存在
	BinNodePosi(T) xx = x = new BinNode<T> ( e,_hot ); _sizet+;//创建新节点x
//此时,x的父亲_hot若增高,则其祖父有可能失衡
	for ( BinNodePosi(T) g = _hot; g; g = g->parent ) { //从x之父出发向上,逐层检查各代祖先g
		if ( !AvlBalanced ( *g ) ) { //一旦发现g失衡,则(采用“3+4”算法)使之复衡,并将子树
			FromParentTo ( *g ) = rotateAt ( tallerChild ( tallerchild ( g ) ) );//重新接入原树
			break; //g复衡后,局部子树高度必然复原;其祖先亦必如此,故调整随即结束
		}else //否则( g依然平衡),只需简单地
			updateHeight ( g );//更新其高度(注意∶即便g未失衡,高度亦可能增加)
	}//至多只需一次调整﹔;若果真做过调整,则全树高度必然复原
	return xx;//返回新节点位置
}//无论e是否存在于原树中,总有AVL::insert(e)->data == e

无论单旋或双旋,经局部调整之后,不仅g(x)能够重获平衡,而且局部子树的高度也必将复原。这就意味着,g(x)以上所有祖先的平衡因子亦将统一地复原。换而言之,在AVL树中插入新节点后,仅需不超过两次旋转,即可使整树恢复平衡

算法首先按照二叉搜索树的常规算法,在O(logn)时间内插入新节点x。既然原树是平衡的,故至多检查O(logn)个节点即可确定g(x);如有必要,至多旋转两次,即可使局部乃至全树恢复平衡。
AVL树的节点插入操作可以在O(logn)时间内完成

AVL树:删除

与插入操作十分不同,在摘除节点x后,以及随后的调整过程中,失衡节点集UT(x)始终至多只含一个节点。而且若该节点g(x)存在,其高度必与失衡前相同。另外还有一点重要的差异是,g(x)有可能就是x的父亲。

与插入操作同理,从_hot节点出发沿parent指针上行,经过O(logn)时间即可确定g(x)位置。作为失衡节点的g(x),在不包含x的一侧,必有一个非空孩子p,且p的高度至少为1。于是,可按以下规则从p的两个孩子(其一可能为空)中选出节点v:若两个孩子不等高,则v取作其中的更高者;否则,优先取vp同向者(亦即,vp同为左孩子,或者同为右孩子)。

单旋

在这里插入图片描述
(a)所示,由于在T3中删除了节点而致使g(x)不再平衡,但p的平衡因子非负时,通过以g(x)为轴顺时针旋转一次即可恢复局部的平衡。平衡后的局部子树如图(b)所示。

双旋

在这里插入图片描述
节点删除后通过两次旋转恢复局部平衡。
g(x)失衡时若p的平衡因子为-1,则经过以p为轴的一次逆时针旋转之后(图(b)),即可转化为图(a)。再以g(x)为轴顺时针旋转,即可恢复局部平衡(图( c ))。

在删除节点之后,尽管也可通过单旋或双旋调整使局部子树恢复平衡,但复平衡之后,局部子树的高就全局而言,依然可能再次失衡。这种由于低层失衡节点的重平衡而致使其更高层祖先失衡的现象,称作“失衡传播
失衡传播的方向必然自底而上,而不致于影响到后代节点

实现

template <typename T> bool AVL<T>::remove ( const T& e ) { //从AVL树中删除关键码e
	BinNodePosi(T) & x = search ( e ); if ( !x ) return false;//确认目标存在(留意_hot的设置)
	removeAt ( x,_hot ); _size--;//先按BST规则删除之(此后,原节点之父_hot及其祖先均可能失衡)
	for ( BinNodePosi(T) g = _hot; g; g = g->parent ) { //从_hot出发向上,逐层检查各代祖先g
		if ( !AvlBalanced ( *g ) )//一旦发现g失衡,则(采用“3+4”算法)使之复衡,并将该子树联至
		g = FromParentTo ( *g ) = rotateAt ( tallerchild ( tallerChild ( g ) ) );//原父亲
		updateHeight ( g );//并更新其高度(注意:即便g未失衡,高度亦可能降低)
	}//可能需做O(logn)次调整——无论是否做过调整,全树高度均可能降低
	return true;//删除成功
}//若目标节点存在且被删除,返回true;否则返回false

较之插入操作,删除操作可能需在重平衡方面多花费一些时间。不过,既然需做重平衡的节点都是x的祖先,故重平衡过程累计只需不过O(logn)时间。
综合各方面的消耗,AVL树的节点删除操作总体的时间复杂度依然是O(logn)

AVL树:(3+4)-重构

同样需要从刚发生修改的位置x出发逆行而上,直至遇到最低的失衡节点g(x)。于是在g(x)更高一侧的子树内,其孩子节点p和孙子节点v必然存在,而且这一局部必然可以g(x)pv为界,分解为四棵子树,将它们按中序遍历次序重命名为 T0 至 T3。
若同样按照中序遍历次序,重新排列g(x)pv,并将其命名为abc,则这一局部的中序遍历序列应为:{T0 , a, T1 , b, T2 , c, T3}
将这三个节点与四棵子树重新“组装”起来,恰好即是一棵AVL树
在这里插入图片描述
这一理解涵盖了此前两节所有的单旋和双旋情况。相应的重构过程,仅涉及局部的三个节点及其四棵子树,故称作 “3 + 4”重构

/********************************************************************
*按照“3+4”结构联接3个节点及其四棵子树,返回重组之后的局部子树根节点位置(即b)
*子树根节点与上层节点之间的双向联接,均须由上层调用者完成
*可用于AVL和RedBlack的局部平衡调整
***********************************************************************/
template <typename T> BinNodePosi(T) BST<T>::connect34 (
	BinNodePosi(T) a,BinNodePosi(T) b, BinNodePosi(T) c,
	BinNodePosi(T)T0,BinNodePosi(T)T1,BinNodePosi(T) T2,BinNodePosi(T) T3
) {
	a->lc = T0; if ( T0 ) T0->parent = a;
	a->rc = T1; if ( T1 ) T1->parent = a; updateHeight ( a );
	c->lc = T2; if ( T2 ) T2->parent = c;
	c->rc = T3; if ( T3 ) T3->parent = c; updateHeight ( c );
	b->lc = a; a->parent = b;
	b->rc = c; c->parent = b; updateHeight ( b );
	return b;//该子树新的根节点
}

利用以上connect34()算法,即可视不同情况,按如下具体方法完成重平衡:

/**********************************************************************
* BST节点旋转变换统一算法( 3节点+4子树),返回调整之后局部子树根节点的位置
*注意:尽管子树根会正确指向上层节点(如果存在),但反向的联接须由上层函数完成
*************************************************************************/
template <typename T> BinNodePosi(T) BST<T>::rotateAt ( BinNodePosi(T)v ) { //v为非空孙辈节点
	BinNodePosi(T) p = v->parent;BinNodePosi(T) g = p->parent;//视v、 p和g相对位置分四种情况
	if ( IsLChild ( *p ) )/* zig */
		if ( IsLChild (*v ) ) {/* zig-zig*/
			p->parent = g->parent;//向上联接
			return connect34 ( v, p, g, v->lc, v->rc, p->rc, g->rc );
		}else { /* zig-zag */
			v->parent = g->parent;//向上联接
			return connect34 ( p, v,g,p->lc, v->lc, v->rc, g->rc );
	else/* zag */
		if ( IsRChild (*v ) ) {/* zag-zag */
			p->parent = g->parent;//向上联接
			return connect34 ( g, p, v, g->lc,p->lc,v->lc, v->rc );
		}else { /* zag-zig */
			v->parent = g->parent;//向上联接
			return connect34 ( g, v, p,g->lc, v->lc, v->rc,p->rc );
		}
}

复杂度也依然是O(1)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1030041.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++之类和函数权限访问总结(二百二十七)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

软件的开发步骤,需求分析,开发环境搭建,接口文档 ---苍穹外卖1

目录 项目总览 开发准备 开发步骤 角色分工 软件环境 项目介绍 产品原型 技术选型 开发环境搭建 前端:默认已有 后端 使用Git版本控制 数据库环境搭建 前后端联调 ​登录功能完善 导入接口文档 使用swagger​ 和yapi的区别 常用注解 项目总览 开发准备 开发步骤…

用tkinter+selenium做一个CSDN热榜爬虫

文章目录 UI设计函数封装功能实现 自从学会了分析热榜&#xff0c;就是CSDN热榜分析&#xff0c;每天都要爬下来分析一下热榜都在干什么。但脚本运行到底还是不方便&#xff0c;所以接下来就想办法将其做成一个带有界面的热榜爬虫 UI设计 做一个热榜爬虫的交互式界面&#xf…

IOS版微信8.0.42正式版已推出:新增多语言“翻译”!

微信最近的更新的真的很快&#xff0c;包括Mac、iPhone、PC端以及安卓版本都进行了更新推送。 微信iOS 8.0.42正式版已经向公众开放&#xff0c;这个新版本在原有功能的基础上&#xff0c;对群管理界面、翻译功能、小程序等方面进行了优化和改进&#xff0c;让用户的使用体验更…

嵌入式Linux--进程间通信--共享内存

1. 回顾之前的通信内容&#xff1a; 进程间通信主要有5种通信方式 1、无名管道&#xff08;只能单向发送或接收&#xff09; 2、命名管道&#xff08;同上&#xff09; 3、消息队列&#xff08;可以发送&#xff0c;也能接收消息&#xff09; 4、共享内存&#xff08;有一块公…

呼叫中心系统角色功能的应用

呼叫中心系统拥有强大的功能&#xff0c;根据角色不同能够使用的功能也是不同的&#xff0c;按规则是角色权限越大&#xff0c;可以使用的功能也越多。如普通坐席最重要的工作就是接打电话&#xff0c;没必要使用全部功能&#xff0c;只需有几个话务相关功能就足够了&#xff0…

phpstudy RCE脚本编写(Python)

文章目录 编写过程脚本优化 编写过程 关于phpstudy 2016-2018 RCE漏洞的验证&#xff0c;请移步我的这篇博客 phpstudy2016 RCE漏洞验证。 将之前漏洞验证的数据包复制下来&#xff0c;编写脚本时需要使用&#xff1a; GET /phpinfo.php HTTP/1.1 Host: 10.9.75.164 Upgrade…

无涯教程-JavaScript - SERIESSUM函数

描述 SERIESSUM函数返回幂级数的总和。幂级数展开可近似许多功能。 语法 SERIESSUM (x, n, m, coefficients)争论 Argument描述Required/OptionalXThe input value to the power series.RequiredNThe initial power to which you want to raise x.RequiredMThe step by whi…

找不到名称 “$“。是否需要安装 jQuery 的类型定义? 请尝试使用 `npm i --save-dev @types/jquery`。

vitevue3环境 1、安装jQuery npm install --save jquery 2、在main.ts文件进行配置 declare const window: any; import jQuery from jquery; window.$ window.jQuery jQuery; 注意&#xff1a;需要声明window属性&#xff1b; 要不然会报错&#xff1a;类型“Window &am…

八、数据类型转换

数据类型转换 1.数据类型转换1.1.隐式类型转换1.2.显式类型转换1.3.训练11.4.训练2 —————————————————————————————————————————————————— 1.数据类型转换 类型转换是将一个值从一种类型更改为另一种类型的过程。例如&…

Navicat 武林小秘籍 | 如何在数据同步期间查看源和目标之间的数据差异

可应用操作系统&#xff1a;Windows、macOS、Linux 可应用 Navicat 产品&#xff1a;Navicat for MySQL、Navicat for PostgreSQL、Navicat for Oracle、Navicat for SQL Server、Navicat for MariaDB、Navicat for SQLite、Navicat for MongoDB、Navicat Premium 可应用 Nav…

【记录】Python 之于 C/C++ 区别

记录本人在 Python 上经常写错的一些地方&#xff08;C/C 写多了&#xff0c;再写 Python 有点切换不过来&#xff09; 逻辑判断符号用 and、or、!可以直接 10 < num < 30 比较大小分支语句&#xff1a;if、elif、else使用 、-&#xff0c;Python 中不支持 、- - 这两个…

LVS 负载均衡集群的DR模式配置

集群 集群的概述 集群技术是一种用于提高系统性能、可用性、容错性和可扩展性的关键方法。它涉及将多个计算资源或节点组合在一起&#xff0c;以协同工作以处理任务、服务请求或数据处理。 集群类型 无论是哪种集群&#xff0c;都至少包括两台节点服务器&#xff0c;而对外…

AB实验总结

互联网有线上系统&#xff0c;可做严格的AB实验。传统行业很多是不能做AB实验的。 匹配侧是采用严格的AB实验来进行模型迭代&#xff0c;而精细化定价是不能通过AB实验来评估模型好坏&#xff0c;经历过合成控制法、双重差分法&#xff0c;目前采用双重差分法来进行效果评估。…

无涯教程-JavaScript - MROUND函数

描述 MROUND函数返回一个舍入为所需倍数的数字。 MROUND函数是Excel中的15个舍入函数之一。 语法 MROUND (number, multiple)争论 Argument描述Required/OptionalNumberThe value to round.RequiredMultipleThe multiple to which you want to round number.Required Note…

如何使用chorme版本对应的ChromeDriver(不用更改Chrome版本)

为什么要这样 我们发现在 官网 下载ChromeDriver的地方&#xff0c;没有对于高版本的下载包&#xff0c;可是我们的浏览器版本是 117.0.5938.88&#xff0c;我又不想降低本地的Chrome版本&#xff0c;因为这样也太蠢了&#xff0c;每次更新后可能老版本就不适用了。 使用方法 …

9.2.3.1 【MySQL】XDES Entry链表

当段中数据较少的时候&#xff0c;首先会查看表空间中是否有状态为 FREE_FRAG 的区&#xff0c;也就是找还有空闲空间的碎片区&#xff0c;如果找到了&#xff0c;那么从该区中取一些零碎的页把数据插进去&#xff1b;否则到表空间下申请一个状态为 FREE 的区&#xff0c;也就是…

链表的分割——哨兵位

现有一链表的头指针 ListNode* pHead&#xff0c;给一定值x&#xff0c;编写一段代码将所有小于x的结点排在其余结点之前&#xff0c;且不能改变原来的数据顺序&#xff0c;返回重新排列后的链表的头指针。 思路&#xff0c;把链表分成两个新链表&#xff0c;然后连接起来 代码…

vue3 @click那些事

vue3element-plus vue3 click那些事 1.动态决定click能否点击 1.动态决定click能否点击 只需要在点击事件前加一个判断条件

MTK联发科MT6762/MT8788安卓核心板性能参数对比

MT6762核心板是联发科的八核处理器&#xff0c;采用12nm制程工艺。MT6762具有集成的蓝牙、fm、WLAN和gps模块&#xff0c;是一个高度集成的基带平台&#xff0c;包括调制解调器和应用处理子系统启用LTE/LTE-A和C2K智能手机应用程序。MT6762芯片集成了ARM Cortex-A53&#xff0c…