- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
建议在清楚二叉搜索树的所有操作之后食用本文。本文将略过部分基础知识 。
本文主要会讲到4中较常用的平衡树:
Treap 。
FHQ-Treap(无旋Treap) 。
Splay 。
WBLT 。
其实WBLT不怎么常用,但是我个人最喜欢用 。
我将会在另一篇文章中讲述其他的平衡树,如AVL,红黑树,替罪羊树等.
可持久化我还会放在另一篇文章中讲述,敬请期待.
Treap也就是 Tree + Heap ,在满足二叉搜索树的前提下,通过维护二叉堆的性质来保证其复杂度不会太大,一般我们认为是 \(O(\log n)\) 的单次操作复杂度。但是毕竟是随机的,还是可能会炸,此乃后话.
先声明一点名词:
权值:也就是数据,每一个结点保存的数据.
优先度:用于维护二叉堆性质,是新建结点的时候随机生成的.
图中我会以二元组的形式给出,格式为 权值,优先度 。
我一般习惯用大根堆,且优先度为非负数,这样要方便一点 。
这是一颗很简单的树,我们现在来考虑各种操作.
不过,我还是给出树的声明:
其他的树定义类似! 。
typedef unsigned int uint;
template<int N = 1e5 + 3>
class Treap {
private:
int lc[N], rc[N]; // 左右节点
uint pri[N]; // 优先度,定义为非负整数!
int val[N]; // 权值
int cnt[N]; // 计数器
int siz[N]; // 以当前结点为根的子树的结点个数(用于查找kth和rank)
int root, usage; // 根节点和使用的结点个数
// int fa[N]; // 记录父亲结点,如果需要的话
public:
// ...
}
这里我们记住一张图即可:
从左到右是右旋,从右到左是左旋.
可以发现,这种旋转并不会改变中序遍历的结果(不会影响二叉搜索树的性质)。但是可以影响二叉堆的性质。所以我们可以以此维护二叉堆的性质.
由于我们需要改变 q 或者 p 父节点对其的指向,我们采用引用的方式修改父节点.
后面的Splay也会有旋转的操作,但是两者需要的旋转方式不一样.
这里是将子节点旋转到当前结点 p 上,而splay中的旋转在实现的时候是把当前节点 p 转到父结点上,别混淆了! 。
void leftRotate(int &p) { // 定义左旋操作
int q = rc[p];
rc[p] = lc[q], lc[q] = p;
p = q; // 更新父节点对此结点的信息
update(lc[p]), update(p); // 更新结点信息。p已经改变过了!
}
void rightRotate(int &p) { // 定义右旋操作,与上面类似,只是变化了一下lc和rc!
int q = lc[p];
lc[p] = rc[q], rc[q] = p, p = q;
update(rc[p]), update(p);
}
是不是该说一下 update 是个啥?
直接看代码吧:
void update(p) { siz[p] = siz[lc[p]] + siz[rc[p]] + cnt[p]; }
与二叉搜索树的插入类似。如果遇到已经插入过的结点可以选择新建结点,或者增加标记( ++cnt[p] ),这里选择后者.
我们先来看新建结点的过程:
int newNode(int x, uint prio = 0) {
++usage;
lc[usage] = rc[usage] = 0;
pri[usage] = prio ? prio : rand();
val[usage] = x;
cnt[usage] = siz[usage] = 1;
return usage;
}
这只是新建一个空结点的操作而已……接下来我们才涉及到插入部分.
新建结点的操作每一种树都很类似,注意灵活修改! 。
首先我们还是正常的插入,例如我们在刚开始的例子的基础上插入了 (6, 8) 。
那么根据二叉搜索树的插入方式,插入后应该如下:
很明显,此时不满足二叉堆的性质,我们需要通过旋转的方法来维护.
不难得知,我们需要对 (7, 7) 做一次右旋操作,以使 (6, 8) 成为 (7, 7) 的父节点,从而满足二叉堆的性质.
结果如下:
接着,由于我们一定是在最底部插入的结点,所以我们只需要在回溯的时候一路向上维护二叉堆的性质即可.
代码如下:
void insert(int &p, int x) { // 由于旋转操作需要来自父节点的引用,新建节点也是!
if (!p) {
p = newNode(x); // 新建结点
return;
}
if (val[p] == x) { // 有相同结点,增加引用即可
++cnt[p], ++siz[p];
return;
} else if (val[p] > x) { // 进入左树
insert(lc[p], x);
if (pri[p] < pri[lc[p]]) rightRotate(p); // 维护二叉堆性质
} else { // 进入右树
insert(rc[p], x);
if (pri[p] < pri[rp[p]]) leftRotate(p);
}
update(p); // 更新信息
}
首先我们先找到需要修改的结点:
void remove(int &p, int x) {
if (!p) return; // 没有找到要删除的结点
if (x != val[p]) {
remove(x < val[p] ? lc[p] : rc[p], x), update(p);
return;
}
// TODO
}
然后我们考虑两种情况:
如果计数大于1,那么我们减少计数,结束即可:
if (cnt[p] > 1) {
--cnt[p], --siz[p];
return;
}
如果计数等于1,那么我们不断再保证二叉堆的性质的情况下不断向下旋转。直到没有子节点,那么我们可以直接 p = 0 ,删除即可.
if (lc[p] || rc[p]) {
if (rc[p] == 0 || pri[lc[p]] > pri[rc[p]]) {
rightRotate(p), remove(rp[p], val);
} else {
leftRotate(p), remove(lc[p], val);
}
} else {
p = 0;
}
依据逻辑把三个部分合起来即可.
例如查找第k大,获取某个数的排名,获取前驱或者后驱.
和二叉搜索树的方法一致,这里就不过多讲述了.
首先我们了解一下这个东西和普通的Treap有啥不同:
FHQ-Treap基于分裂和合并的操作,代替了旋转,使得可持久化更为容易 。
操作相对简单,时间复杂度的阶没有变化,还是 \(O(\log n)\) ,但是常数要大一点 。
我们先来看最重要的分裂部分 。
分裂有两种形式 。
按权值 v 分裂:将小于等于 v 的所有节点分裂为一棵树,大于 v 的放在一棵树 。
按排名 k 分裂:将前 k 个数放在一棵树,其他的放在另一颗树.
两者十分类似,代码几乎一模一样,所以这里只细述按权值分裂.
我们还是拿最初的那张图为例子来讲:
假如我们要按权值 4 分裂,也就相当于分成如下两棵树:
按权值 3 分裂,也就相当于分成如下两棵树:
蓝色的表示新建的边,红色的表示断开的边 。
似乎有点感觉了?我们来细谈分裂的全过程 。
约定此处红色为左树(权值小于等于 v ),蓝色为右树(权值大于 v ),绿色为下一步进入的结点 。
x 结点指新入的结点需要拼接到的位置, y 同样 。
这里以按权值 3 分裂为例子:
肯定是从根节点开始,明显,根节点的权值 \(\gt 3\) ,所以根节点及其右子树的所有结点都应该放到蓝色树上:
接下来我们判断结点 (3, 8) ,明显,节点的权值 \(\le 3\) ,所以此节点及其左子树都应该放在红色树上,下一步进入结点 (4, 6) 。
同样的判断,将 (4, 6) 放置在蓝树 。
下一步的结点为空,结束分裂.
通过观察,我们可以发现分裂后树的相对形态没有改变,所以我们可以尝试着直接在原树上直接分裂,避免复制结点的操作.
在我的代码中, sync 和其他人写的 pushdown 是一样的,只是我不习惯如此写……所以使用 sync 代替 pushdown ,使用 update 代替 pushup …… 。
代码如下:
void splitByVal(int p, int v, int &x, int &y) {
if (!p) return (void)(x = y = 0);
// sync(p); // 如果需要,下传标记!!
if (val[p] <= x)
x = p, splitByVal(rc[p], v, rc[p], y);
else
y = p, splitByVal(lc[p], v, x, lc[p]);
update(p);
}
而按照排名分裂十分类似,这里直接给出代码:
void splitByRand(int p, int k, int &x, int &y) {
if (!p) return (void)(x = y = 0);
// sync(p); // 如果需要,下传标记!!
if (siz[lc[p]] < k)
x = p, splitByRank(rc[p], k - siz[lc[p]] - cnt[p], rc[p], y);
else
y = p, splitByRand(lc[p], k, x, lc[p]);
update(p);
}
由于我们保证了左树的最大值一定不大于右树的最大值,所以我们只需要考虑优先度即可.
那么我们来演示上面分裂后的两棵树合并的过程.
此时 x , y 为左树和右树的当前结点。返回的是合并后的结点编号.
首先, y 的优先度较大,那么此时 y 作为父节点,转而合并 x 和 y 的左子树,作为 y 的左子树:
此时 x 的优先度较大,所以此时 x 作为父节点,合并 x 的右子树和 y ,作为 x 的右子树.
此时 x 为空,直接接上即可.
至此,合并完成.
合并会遇到的两种情况这里都涉及到了,那么我们来看代码:
int merge(int x, int y) {
if (!x | !y) return x | y;
// sync(x), sync(y); // if need
if (pri[x] > pri[y]) {
rc[x] = merge(rc[x], y);
return update(x), x;
} else {
lc[y] = merge(x, lc[y]);
return update(y), y;
}
return -1; // NEVER REACH!
}
其他操作很容易通过分裂和合并的操作完成,这里讲述思路即可.
插入:新建一个结点作为单独的一棵树,将原树按权值分裂,三者合并即可.
void insert(int &root, int v) {
int p = newNode(v);
int x(0), y(0);
splitByVal(root, v, x, y);
root = merge(merge(x, p), y);
}
删除:分裂成三棵树,中间的树结点的权值全部为 v ,分别合并即可.
void remove(int &root, int v) {
int x(0), y(0), z(0), tmp(0);
splitByVal(root, v - 1, x, tmp); // 分裂为 < v 和 >= v 的两棵树
splitByVal(tmp, v, y, z); // 再分裂为 = v 和 > v 的两棵树
// 以此避免判断没有v的情况
root = merge(merge(x, lc[y]), merge(rc[y], z));
}
第 k 大:按排名分裂即可.
查询排名:按权值分裂(为 \(\lt x\) 和 \(\ge x\) 的两棵树),使用左树的大小即可.
前驱或者后驱:分裂即可…… 。
整体代码我就不给了,核心就这些了.
伸展树,由Tarjan(对,就是他)在1985年发明。它与正常的平衡树不同,不能保证每一次的操作在 \(O(\log n)\) 的复杂度内,只能均摊下来 \(O(\log n)\) 。所以说,尽量少用.
Splay最大的特点是每次对一个结点进行操作(读取,或者修改)之后,都会把他转到根节点上.
我们先来看旋转操作.
还是要记住上面给出的旋转的图,这样方便于理解。这里就不细讲了.
注意和Treap的旋转操作区分开来,这里的旋转是将 当前结点旋转到其父节点的位置 ! 。
// 0 表示是父节点的左子节点,1表示为右子节点
#define side(x) ((x) == rc[fa[(x)]])
// 利用 side 获取对应的子节点
#define ch(x, k) ((k) ? rc[x] : lc[x])
// rotate x to it's fathers position!
void rotate(int x) {
int y = fa[x], z = fa[y], k = side(x);
ch(y, k) = ch(x, k ^ 1);
if (ch(x, k ^ 1)) fa[ch(x, k ^ 1)] = y;
ch(x, k ^ 1) = y, fa[y] = x, fa[x] = z;
if (z) ch(z, y == rc[z]) = x;
update(y), update(x);
}
在Splay的实现中, update 操作 没有变化 ,还是 siz[p] = siz[lc[p]] + siz[rc[p]] + cnt[p] 。
伸展是Splay最最核心的操作,也就是把一个结点旋转到根节点(或者其他地方).
但是仅仅是一路把当前结点向上旋转就可以了吗?并不是的.
如果仅仅是一路向上,那么会有上图的结果,此时我们只需要来回不断地操作,就可以卡成单次 \(O(n)\) 的复杂度,因此我们不能这么做,所以天才的Tarjan想到了另一种旋转的方法,用以保证其复杂度均摊在 \(O(\log n)\) .
对于每一次旋转我们分类讨论:
旋转一次可以到达目的地:直接旋转即可.
至少需要两次旋转:
当前结点与其父节点为同侧:先转父节点,再转子节点.
不同侧,直接两次向上转即可.
那么通过这个规则,我们就可以得到这样一张图:
是不是矮了一点 _ 。
我们可以理解为通过这种规则旋转,每次至多可以减少二分之一的高度,最终下来,高度可以保证在 \(O(\log n)\) 的样子.
所以,伸展的核心代码就可以写出来了:
target指的是向上不停转之后的最终父节点是什么,注意! 。
void splay(int x, int target) {
// sync if need!
// for (int i = x; i; i = fa[i]) stack[++top] = i;
// while (top) sync(stack[top--]);
while (fa[x] != target) {
int y = fa[x], z = fa[y];
if (z != target) rotate(side(x) == side(y) ? y : x);
rotate(x);
}
if (!target) root = x;
}
删除操作非常特殊,后文里会讲,还有,记得每一次无论是查找还是修改什么的,记得把目标节点Splay到根!!! 。
大部分和二叉搜索树一样。这里不细讲了.
不过这里建议一下Splay的写法细节:
写两个辅助函数,一个用于寻找对于排名的结点,一个用于寻找对于值的结点,都是返回的结点编号.
int _findVal(int v) {
int cur = root;
while (cur) {
// sync(cur); // if need
if (val[cur] == v) return cur;
else if (v < val[cur]) cur = lc[cur];
else cur = rc[cur];
}
return cur; // 0 for not found!
}
// return the node index of the kth element
int _findKth(int k) {
int cur = root;
while (cur) {
// sync(cur); // if need
if (siz[lc[cur]] >= k) cur = lc[cur];
else if (siz[lc[cur]] + 1 == k) return cur;
else k -= siz[lc[cur]] + 1, cur = rc[cur];
}
return cur;
}
其他的操作都一定程度上可以基于这两个操作(或者这两个操作的变换)。例如 remove 。具体操作看注释 。
void remove(int v) {
int p = _findVal(v);
if (!p) return;
splay(p, 0); // 转到根节点
if (--cnt[p]) return update(p), (void)0; // 减少计数即可
// 如果只有一个子节点,直接作为根节点即可。
if (!(lc[p] && rc[p])) {
root = lc[p] | rc[p];
fa[root] = 0; // 不需要更新!
return;
}
// 否则寻找后驱
int nxt = rc[p]; // sync(nxt); // if need
while (lc[nxt]) nxt = lc[nxt]/*, sync(nxt) */;
// 将后驱转到p的右节点的位置
splay(nxt, p);
// 此时 lc[nxt] 一定为空,考虑二叉搜索树的性质
// 所以我们直接把nxt作为根节点,连接原本p的左子树即可。
lc[nxt] = lc[p];
update(root = fa[lc[p]] = nxt); // 更新信息
}
其实主要是一些奇怪的操作需要……可恶.
这是我最喜欢用的一种平衡树,而且借鉴FHQ-Treap的思路,可以实现类似的操作,这类似的操作这里不细讲。本文只讲述基本.
定义:
叶节点:没有子节点的节点.
内节点:有子节点的节点.
WBLT全称是 Weight Balanced Tree ,有一点类似于 Size Balanced Tree ,只是WBLT是一种 Leafy Tree ,只有叶节点(没有子节点的结点)保存有信息。而其他的内节点都是用于辅助搜索的.
而且WBLT的每一个节点要么是满的,要么是空的。或者说要么左右儿子都有,要么都没有。再或者说内节点既有左节点也有右节点。这使得操作非常方便…… 。
那么内节点如何辅助搜索?在WBLT中,内节点存储的是右节点的值.
语言描述太罗嗦,我们还是直接上图:
结构还是挺一目了然的.
真正保存着信息的其实只有红色圈起来的节点.
不难看出,总共有9个节点,总而言之,如果有 \(n\) 个数据,那么将会有 \(2n - 1\) 个节点。那么我们认为其空间复杂度为 \(O(n)\) ,只是常数为普通平衡树的两倍而已.
这或许是唯一一个需要从搜索开始讲述的平衡树了 。
例如我们需要搜索值 3 ,那么搜索路径应该是这样的.
由于WBLT内节点保存信息的特性,我们进入下一个节点可以分如下情况讨论:
到达叶子节点:如果当前节点的值等于所需节点的值,那么搜索成功,否则,没有搜到.
在内节点上:如果左子节点的值大于等于目标值,则进入左子树,否则进入右子树.
其实看图不难发现,当前节点的值就是当前子树中的最大值,所以可以进行上述操作.
这个规则同样适用于其他的操作!请务必画下来试一下! 。
当然还是需要用到旋转的操作.
别忘了那张图!!! 。
但是在WBLT中旋转的实现又不一样了.
因为内节点并没有保存实际的信息,所以我们只需要 swap 三次, update 两次即可.
对了,我好像还没有讲过 update .
在WBLT中,有所变化:
void update(int p) { if (lc[p]) { // 防止更新叶节点 // sync(p) // if need siz[p] = siz[lc[p]] + siz[rc[p]]; val[p] = val[rc[p]]; } }
为了减少代码,采用了一点奇技淫巧:
#define ch(p, k) (k ? rc[p] : lc[p])
// k 0 left to here, k 1 right to her
// k是0则将左子节点旋转到此处,否则右节点旋转到此处。
// 但是这里没有旋转,只是交换节点而已,简化操作。
void rotate(int p, int k) {
int q = ch(p, k); // sync(q); // sync if need
if (!p || !q) return;
swap(lc[q], rc[q]), swap(lc[p], rc[p]);
swap(ch(q, k ^ 1), ch(p, k));
fa[ch(p, k)] = p, fa[ch(q, k ^ 1)] = q;
update(q), update(p);
}
那么我们考虑如何维护平衡:
其实一般来说, WBLT 常数本来就小,所以不需要那么严谨的旋转也可以很好的通过很多题.
// 非严谨写法!!! void fix(int p) { const int ratio = 2; if (siz[lc[p]] * ratio < siz[rc[p]]) rotate(p, 1); // 左子树太大 if (siz[rc[p]] * ratio < siz[lc[p]]) rotate(p, 0); // 右子树太大 }
但是为了严谨,肯定不能就这么水过去, 万一有人卡WBLT的常呢??
所以我们还是要稍微严谨一点.
这一部分感谢大佬的文章: 抛弃随机,保持理性——Leafy Tree 到 WBLT - 听风响,闻虫鸣。 - 洛谷博客 。
在WBLT中, 平衡是加权的 (并不是严格平衡的),一般来说,我们令平衡因子为 \(\alpha\) .
那么节点 x 平衡意味着 \(\alpha \cdot siz[x] \lt \min\{siz[lc_x], siz[rc_x]\}\) 。
不难发现, \(\alpha\) 应该 \(\le 0.5\) .
如果所有节点都满足加权平衡,则这棵树是加权平衡的,并且其高度满足 \(h \le \log_{\frac 1{1-\alpha}}n = O(\log n)\) 。而树高正保证了复杂度.
那么应该如何旋转?很明显,在非严谨写法中已经体现出了雏形了: 哪一棵子树大就把那一棵子树转上来 .
但是观察上图可以知道 b 所在的子树 相对位置是不会变的 ,也就是说如果 \(siz_a \lt \alpha \cdot (siz_a + siz_b + siz_c)\) ,并且 \(siz_c < \alpha \cdot (siz_a + siz_b + siz_c)\) ,旋转之后任然不满足加权平衡.
所以此时我们应该把 b 向上转一下,然后再进行类似的维护.
这里讲的相对感性,更严谨的证明请参考上面给出的文章 。
操作类似于:
相当于把 b 阉割成俩,然后分别和 a, c 在一起…… 。
所以我们可以得出如下代码:
#define ch(p, k) (k ? rc[p] : lc[p])
void fix(int p, double ratio = 0.29) {
int d;
if (siz[lc[p]] < siz[p] * ratio) d = 1;
else if (siz[rc[p]] < siz[p] * ratio) d = 0;
else return;
int q = ch(p, d);
if (siz[ch(q, d)] < siz[p] * ratio) rotate(q, d ^ 1);
rotate(p, d);
}
还是以上图为例。不妨假设我们需要插入 3 ,那么自然我们到了原来 3 所在的节点。那么我们如何变成两个呢?
很明显,将当前节点作为父节点,左右节点分别为当前节点的值和新增的值.
记得保证左节点不大于右节点的值 。
然后回溯更新即可.
void insert(int x) {
if (!root) {
root = newNode(x, 0);
return;
}
int cur = root;
while (true) {
// sync(cur); // if needed
if (!lc[cur]) { // new node down here!
int y = val[cur];
if (x > y) swap(x, y); // make sure x < y
// 第二个参数是将其父节点设为cur!和Treap的newNode声明不一样了
lc[cur] = newNode(x, cur), rc[cur] = newNode(y, cur);
break;
}
cur = (x > val[lc[cur]]) ? rc[cur] : lc[cur];
}
while (cur) {
update(cur), fix(cur);
cur = fa[cur];
}
}
我们还是先找到要删除的值所对应的节点吧(任意一个).
那么我们用其兄弟节点直接替代其父节点即可.
兄弟节点指的是其父节点的另一个子节点 。
听着很简答吧。在纸上画一下先!! 。
参考代码:
// 这一块的代码未经编译验证!!可能会有问题
void copyFrom(int p, int q) {
lc[p] = lc[q], rc[p] = rc[q];
fa[lc[p]] = fa[rc[p]] = p;
val[p] = val[q], siz[p] = siz[q];
}
void remove(int p, int x) {
if (!p) return;
if (val[lc[p]] >= x) {
if (siz(lc[p]) == 1 && val[lc[p]] == val) {
// _del(rc[p]), _del(lc[p]); 标记回收节点,如果需要的话(记得别清空,copyFrom需要用!)
copyFrom(p, lc[p]);
} else {
remove(lc[p], val), update(p), fix(p);
}
} else {
if (siz(rp) == 1 && val(rp) == val) {
// _del(lp), _del(rp);
copyFrom(p, rc[p]);
} else {
remove(rc[p], val), update(p), fix(p);
}
}
}
当然,你也可以写成非递归的形式,这里就不展示了.
其实我觉得其他操作都很简单,不想多说,掌握了二叉搜索树的算法,应该自然就会了.
这里就直接展示部分基础操作的代码吧.
// 统计所有 < x 的值的个数,也可以用于获取排名
int count(int x) {
int cur(root), cnt(0);
while (cur) {
if (siz[cur] == 1) return cnt;
else if (val[lc[cur]] >= val) cur = lc[cur];
else cnt += siz[lc[cur]], cur = rc[cur];
}
}
int kth(int k) {
int cur(root);
while (cur) {
if (siz[cur] == 1) return k == 1 ? val[cur] : -1; // -1: not found!
else if (siz[lc[cur]] >= k) cur = lc[cur];
else k -= siz[lc[cur]], cur = lc[cur];
}
}
int getpre(int val) {
return kth(count(val));
}
int getpost(int val) {
return kth(count(val + 1) + 1);
}
到这里,你应该学会了四种在OI中较为常用的平衡树了吧.
那么模板题肯定可以用四种方法过了吧:
【模板】普通平衡树 - 洛谷 。
【模板】普通平衡树(数据加强版) - 洛谷 。
【模板】文艺平衡树 - 洛谷 。
哦,文艺平衡树啊,并不是一种平衡树,它可以通过FHQ-Treap或者Splay实现,拓展的WBLT也可以实现 (只是这里没有讲) .
附赠各种树速度比较(基于正常平衡树随机数据的操作):
Splay < FHQ-Treap < Treap < WBLT < Better-Optimized FHQ-Treap 。
+ ?? 阳寿随机种子Treap 。
更加优化的Treap参考: treap怎样跑得更快 - zx2003 的博客 - 洛谷博客 。
最后此篇关于算法学习笔记(18):平衡树(一)的文章就讲到这里了,如果你想了解更多关于算法学习笔记(18):平衡树(一)的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我正在尝试了解二叉树的属性。但我不确定一件事: 定义。二叉树的声明: 如果对于每个节点,它认为左子树中的内部节点数和右子树中的内部节点数最多相差 1,则二叉树是平衡的。 如果任意两个叶子的差异都存在,
我有一个带有分片键和索引的集合。但是当我运行平衡时,不会为这个集合移动 block ,因为其他集合 block 正在按预期移动到其他机器。此集合中仅移动了一个 block 。 最佳答案 目前(这将在不
给定一个data.table如下,id1是一个subject-level ID,id2是一个within-subject repeated-measure ID,X 是数据变量,其中有很多。我想平衡数
由于 C++ 集合是在二叉树中实现的,如果我们以递增或递减顺序插入项目,那么集合将更像是一个列表而不是树。有没有什么方法可以在插入项目后平衡树? 最佳答案 C++ 集(即 std::set)通常实现为
我是一名初学者程序员,设计了一个智能手机网站,我有一个主体背景图像,我想慢慢改变颜色平衡,交替颜色,就像有人将 Photoshop 颜色平衡控制条调整一定百分比一样。任一方向。当您查看页面时,这种情况
我开发了一段多线程代码。该代码在 Web 应用程序中调用,因此可能由多个线程(请求)并行调用。为了控制此代码将要创建的线程数量(通过多个并行请求调用),我使用静态共享 ThreadPoolExecut
我正在为 Linux 内核开发一些网络驱动程序。我有几个 if-else 条件,我正在重新分配或释放“skb”结构——这是我有点困惑的地方。关于我在那些 if-else 中做什么 - 我遇到了 2 种
平衡 BST 的最佳和最差搜索性能是什么?每种情况发生时如何用一句话解释? 最佳答案 最佳情况:当搜索到的元素位于树的根部时。你得到 O(1)。 最坏情况:当搜索元素在最长分支的叶子处时,树是单边的。
我在平衡 AVL 树问题上遇到了麻烦,因为我的解决方案似乎与教科书后面的解决方案冲突。我查看了 AVL 树的在线可视化,他们认为我的是正确的。我的课本错了吗? 这是树: 然后我必须将 65 插入到这个
我有一个系统,我在其中使用 RS232 来控制一个灯,该灯接受以浮点形式给出的表示电压(在 2.5 - 7.5 范围内)的输入。然后控件会给出 0 到 6000 范围内的输出,这是传感器拾取的亮度。
我有一个分层目录,每个目录中有很多文件,每个文本文件中有很多 URL 字符串。我想下载 Hadoop 中所有文件中的所有 URL,以实现更好的平衡。 例如,如果我有 1+5 个节点的 Hadoop 集
请查看附件图片,这是一种跷跷板。但从图像来看,黑体具有相同的密度。并且水平矩形使用“Revolute”关节与三角形相连。但仍然没有任何建议。在目前的情况下,它需要平衡。 最佳答案 由于浮点精度等限制导
因此,在平衡 KD 树时,您应该找到中位数,然后将所有较小的元素放在左子树上,将较大的元素放在右子树上。但是,如果您有多个元素与中位数具有相同的值,会发生什么情况?他们进入左子树,右子树还是丢弃它们?
请帮我找到一种干净的方法来从现有数组中创建一个新数组。如果任何类的示例数小于该类中的最大示例数,则应该进行过采样。样本应该从原始数组中提取(随机或顺序都没有区别) 比方说,初始数组是这样的: [ 2
我是一名软件开发人员,但想成为服务器可扩展性领域的新架构师。 在多个服务使用同一数据集的情况下,旨在扩展冗余和负载平衡。 问题是:在一个理想主义的系统中,服务是否应该尝试优化它们的内部处理以减少对远程
假设我有 10 个分区用于 Kafka 中的给定主题。 我的选择是在消费者之间自动平衡这 10 个分区的负载? 我已经阅读了这篇文章 https://stackoverflow.com/a/28580
假设我有一个 B 树,其节点为 3-4 配置(3 个元素和 4 个指针)。假设我按照规则合法地建立我的树,我是否有可能达到一层中有两个节点并且一个节点有 4 个退出指针而另一个节点只有两个退出指针的情
当光标在一个括号上时,如何跳转到配对括号。很高兴在 工作emacs -nw . 就像 % 在 Vim 中。 ;;从@Lindy、@Francesco 得到提示后,我发现了更多: C-M-f
我在平衡 AVL 树时遇到问题。我一直在寻找如何平衡它们的步骤,但我找不到任何有用的东西。 我知道有4种: 单左旋 单右旋 双左右旋转 双左右旋转 但我就是无法得到如何选择其中之一和 在哪个节点上应用
我想获得类似于打印中平衡文本行但用于 block 元素的结果。假设在一个 300/100 像素的容器中有一组 50/50 像素的盒子。在容器中 float 盒子将使它们填满一“行”,然后像这样包裹到下
我是一名优秀的程序员,十分优秀!