寻找更好的树状数据结构

时间:2012-11-29 14:13:43

标签: java data-structures tree

我有一个可扩展的树(在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

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

3 个答案:

答案 0 :(得分:3)

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

这将允许您扩展网址中任何焦点的子树,前端(假设为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;
    }
}

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

生成不可变菜单

如果不变性是大不了,您可以随时将菜单和项目更改为接口,并实现复制原始菜单和项目的不可变变体,然后添加 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. 菜单类中查找项目(处理潜在错误)
  2. 将每个父级重复到该菜单并记录 pathTaken
  3. 当达到根节点时,将其存储为 activeRoot
  4. 菜单按顺序迭代所有根节点并渲染它们。点击 activeRoot 时,递归渲染所有子项,但只输入在 pathTaken 中注册的子项。
  5. 我希望这能够为您提供一个解决方案,为您解决问题提供灵感!

答案 1 :(得分:3)

在前端制作树的副本是必要的,因为它代表了一个不同的概念:虽然后端的树代表模型,但前端的树代表< em>模型的视图。这是一个非常重要的区别:后端的树表示要显示的内容,而前端的树表示如何显示它。具体来说,前端知道树的哪些部分被扩展:后端不应该知道它,因为树节点的打开/关闭状态对于不同的用户将是不同的。

也许你应该做的是明确职责分离。不是复制树并为其资源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()方法,为您提供唯一标识符。

答案 2 :(得分:1)

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

生成的URL是否“永久链接”,即用户可以安全地为它们添加书签,并在树结构可能已更改的n年时间内返回?树结构会不会改变?特别是,您是否会将资源移至其他类别,但希望其链接将用户带到位置?

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

否则,您可以使用树中项目的位置为您提供唯一ID(因为树结构不会更改,或者用户无法依赖保存的链接如果资源在树中发生显着变化,将来会使用资源。

例如后者,给定一棵树:

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

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

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

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

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