目录
IBTreeDictionary:扩展泛型IDictionary
介绍
B-Tree是平衡的m-way树。Wiki上的这个讨论是一个很好的材料,可以介绍B树数据结构的特征和节点布局:http://en.wikipedia.org/wiki/B-tree。
本文讨论内存中的B树实现。尽管B-Tree通常用于文件/数据库系统索引(请参阅磁盘上的开源B-Tree@:https://github.com/SharedCode/sop),但在.NET框架或任何语言/平台中实现基于B树的集合或字典具有重要价值。
B-Tree In-Memory的优点
目前典型的内存中排序字典数据结构基于二叉树算法,不要将其与B树混淆。二叉树的每个节点可以包含一个项目,而B树可以包含每个节点用户定义的项目数量,并且其节点保持平衡。这是一个非常重要的区别。由于每个节点都有一个项目,因此在二叉树中存储大量项目将生成一个具有众多节点的高而窄的节点图。
在相应的B树实现中,图形往往会更短、更宽,节点要少得多。这是因为B树中的节点通常配置为具有许多项目;例如,一个有12个项目的B树字典需要一个节点来包含这些项目,而二叉树需要12个节点,这些节点可以分散在堆(内存)中。将我们的项目采样增加到数千、数十万或数百万,我们谈论的是相应的基于B树的排序字典可以提供的显着差异化和显着优化。对于12个项目节点的设置,二叉树的单项节点为100万个,而B树的单项节点约为8.3000个,如果用户为每个节点指定的项目数比上述数目多,则甚至更少。
通过这种表征,很容易想象二叉树将倾向于使用更多的内存,并且倾向于比使用B树算法的相应字典产生更多的内存碎片。在生产环境中,我们不能让系统容易出现内存碎片,因为随着时间的推移,应用程序的性能会下降,并且由于缺乏连续的可分配空间而导致内存不足的情况。
实现
在本文和相关的代码实现中,我选择了C#和.NET Framework作为实现的语言和平台。由于B-Tree是一种成熟的算法,我将重点讨论包括接口协定在内的高级设计/实现问题。完整的源代码可供下载。
IBTreeDictionary:扩展泛型IDictionary
泛型是强制执行编码和编译时数据类型检查的一项重要功能。因此,在这个基于B树的排序字典的实现中支持它是一个很棒的功能(注意:也支持非泛型实现)。Sorted Dictionary实现泛型IDictionary接口,以提供上述功能。
为了公开B-Tree,允许使用重复键等特定功能,因此需要显式的Item Iterator来对项目遍历进行细粒度控制,我正在扩展IDictionary接口以添加上述功能的方法,从而产生了IBTreeDictionary接口。以下是上述接口的样子:
public interface IBaseCollection<T> : ICloneable
{
bool MoveNext();
bool MovePrevious();
bool MoveFirst();
bool MoveLast();
bool Search(T Value);
}
public interface IBTreeDictionary<TKey, TValue> :
System.Collections.Generic.IDictionary<TKey,
TValue>, IBaseCollection<TKey>
{
Sop.Collections.BTree.SortOrderType SortOrder{ get;set; }
BTreeItem<TKey, TValue> CurrentEntry{ get; }
TKey CurrentKey{ get; }
TValue CurrentValue{ get; set; }
void Remove();
}
为了使用具有重复键的项目,字典需要一个功能,以便能够将项目指针设置为所需位置,然后在使用区域中项目的值时遍历范围。这就是需要Movexxx方法和CurrentKey/CurrentValue项指针的原因。
此外,B-Tree还提供了一个功能,允许用户按升序或降序排序遍历项目,因此提供了SortOrder属性。
管理和搜索功能
以下是一些选定的B-Tree管理和搜索函数,以说明一些行为,这些行为表征并让用户了解非常有用的B-Tree算法的这种风格实现:
Add方法
将项目添加到B树需要确定在何处添加项目的相应节点。这是必需的,因为树中的项目在插入或删除时会保持排序。遍历树以查找节点可以通过递归方式或通过while循环结构(如循环)完成。后者比递归更可取,因为在这种情况下,它消除了堆栈使用的要求,即,当完成时,没有隐式堆栈(pop)操作,因为最里面的函数展开/返回到主调用函数。
此块演示了一个while循环,该循环用于遍历树以查找目标节点。它对树的每个节点使用BinarySearch来进一步优化每个节点的搜索。这是可能的,因为节点中的每个项目都经过排序,因此BinarySearch允许对键进行最佳搜索和最少的比较。
TreeNode CurrentNode = this;
int Index = 0;
while (true)
{
Index = 0;
byte NoOfOccupiedSlots = CurrentNode.count;
if (NoOfOccupiedSlots > 1)
{
if (ParentBTree.SlotsComparer != null)
Index = Array.BinarySearch(CurrentNode.Slots, 0,
NoOfOccupiedSlots, Item, ParentBTree.SlotsComparer);
else
Index = Array.BinarySearch(CurrentNode.Slots, 0,
NoOfOccupiedSlots, Item);
if (Index < 0)
Index = ~Index;
}
else if (NoOfOccupiedSlots == 1)
{
if (ParentBTree.Comparer != null)
{
if (ParentBTree.Comparer.Compare(CurrentNode.Slots[0].Key, Item.Key) < 0)
Index = 1;
}
else
{
try
{
if (System.Collections.Generic.Comparer<TKey>.Default.Compare(
CurrentNode.Slots[0].Key, Item.Key) < 0)
Index = 1;
}
catch (Exception)
{
if (CurrentNode.Slots[0].Key.ToString().CompareTo(Item.Key.ToString()) < 0)
Index = 1;
}
}
}
if (CurrentNode.Children != null)
// if not an outermost node, let next lower level node do the 'Add'.
CurrentNode = CurrentNode.Children[Index];
else
break;
}
此时,将找到要将项添加到的正确节点。现在,调用实际Add函数:
CurrentNode.Add(ParentBTree, Item, Index);
CurrentNode = null;
下面是将实际项目插入到找到的节点的Add函数。它维护要排序的节点的项,并在节点满时管理节点的分解,以及需要的其他B树维护操作,例如分解节点的父提升等:
void Add(BTreeAlgorithm<TKey, TValue> ParentBTree,
BTreeItem<TKey, TValue> Item, int Index)
{
byte NoOfOccupiedSlots = count;
// Add. check if node is not yet full..
if (NoOfOccupiedSlots < ParentBTree.SlotLength)
{
ShiftSlots(Slots, (byte)Index, (byte)NoOfOccupiedSlots);
Slots[Index] = Item;
count++;
return;
}
else
{
Slots.CopyTo(ParentBTree.TempSlots, 0);
byte SlotsHalf = (byte)(ParentBTree.SlotLength >> 1);
if (Parent != null)
{
bool bIsUnBalanced = false;
int iIsThereVacantSlot = 0;
if (IsThereVacantSlotInLeft(ParentBTree, ref bIsUnBalanced))
iIsThereVacantSlot = 1;
else if (IsThereVacantSlotInRight(ParentBTree, ref bIsUnBalanced))
iIsThereVacantSlot = 2;
if (iIsThereVacantSlot > 0)
{
// copy temp buffer contents to the actual slots.
byte b = (byte)(iIsThereVacantSlot == 1 ? 0 : 1);
CopyArrayElements(ParentBTree.TempSlots, b,
Slots, 0, ParentBTree.SlotLength);
if (iIsThereVacantSlot == 1)
// Vacant in left, "skud over"
// the leftmost node's item to parent and left.
DistributeToLeft(ParentBTree,
ParentBTree.TempSlots[ParentBTree.SlotLength]);
else if (iIsThereVacantSlot == 2)
// Vacant in right, move the rightmost
// node item into the vacant slot in right.
DistributeToRight(ParentBTree, ParentBTree.TempSlots[0]);
return;
}
else if (bIsUnBalanced)
{ // if this branch is unbalanced..
// _BreakNode
// Description :
// -copy the left half of the slots
// -copy the right half of the slots
// -zero out the current slot.
// -copy the middle slot
// -allocate memory for children node *s
// -assign the new children nodes.
TreeNode LeftNode;
TreeNode RightNode;
try
{
RightNode = ParentBTree.GetRecycleNode(this);
LeftNode = ParentBTree.GetRecycleNode(this);
CopyArrayElements(ParentBTree.TempSlots, 0,
LeftNode.Slots, 0, SlotsHalf);
LeftNode.count = SlotsHalf;
CopyArrayElements(ParentBTree.TempSlots,
(ushort)(SlotsHalf + 1), RightNode.Slots, 0, SlotsHalf);
RightNode.count = SlotsHalf;
ResetArray(Slots, null);
count = 1;
Slots[0] = ParentBTree.TempSlots[SlotsHalf];
Children = new TreeNode[ParentBTree.SlotLength + 1];
ResetArray(Children, null);
Children[(int)Sop.Collections.BTree.ChildNodes.LeftChild] = LeftNode;
Children[(int)Sop.Collections.BTree.ChildNodes.RightChild] = RightNode;
//SUCCESSFUL!
return;
}
catch (Exception)
{
Children = null;
LeftNode = null;
RightNode = null;
throw;
}
}
else
{
// All slots are occupied in this and other siblings' nodes..
TreeNode RightNode;
try
{
// prepare this and the right node sibling
// and promote the temporary parent node(pTempSlot).
RightNode = ParentBTree.GetRecycleNode(Parent);
// zero out the current slot.
ResetArray(Slots, null);
// copy the left half of the slots to left sibling
CopyArrayElements(ParentBTree.TempSlots, 0, Slots, 0, SlotsHalf);
count = SlotsHalf;
// copy the right half of the slots to right sibling
CopyArrayElements(ParentBTree.TempSlots,
(ushort)(SlotsHalf + 1), RightNode.Slots, 0, SlotsHalf);
RightNode.count = SlotsHalf;
// copy the middle slot to temp parent slot.
ParentBTree.TempParent = ParentBTree.TempSlots[SlotsHalf];
// assign the new children nodes.
ParentBTree.TempParentChildren[
(int)Sop.Collections.BTree.ChildNodes.LeftChild] = this;
ParentBTree.TempParentChildren[
(int)Sop.Collections.BTree.ChildNodes.RightChild] = RightNode;
ParentBTree.PromoteParent = (TreeNode)Parent;
ParentBTree.PromoteIndexOfNode = GetIndexOfNode(ParentBTree);
//TreeNode o = (TreeNode)Parent;
//o.Promote(ParentBTree, GetIndexOfNode(ParentBTree));
//SUCCESSFUL!
return;
}
catch (Exception)
{
RightNode = null;
throw;
}
}
}
else
{
// _BreakNode
// Description :
// -copy the left half of the temp slots
// -copy the right half of the temp slots
// -zero out the current slot.
// -copy the middle of temp slot to 1st elem of current slot
// -allocate memory for children node *s
// -assign the new children nodes.
TreeNode LeftNode;
TreeNode RightNode;
try
{
RightNode = ParentBTree.GetRecycleNode(this);
LeftNode = ParentBTree.GetRecycleNode(this);
CopyArrayElements(ParentBTree.TempSlots, 0,
LeftNode.Slots, 0, SlotsHalf);
LeftNode.count = SlotsHalf;
CopyArrayElements(ParentBTree.TempSlots,
(ushort)(SlotsHalf + 1), RightNode.Slots, 0, SlotsHalf);
RightNode.count = SlotsHalf;
ResetArray(Slots, null);
Slots[0] = ParentBTree.TempSlots[SlotsHalf];
count = 1;
Children = new TreeNode[ParentBTree.SlotLength + 1];
Children[(int)Sop.Collections.BTree.ChildNodes.LeftChild] = LeftNode;
Children[(int)Sop.Collections.BTree.ChildNodes.RightChild] = RightNode;
return; // successful
}
catch (Exception)
{
LeftNode = null;
RightNode = null;
RightNode = null;
throw;
}
}
}
}
Remove方法
在B树中Remove有两个版本;一个将删除给定Key的项目,另一个将删除树中当前选定的项目。前者将导致B树在给定键的情况下搜索项目,并将项目指针定位到树中具有匹配键的项目。Add模块中列出的相同节点确定用于查找要删除的节点和项目。将项目指针定位在右侧指针后,该项目实际上将从树中删除。下面是说明项目删除的代码。
当要删除的项目不在最外层节点中时,树会从最外层节点移动下一个项目以覆盖当前选定项目的插槽,从而导致从树中删除所述项目。
internal protected bool Remove(BTreeAlgorithm<TKey, TValue> ParentBTree)
{
if (Children != null)
{
byte byIndex = ParentBTree.CurrentItem.NodeItemIndex;
MoveNext(ParentBTree);
Slots[byIndex] =
ParentBTree.CurrentItem.Node.Slots[ParentBTree.CurrentItem.NodeItemIndex];
}
return true;
}
然后,它负责管理最外层的节点,将移动项空出的槽设置为null,或者通过从左侧或右侧的同级节点拉取项目来保持树的平衡。
internal void FixTheVacatedSlot(BTreeAlgorithm<TKey, TValue> ParentBTree)
{
sbyte c;
c = (sbyte)count;
if (c > 1) // if there are more than 1 items in slot then..
{ //***** We don't fix the children since there are no children at this scenario.
if (ParentBTree.CurrentItem.NodeItemIndex < c - 1)
MoveArrayElements(Slots,
(ushort)(ParentBTree.CurrentItem.NodeItemIndex + 1),
ParentBTree.CurrentItem.NodeItemIndex,
(ushort)(c - 1 - ParentBTree.CurrentItem.NodeItemIndex));
count--;
Slots[count] = null; // nullify the last slot.
}
else
{ // only 1 item in slot
if (Parent != null)
{
byte ucIndex;
// if there is a pullable item from sibling nodes.
if (SearchForPullableItem(ParentBTree, out ucIndex))
{
if (ucIndex < GetIndexOfNode(ParentBTree))
PullFromLeft(ParentBTree); // pull an item from left
else
PullFromRight(ParentBTree); // pull an item from right
}
else
{ // Parent has only 2 children nodes..
if (Parent.Children[0] == this)
{ // this is left node
TreeNode RightSibling = GetRightSibling(ParentBTree);
Parent.Slots[1] = RightSibling.Slots[0];
Parent.count = 2;
ParentBTree.AddRecycleNode(RightSibling);
RightSibling = null;
}
else
{ // this is right node
Parent.Slots[1] = Parent.Slots[0];
TreeNode LeftSibling = GetLeftSibling(ParentBTree);
Parent.Slots[0] = LeftSibling.Slots[0];
Parent.count = 2;
ParentBTree.AddRecycleNode(LeftSibling);
LeftSibling = null;
}
// nullify Parent's children will cause this
// tree node instance to be garbage collected
// as this is child of parent!
Parent.Children[0] = null;
Parent.Children[1] = null;
Parent.Children = null;
Clear();
}
}
else
{ // only 1 item in root node !
Slots[0] = null; // just nullIFY the slot.
count = 0;
ParentBTree.SetCurrentItemAddress(null, 0);
// Point the current item pointer to end of tree
}
}
}
搜索方式
该Search方法包含的代码与上面列出的Add方法的第一部分具有相同的逻辑。它从根节点向下遍历树,直到使用迭代方法(while循环)找到目标节点和项。但是,当找到项时,Search只是将当前项指针定位到找到的节点和节点中的项,而不是添加项。
将项指针与Search和Movexxx方法结合使用是一项强大的功能。例如,在这种方法组合中,执行范围查询非常容易。将项目指针定位到要查询的(开始)项目,然后使用MoveNext或MovePrevious方法按顺序遍历附近的项目,直到读取范围内感兴趣的所有项目。
使用代码
下载源代码zip文件,将其解压缩到所需的目标文件夹,然后在VS 2008中打开B-Tree.sln解决方案。浏览SampleUsage项目源代码,以了解如何使用BTreeSortedDictionary、运行它、试验使用其他可用方法,以及重用使用泛型SortedDictionary!
比较结果
1 Million Inserts on .Net SortedDictionary: 12 sec 308 MB peak 286 MB end
1 Million Inserts on BTreeDictionary: 9 sec 300 MB peak 277 MB end
1 Million Search on .Net SortedDictionary: 7 sec 309 MB peak 286 MB end
1 Million Search on BTreeDictionary: 7 sec 309 MB peak 277 MB end
结论:此B树排序字典在内存利用率和操作速度方面都优于.NET Framework的SortedDictionary。插入100万个项目,此BTree的表现优于.NET缩短了3秒,并减少了~9MB的内存。
在搜索中,两者的速度相同,但话又说回来,在内存利用率方面,BTree字典优于.NET通过使用 9MB 的内存来实现的SortedDictionary。将负载乘以10倍并在生产环境中同时使用两者,这是真正的测试,我预计您会对这个BTree字典非常满意,因为我在本文顶部讨论过的架构原因。
以下是使用的压力程序:
static void Main(string[] args)
{
SortedDictionary<string, string> l1 =
new SortedDictionary<string, string>();
StressAdd(l1, 1000000);
StressSearch(l1, 1000000);
//** uncomment to run the BTreeDictionary test
// and comment out the above block to turn off SortedDictionary test
//BTreeDictionary<string, string> l2 =
// new BTreeDictionary<string, string>();
//StressAdd(l2, 1000000);
//StressSearch(l2, 1000000);
Console.ReadLine();
return;
}
static void StressAdd(IDictionary<string, string> l, int Max)
{
Console.WriteLine("Add: {0}", DateTime.Now);
for (int i = 0; i < Max; i++)
{
l.Add(string.Format("{0} key kds vidsbnvibsnv siuv " +
"siuvsiubvisubviusvifsudjcnjdnc", i),
"value kds vidsbnvibsnv siuv " +
"siuvsiubvisubviusvifsudjcnjdnccscs");
}
Console.WriteLine("Add: {0}", DateTime.Now);
}
static void StressSearch(IDictionary<string, string> l, int Max)
{
Console.WriteLine("Search: {0}", DateTime.Now);
for (int i = 0; i < Max; i++)
{
string v = l[string.Format("{0} key kds vidsbnvibsnv siuv " +
"siuvsiubvisubviusvifsudjcnjdnc", i)];
}
Console.WriteLine("Search: {0}", DateTime.Now);
}
兴趣点
我真的学到了很多东西,并且喜欢编写这个B-Tree实现,我在90年代中期做了C++版本;事实上,我非常喜欢它,它促使我开发磁盘版本,该版本现在在GitHub(https://github.com/SharedCode/sop)中作为开源软件提供。请随时加入、参与和使用新的“磁盘”版本。
您好,“sop”项目已重新定位到GitHub:GitHub - SharedCode/sop: M-Way Trie for Scaleable Objects Persistence (SOP)
而且,当前版本的V2实现了所有宣传的“磁盘上”功能,是在Golang中实现的。B-Tree也得到了优化,在“删除”时,节点负载平衡被关闭。事实证明,这是一个非常酷的ACID事务,Cassandra和Redis之上的对象持久性使能器/适配器。快来看看吧,加入这个项目,它等待着你的“创意天赋”!
https://www.codeproject.com/Articles/96397/B-Tree-Sorted-Dictionary