B+ Tree на Javascript
by Michel Beloshitsky
/** * btree.js - plain javascript b+tree implementation. * * 2010, Michel Belohshitsky * * Placed in public domain. */
function btree(order) {
/* Our tree basic structure. */
var tree = {node:[{leaf:[]}]}
/* Insert value (k-v) in the node or lead "n". Return null or new
k-v pair which we need to insert in parent node. */
function insAtomic (n, k, v) {
var pos, retval, arr = n.leaf || n.node, isNode = n.node !== undefined
/* Find position to insert k-v to */
pos = isNode ? 1 : 0; while (k >= arr[pos] && pos < arr.length) pos+=2
/* Insert k-v */
arr.splice(pos, 0, k, v)
/* Check for order overflow */
if (arr.length / 2 > order) {
/* Split if so */
retval = arr.splice(Math.ceil(order / 2) * 2, arr.length)
if (isNode) {
return [arr.pop(), {node:retval}]
} else {
return [retval[0], {leaf:retval}]
}
}
return null
}
/* Find value by key. Returns
[value, position in leaf, value path]. */
function find(k) {
var curr = tree.node, pos, i, path = [tree]
while (true) {
/* Find desired range in node */
pos = 1; while (k >= curr[pos] && pos < curr.length) pos+=2
path.push(curr[pos-1])
/* If found child is node */
if (curr[pos-1].node) {
/* Continue search */
curr = curr[pos-1].node
} else if (curr[pos-1].leaf) {
curr = curr[pos-1].leaf
/* Else it may be leaf - search leaf for key */
pos = 0; while (k !== curr[pos] && pos < curr.length) pos+=2
/* Return found key or null */
return [curr[pos+1], pos, path]
} else {
/* Wrong way (e.g. possible in {node:[ptr1,key]} case) -
* also null */
return [curr[pos-1], pos, path]
}
}
}
/* Weak deletion here */
function weakDel (k) {
var found = find(k), curr, i
if (!found[0] || !found[2] || !(curr = found[2].pop()).leaf)
return false
/* Delete value */
curr.leaf.splice(found[1], 2)
/* Cascade delete keys if needed */
while (!(curr.leaf || curr.node).length) {
/* Mark object for deletion*/
curr.todelete = true
/* Find link for it in parent */
curr = found[2].pop()
i = 0; while ( !(curr.leaf || curr.node)[i].todelete ) i+=2
/* Delete it */
if (found[2].length || curr.node.length > 1)
(curr.leaf || curr.node).splice(i, 2)
else
curr.node[i] = {leaf:[]} /* Here we handle special case
when deleting from top. Our
tree must has at least
{node:{leaf:[]}} structure so
we should not delete more
objects that we can. */
/* If we are at the top */
if (!found[2].length && curr.node.length == 1) {
/* Rebase tree */
if (!curr.node[0].leaf) {
tree = curr.node[0]
curr = tree
}
}
}
return true
}
function put (k, v) {
var found = find(k), top = found[2].pop(), next
/* Insert and split if needed */
while(next = insAtomic(top, k, v)) {
if (top.leaf)
top.next = next[1]
if (!found[2].length) {
/* Rebase tree case */
top = tree = {node:[top]}
} else {
top = found[2].pop()
}
k = next[0]; v = next[1]
}
}
function first () {
var curr = tree, stack = []
while (!curr.leaf) {
stack.push(curr)
curr = curr.node[0]
}
stack.push(curr)
/* Find compatible output */
return [curr.leaf[1], 0, stack]
}
function iterate(s, e, cb) {
var found = s !== null ? find(s) : first()
/* Find first key */
if (found) {
var part = found[2].pop()
var pos = found[1]
cb('start')
} else {
cb('error', 1)
}
while (part.leaf[pos+1] !== undefined) {
if (part.leaf[pos] > e || (part.leaf[pos+2] === undefined && !part.next))
return cb('end')
cb('k-v', part.leaf[pos], part.leaf[pos+1])
if (part.leaf[pos+2] !== undefined) {
pos +=2
} else {
part = part.next; pos = 0
}
}
cb('end')
}
/* Public interface.
Members used for debug commented out. You may enable it again,
for e.g. look at internal structure or better understandance how
staff works. */
return {
put: put,
get: function (k) { return find(k)[0] },
del: weakDel,
iter: iterate,
// f:first,
// find: find,
// insAtomic: insAtomic,
// tree: function () { return tree }
}
}
Игры с map-reduce продолжаются. На этот раз написано то, что ляжет в основу хранилища данных для моего assou.
Осталось лишь продумать механиза синхронизации всего этого дела в файлы и можно будет выпускать на волю.
Смущает лишь отсутствие у меня профильного образования: наверняка я уже где-то прокололся, и проколюсь еще раз. Но экспериментировать-то с другой стороны мне это не мешает.
Запустил в консоли разработчиков хромиума нехитрое
var bt = btree(16), i for (i = 0; i < 1000000; i++) bt.put(i,'r'+i)
Памяти заняло все это хозяйство 133Мб. Не так много, учитывая что простой хэш из миллиона подобных элементов занял 86Мб.
Потом запустил:
for (i = 0; i < 300000; i++) bt.del(i)
Хром подождал минуты 2 и освободил пропорциональный кусок памяти. Сборка мусора работает, это хорошо, значит безумная идея близка к реализации.
Сейчас даже вижу несколько путей по повышению производительности этой реализации. За исключением переписывания на ЯП больше яваскрипта подходящем для таких целей (это неспортивно) можно искать в узлах дерева не при помощи while (как делается сейчас), а бинарным поиском. Это улучшит произовдительность на деревьях большого порядка btree(N), где N ~ >= 40. Но пока есть дела поважнее, я просто запишу здесь, чтобы не забыть.
Еще можно поработать с удалением: сейчас реализован т.н. “слабый” алгоритм удаления – узел удаляется только когда он полностью пуст. Есть и другие подходы.