我有一个"项目"其中涉及绘制对称二进制B树,如下所示:
但我无法找到正确计算每个节点位置(x,y)的方法。我现在这样做的方式,随着树木高度的增加,一些节点往往会被其他节点重叠。
任何人都可以告诉我如何计算节点的位置?
我正在使用C#,这是我现在拥有的代表节点的类:
class SBBTreeNode<T> where T : IComparable {
public SBBTreeNode(T item) {
Data = item;
Left = null;
Right = null;
}
public T Data { get; private set; }
public SBBTreeNode<T> Left;
public SBBTreeNode<T> Right;
public bool IsHorizontal { get; set; } //Is this node horizontal?
public bool IsLeaf() {
return Left == null && Right == null;
}
}
答案 0 :(得分:2)
这是一个绘图程序:
void drawTree(Graphics G)
{
if (flatTree.Count <= 0) return;
if (maxItemsPerRow <= 0) return;
if (maxLevels <= 0) return;
int width = (int)G.VisibleClipBounds.Width / (maxItemsPerRow + 2);
int height = (int)G.VisibleClipBounds.Height / (maxLevels + 2);
int side = width / 4;
int textOffsetX = 3;
int textOffsetY = 5;
int graphOffsetY = 50;
Size squaresize = new Size(side * 2, side * 2);
foreach (SBBTreeNode<string> node in flatTree)
{
Point P0 = new Point(node.Col * width, node.Row * height + graphOffsetY);
Point textPt = new Point(node.Col * width + textOffsetX,
node.Row * height + textOffsetY + graphOffsetY);
Point midPt = new Point(node.Col * width + side,
node.Row * height + side + graphOffsetY);
if (node.Left != null)
G.DrawLine(Pens.Black, midPt,
new Point(node.Left.Col * width + side,
node.Left.Row * height + side + graphOffsetY));
if (node.Right != null)
G.DrawLine(Pens.Black, midPt,
new Point(node.Right.Col * width + side,
node.Right.Row * height + side + graphOffsetY));
G.FillEllipse(Brushes.Beige, new Rectangle(P0, squaresize));
G.DrawString(node.Data, Font, Brushes.Black, textPt);
G.DrawEllipse(Pens.Black, new Rectangle(P0, squaresize));
}
}
及其结果:
用法:
flatTree = FlatTree();
setRows();
setCols();
panel_tree.Invalidate();
现在有各种各样的内容:
drawTree例程显然是从Panel的Paint事件中触发的。
我使用了一些类级变量:
这是我在测试中构建的树;请注意,为了使事情变得更简单,我已将您的通用类型T
转储为string
:
Dictionary<string, SBBTreeNode<string> > tree
= new Dictionary<string, SBBTreeNode<string>>();
这是树的平面遍历副本,也就是说,它的元素按层次从左到右排序:
List<SBBTreeNode<string>> flatTree = new List<SBBTreeNode<string>>() ;
以下是树的尺寸:
int maxItemsPerRow = 0;
int maxLevels = 0;
这是使用队列创建平面树的方式:
List<SBBTreeNode<string>> FlatTree()
{
List<SBBTreeNode<string>> flatTree = new List<SBBTreeNode<string>>();
Queue<SBBTreeNode<string>> queue = new Queue<SBBTreeNode<string>>();
queue.Enqueue((SBBTreeNode<string>)(tree[tree.Keys.First()]));
flatNode(queue, flatTree);
return flatTree;
}
这是按顺序获取节点的递归调用:
void flatNode(Queue<SBBTreeNode<string>> queue, List<SBBTreeNode<string>>flatTree)
{
if (queue.Count == 0) return;
SBBTreeNode<string> node = queue.Dequeue();
if (!node.IsHorizontal) flatTree.Add(node);
if (node.Left != null) { queue.Enqueue(node.Left); }
if (node.Left != null && node.Left.Right != null && node.Left.Right.IsHorizontal)
queue.Enqueue(node.Left.Right);
if (node.Right != null)
{
if (node.Right.IsHorizontal) flatTree.Add(node.Right);
else queue.Enqueue(node.Right);
}
flatNode(queue, flatTree);
}
最后,我们可以设置每个节点的(虚拟)坐标:
void setCols()
{
List<SBBTreeNode<string>> FT = flatTree;
int levelMax = FT.Last().Row;
int LMaxCount = FT.Count(n => n.Row == levelMax);
int LMaxCount1 = FT.Count(n => n.Row == levelMax-1);
if (LMaxCount1 > LMaxCount)
{ LMaxCount = LMaxCount1; levelMax = levelMax - 1; }
int c = 1;
foreach (SBBTreeNode<string> node in FT) if (node.Row == levelMax)
{
node.Col = ++c;
if (node.Left != null) node.Left.Col = c - 1;
if (node.Right != null) node.Right.Col = c + 1;
}
List<SBBTreeNode<string>> Exceptions = new List<SBBTreeNode<string>>();
for (int n = FT.Count- 1; n >= 0; n--)
{
SBBTreeNode<string> node = FT[n];
if (node.Row < levelMax)
{
if (node.IsHorizontal) node.Col = node.Left.Col + 1;
else if ((node.Left == null) | (node.Right == null)) {Exceptions.Add(node);}
else node.Col = (node.Left.Col + node.Right.Col) / 2;
}
}
// partially filled nodes will need extra attention
foreach (SBBTreeNode<string> node in Exceptions)
textBox1.Text += "\r\n >>>" + node.Data;
maxLevels = levelMax;
maxItemsPerRow = LMaxCount;
}
请注意,我没有编写部分填充节点的特殊情况,只是将它们添加到例外列表中;你必须决定如何处理这些问题,即它们是否会发生以及应该在哪里进行涂漆。
好的,这几乎就是这样。我们还要做两件事:我冒昧地向节点类添加两个坐标字段:
public int Row { get; set; }
public int Col { get; set; }
我已经以这样的方式编写了我的AddNode例程,即每个节点的级别都设置在那里。
您肯定希望/需要采用不同的方式。一个简单的SetRows例程非常简单,特别是当您使用flatTree进行横向时:
void setRows()
{
foreach (SBBTreeNode<string> node in flatTree)
{
if (node.Left != null) node.Left.Row = node.Row + 1;
if (node.Right != null) node.Right.Row =
node.Row + 1 - (node.Right.IsHorizontal ? 1:0);
}
}
<强>解释强>
除了flatTree,我用于绘图,解决方案的核心是SetCols例程。
在平衡B树中,最后一行或倒数第二行达到最大宽度。
这里我计算该行中的节点数。这给了我整个树的宽度maxItemsPerRow。例程还将高度设置为maxLevels
现在我第一次从左到右设置Col值在最宽的行(如果存在,则为最后一行中的悬空子项。)
然后我逐级向上移动并将每个Col值计算为Left和Right Child之间的中间值,始终注意水平节点。
请注意,我假设所有水平节点都是正确的孩子!如果不是这样,您将不得不在FlatTree和setCol例程中进行各种调整。
答案 1 :(得分:1)
我首先将根节点置于(0,0)(在你开始的地方并不重要)。调用此点(parent_X,parent_Y)。然后选择一个起始宽度(例如2 ^(树中的级别数),如果你知道你的树有多少级别,否则,只选择任何宽度)。
左子进入位置(parent_X-width / 2,parent_Y-1),右子进入位置(parent_X + width / 2,parent_Y-1)。然后将宽度更改为width = width / 2。如果孩子碰巧是水平的,你可以忘记parent_Y-1部分并保留parent_Y。然后只重复头节点的每个子节点。每次向下移动一个级别时,将宽度替换为宽度/ 2 - epsilon。
希望这有帮助。