我想为树结构实现一个通用层次结构,以后可以用一种与实现无关的方式来描述树上的通用算法。
我从这个层次结构开始:
interface BinaryTree<Node> {
Node left(Node);
bool hasLeft(Node);
Node right(Node);
bool hasRight(Node);
}
interface BinaryTreeWithRoot<Node> : BinaryTree<Node> {
Node root();
}
interface BinaryTreeWithParent<Node> : BinaryTree<Node> {
Node parent(Node);
bool hasParent(Node);
}
现在,基本上我希望能够以通用的方式实现子树的概念: 对于每个类T:BinaryTree,我想要一个“类”子树(T),它提供与T相同的功能(因此它必须从中派生),并且还重写root()功能。
这样的事情:
class Subtree<T, Node> : T, BinaryTreeWithRoot<Node>
where T : BinaryTree<Node>
{
T reference;
Node root;
void setRoot(Node root) {
this.root = root;
}
override Node BinaryTreeWithRoot<Node>::root() {
return this.root;
}
// Now, inherit all the functionality of T, so an instance of this class can be used anywhere where T can.
forall method(arguments) return reference.method(arguments);
}
现在使用这段代码我不知道如何创建一个类型为子树的对象,因为树对象应该以某种方式注入。
一种方法是为我创建的每个树类创建一个子树类,但这意味着代码重复,毕竟它是同样的东西。
因此,一种方法是mixins,它允许泛型类从其模板参数派生。
我也很感兴趣如何在Haskell中实现这样的层次结构,因为Haskell有一个很棒的类型系统,我认为注入这样的功能会更容易。
例如在Haskell中,它可能类似于:
class BinaryTree tree node where
left :: tree -> node -> node
right :: tree -> node -> node
class BinaryTreeWithRoot node where
left :: tree -> node -> node
right :: tree -> node -> node -- but this is a duplication of the code of BinaryTree
root :: tree -> node
instance BinaryTree (BinaryTreeWithRoot node) where
left = left
right = right
data (BinaryTree tree node) => Subtree tree node =
...
instance BinaryTreeWithRoot (Subtree tree node) where ...
我很感兴趣是否以及如何在oop语言(c ++,c#,d,java)中完成此操作,因为c ++和d提供了开箱即用的mixins(我不确定d),并且出于对Haskell类型系统的好奇心。
答案 0 :(得分:8)
由于D具有“真实”模板,而不是泛型,因此使模板类从其模板参数继承是微不足道的:
class A {}
class B(T) : T {
static assert(is(B!T : T)); // Passes.
}
至于让Subtree
在D中工作,假设你还有一个模板类Node
,那么这样的事情就应该这样做:
class Subtree(T) : T, BinaryTreeWithRoot!(Node!(T))
{
T reference;
Node root;
void setRoot(Node root) {
this.root = root;
}
override Node root() {
return this.root;
}
}
然而,IIUC(如果我错了,请纠正我),T
是树的有效载荷,因此可以是原始的。如果是这种情况,那么最好能够通过alias this
将Subtree!(T)
用作T
,这样就可以在没有继承的情况下进行子类型化并使用基元:
class Subtree(T) : BinaryTreeWithRoot!(Node!(T))
{
T reference;
alias reference this; // Make this implicitly convertible to reference.
Node root;
void setRoot(Node root) {
this.root = root;
}
override Node root() {
return this.root;
}
}
答案 1 :(得分:5)
在Haskell中创建这样的树界面是......不寻常的。 Node
和Subtree
都是多余的。这部分是由于代数类型,部分是因为Haskell数据是不可变的,因此需要不同的技术来完成某些事情(比如设置根节点)。可以这样做,界面看起来像:
class BinaryTree tree where
left :: tree a -> Maybe (tree a)
right :: tree a -> Maybe (tree a)
-- BinaryTreeWithRoot inherits the BinaryTree interface
class BinaryTree tree => BinaryTreeWithRoot tree where
root :: tree a -> tree a
然后,使用非常标准的二叉树定义:
data Tree a =
Leaf
| Branch a (Tree a) (Tree a)
instance BinaryTree Tree where
left Leaf = Nothing
left (Branch _ l r) = Just l
right Leaf = Nothing
right (Branch _ l r) = Just r
data TreeWithRoot a =
LeafR (TreeWithRoot a)
| BranchR a (TreeWithRoot a) (TreeWithRoot a) (TreeWithRoot a)
instance BinaryTree TreeWithRoot where
-- BinaryTree definitions omitted
instance BinaryTreeWithRoot TreeWithRoot where
root (LeafR rt) = rt
root (BranchR _ rt l r) = rt
由于此接口返回Maybe (tree a)
,您还可以使用left
和right
检查分支是否存在,而不是使用单独的方法。
它没有什么特别的错,但我不相信我见过有人真正实现过这种方法。更常用的技术是根据Foldable
和Traversable
定义遍历或创建zipper。拉链很容易手动派生,但有几种通用的拉链实现,例如zipper,pez和syz。
答案 2 :(得分:2)
在C#4中,我将使用动力学来实现这一目标。例如,您可以尝试将SubtTree类定义为:
public class Subtree<T, Node> : DynamicObject, BinaryTreeWithRoot<Node> where T : BinaryTree<Node>
{
private readonly T tree;
public Subtree(T tree)
{
this.tree = tree;
}
}
并使用树的方法/属性覆盖DynamicObject的适当方法。更多信息(和示例代码)可以在这篇关于Using C# 4.0 dynamic to drastically simplify your private reflection code的博文中找到。
值得一提的是,由于使用动态功能和反射,将引入较小的性能开销以及降低安全性(因为它可能涉及违反封装)。
答案 3 :(得分:1)
正如您所指出的,一种方法是为我创建的每个树类创建一个子树类, 这意味着代码重复,但它可以使用反射和T4以某种方式“避免”或更好地自动化。我自己做了一个过去的项目,它运作得很好!
您可以从Oleg Synch博客开始,了解有关T4的概述。这是自动生成的类的一个很好的例子:http://www.olegsych.com/2007/12/how-to-use-t4-to-generate-decorator-classes/
答案 4 :(得分:1)
我认为通过“BinaryTree”的方法假设过多的固定结构并且以非通用方式不必要地定义您的接口。这样做会使您在树扩展为非二进制形式时难以重用算法。相反,如果没有必要或有用,您将需要为多种样式编写接口代码。
此外,使用hasLeft / hasRight检查进行编码意味着每次访问都是一个两步过程。检查固定位置的存在将不提供有效的算法。相反,我认为您会发现添加可能是二进制左/右或二进制红/黑或字符索引或其他任何内容的通用属性将允许更多地重用您的算法并检查数据只能由这些算法完成需要它(特定的二进制算法)。
从语义视图中,您希望首先编码一些基本属性,然后进行专门化。当您“处于”算法内的节点时,您希望能够首先找到子边。这应该是容器范围的边缘结构,允许您导航到子节点。由于它可以是通用容器,因此它可以包含0,2,5,1或甚至100个边。许多算法都不关心。如果它为0,则迭代范围将不执行任何操作 - 无需检查hasX或hasY。对于每个边缘,您应该能够获得子节点,并递归所需的算法。
这基本上是方法提升它的图形库,它允许将树算法扩展到适用的图形,以便更好地通用算法重用。
所以你已经有了这个
的基本界面TreeNode:
getChildEdges: () -> TreeEdgeRange
TreeEdge:
getChildNode: () -> TreeNode
以及您喜欢的语言所享有的任何范围对象。例如,D具有特别有用的范围语法。
您需要一些基本的Tree对象来为您提供节点。像
这样的东西Tree:
getTreeNodes: () -> TreeNodeRange
开始你。
现在,如果您想支持BinaryTrees,请将此作为对此接口的限制。请注意,您实际上并不需要任何新的接口方法,只需要强制执行更多不变量 - 每个TreeNode都有0,1或2个childEdges。只需创建一个指示此语义限制的接口类型:
BinaryTree : Tree
如果您想支持有根树,请使用
添加接口层RootedTree : Tree:
getRoot: () -> TreeNode
增加了这种能力。
基本思想是,如果要使类在层次结构中更具体,则不必添加接口方法来添加语义要求。如果存在需要访问的新语义行为,则仅添加接口方法。否则 - 在通用接口上强制执行新的不变量。
最终,您需要使用包含节点或边缘数据的结构来装饰节点和边缘,这样您就可以构建Tries和Red-Black树以及高级算法的所有优秀工具。所以你想拥有
PropertiedTreeNode<Property> : TreeNode:
getProperty: () -> Property
PropertiedTreeEdge<Property> : TreeEdge:
getProperty: () -> Property
由于这是您希望允许通用算法工作的内容,因此属性是否是Tree的一部分的类型信息应该是通用的,并且算法可以忽略。这将使您进入boost的设计轨道,这些问题已得到非常优雅的解决。如果您想了解如何构建通用树算法库的想法,我建议您研究该库。
如果你遵循上面的类型等同于语义描述的指导原则,那么SubTree应该是显而易见的 - 它与它来自的树完全相同!实际上,你真的不应该有SubTree类型。相反,您应该只有一个您正在处理的特定TreeNode类型的方法
PropertiedTreeNode<Property>:
getSubTree: () -> PropertiedTree<Property>
而且,就像在boost中一样,当你在它的泛型属性中编码Tree的更多功能信息时,你可以获得具有更广泛接口契约的新Tree类型。
答案 5 :(得分:0)
你可以这样做: -
public class Node {
public Node Left {get; set:}
public Node Right {get; set;}
public Node Parent {get; set;} // if you want to be able to go up the tree
public Node Root {get; set;} // only if you want a direct link to root
}
子树只是一棵树,每棵树都可以表示为该树的根节点,然后该节点可以具有启用树导航的属性。
将此设为通用Node<T>
并存储该值。如果您不喜欢公共设置器,请将它们设为私有,并仅在构造函数或某些安全的AddLeft(...)
等方法中设置它们。
您也可以摆脱Root
并直接遍历Parent
链接,直到找到空Parent
值(或到达子树案例的顶级节点)。