数据结构 树(下)
一、概述
AVL树、伸展树、红黑树搜索树算法保证最坏情况或者一系列操作情况下,搜索、插入和删除的操作的时间复杂度是O(logn)。本文主要内容包含:平衡搜索树中的AVL树、伸展树、(2,4)树、红黑树 和(a,b)树、B树等实际运用的树数据结构。
二叉搜索树的删除
二、AVL树
1、基本知识
1、AVL树是维持对数O(logn)的高度的特殊二叉搜索树。“高度”指根节点到叶子节点最长路径上的节点的数量。“None”的孩子的高度是0,叶子节点的高度是1,父节点是叶子节点的高度加1
2、AVL树具有 高度平衡属性:对于树T的每个位置P,P的孩子的高度最多相差 1。AVL树子树也是一颗AVL树!
3、一个高度为h(h≥3)的拥有最少节点的AVL树,必须有两颗子树:一颗高度 h-1,另一颗高度为 h-2:n(h)=1+n(h-1)+n(h-2) h≥3 (n(1)=1、n(2)=2)
4、AVL树的高度h和节点总数n的关系:h < 2log n +2
5、AVL树平衡因子是指位置p的两颗子树高度差(左树减右树/右树减左树),平衡因子的绝对值不大于1。
2、更新操作
1、插入,AVL树在叶子节点产生一个新的节点插入新节点p,导致p的祖先节点不满足高度平衡属性,通过trinode重组,两次“旋转”重建树T,使其满足AVL树的高度平衡属性
2、删除,AVL树删除操作导致一个节点拥有或一个孩子,导致树不满足AVL树的高度平衡属性,
3、更新后不满足高度平衡属性的树通过“旋转”:不满足高度平衡属性的节点p需要“旋转”。如果p的孩子节点和子孙节点(非叶子节点的子孙节点)都是左孩子或右孩子,单次旋转,否则trinode重组,从而满足高度平衡属性。如果左、右子孙节点都不是叶子节点,平衡操作是不唯一的!可以单次旋转,也可以 trinode 重组。如果单次旋转和trinode重组都能满足操作,优先进行单次旋转!注:有些子树是维持不变的(static),可以快速确定平衡树结构!
3、AVL树性能
1、标准的二叉搜索树的操作运行时间O(h),AVL树是维持对数O(logn)的高度的特殊二叉搜索树,但是AVL树维护平衡因子和重组一颗AVL树的额外工作受树中路径长度的限制。
2、AVL树运行时间
4、AVL树Python实现
三、伸展树
1、基本知识
1、伸展树在树的高度上没有一个严格的对数上限。伸展树无须额外的高度、平衡或与此树节点关联的其他辅助数据。
2、伸展树的效率取决于某一位置移动到根节点的操作(伸展),每次在插入、删除甚至搜索都是从最底层的位置p开始。
3、伸展操作会使得被频繁访问的元素更快接近于根,从而减少典型的搜索时间。伸展树保障了混合插入、删除、搜素一系列操作的平均操作时间是对数运行的时间
2、伸展(重复操作伸展,知道节点x变成伸展树的根节点)
2.1、zig-zig型 z>y>x-----x>y>z # y和x都在树的左边或者右边,祖先>父节点>子孙节点;父节点>左孩子+右孩子,位置x深度减少2层
2.2、zig-zag型 z>y>x----x>y+z # y和x在树的位置相反,祖先>父节点>子孙节点;父节点>左孩子+右孩子,位置x深度减少2层
2.3、zig型 y>x------------x>y # 父节点>孩子节点,位置x深度减少1层
3、伸展规则
3.1、当搜索键k时,如果在位置P出找到k,则伸展p。否则,在搜素失败的位置伸展叶子节点。存在叶子节点p的key是14并且p的父节点的key是16,没有节点的key是15,搜索14和搜索15都是伸展叶子节点p!
3.2、当插入键k时,伸展新插入的内部节点k
3.3、当删除键k时,在位置p进行伸展,其中p是被移除节点的父节点。del M[k]的两种情况:被移除节点有两种情况:一、移除节点是原来包含键k的节点;二、移除的节点是一个有替代键的后代节点(两个孩子,删除包含键k的节点原来的元素之后,又使用r=befor(p)元素代替p,p节点没有被删除只是替换节点,r节点才被删除,r节点的父节点才是p)。
3.4、位置p和父节点、祖父节点的关系按照zig-zig型、zig-zag型、zig型进行一次或多次伸展操作,直到位置p变为伸展树的根节点
4、伸展摊销分析
4.1、一个简单的zig-zig型、zig-zag型、zig型伸展影响常数数量级的节点,操作时间是O(1)。将位置p进行伸展需要多个伸展组合,如果位置p的深度是d,则将位置p伸展至根节点需要时间是O(d),也就是从位置p的伸展所消耗的时间等同于从根节点到位置p自上而下的搜索!
4.2、最坏的情况:一颗高度为h的伸展树,进行搜索、插入、删除的全部运行时间是O(h),h最大可能接近 n!
4.3、最坏的情况下,伸展树不是一个好的数据结构,但是一系列混合搜索、插入、删除操作中,平均每个操作所需要的时间仅仅是对数时间,在平摊的意义上,伸展树的性能良好!
四、(2,4)树(2-4或2-3-4)
1、基本知识
1、链式存储结构表示一般树(通用树),每个节点配置一个容器,该容器存储指向每个孩子的引用。二级映射:节点的children字段使用列表,表项是指向该节点孩子的引用。
2、使用通用树表示多路搜索树时,必须在每个节点存储一个或多个与该节点相关联的键值对。
3、多路搜索树:树T中每个内部节点w(d-node)至少有两个孩子(d≥2);树T中的每个d-node w,其孩子c1、c2、c3...cd 按顺序存储 d-1 个键值对(k1,v1)、(k2,v2)..........(kd-1,vd-1);定义:k0=- ∞、kd=+∞,每个条目(k,v)存储在一个以 Ci 为根的 w 的子树的一个节点上,其中 i=1,...,d,ki-1≤ k ≤ ki,- ∞≤ k ≤ +∞。
4、d-node w存有 d-1 个常规键,如果认为 d-node w 中键集合包含k0=- ∞、kd=+∞,那么存储在一个以 Ci 为根的 w 的子树上的键 k 一定是存储在w上的两个键(k0=- ∞、kd=+∞)“之间” 的一个。
5、一个有n个节点的多路搜索树有n+1个外部节点,多路搜索的外部节点不存储任何数据,仅仅作为占位符,以 None 引用表示!
6、(2,4)树是多路搜索树的一种特殊情况:大小属性:每个内部节点最多有4个孩子(dmax=4);深度属性:所有外部节点具有相同的深度
7、(2,4)树父节点使用一个无序列表或有序数组存储指向孩子节点的引用,因为dmax=4,所有的操作可以达到O(1),
8、由大小属性,深度为h的树的外部节点4h,由深度属性,至少有2h个外部节点。(2,4)树:2h≤ n+1 ≤ 4h>>> h ≤ log(n+1) ≤ 2h >>> 1/2 log(n+1) ≤ h ≤ log(n+1)
9、分裂操作:(2,4)树节点溢出时,将节点 w 分裂成两个节点 w' 和 w''。w'是 3-node,保存 2个常规键 k1、k2;w''是 2-node,保存 1个常规键 k4;如果 w 是根节点,创建一个新的根节点 U,让 U 称为 w 的父节点,U是一个 2-node,保存 1个常规键 k3;如果 w 不是根节点,分裂后的两个节点 w' 和 w''替代 w 称为父节点的两个子节点,并且在父节点新增加1个常规键 k3这可能导致溢出传播到 w 的父节点!
10、转移操作:下溢时,节点 w 的兄弟节点 s 是一个 3-node 或者 4-node节点,将 s 的一个键转移到 w 的父节点 u 上,并将 u 的一个键移动到 w 。(根据位置和键的顺序选择合适的 k)
11、融合操作:下溢时,节点 w 的兄弟节点 s 都是 2-node 节点。创建一个新节点 w' 将 ,w 的父节点 u 上的一个键和兄弟节点 s的一个键融合到w' 。(根据位置和键的顺序选择合适的 k)
12、(2,4)树新的(k,v)插入是自下向上插入。w 是 4-node时,由于dmax=4 出现溢出情况,才会进行分裂,根据属性调整。否则(k,v)插入在叶子节点。
2、(2,4)树操作:插入、删除
1、插入:新的(k,v)插入到(2,4)树中,如果树 T 中没有键为k的节点,搜索停在外部节点z,令w指向外部节点z的父节点,在w上插入新的(k,v),并且在z的左边对w添加一个新的孩子节点y(外部节点)>>满足深度属性;如果插入前, w 已经是一个 4-node,即 w 存有 3 个常规键。由于dmax=4,增加节点y和新的键值对使得 w 不满足大小属性!即在节点 w 节点溢出。进行分裂操作,调节节点溢出的节点及其父节点的键和孩子节点,可能溢出会一直传播根节点从而创建一个新的根节点!
2、删除:情况一、删除键为k的节点的孩子节点全为None,而且该节点 d≥3,直接删除该节点。情况二、删除键为k的节点的孩子节点全为None,而且该节点 d=2,即该节点的键值对的键只有k,删除k>>不满足深度属性!即在节点 w 下溢。检查 w 的兄弟节点是否有一个 3-node 或者 4-node s,如果有,就进行转移操作;如果没有,进行融合操作。融合操作可能一直传播根节点,那根节点被删除!并融合产生新的根节点!
3、(2,4)树性能分析
1、(2,4)树的高度是O(logn)
2、(2,4)树的分裂操作、转移操作、融合操作的操作时间需要O(1)。
3、(2,4)树搜索、插入、删除一个节点需要访问O(logn)个节点
五、红黑树
1、基本知识
1、红黑树:一颗带有红色和黑色节点的二叉搜索树,具有属性:
根属性:根节点是黑色的;
红色属性:红色节点(如果有)的子节点都是黑色的;>>>>最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节!
深度属性:具有零个或一个子节点的所有节点都具有相同的黑色深度(被定义为黑色祖先节点(包含父结点,父结点的父结点...)的数量);
所有的 NULL 叶子节点都是黑色
所有的 NULL 节点到根节点的路径上的黑色节点数量一定是相同的
2、红黑树:从任一节点到其叶子节点(null)的所有路径都包含相同数目的黑色节点。>>>>所有最长的路径都有相同数目的黑色节点!
3、红黑树通过对从根到叶子节点的路径中各个节点着色方式的限制(红色属性、),从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。
4、给定的一颗红黑树可以构建对应的(2,4)树:合并每一个红色节点 w 到它的父节点,从 w 存储条目到其父节点,并使 w 的子节点变得有序。
5、红黑树的深度属性与(2,4)树的深度属性相对应,每个红黑树的每个黑色节点查号对相应的(2,4)树的每个节点有贡献,所以可以将任意的(2,4)树改编成对应的红黑树。w 有 2-node、3-node、4-node
6、在 2-3-4 树上的插入和删除操作也等同于在红黑树中颜色翻转和旋转
7、红黑树的高度 h:log(n+1)-1 ≤ h ≤ 2 log(n+1)- 2;d=树T深度属性值,令T‘为树 T 对应的(2,4)树,(2,4)树 T‘的高度是 h',有红黑树和(2,4)树对应关系得,h'=d。由(2,4)树性质得:d ≤ log(n+1)- 1,而且根据红色属性,h ≤ 2d >>> h ≤ 2 log(n+1)- 2;又有二叉树的性质得:log(n+1)-1 ≤ h。最后:log(n+1)-1 ≤ h ≤ 2 log(n+1)- 2
2、红黑树操作:插入、删除
2.1、红黑树插入
1、新插入的节点,如果是根节点,节点为黑色;否则新插入的节点默认为红色
2、红黑树新插入的(k,v),由于红黑树的是二叉树,增加新节点保存(k,v)。
3、新插入的节点(k,v)插入到(2,4)树,插入维持了(2,4)树的深度属性和大小属性,但是可能违反红黑树的红色属性
新插入的节点 x 的父节点 p 是黑色,直接插入!
节点 x 处的双红色:新插入的节点 x 的父节点 p 是红色。由节点 p 是红色,所以节点 p 不是根节点;由于原来树 T 满足红色属性,所以节点 p 的父节点 G 是黑色
3.1、 情况一、节点 p 的兄弟节点 s 是黑色或无:如果节点 x 的父节点 y都在树的左边或右边 单次旋转;如果节点 x 的父节点 y在树的相反边 trinode重组
单次旋转 着色 trinode重组 着色
3.2、情况二、节点 p 的兄弟节点 s 是红色,节点 x 处的双红色表示在相应的(2,4)树 T‘中溢出。红黑树重新着色相当于(2,4)树 T‘的分裂操作。红黑树重新着色:将节点 p 和节点 s 着色为黑色,父节点 G 着色为红色(如果父节点 G 是根节点,父节点 G 还是黑色)注:上滤需要一个栈或者保持父链来实现,并且过程复杂!
注:双红问题在重新着色后,双红色会在树 T 更高位置出现,因为 G 可能还有一个红色的父亲节点 Gp。双红问题就从节点 x 传播到节点 G ,在节点 G重新考虑出现双红问题的两种情况!
一、自底向上插入
二、:自顶向下插入
/** * 自顶向下插入 */ public void insert( AnyType item ){ nullNode.element=item; current=parent=grand=header; //自顶向下调整,避免插入的父节点和父节点的兄弟节点为黑色的情况,该情况复杂不利于恢复平衡信心. while(compare(item,current)!=0){ great=grand; grand=parent; parent=current; current=compare(item,current)<0?current.left:current.right; if(current.left.color==RED&¤t.right.color==RED){ handleReorientAfterInsert(item); } } if(current!=nullNode){//重复元素跳过 return; } //找到位置 //构建新的节点 current=new RedBlackNode<AnyType>(item,nullNode,nullNode); //维护与父节点的关系 if(compare(item,parent)<0){ parent.left=current; }else{ parent.right=current; } //插入完成后,维护平衡信息 handleReorientAfterInsert(item); nullNode.element=null; } /** * 插入后维护平衡信息 * @param item */ private void handleReorientAfterInsert(AnyType item) { //初步调整的变换颜色 自己变为红色,两个儿子变为红色 current.color=RED; current.left.color=BLACK; current.right.color=BLACK; if(parent.color==RED){ //调整后破坏了红黑树性质,需要旋转 //分两种类型 一字形和之字形,之字形比一字形调整了多一步 grand.color = RED; if((compare(item,grand)<0)!=(compare(item,parent)<0)){//之字形 parent=rotate(item,grand); //调整parent和他的儿子,并将调整后的节点W设置成parent } //调整完成,重新设置当前节点 current=rotate(item,great); //并将当前节点设置为黑色 current.color=BLACK; } //保证根节点是黑色 header.right.color=BLACK; }
2.2、红黑树删除
1、红黑树 T 中删除键 K 的项和二叉搜索树的删除过程相似。在结构上,删除至多有一个孩子的节点(或者是最初包含键 K 的节点 p 或者是节点 p 的前继节点),并提升其剩余的子节点
2、红黑树删除节点 p 是红色的==(2,4)树 T‘表示为 3-node或4-node的转移操作,树的结构没有改变同时满足要求的属性!红黑树删除节点 p 是黑色的==(2,4)树 T‘表示为 2-node 除去一个项目,树的结构发生改变!在结构上删除至多有一个孩子的节点,并提升其剩余的子节点!
3、删除操作归结于可以删除红色的树叶。一、如果要删除的节点有右儿子,以右儿子的最小元内容替换要删除节点内容,之后删除右儿子最小元来进行删除(最左项)。二、如果只有左儿子,以左儿子最大元内容替换要删除节点的内容,之后删除左儿子最大元(最右项)。三、如果要删除的节点没有儿子, 将该节点调整成红色,将父节点对应的引用设置成nullNode:若父节点为header,将树变为空树;否则如果当前节点为黑色,进行调整,保证删除项为红色,之后将要删除项的父节点的引用设置为nullNode。
4、红黑树删除:情况一、如果删除的节点 p 是红色的,这种结构性变化不会影响树中任何路径的黑色深度,也没有违反红色属性。情况一删除操作在其对应的(2,4)树 T‘表示为 3-node或4-node的转移操作。情况二、如果删除的非根节点 p 是黑色的:1、节点 p 与兄弟 T 的儿子都是黑色;2、节点 p 的儿子是黑色,兄弟T有一个左儿子是红色;3、节点 p 的儿子是黑色,兄弟T有一个右儿子是红色;4、节点 p 的儿子是黑色,兄弟T儿子都是红色。在相应的(2,4)树 T‘表示为从一个 2-node 节点中除去一个项目。如果没有重新平衡,删除操作会导致沿着通往删除项的路径的黑色深度不足!
5、如果删除的是左节点,则将前驱的值复制到该节点中,然后删除前驱;如果删除的是右节点,则将后继的值复制到该节点中,然后删除后继。
6、删除前驱或后驱相当于是一种“取巧”的方法,红黑树删除删除节点的目的是使该节点的(k,v)在红黑树上不存在。因此专注于该目的,不关注删除节点时是否真是想删除的那个节点,同时也不需考虑树结构的变化,因为树的结构本身就会因为自动平衡机制而经常进行调整。因为要删除的是左节点 64,找到该节点的前驱 63;然后用前驱的值 63替换待删除节点的值 64,此时两个节点(待删除节点和前驱)的值都为 63!
7、个人理解:前继或后继都是满足至多只有一个孩子的节点。如果是将位置p的(k,v)赋值为前继或后继节点的(k,v)都能维持搜索树的键的结构特性!选择前继和后继节点是一样的而效果!