使用Java对父母和孩子进行排序

时间:2017-02-08 13:44:43

标签: java sorting comparator

我有一个“Item”类,其中包含以下字段(简称):id(与SQL Server上Item项的主键相关),description,sequence(非null整数)和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);
        }
    }
}

我该怎么做?

3 个答案:

答案 0 :(得分:0)

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

你需要在o1o2的树上走,直到找到一个共同的祖先。完成后,沿两个分支向后退一步,找到它们的相对顺序(使用Sequence

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

答案 1 :(得分:0)

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

相反,我建议通过以下几个步骤进行排序:

  1. 按父项将项目分组。因此,一个组将是id为1的项及其所有子项。没有孩子的物品将自己组成一组。
  2. 对每个组进行排序,使父母先行,然后按正确顺序排列所有子女。
  3. 按父母的顺序对组进行排序。
  4. 将已排序的组连接到一个列表中。
  5. 像这样,使用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)));
    

答案 2 :(得分:0)

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

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

    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值足以区分所有唯一项目。