【问题标题】:Sorting parents and childs using Java使用 Java 对父母和孩子进行排序
【发布时间】:2017-06-26 04:06:25
【问题描述】:

我有一个“Item”类,它包含以下字段(简而言之):id(与 SQL Server 上 Item 表的主键相关)、description、sequence(非空整数)和 link(a引用父对象的id),可以为null)

我想使用Java进行如下排序:

Id    Sequence   Link    Description
1     1          null    Item A
99    ..1        1       Son of A, first of the sequence
57    ..2        1       Son of A, second of the sequence
66    ..3        1       Son of A, third of the sequence
2     2          null    Item B
3     3          null    Item C
...

(我把点放在更好的可视化上)

也就是说,我希望某个项目的子项直接位于其父项的下方,按“序列”字段排序。

我尝试使用比较器,但失败了:

public class SequenceComparator implements Comparator<Item> {
    @Override
    public int compare(Item o1, Item o2) {
        String x1 = o1.getSequence().toString();
        String x2 = o2.getSequence().toString();
        int sComp = x1.compareTo(x2);

        if (sComp != 0) {
            return sComp;
        } else {
            x1 = o1.getLink().toString();
            x2 = o2.getLink() == null?"":o2.getLink().toString();
            return x1.compareTo(x2);
        }
    }
}

我该怎么做?

【问题讨论】:

  • 为什么不用SQL直接排序呢?实际上,这会更容易,因为您正在尝试按数字对项目进行排序
  • 这是DAG。你正在寻找topological sort
  • 我需要在 Java 中执行此操作。这不是我的决定
  • 您提供了许多有关不相关字段的详细信息,但没有提供重要的单个字段:序列。它看起来如何,点代表什么?
  • 我认为这些点只是为了视觉表示。除了它是基于 SQL 的以外,我没有看到其他不相关的信息

标签: java sorting comparator


【解决方案1】:

考虑到您的数据结构是一个没有循环的树(以null 作为根节点):

你必须为o1o2 爬上树,直到找到一个共同的祖先。一旦你这样做了,沿着两个分支后退一步以找到它们的相对顺序(Sequence

找到共同的祖先可能会很棘手,我不知道它是否在线性时间内是可能的,但在 O(n log n) 时间内肯定是可能的 s> (n 是树枝的长度)

【讨论】:

  • 提问者在评论中说,“只有一个级别,如示例所示”。如果有多个级别,这将是一个非常好的方法。
  • 当您知道所有路径都以null 根结尾时,我认为您可以在线性时间内找到共同祖先。例如,首先计算到根的每条路径的长度。从树叶开始,首先计算沿着较长路径的适当步数,以便您处于同一水平。然后是成对比较的问题,直到你遇到共同的祖先。
【解决方案2】:

鉴于层次结构中只有两层,这可以归结为经典的多级排序。有两种项目,父母和孩子,以link 字段是否为空来区分。诀窍是每个级别的排序不在特定字段上。相反,排序依据的值取决于它是什么类型的项目。

第一级排序应该在父值上。父项的父值是它的序列,但子项的父值是它链接到的父项的序列。子项通过它们的 id 链接到父项,所以我们需要做的第一件事是建立一个从 ids 到父节点序列值的映射:

    Map<Integer, Integer> idSeqMap =
        list.stream()
            .filter(it -> it.getLink() == null)
            .collect(Collectors.toMap(Item::getId, Item::getSequence));

(假设 id 是唯一的,这是合理的,因为它们与表主键相关。)

现在我们有了这个映射,您可以编写一个 lambda 表达式来从项目中获取适当的父值。 (这假定所有非空链接值都指向现有项目。)如下:

(Item it) -> it.getLink() == null ? it.getSequence() : idSeqMap.get(it.getLink())

第二级排序应该在子值上。父项的子值为 null,因此需要在任何非 null 值之前对 null 进行排序。子项的子值是它的序列。获取子值的 lambda 表达式是:

(Item it) -> it.getLink() == null ? null : it.getSequence()

现在,我们可以使用 Java 8 中引入的 Comparator 辅助函数将它们组合起来。结果可以直接传递给 List.sort() 方法。

list.sort(Comparator.comparingInt((Item it) -> it.getLink() == null ? it.getSequence() : idSeqMap.get(it.getLink()))
                    .thenComparing((Item it) -> it.getLink() == null ? null : it.getSequence(),
                                   Comparator.nullsFirst(Integer::compare))
                    .thenComparingInt(Item::getId));

第一级排序非常简单;只需将第一个 lambda 表达式(提取父值)传递给Comparator.comparingInt

第二级排序有点棘手。我假设getLink() 的结果是可以为空的Integer。首先,我们必须使用第二个 lambda 表达式提取子值。这会产生一个可为空的值,因此如果我们将其传递给thenComparing,我们将得到一个NullPointerException。相反,thenComparing 允许我们通过辅助比较器。我们将使用它来处理空值。对于这个辅助比较器,我们通过

    Comparator.nullsFirst(Integer::compare)

这比较 Integer 对象,首先使用空值排序,然后使用 Integer.compare 方法依次比较非空值。

最后,我们比较 id 值作为最后的手段。如果您仅将此比较器用于排序,则这是可选的;重复项将彼此相邻。但是,如果您将此比较器用于TreeSet,您将需要确保不同的项目永远不会比较相等。据推测,数据库 id 值足以区分所有唯一项目。

【讨论】:

  • 您是否假设父母的序列与 id 一致?虽然在提供的示例数据中正确,但只有原始提问者才能判断它是否总是如此。
  • @OleV.V.哦,是的,好点。我假设非空链接值与序列号位于相同的值空间中。但是仔细阅读这个问题,我看到 O.P. 说链接是父级的 id 而不是父级的序列。这可以通过构建从 id 到父对象序列的映射来处理,然后在使用链接值时在第一个 lambda 中查看此映射。我想我们应该等待 O.P. 回答。
  • @OleV.V.我继续按照我上面描述的路线改造了代码。这很简单,等待 O.P. 回应是没有意义的,因为 cmets 并不清楚我们在等他。
【解决方案3】:

新答案:我认为您不需要一个比较器来控制完整的排序,因为在对子项进行排序时,您需要父项的序列,而您无法从比较器中轻松或自然地访问它.

我建议分几个步骤进行排序:

  1. 按父项将项分组。因此,一组将是 id 为 1 的项目及其所有子项。没有子项的项目将单独分组。
  2. 对每个组进行排序,使父母排在第一位,然后以正确的顺序排列所有孩子。
  3. 按父级的顺序对组进行排序。
  4. 将已排序的组连接到一个列表中。

像这样,同时使用 Java 8 流和List.sort()

    // group by parent id
    Map<Integer, List<Item>> intermediate = input.stream()
            .collect(Collectors.groupingBy(i -> i.getLink() == null ? Integer.valueOf(i.getId()) : i.getLink()));

    // sort each inner list so that parent comes first and then children by sequence
    for (List<Item> innerList : intermediate.values()) {
        innerList.sort((i1, i2) -> {
            if (i1.getLink() == null) { // i1 is parent
                return -1; // parent first
            }
            if (i2.getLink() == null) {
                return 1;
            }
            return i1.getSequence().compareTo(i2.getSequence());
        });
    }

    // sort lists by parent’s sequence, that is, sequence of first item
    List<Item> result = intermediate.values().stream()
            .sorted(Comparator.comparing(innerList -> innerList.get(0).getSequence()))
            .flatMap(List::stream)
            .collect(Collectors.toList());

输出是(省略项目描述):

 1 1   null
99 ..1 1
57 ..2 1
66 ..3 1
 2 2   null
 3 3   null

(此输出是使用 toString 方法生成的,该方法在将具有父项的项目转换为 String 时打印点。)

如果你不能使用 Java 8,我仍然相信上面提到的步骤的总体思路是可行的,只是其中一些步骤需要更多的代码。

我删除了我之前的答案,因为我误解了getLink() 返回的部分,然后认为该答案不值得尝试挽救。

编辑:

我实际上忽略了Collectors.groupingBy() 文档中的这一段:“无法保证返回的...List 对象的...可变性。”它仍然适用于我的 Java 8。如果列表的不变性应该阻止排序,则解决方案是创建一个包含相同项目的新 ArrayList

感谢 Stuart Marks 的启发,用于排序内部列表的比较器不必像上面那样笨拙。排序可以这样写:

        innerList.sort(Comparator.comparing(itm -> itm.getLink() == null ? null : itm.getSequence(),
                Comparator.nullsFirst(Integer::compare)));

【讨论】:

    猜你喜欢
    • 2018-03-22
    • 1970-01-01
    • 1970-01-01
    • 2020-09-30
    • 1970-01-01
    • 2021-10-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多