如何构建一个树数组可以拼接到哪些项目中,只允许1、2、4、8、16或32个项目的数组?
所以对类似的问题有一个非常优雅的答案。问题是要构建一个数组树,其中每个数组只有 1、2、4、8、16 或 32 个项目,并且每个项目都处于相同的嵌套级别。我在没有考虑整个系统的情况下制定了这个问题(我猜是做快速原型设计),但是我认为当前的系统不能真正用于从数组中间删除项目,或将项目添加到数组中间. 很遗憾。
我需要能够在数组中间添加/删除项目,因为这将用于分桶哈希表中的数组,或快速添加和删除项目的通用数组(如管理内存块)。所以我在考虑如何平衡它与只拥有 1、2、4、8、16 或 32 个项目的内存块大小的愿望。因此,树,但我认为该树需要工作稍有不同从所造成的问题这个问题。
我在想的是有一个如下的系统。嵌套数组树中的每个数组可以有 1、2、4、8、16 或 32 个项目,但这些项目不需要位于同一级别。将项目放在同一级别的原因是因为有一个非常有效的算法来getItemAt(index)判断它们是否处于同一级别。但它存在不允许有效插入/删除的问题。但是我认为这可以通过让每个父数组“容器”跟踪它有多少深度嵌套的子元素来解决数组中的项目处于不同级别的问题。它本质上会跟踪子树的大小。然后要找到带有 的项目getItemAt(index),您将遍历树的顶层,计算顶层树的大小,然后像这样缩小树的搜索范围。
此外,叶数组(每个数组有 1、2、4、8、16 或 32 个项目)可以删除项目,然后您只需调整该短数组项目的位置。所以你会从这个开始:
[1, 2, 3, 4, 5, 6, 7, 8]
[1, 2, 3, 4, 5, 6, 7, 8]
...删除6,并得到这个(这里-是null):
[1, 2, 3, 4, 5, 7, 8, -]
然后,如果您9在位置 3添加一个项目,则会导致:
[1, 2, 9, 3, 4, 5, 7, 8]
这很好,因为假设您有一百万个项目数组。您现在只需调整最多包含 32 个项目的单个数组,而不是移动一百万个项目。
但是,当你在“这个树数组的中间”添加一个项目时,它会变得有点复杂,但是在一个 32 个项目的数组的末尾。您首先会认为您必须移动每个后续数组。但是有一种方法可以做到这一点,因此您不必进行这种转变!这是一个案例。
我们从这里开始:
[
[ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ,
9 , 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28, 29, 30, 31, 32],
[ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ,
9 , 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28, 29, 30, 31, 32]
]
现在我们90在第 16 个位置添加一个项目。我们应该这样结束,因为这个数组的长度必须增长到 4:
[
[ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ,
9 , 10, 11, 12, 13, 14, 15, 90,
16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 21],
[32],
-,
[ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ,
9 , 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28, 29, 30, 31, 32]
]
如果我们90现在删除,我们将以这样的方式结束:
[
[ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ,
9 , 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28, 29, 30, 31, - ],
[32],
-,
[ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ,
9 , 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28, 29, 30, 31, 32]
]
基本上,它正在最小化所做的更改。为了getByIndex(index)将这样的工作,与阵列上的更多的元数据:
{
size: 64,
list: [
{
size: 31,
list: [
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ,
9 , 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28, 29, 30, 31, - ] },
{
size: 1,
list: [32] },
null,
{
size: 32,
list: [
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ,
9 , 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28, 29, 30, 31, 32] }
]
}
所以我的问题是,如何构建这样一棵每层只有 1、2、4、8、16 或 32 个节点的树,它允许在整个概念“数组”中的任何位置插入或删除节点,树中的叶节点不需要处于任何特定级别?如何实现该splice方法?
对于这个问题,暂时不要担心压缩,我会尝试看看我是否可以先自己解决。对于这个问题,只需将垃圾和空值留在它们最终出现的地方即可,这不太理想。我的意思是,如果你知道如何轻松地压缩事物,那么无论如何都要包括它,但我认为它会显着增加答案,所以默认应该是将它排除在答案之外:)
另请注意,应将数组视为静态数组,即它们的大小不能动态增长。
算法insertItemAt(index)会像这样工作:
- 找到合适的叶数组来放置项目。(根据大小信息向下遍历)。
- 如果叶子中有一些空间(作为叶子数组末尾的空指针),则只需移动项目以为其确切索引处的项目腾出空间。
- 如果叶子太短,用更长的替换它,然后将物品放在那片叶子中。
- 如果叶子是最大长度 (32),则尝试添加另一个叶子兄弟。如果有 32 个兄弟姐妹,就不能轻易做到这一点......或者如果它更短的话,如果还没有空兄弟姐妹。
- 如果叶子是最大长度并且没有最大长度的兄弟姐妹,那么检查一个空闲的空兄弟姐妹。如果没有更多的空闲兄弟节点,则将具有空指针的兄弟节点的数量加倍,然后创建下一个数组并将其放在那里。
- 如果叶子是最大长度,兄弟姐妹是最大长度,而父级是最大长度,我很难想象算法应该做什么才能在遵守这些约束的同时增长,这就是我在这里挣扎的原因。
removeItemAt(index)(的第二个功能splice)的算法会做这样的事情:
- 根据树中每个数组节点的索引和大小信息找到合适的项目。
- 将其设置为空。
- 如果在同一级别有多个空指针,则压缩周围的空指针。将它们降下来,使它们等于 1、2、4、8、16 或 32(可能因为我们要删除它永远不会等于 32)。但是这部分算法可以省略,我可能最终会弄清楚这一点,除非您知道如何快速完成。
到目前为止,这是我所拥有的。
回答
要求与B+ 树提供的要求接近。32 元 B+ 树将提供以下属性:
- 树的叶子都在同一水平线上
- 实际数据仅存储在叶子中
- 所有内部节点,除了根,至少有 16 个孩子(不包括
null填充物) - 所有叶子,除了根,至少有 16 个值(不包括
null填充物) - 如果根不是叶子,那么它将至少有 2 个孩子
此外,它还具有以下有用的功能:
- 叶节点通常维护在一个链表中,因此该列表的迭代将按其预期顺序访问数据
B+树是搜索树,是不符合你要求的特性。所以这意味着这里不需要存储在 B+ 树的内部节点中的典型键。
另一方面,您要求元素可以通过索引来标识。正如您已经建议的那样,您可以使用一个属性扩展每个节点,该属性提供存储在以该节点为根的子树的叶子中的数据值的总数。这将允许在对数时间内通过索引找到数据值。
动态节点大小
至于节点大小应该是 2 到 32 的幂的要求:B+ 树不提供可变节点大小。但请注意,此 B+ 树中的所有节点都保证至少有 16 个使用过的条目。唯一的例外是根,它可以有任意数量的使用条目。
因此,在我的答案的第一个版本中,我并没有过多关注这个要求:实现它意味着有时您可以通过将其大小限制为 16 而不是 32 来节省非根节点中的一些空间。但是下一次插入在该节点中将需要它(再次)扩展到 32 的大小。所以我认为这可能不值得付出努力。调整根的记录大小也不会带来太大的收益,因为它仅适用于该单个节点。
在对此发表评论后,我调整了实现,因此每个节点都会尽快/需要将其大小减小或扩展到 2 的上一个/下一个幂。这意味着非根节点有时可能会将它们的大小减少到 16(而它们有 16 个项目/子节点),并且根节点可以具有任何可能的幂(1、2、4、8、16 或 32)作为尺寸。
其他实现选择
根据您的偏好,我避免使用递归。
我选择在每个节点中包含以下属性:
children:这是子节点或(在叶的情况下)数据值的列表。此列表是一个具有 1、2、4、8、16 或 32 个插槽的数组,其中未使用的插槽用null. 这非常不像 JavaScript,但我知道您实际上是针对不同的语言,所以我选择了它。childCount: 这表示实际使用了上述数组中的插槽数。如果我们可以假设null永远不能用作数据,并且出现将表明真实内容的结束,我们就可以没有这个。但无论如何,我去了这个属性。因此,理论上,内容现在实际上可以包括预期null值。treeSize. 这是以该节点为根的子树的叶子中的数据项总数。如果这本身就是一个叶节点,那么它总是等于childCount。parent. B+ 树并不真正需要从子级到父级的反向引用,但我在此实现中采用了它,也是因为它使提供基于非递归的实现(您似乎更喜欢)更容易一些。prev,next: 这些属性引用兄弟节点(在同一层),所以树的每一层都有它的节点在一个双向链表中。B+ 树通常仅在底层具有此功能,但在其他级别具有此功能也很方便。
B+树算法
插入
您已经草拟了一个插入算法。确实会是这样:
- 找到应该插入值的叶子
- 如果那片叶子有空间容纳它,在那里插入值并更新 treeSize(你称之为 size),在树中向上传播增加到根。
- 否则,检查节点是否有一个邻居,它可以将某些项目转移到该节点,以便为新值腾出空间。如果是这样,那就去做,然后停下来。
- 否则,创建一个新叶,并将节点值的一半移入其中。然后有空间插入值。根据索引,它将在旧节点或新节点中
- 现在的任务是将新节点作为兄弟节点插入父节点。使用相同的算法来执行该操作,但在上面的级别上。
如果根需要拆分,则创建一个新的根节点,将两个拆分节点作为其子节点。
在该算法的执行过程中,受影响节点的属性当然应该得到很好的维护。
删除
- 找到应该删除值的叶子。
- 从该叶子中删除值,并更新 treeSize 属性,并在树中向上直到根。
- 如果节点是根节点,或者节点有超过 16 个已用槽,则停止。
- 该节点的值太少,因此请查看邻居以查看它是否可以与此节点合并,或者以其他方式共享其某些条目以进行重新分配。
- 如果所选邻居中的项目不能放入一个节点,则重新分配这些项目,因此每个项目仍然至少有 16 个,然后停止。有一个边界情况,节点有 16 个项目,但邻居有 17 个。在这种情况下,重新分配值没有优势。然后也停下来。
- 否则将来自所选邻居的项目合并到当前节点中。
- 重复此算法以从上面的级别删除现在为空的邻居。
如果根最终只有一个子节点,则使根成为该单个子节点,移除顶层。
执行
您将在下面找到一个Tree类的实现。它包括您要求的方法:
getItemAtsetItemAtremoveItemAtinsertItemAt
它还包括一些额外的方法:
-
Symbol.iterator:这使得树实例可迭代。这允许使用树底层的链表轻松迭代值。 -
print: 不言自明 -
verify:这会在广度优先遍历中访问树的每个节点,检查是否满足所有条件,并且不存在不一致。在许多其他测试中,它还验证每个节点的填充因子是否超过 50%,或者说:数组大小是承载内容的 2 的最小幂。如果测试失败,将抛出一个简单的异常。我没有努力为错误提供上下文:它们不应该发生。
片段
运行下面的代码片段将创建一个树实例,并执行 1000 次插入、1000 次更新/检索和 1000 次删除。并行地对一维数组执行相同的操作,使用splice. 在每一步之后,将树迭代的值与数组进行比较,并验证树的一致性。测试在最大节点容量为 8 个(而不是 32 个)的情况下执行,因此树在垂直方向上增长得更快,并且需要进行更多的移位、分裂和合并。
class Node {
constructor(capacity) {
// Mimic fixed-size array (avoid accidentally growing it)
this.children = Object.seal(Array(capacity).fill(null));
this.childCount = 0; // Number of used slots in children array
this.treeSize = 0; // Total number of values in this subtree
// Maintain back-link to parent.
this.parent = null;
// Per level in the tree, maintain a doubly linked list
this.prev = this.next = null;
}
setCapacity(capacity) {
if (capacity < 1) return;
// Here we make a new array, and copy the data into it
let children = Object.seal(Array(capacity).fill(null));
for (let i = 0; i < this.childCount; i++) children[i] = this.children[i];
this.children = children;
}
isLeaf() {
return !(this.children[0] instanceof Node);
}
index() {
return this.parent.children.indexOf(this);
}
updateTreeSize(start, end, sign=1) {
let sum = 0;
if (this.isLeaf()) {
sum = end - start;
} else {
for (let i = start; i < end; i++) sum += this.children[i].treeSize;
}
if (!sum) return;
sum *= sign;
// Apply the sum change to this node and all its ancestors
for (let node = this; node; node = node.parent) {
node.treeSize += sum;
}
}
wipe(start, end) {
this.updateTreeSize(start, end, -1);
this.children.copyWithin(start, end, this.childCount);
for (let i = this.childCount - end + start; i < this.childCount; i++) {
this.children[i] = null;
}
this.childCount -= end - start;
// Reduce allocated size if possible
if (this.childCount * 2 <= this.children.length) this.setCapacity(this.children.length / 2);
}
moveFrom(neighbor, target, start, count=1) {
// Note: `start` can have two meanings:
// if neighbor is null, it is the value/Node to move to the target
// if neighbor is a Node, it is the index from where value(s) have to be moved to the target
// Make room in target node
if (this.childCount + count > this.children.length) this.setCapacity(this.children.length * 2);
this.children.copyWithin(target + count, target, Math.max(target + count, this.childCount));
this.childCount += count;
if (neighbor !== null) {
// Copy the children
for (let i = 0; i < count; i++) {
this.children[target + i] = neighbor.children[start + i];
}
// Remove the original references
neighbor.wipe(start, start + count);
} else {
this.children[target] = start; // start is value to insert
}
this.updateTreeSize(target, target + count, 1);
// Set parent link(s)
if (!this.isLeaf()) {
for (let i = 0; i < count; i++) {
this.children[target + i].parent = this;
}
}
}
moveToNext(count) {
this.next.moveFrom(this, 0, this.childCount - count, count);
}
moveFromNext(count) {
this.moveFrom(this.next, this.childCount, 0, count);
}
basicRemove(index) {
if (!this.isLeaf()) {
// Take node out of the level's linked list
let prev = this.children[index].prev;
let next = this.children[index].next;
if (prev) prev.next = next;
if (next) next.prev = prev;
}
this.wipe(index, index + 1);
}
basicInsert(index, value) {
this.moveFrom(null, index, value);
if (value instanceof Node) {
// Insert node in the level's linked list
if (index > 0) {
value.prev = this.children[index-1];
value.next = value.prev.next;
} else if (this.childCount > 1) {
value.next = this.children[1];
value.prev = value.next.prev;
}
if (value.prev) value.prev.next = value;
if (value.next) value.next.prev = value;
}
}
pairWithSmallest() {
return this.prev && (!this.next || this.next.childCount > this.prev.childCount)
? [this.prev, this] : [this, this.next];
}
toString() {
return "[" + this.children.map(v => v??"-").join() + "]";
}
}
class Tree {
constructor(nodeCapacity=32) {
this.nodeCapacity = nodeCapacity;
this.root = new Node(1);
this.first = this.root; // Head of doubly linked list at bottom level
}
locate(offset) {
let node = this.root;
// Normalise argument
offset = offset < 0 ? Math.max(0, node.treeSize + offset) : Math.min(offset, node.treeSize);
while (!node.isLeaf()) {
let index = 0;
let child = node.children[index];
while (offset > child.treeSize || offset === child.treeSize && child.next) {
offset -= child.treeSize;
child = node.children[++index];
}
node = child;
}
return [node, offset];
}
getItemAt(offset) {
let [node, index] = this.locate(offset);
if (index < node.childCount) return node.children[index];
}
setItemAt(offset, value) {
let [node, index] = this.locate(offset);
if (index < node.childCount) node.children[index] = value;
}
removeItemAt(offset) {
let [node, index] = this.locate(offset);
if (index >= node.childCount) return;
while (true) {
console.assert(node.isLeaf() || node.children[index].treeSize === 0);
node.basicRemove(index);
// Exit when node's fill ratio is fine
if (!node.parent || node.childCount * 2 > this.nodeCapacity) return;
// Node has potentially too few children, we should either merge or redistribute
let [left, right] = node.pairWithSmallest();
if (!left || !right) { // A node with no siblings? Must become the root!
this.root = node;
node.parent = null;
return;
}
let sumCount = left.childCount + right.childCount;
let childCount = sumCount >> 1;
// Check whether to merge or to redistribute
if (sumCount > this.nodeCapacity) { // redistribute
// Move some data from the bigger to the smaller node
let shift = childCount - node.childCount;
if (!shift) { // Boundary case: when a redistribution would bring no improvement
console.assert(node.childCount * 2 === this.nodeCapacity && sumCount === this.nodeCapacity + 1);
return;
}
if (node === left) { // move some children from right to left
left.moveFromNext(shift);
} else { // move some children from left to right
left.moveToNext(shift);
}
return;
}
// Merge:
// Move all data from the right to the left
left.moveFromNext(right.childCount);
// Prepare to delete right node
node = right.parent;
index = right.index();
}
}
insertItemAt(offset, value) {
let [node, index] = this.locate(offset);
while (node.childCount === this.nodeCapacity) { // No room here
if (index === 0 && node.prev && node.prev.childCount < this.nodeCapacity) {
return node.prev.basicInsert(node.prev.childCount, value);
}
// Check whether we can redistribute (to avoid a split)
if (node !== this.root) {
let [left, right] = node.pairWithSmallest();
let joinedIndex = left === node ? index : left.childCount + index;
let sumCount = left.childCount + right.childCount + 1;
if (sumCount <= 2 * this.nodeCapacity) { // redistribute
let childCount = sumCount >> 1;
if (node === right) { // redistribute to the left
let insertInLeft = joinedIndex < childCount;
left.moveFromNext(childCount - left.childCount - +insertInLeft);
} else { // redistribute to the right
let insertInRight = index >= sumCount - childCount;
left.moveToNext(childCount - right.childCount - +insertInRight);
}
if (joinedIndex > left.childCount ||
joinedIndex === left.childCount && left.childCount > right.childCount) {
right.basicInsert(joinedIndex - left.childCount, value);
} else {
left.basicInsert(joinedIndex, value);
}
return;
}
}
// Cannot redistribute: split node
let childCount = node.childCount >> 1;
// Create a new node that will later become the right sibling of this node
let sibling = new Node(childCount);
// Move half of node node's data to it
sibling.moveFrom(node, 0, childCount, childCount);
// Insert the value in either the current node or the new one
if (index > node.childCount) {
sibling.basicInsert(index - node.childCount, value);
} else {
node.basicInsert(index, value);
}
// Is this the root?
if (!node.parent) {
// ...then first create a parent, which is the new root
this.root = new Node(2);
this.root.basicInsert(0, node);
}
// Prepare for inserting the sibling node into the tree
index = node.index() + 1;
node = node.parent;
value = sibling;
}
node.basicInsert(index, value);
}
/* Below this point: these methods are optional */
* [Symbol.iterator]() { // Make tree iterable
let i = 0;
for (let node = this.first; node; node = node.next) {
for (let i = 0; i < node.childCount; i++) yield node.children[i];
}
}
print() {
console.log(this.root && this.root.toString());
}
verify() {
// Raise an error when the tree violates one of the required properties
if (!this.root) return; // An empty tree is fine.
if (this.root.parent) throw "root should not have a parent";
// Perform a breadth first traversal
let q = [this.root];
while (q.length) {
if (q[0].isLeaf() && this.first !== q[0]) throw "this.first is not pointing to first leaf";
let level = [];
let last = null;
for (let parent of q) {
if (!(parent instanceof Node)) throw "parent is not instance of Node";
if (parent.children.length > this.nodeCapacity) throw "node's children array is too large";
if (parent.childCount > 0 && parent.childCount * 2 <= parent.children.length) throw "node's fill ratio is too low";
for (let i = parent.childCount; i < parent.children.length; i++) {
if (parent.children[i] !== null) throw "child beyond childCount should be null but is not";
}
let treeSize = parent.treeSize;
if (parent.isLeaf()) {
for (let value of parent.children.slice(0, parent.childCount)) {
if (value === null) throw "leaf has a null as value";
if (value instanceof Node) throw "leaf has a Node as value";
}
if (parent.treeSize !== parent.childCount) throw "leaf has mismatch in treeSize and childCount";
} else {
for (let node of parent.children.slice(0, parent.childCount)) {
if (node === null) throw "internal node has a null as value";
if (!(node instanceof Node)) throw "internal node has a non-Node as value";
if (node.parent !== parent) throw "wrong parent";
if (node.prev !== last) throw "prev link incorrect";
if (last && last.next !== node) throw "next link incorrect";
if (last && last.children.length + node.children.length <= this.nodeCapacity) {
throw "two consecutive siblings have a total number of children that is too small";
}
if (node.childCount * 2 < this.nodeCapacity) {
throw "internal node is too small: " + node;
}
level.push(node);
last = node;
treeSize -= node.treeSize;
}
if (treeSize) throw "internal node treeSize sum mismatches";
}
}
if (last && last.next) throw "last node in level has a next reference";
q = level;
}
}
test(count=100, option=3) {
// option:
// 0 = always insert & delete at left side (offset 0)
// 1 = always insert & delete at right side
// 2 = always insert & delete at middle
// 3 = insert & delete at random offsets
// Create array to perform the same operations on it as on the tree
let arr = [];
// Perform a series of insertions
for (let i = 0; i < count; i++) {
// Choose random insertion index
let index = Array.isArray(option) ? option[i] : [0, i, i >> 1, Math.floor(Math.random() * (i+1))][option];
// Perform same insertion in array and tree
arr.splice(index, 0, i);
this.insertItemAt(index, i);
// Verify tree consistency and properties
this.verify();
// Verify the order of values in the array is the same as in the tree
if (arr+"" !== [...this]+"") throw i + ": tree not same as array";
}
// Perform a series of updates
for (let i = 0; i < count; i++) {
// Choose random update index
let index = Math.floor(Math.random() * count);
// Perform same insertion in array and tree
arr[index] += count;
this.setItemAt(index, this.getItemAt(index) + count);
// Verify tree consistency and properties
this.verify();
// Verify the order of values in the array is the same as in the tree
if (arr+"" !== [...this]+"") throw "tree not same as array";
}
// Perform a series of deletions
for (let i = arr.length - 1; i >= 0; i--) {
// Choose random deletion index
let index = [0, i, i >> 1, Math.floor(Math.random() * (i+1))][option];
// Perform same deletion in array and tree
arr.splice(index, 1);
this.removeItemAt(index);
// Verify tree consistency and properties
this.verify();
// Verify the order of values in the array is the same as in the tree
if (arr+"" !== [...this]+"") throw "tree not same as array";
}
}
}
// Perform 1000 insertions, 1000 updates, and 1000 deletions on a tree with node capacity of 8
new Tree(8).test(1000);
console.log("all tests completed");