So there is a very elegant answer to a similar problem. The problem there was to build an array tree where every array had only 1, 2, 4, 8, 16, or 32 items, and where every item was at the same nesting level. I formulated this problem without having the entire system in mind (doing rapid prototyping I guess), but the current system I don't think will really work for deleting items from the middle of the array, or adding items into the middle of the array. Unfortunately.
I need the ability to add/remove items in the middle of the array because this will be used for arrays in bucketed hash tables, or general arrays which items are rapidly added and removed (like managing memory blocks). So I am thinking how to balance that with the desire to have memory block sizes of 1, 2, 4, 8, 16, or 32 items only. Hence the tree, but I think the tree needs to work slightly differently from the problem posed in that question.
What I'm thinking is having a system like follows. Each array in the nested array tree can have 1, 2, 4, 8, 16, or 32 items, but the items don't need to sit at the same level. The reason for putting items at the same level is because there is a very efficient algorithm for getItemAt(index)
if they are at the same level. But it has the problem of not allowing efficient inserts/deletes. But I think this can be solved where items in an array are at different levels by having each parent array "container" keep track of how many deeply nested children it has. It would essentially keep track of the size of the subtree. Then to find the item with getItemAt(index)
, you would traverse the top level of the tree, count the top-level tree sizes, and then narrow your search down the tree like that.
In addition, the leaf arrays (which have 1, 2, 4, 8, 16, or 32 items each) can have items removed, and then you only have to adjust that short array item's positions. So you'd go from this:
[1, 2, 3, 4, 5, 6, 7, 8]
...delete 6
, and get this (where -
is null
):
[1, 2, 3, 4, 5, 7, 8, -]
Then if you add an item 9
at say position 3, it would result in:
[1, 2, 9, 3, 4, 5, 7, 8]
This is nice because say you have a million items array. You now only have to adjust a single array with up to 32 items, rather than shifting a million items.
BUT, it gets a bit complicated when you add an item in the "middle of this tree array", but at the end of a 32-item array. You would first think you would have to shift every single subsequent array. But there is a way to make it so you don't have to do this shifting! Here is one case.
We start here:
[
[ 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]
]
Now we add an item 90
at the 16th position. We should end up with this, since this array must grow to be 4 in length:
[
[ 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]
]
If we delete 90
now, we would end with this:
[
[ 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]
]
Basically, it is minimizing the changes that are made. To getByIndex(index)
it would work like this, with more metadata on the arrays:
{
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] }
]
}
So my question is, how can you build such a tree that only has 1, 2, 4, 8, 16, or 32 nodes at each level, which allows for inserting or removing nodes at any place in the overall conceptual "array", where the leaf nodes in the tree don't need to be at any specific level? How to implement the splice
method?
For this question, don't worry about compactification just yet, I will try and see if I can figure that out on my own first. For this question, just leave junk and nulls around wherever they end up being, which is less than ideal. I mean, if you know how to compactify things easily, then by all means include it, but I think it will add significantly to the answer, so the default should be to leave it out of the answer :)
Also note, the arrays should be treated as if they are static arrays, i.e. they can't dynamically grow in size.
The algorithm for insertItemAt(index)
would work something like this:
- Find the appropriate leaf array to put the item in. (By traversing down based on size information).
- If the leaf has some room in it (as null pointers at the end of the leaf array), then just shift the items to make room for the item at its exact index.
- If the leaf is too short, replace it with a longer one, and place the item in that leaf.
- If the leaf is max length (32), then attempt to add another leaf sibling. Can't just easily do that if there are 32 siblings... Or if there are not already null siblings in place if it's shorter.
- If the leaf is max length and there aren't max length siblings, then check for a free null sibling. If there are no more free siblings, then double the number of siblings with null pointers, and create the next array and put it there.
- If the leaf is max length and the siblings are max length, and the parent is max length, I have a hard time imagining what the algorithm should do exactly in order to grow while adhering to these constraints, which is why I struggle here.
The algorithm for removeItemAt(index)
(the second piece of functionality of splice
) would do something like this:
- Find the appropriate item based on index and size information of each array node in the tree.
- Set it to null.
- Compactify surrounding null pointers if there are multiple of them at the same level. Bring them down so they equal 1, 2, 4, 8, 16, or 32 (probably since we're deleting it will never equal 32). But this portion of the algorithm can be left out, I can probably eventually figure this out, unless you know how to do it quickly.
Here is what I have basically so far.
const createTree = () => createTreeLeaf()
const createTreeContainer = () => {
return {
type: 'container',
list: [],
size: 0
}
}
const createTreeLeaf = () => {
return {
type: 'leaf',
list: [],
size: 0
}
}
const setItemAt = (tree, item, index) => {
let nodes = [tree]
let startSize = 0
a:
while (true) {
b:
for (let i = 0, n = nodes.length; i < n; i++) {
let node = nodes[i]
let endSize = startSize + node.size
if (startSize <= index && index < endSize) {
// it's within here.
if (node.type == 'container') {
nodes = node.list
break b
} else {
let relativeIndex = index - startSize
// grow if less than max
if (relativeIndex > node.size - 1) {
if (node.size == 32) {
// grow somehow
} else {
let newArray = new Array(node.size * 2)
node.list.forEach((x, i) => newArray[i] = x)
node.list = newArray
}
}
if (node.list[relativeIndex] == null) {
node.size++
}
node.list[relativeIndex] = item
break a
}
}
}
}
}
const insertItemAt = (tree, item, index) => {
let nodes = [tree]
let startSize = 0
a:
while (true) {
b:
for (let i = 0, n = nodes.length; i < n; i++) {
let node = nodes[i]
let endSize = startSize + node.size
if (startSize <= index && index < endSize) {
// it's within here.
if (node.type == 'container') {
nodes = node.list
break b
} else {
let relativeIndex = index - startSize
// grow if less than max
if (relativeIndex > node.size - 1 || isPowerOfTwo(node.size)) {
if (node.size == 32) {
// grow somehow
} else {
let newArray = new Array(node.size * 2)
node.list.forEach((x, i) => newArray[i] = x)
node.list = newArray
}
}
// todo: shift the items over to make room for this item
break a
}
}
}
}
}
const removeItemAt = (tree, item, index) => {
}
const appendItemToEndOfTree = (tree, item) => {
}
const getLeafContainingIndex = (tree, index) => {
if (index > tree.size - 1) {
return { node: null, index: -1 }
}
let nodes = [tree]
let startSize = 0
a:
while (true) {
b:
for (let i = 0, n = nodes.length; i < n; i++) {
let node = nodes[i]
let endSize = startSize + node.size
if (startSize <= index && index < endSize) {
if (node.type == 'container') {
nodes = node.list
break b
} else {
let relativeIndex = index - startSize
return { node, index: relativeIndex }
}
} else {
startSize = endSize
}
}
}
}
const getItemAt = (tree, getIndex) => {
const { node, index } = getLeafContainingIndex(tree, getIndex)
if (!node) return null
return node.list[index]
}
const isPowerOfTwo = (x) => {
return (x != 0) && ((x & (x - 1)) == 0)
}
const log = tree => console.log(JSON.stringify(tree.list))
// demo
const tree = createTree()
setItemAt(tree, { pos: '1st', attempt: 1 }, 0)
log(tree)
setItemAt(tree, { pos: '2nd', attempt: 2 }, 1)
log(tree)
setItemAt(tree, { pos: '2nd', attempt: 3 }, 1)
log(tree)
setItemAt(tree, { pos: '3rd', attempt: 4 }, 2)
log(tree)
See Question&Answers more detail:
os