【问题标题】:Looking for better structure for tree-like data为树状数据寻找更好的结构
【发布时间】:2012-11-29 14:13:42
【问题描述】:

我有一个可展开的树(在 HTML 页面中):

+ Category 1
- Category 2
  + Subcategory 1
  - Subcategory 2
    |- Foo
    |- Bar
    |- Link 42

由结构表示(在后端定义):

class Demo {
  static ImmutableList<Item> sample() {
    return ImmutableList.of(
        new Item("Category 1", ImmutableList.of(
            new Item("Some link title", "resource_id_1"),
            new Item("Another link title", "resource_id_2"))),
        new Item("Category 2", ImmutableList.of(
            new Item("Subategory 1", ImmutableList.of(
                new Item("Another link title", "resource_id_3"))),
            new Item("Subcategory 2", ImmutableList.of(
                new Item("Foo", "resource_id_1"),
                new Item("Bar", "resource_id_2"),
                new Item("Link 42", "resource_id_42"))))));
  }
}

Item 定义如下:

public class Item {
  private String readableName;
  private String resourceId;
  private ImmutableList<Item> children;

  Item(String name, String resourceId) {
    this.readableName = name;
    this.resourceId = resourceId;
  }

  Item(String name, ImmutableList<Item> children) {
    this.readableName = name;
    this.children = children;
  }

  public String getReadableName() {
    return readableName;
  }

  public String getResourceId() {
    return resourceId;
  }

  public ImmutableList<Item> getChildren() {
    return children;
  }
}

resourceId 可以有不同的可读名称,并且可以在整个结构中多次放置,但在当前类别/子类别中只能放置一次。

当前,当用户点击链接或写入 URL 时,资源被加载(例如链接 Foo 映射到 /showResource?id=resource_id_1:uniqe_magic_id)并展开树。它的工作原理只是因为 hack - 前端创建自己的结构副本,将一些 :uniqe_magic_id 字符串附加到每个资源 id(每个叶子),并且在向后端发送请求时,它会删除魔术部分。 :uniqe_magic_id 仅由前端用于扩展上面显示的树中的适当项目。对我来说,这似乎是一个巧妙的解决方案(我正在重构这段代码并删除了 cleanId 方法,我认为这不是必需的,但在向后端发送请求之前正在剥离魔法......)我正在寻找一个更好的方法。

我可以同时修改前端和后端。我想到了某种带有节点的树,例如:

class Node {
  Node next;
  Node child;
  String readableName;
  String resourceId;
  String someUniqueHash;
}

并使用someUniqueHash。

有没有更好的方法来实现相同的结果而不在前端复制整个结构?

【问题讨论】:

  • 我认为这不会解决您的问题,但是由于您不需要在任何级别强制执行重复值,您是否考虑过使用 LinkedHashSet(或其不可变)而不是 List?
  • List 最初在这里用于表示它以某种方式排序并且Items 到目前为止还没有 hashCode / equals,但这是一个小(且有用)的建议,实际上并没有解决我的问题。
  • 只在后端维护结构怎么样。在前端显示一棵树并始终从服务器请求从单击的节点开始的子树?此外,使用weakhashmap 可能很有用。
  • 当您获得被点击节点的子树时,也可能会为一级节点获取子节点。
  • 通过new Item(String, ImmutableList&lt;Item&gt;)构造函数将孩子的id改为父母的id怎么办?这样您将拥有唯一的 ID,无需复制或更改结构。

标签: java data-structures tree


【解决方案1】:

仅断言项目的 id 对于您正在创建的菜单在本地是唯一的,并且当子项附加到每个项目时更新对父项的引用?

这将允许您扩展 url 中关注的任何内容的子树,前端(假设为 html)将动态导航菜单,向用户呈现各种类别。

可以通过工厂模式声明项目的本地唯一性,方法是添加一个名为 Menu 的新类并使 Menu 中的 children 可变。

class Menu {
    final HashMap<String, Item> items = new HashMap<String, Item>();
    final List<Item> root = new ArrayList<Item>();

    public Item createItem(String title, String id, Item parent) {
        if (items.containsKey(id)) {
            raise SomeRuntimeException();
        }

        final Item item = new Item(title, id, parent, this);

        if (parent == null) {
            root.add(item);
        }
        else {
            parent.addChild(item);
        }

        items.put(id, item);
    }

    /* default to have no parent, these are root items. */
    public Item createItem(String title, String id, Item parent) {
        return addItem(title, id, null);
    }
}

对 Item 类的一些修改。

class Item {
    private final Menu menu;
    private final Item parent;
    private final List<Item> children = new ArrayList<Item>();

    public Item(String name, String resourceId, Menu menu, Item parent) {
        ...
        this.menu = menu;
        this.parent = parent;
    }

    public Item addChild(String name, String resourceId) {
        final Item item = this.menu.createItem(name, resourceId, this);
        this.children.add(item);
        return item;
    }
}

现在我牺牲了一些不变性,因为我相信这种模式在处理错误时比提供一组嵌套列表更具表现力。

生成不可变菜单

如果不可变性是一个大问题,您可以随时将 Menu 和 Item 更改为接口并实现复制原始 Menu 和 Item 的不可变变体,然后添加一个 copyImmutable 方法将构建所请求结构的 Menu 类。

class MenuBuilder {
    /* ... contains all things previously declared in Menu ... */
    Menu copyImmutable() {
        ImmutableList<Item> root = ...
        ImmutableMap<String, Item> items = ...
        return new ImmutableMenu(root, items)
    }
}

这意味着您递归地对所有项目执行相同的操作。

生成菜单的算法

  1. 从 Menu 类中查找项目(处理潜在错误)
  2. 将每个父级迭代到该菜单并记录 pathTaken。
  3. 到达根节点后,将其存储为 activeRoot。
  4. 从Menu开始依次迭代所有根节点并渲染它们。当点击 activeRoot 时,递归渲染所有子节点,但只输入在 pathTaken 中注册的子节点。

我希望本文描述的解决方案可能会给您如何解决问题提供灵感!

【讨论】:

    【解决方案2】:

    在前端复制你的树是必要的,因为它代表不同的概念:后端的树代表模型,前端的树代表模型视图。这是一个非常重要的区别:后端的树说明要显示什么,而前端的树说明如何显示。具体来说,前端知道树的哪些部分被展开,后端不应该知道,因为不同用户的树节点的打开/关闭状态会有所不同。

    也许你应该做的是明确职责分离。与其制作树的副本并将唯一标识符添加到其资源 ID,不如为 VisualItem 创建一个单独的类,封装实际项目的 ID,并拥有自己的唯一 ID(唯一 ID 永远不会去后端):

    class VisualItem {
        String resourceId;
        ImmutableList<VisualItem> children;
        String uniqueId;    // Used only in the front end
        boolean isExpanded; // Tells you if the subtree is expanded or not
        // You can add more attributes here to avoid requesting the Item.
        // For example, you could add a readableName here
    }
    

    确保 ID 唯一性的一种方法是 use the UUID class:它提供了一种非常方便的 randomUUID() 方法,为您提供唯一标识符。

    【讨论】:

    • 基本上我得出的结论就是你在这里写的 - 如果不在前端复制我的结构,我就无法获得(优雅的)解决方案。在这里添加一些唯一的 id 更容易(其他人建议的前端结构的 hashCode 和 equals 等功能在此选项中免费提供)并且在支持中将只有一个包含纯数据的简单模型 - 不多不少。我稍后会发布最终解决方案。
    【解决方案3】:

    如果我理解正确,您是在尝试可靠地识别项目在仅给定非唯一 ID 的树结构中的位置。

    生成的 URL 是否是“永久链接”,即用户是否可以安全地为它们添加书签并在 n 年后树结构可能发生变化时返回?树结构会改变吗?特别是,您是否曾经将资源移动到不同的类别,但希望其旧链接将用户带到新位置?

    如果是后者,您别无选择,只能为每个资源 ID 生成或指定唯一标识符。您可以生成/使用 UUID,或者让用户指定一个 ID 并让代码强制唯一性。

    否则,您可以使用树中项目的位置为您提供唯一 ID(因为树结构不会改变,或者用户不能依赖保存的链接到如果资源在树中剧烈移动,则该资源将在未来工作)。

    例如后者,给定一棵树:

    - A
      |- Resource1
    - B
      + X
      + Y
      - Z
        |- Resource1
        |- Resource24
    

    因此,可以根据每个项目在树中的位置及其非唯一资源标识符,为每个项目分配一个复合资源 ID 服务器端。例如,“A/Resource1”和“B/Z/Resource1”。

    或者,如果您不想依赖显示名称或始终分配 ID,请使用每个类别在其父类别中的序号位置,例如“1/Resource1”(第一个类别,然后是 Resource1)可以与“2/3/Resource1”(第 2 个类别,然后是它的第 3 个子类别,然后是 Resource1)。

    这正是普通文件系统所做的:识别没有唯一名称的资源(文件/文件夹),给定项目的唯一路径。

    (由于服务器端可以执行此分配 - 将 Parent 字段添加到 Item,然后添加一个简单的 getUniqueResourceId() 方法,该方法通过 Parent 链接迭代树以组成唯一路径 + getResourceId(),并将其传递出去到客户端而不是 getResourceId() - HTML 前端不需要受到影响。)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-13
      • 1970-01-01
      • 2012-07-06
      • 1970-01-01
      • 2011-10-24
      相关资源
      最近更新 更多