我有一个“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);
}
}
}
我该怎么做?
答案 0 :(得分:0)
考虑到您的数据结构是一个树(以null
为根节点),没有周期:
你需要在o1
和o2
的树上走,直到找到一个共同的祖先。完成后,沿两个分支向后退一步,找到它们的相对顺序(使用Sequence
)
找到共同的祖先可能很棘手,而且我不知道它是否可能在线性时间内,但在O(n log n)时间内肯定是可能的(n分支的长度)
答案 1 :(得分:0)
新答案:我认为您不希望有一个比较器来控制完整的排序,因为在对子项进行排序时,您需要父级的序列,并且您无法从比较器中轻松或自然地访问它
相反,我建议通过以下几个步骤进行排序:
像这样,使用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值足以区分所有唯一项目。