平衡树——AVL算法

  • 平衡树建立在二叉搜索树的基础上,加入了两侧子树大小相对平衡的特性而避免了很多情况下的算法退化。这里AVL算法实现的AVL树就是平衡树的一种。

1.二叉搜索树

在说平衡树之前我们得先复习一下二叉搜索树BST的定义:

  • 一棵二叉树为二叉搜索树当且仅当它是一颗空树或者同时满足下列条件

    • 1.根结点的值大于左子树上所有结点的值。
    • 2.根结点的值小于右子树上所有结点的值。
    • 3.左、右子树都是二叉搜索树。

显然我们如果有一个已经建立好二叉搜索树的序列,那就可以很容易地找出某个数的前驱、排名(或者求第k大的数)等,时间复杂度与树的高度有关,一般为 \(O(log_2n)\)

不过,参考下列的序列,如果建立二叉搜索树,则收效甚微:

\[1, 2, 4, 5, 7, 9, 3, 10, 14, 13, 17
\]

这一序列大部分是有序递增的,这就导致我们总是插入右子树,也就使得二叉树变成了“蚯蚓形”,高度大大增加。进而时间复杂度也接近 \(O(n)\),失去了树结构的优势

2.平衡树的一种——AVL树

平衡树要实现的特性比较直接:让每棵二叉搜索树的左右子树高度相差不大,这样就能保持住 \(O(log_2n)\) 的时间优势,AVL算法是实现途径之一

  • 建立一棵AVL树需要在二叉搜索树BST每个节点上加入 平衡因子 这一概念:

    • 0代表左右子树高度相同
    • 1代表右子树比左子树高1
    • -1代表左子树比右子树高1,以此类推
  • 记录平衡因子的过程很简单,只需要在插入的时候对经过的父节点进行更新即可

  • 不过我们并不会让这一数字的绝对值大于等于2,因为每次插入之后我们会回溯,如果检查到某一节点的平衡因子绝对值大于等于2,则对此节点进行旋转操作。进而将平衡因子绝对值控制到小于等于1

如何旋转在下面介绍

旋转操作的实现

先表明一下我们在这棵AVL树中用到的变量:

struct avl
{
int fa; //父节点
int ls; //左儿子
int rs; //右儿子
int v; //节点权值
int bt; //平衡因子
}

可知,我们旋转的时候,有可能是bt <= -2或者bt >= 2(即左子树偏高与右子树偏高),之后便涉及到四种旋转:LL,RR,LR,RL,先介绍简单情况下的前两种

基础简单旋转

  • 1.LL旋转

我们遇到下面这种树时

显然应该这样做:把6变为4的右儿子,把4设置为根,1不变

这是最为简单的LL旋转

较为完整的表述:对某一节点进行LL旋转,就是让他的左儿子替代它的位置,它成为左儿子的右儿子,然后左儿子的右儿子成为它的左儿子。 下图涵盖了这一情况

经过LL旋转后:

完整地实践了上述加粗的表述

实现函数如下

void ll(int o)
{
int oo = aa[o].ls;
aa[oo].fa = aa[o].fa;
if (aa[oo].fa == 0)
{
ro = oo;
}
if (aa[o].fa)
{
if (aa[aa[o].fa].v < aa[o].v)
{
aa[aa[o].fa].rs = oo;
}
else
{
aa[aa[o].fa].ls = oo;
}
}
aa[o].fa = oo;
aa[o].ls = aa[oo].rs;
if (aa[oo].rs)
{
aa[aa[oo].rs].fa = o;
}
aa[oo].rs = o;
}
  • 2.RR旋转

这里要说的是,如果理解了LL旋转,则RR旋转也就没有问题了,因为它就是LL旋转的镜像操作:

经过RR旋转后:

实现函数如下

void rr(int o)
{
int oo = aa[o].rs;
aa[oo].fa = aa[o].fa;
if (aa[oo].fa == 0)
{
ro = oo;
}
if (aa[o].fa)
{
if (aa[aa[o].fa].v < aa[o].v)
{
aa[aa[o].fa].rs = oo;
}
else
{
aa[aa[o].fa].ls = oo;
}
}
aa[o].fa = oo;
aa[o].rs = aa[oo].ls;
if (aa[oo].ls)
{
aa[aa[oo].ls].fa = o;
}
aa[oo].ls = o;
}

组合旋转

  • 1.LR旋转

先看下面这个情况,我们应该如何旋转?

显然由于这棵树的最底部的节点在“左子树的右子树上”,所以即使经过LL旋转,按照规则,我们也不能使其左右子树平衡

正解是先让根的左儿子节点进行一次RR旋转,变为之前的情形:

然后进行LL旋转,这样就可以两次旋转来使其平衡,较复杂情况如下:

先对左儿子节点进行RR旋转:

再对根进行LL旋转后:

  • 2.RL旋转

如果理解了LR旋转,那其实RL旋转也不需要解释了,因为仍然是LR旋转的镜像操作————先让根的右儿子节点进行一次LL旋转,然后进行RR旋转

实际旋转条件

  • 我们是以bt的值来判断这一节点是否需要旋转的,但是如何知道用什么旋转?

  • 可以参考之前给出的例子,下面标出了每个节点的bt值:

此时我们进行的是LL旋转

此时我们进行的是LR旋转

  • 这里的道理也很明显:

1.我们首先发现根节点bt == -2,说明左子树偏高

2.然后去检查左子树

3.在第一个图中发现bt == -1,两个值都是负,说明:这棵树的最底部的点在左子树的左子树上,所以只需要进行一次LL旋转就可以

4.在第二个图中发现bt == 1,前正后负,说明:这棵树的最底部的点在左子树的右子树上,需要进行LR旋转

而像下面对右旋的分析就不再展开,原理也是相似的

  • 方法总结:

    • 如果根节点平衡因子等于-2,左儿子的为-1,则进行LL旋转
    • 如果根节点平衡因子等于-2,左儿子的为1,则进行LR旋转
    • 如果根节点平衡因子等于2,右儿子的为1,则进行RR旋转
    • 如果根节点平衡因子等于2,右儿子的为-1,则进行RL旋转

例题

洛谷P1168 中位数

AVL平衡树代码(太长了):

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std; int n, a;
int nw = 1;
int ro = 1; struct avl
{
int fa;
int ls;
int rs;
int su;
int v;
int bt;
int lt;
} aa[100005]; void ll(int o)
{
int oo = aa[o].ls;
aa[oo].fa = aa[o].fa;
if (aa[oo].fa == 0)
{
ro = oo;
}
if (aa[o].fa)
{
if (aa[aa[o].fa].v < aa[o].v)
{
aa[aa[o].fa].rs = oo;
}
else
{
aa[aa[o].fa].ls = oo;
}
} aa[o].fa = oo;
aa[o].ls = aa[oo].rs; if (aa[oo].rs)
{
aa[aa[oo].rs].fa = o;
}
aa[o].lt -= aa[oo].su + aa[oo].lt;
aa[oo].rs = o;
} void rr(int o)
{
int oo = aa[o].rs;
aa[oo].fa = aa[o].fa;
if (aa[oo].fa == 0)
{
ro = oo;
}
if (aa[o].fa)
{
if (aa[aa[o].fa].v < aa[o].v)
{
aa[aa[o].fa].rs = oo;
}
else
{
aa[aa[o].fa].ls = oo;
}
}
aa[o].fa = oo;
aa[o].rs = aa[oo].ls;
if (aa[oo].ls)
{
aa[aa[oo].ls].fa = o;
}
aa[oo].lt += aa[o].su + aa[o].lt;
aa[oo].ls = o;
} void rtt(int o)
{
if (aa[o].bt > 0)
{
int ooo = aa[o].ls;
if (aa[ooo].bt > 0)
{
ll(o);
aa[o].bt = aa[ooo].bt = 0; return;
}
if (aa[ooo].bt < 0)
{
int ors = aa[ooo].rs;
rr(ooo);
if (aa[ors].bt != -1)
{
aa[ooo].bt = 0;
}
else
{
aa[ooo].bt = 1;
}
aa[ors].bt = 1; ll(o);
aa[o].bt = aa[ors].bt = 0; return;
} }
if (aa[o].bt < 0)
{
int ooo = aa[o].rs;
if (aa[ooo].bt < 0)
{
rr(o);
aa[o].bt = aa[ooo].bt = 0;
//rrn(o);
return;
}
if (aa[ooo].bt > 0)
{
int ols = aa[ooo].ls;
ll(ooo);
if (aa[ols].bt != 1)
{
aa[ooo].bt = 0;
}
else
{
aa[ooo].bt = -1;
}
aa[ols].bt = -1;
//lln(ooo);
rr(o);
aa[o].bt = aa[ols].bt = 0;
//rrn(o);
return;
}
}
} void bu(int o, int f, int x)
{
aa[o].v = x;
aa[o].su++;
aa[o].fa = f;
} int cr(int o, int x)
{
if (x == aa[o].v)
{
++aa[o].su;
return 0;
}
else if (x < aa[o].v)
{
++aa[o].lt;
if (aa[o].ls)
{
int cc = cr(aa[o].ls, x);
aa[o].bt += cc;
if (aa[o].bt == 2 || aa[o].bt == -2)
{
rtt(o);
return 0;
}
if (!aa[o].bt)
{
return 0;
}
if (aa[o].bt == 1 || aa[o].bt == -1)
{
if (aa[aa[o].fa].ls == o)
{
return 1;
}
else
{
return -1;
}
}
return 0;
}
aa[o].ls = nw;
bu(nw++, o, x);
++aa[o].bt;
if (aa[o].bt == 2)
{
rtt(o);
return 0;
}
if (aa[o].bt == 1)
{
if (aa[aa[o].fa].ls == o)
{
return 1;
}
else
{
return -1;
}
}
return 0;
}
else
{
if (aa[o].rs)
{
int cc = cr(aa[o].rs, x);
aa[o].bt += cc;
if (aa[o].bt == 2 || aa[o].bt == -2)
{
rtt(o);
return 0;
}
if (!aa[o].bt)
{
return 0;
}
if (aa[o].bt == 1 || aa[o].bt == -1)
{
if (aa[aa[o].fa].ls == o)
{
return 1;
}
else
{
return -1;
}
}
return 0;
}
aa[o].rs = nw;
bu(nw++, o, x);
--aa[o].bt;
if (aa[o].bt == -2)
{
rtt(o);
return 0;
}
if (aa[o].bt == -1)
{
if (aa[aa[o].fa].ls == o)
{
return 1;
}
else
{
return -1;
}
}
return 0;
}
} void md(int o, int p)
{
if (aa[o].lt < p && aa[o].lt + aa[o].su >= p)
{
printf("%d\n", aa[o].v);
return;
}
if (aa[o].lt >= p)
{
md(aa[o].ls, p);
}
if (aa[o].lt + aa[o].su < p)
{
md(aa[o].rs, p - (aa[o].lt + aa[o].su));
}
}
int main()
{
// freopen("P1168.txt", "r", stdin);
// freopen("P1168_1.in", "r", stdin);
// freopen("P1168_2.out", "w", stdout);
scanf("%d", &n);
scanf("%d", &a);
bu(nw++, 0, a);
printf("%d\n", a);
for (int i = 2; i <= n; i++)
{
scanf("%d", &a);
cr(ro, a);
if (i & 1)
{
md(ro, i / 2 + 1);
}
}
return 0;
}

平衡树——AVL算法的更多相关文章

  1. Algorithms: 二叉平衡树(AVL)

    二叉平衡树(AVL):   这个数据结构我在三月份学数据结构结构的时候遇到过.但当时没调通.也就没写下来.前几天要用的时候给调好了!详细AVL是什么,我就不介绍了,维基百科都有.  后面两月又要忙了. ...

  2. 树-二叉平衡树AVL

    基本概念 AVL树:树中任何节点的两个子树的高度最大差别为1. AVL树的查找.插入和删除在平均和最坏情况下都是O(logn). AVL实现 AVL树的节点包括的几个组成对象: (01) key -- ...

  3. 二叉平衡树AVL的插入与删除(java实现)

    二叉平衡树 全图基础解释参考链接:http://btechsmartclass.com/data_structures/avl-trees.html 二叉平衡树:https://www.cnblogs ...

  4. (4) 二叉平衡树, AVL树

    1.为什么要有平衡二叉树? 上一节我们讲了一般的二叉查找树, 其期望深度为O(log2n), 其各操作的时间复杂度O(log2n)同时也是由此决定的.但是在某些情况下(如在插入的序列是有序的时候), ...

  5. 平衡二叉树(AVL Tree)

    在学习算法的过程中,二叉平衡树是一定会碰到的,这篇博文尽可能简明易懂的介绍下二叉树的相关概念,然后着重讲下什么事平衡二叉树. (由于作图的时候忽略了箭头的问题,正常的树一般没有箭头,虽然不影响描述的过 ...

  6. AVL树(平衡二叉树)

    定义及性质 AVL树:AVL树是一颗自平衡的二叉搜索树. AVL树具有以下性质: 根的左右子树的高度只差的绝对值不能超过1 根的左右子树都是 平衡二叉树(AVL树) 百度百科: 平衡二叉搜索树(Sel ...

  7. 006-数据结构-树形结构-二叉树、二叉查找树、平衡二叉查找树-AVL树

    一.概述 树其实就是不包含回路的连通无向图.树其实是范畴更广的图的特例. 树是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合. 1.1.树的特性: 每个结点有零个或多个子 ...

  8. 转载:平衡二叉树(AVL Tree)

    平衡二叉树(AVL Tree) 转载至:https://www.cnblogs.com/jielongAI/p/9565776.html 在学习算法的过程中,二叉平衡树是一定会碰到的,这篇博文尽可能简 ...

  9. 算法与设计模式系列1之Python实现常见算法

    preface 常见的算法包括: 递归算法 二分法查找算法 冒泡算法 插入排序 快速排序 二叉树排序 下面就开始挨个挨个的说说原理,然后用Python去实现: 递归算法 一个函数(或者程序)直接或者间 ...

  10. [poj3017] Cut the Sequence (DP + 单调队列优化 + 平衡树优化)

    DP + 单调队列优化 + 平衡树 好题 Description Given an integer sequence { an } of length N, you are to cut the se ...

随机推荐

  1. Django框架——中间件、Auth模块、ContentType

    文章目录 一 什么是中间件 二 中间件有什么用 三 自定义中间件 process_request和process_response process_view process_exception pro ...

  2. 前端三件套系例之JS——JavaScript基础、JavaScript基本数据类型、JavaScript函数

    文章目录 1 JavaScript基础 1.JavaScript是什么 2.JavaScript介绍 2-1 ECMAScript和JavaScript的关系 2-2 ECMAScript的历史 3. ...

  3. PostgreSQL学习笔记-5.基础知识:触发器、索引

    PostgreSQL 触发器是数据库的回调函数,它会在指定的数据库事件发生时自动执行/调用. 下面是关于 PostgreSQL 触发器几个比较重要的点: PostgreSQL 触发器可以在BEFORE ...

  4. Oracle中的substr()函数和INSTR()函数和mysql中substring_index函数字符截取函数用法:计算BOM系数用量拼接字符串*计算值方法

    最近一直在研究计算产品BOM的成本系数,将拼接的元件用量拼接后拆分计算是个问题,后来受到大佬在mysql中截取字符串的启发在oracle中以substr和instr实现了  1.以下是我在mysql中 ...

  5. VideoCapture

    from xgoedu import XGOEDU import time #实例化edu XGO_edu = XGOEDU() XGO_edu.lcd_text(50,50,'hello',colo ...

  6. Python--乱码转化为中文

    1. \u和\x的含义 \u:代表的是unicode码 \x:代表的是16进制码 2. 代码实现 :\x类型 # \xe4\xb8\xad\xe6\x96\x87 代表的意思是'中文' s = u'\ ...

  7. 14.10 Socket 套接字选择通信

    对于网络通信中的服务端来说,显然不可能是一对一的,我们所希望的是服务端启用一份则可以选择性的与特定一个客户端通信,而当不需要与客户端通信时,则只需要将该套接字挂到链表中存储并等待后续操作,套接字服务端 ...

  8. Fiddler安装,使用及汉化教程

    Fiddler安装及汉化教程 一.下载安装 1.下载 官网链接:https://www.telerik.com/download/fiddler 左侧填写用途,邮箱及城市,然后下载就可以 左侧下载即D ...

  9. CSP2023 模拟赛总结合集

    9.9 ZZFLS 感觉 ucup 剩下的题完全不可做了啊!先对比赛时间来写总结对队友道歉(鞠躬.jpg 开题策略很失败.开场 30min 得的分数是一整场考试的分数. 开题,发现 T1 是水题,30 ...

  10. 堆优化模拟退火(List-Based Simulated Annealing|List-Based SA|LBSA|模拟退火) 算法

    图炸了的话请多刷新几次(upd:8.9) 堆优化模拟退火(List-Based Simulated Annealing) 算法 引入 堆优化模拟退火(List-Based Simulated Anne ...