【问题标题】:Partial sort Collection with limit and custom Comparator具有限制和自定义比较器的部分排序集合
【发布时间】:2019-01-16 18:30:50
【问题描述】:

我想像这样对一个名为 imageList 的 ArrayList 进行排序:

Collections.sort(imageList, new MapComparator(Function.KEY_TIMESTAMP, "dsc"));

这很好用,但现在出于性能原因,我希望能够设置一个限制(仅显示最新的 100 张图像,其中 ArrayList 未排序,因此简单地创建子列表将不起作用)。

我的 MapComparator 类如下所示:

class MapComparator implements Comparator<HashMap<String, String>>
{
    private final String key;
    private final String order;

    public MapComparator(String key, String order)
    {
        this.key = key;
        this.order = order;
    }

    public int compare(HashMap<String, String> first,
                       HashMap<String, String> second)
    {
        String firstValue = first.get(key);
        String secondValue = second.get(key);
        if(this.order.toLowerCase().contentEquals("asc"))
        {
            return firstValue.compareTo(secondValue);
        }else{
            return secondValue.compareTo(firstValue);
        }

    }
}

有人知道如何实现吗? 提前致谢!

【问题讨论】:

  • 您只想对数组列表中的最后 100 个项目进行排序?
  • 一般来说,不要在比较器中构建诸如颠倒顺序之类的东西。只需使用比较器接口上的reversed() 方法来颠倒顺序即可。
  • @AndrewTobilko 你一定要看看整个系列;不过,您不必对整个集合进行排序

标签: java collections comparator


【解决方案1】:

我不知道此类问题的正式名称,但它确实经常发生,并且通常被称为 top-k 或 best-k 问题。

您当然必须处理输入中的所有元素,因为最后一个元素可能属于“top k”集合,并且在处理完每个最后一个元素之前您不知道。但是,您不必对整个输入进行排序。执行排序然后获取子列表,或者使用流,调用sorted() 后跟limit(),可能会非常昂贵,因为对于 N 个输入元素,排序是 O(N log N)。但是,只需跟踪您在列表中看到的最大 k 个元素,就可以将时间复杂度降低到 O(N)。

Guava 有一个收集器可以做到这一点:Comparators.greatest(k, comparator)

如果您不想使用 Guava,构建自己的或多或少等效的收集器并不难。 PriorityQueue 对此非常有用。这是它的第一个剪辑:

static <T> Collector<T,PriorityQueue<T>,List<T>> topK(int k, Comparator<? super T> comp) {
    return Collector.of(
        () -> new PriorityQueue<>(k+1, comp),
        (pq, t) -> {
            pq.add(t);
            if (pq.size() > k)
                pq.poll();
        },
        (pq1, pq2) -> {
            pq1.addAll(pq2);
            while (pq1.size() > k)
                pq1.poll();
            return pq1;
        },
        pq -> {
            int n = pq.size();
            @SuppressWarnings("unchecked")
            T[] a = (T[])new Object[n];
            while (--n >= 0)
                a[n] = pq.poll();
            return Arrays.asList(a);
        },
        Collector.Characteristics.UNORDERED);
}

这使用PriorityQueue 作为中间数据结构。随着元素的添加,当队列的大小超过 k 时,最小的元素会被剪掉。最后,从队列中取出元素并以相反的顺序放入一个列表中,因此结果列表从高到低排序。

例如,给定一个包含

List&lt;Integer&gt;
[920, 203, 880, 321, 181, 623, 496, 576, 854, 323,
 339, 100, 795, 165, 857, 935, 555, 648, 837, 975]

一个人可以做

List<Integer> out = input.stream()
                         .collect(topK(5, Comparator.naturalOrder()));

导致

[979, 936, 890, 875, 831]

顺便说一句,使用Comparator 类中的组合器方法可以更简单地创建映射比较器。例如,假设您的输入如下所示:

    List<Map<String, String>> input =
        List.of(Map.of("name", "map1", "timestamp", "00017"),
                Map.of("name", "map2", "timestamp", "00192"),
                Map.of("name", "map3", "timestamp", "00001"),
                Map.of("name", "map4", "timestamp", "00072"),
                Map.of("name", "map5", "timestamp", "04037"));

您可以像这样轻松地按时间戳对地图进行排序:

    input.stream()
         .sorted(Comparator.comparing(map -> map.get("timestamp")))
         .forEach(System.out::println);

或者将它们收集到一个列表中,或者使用sort(comparator) 或其他方式就地排序。您可以通过执行以下操作来反转排序:

    input.stream()
         .sorted(Comparator.comparing(map -> map.get("timestamp"), Comparator.reverseOrder()))
         .forEach(System.out::println);

后者的输出将是:

{name=map5, timestamp=04037}
{name=map2, timestamp=00192}
{name=map4, timestamp=00072}
{name=map1, timestamp=00017}
{name=map3, timestamp=00001}

【讨论】:

  • 非常感谢这个详尽的回答!
  • @StuartMarks 这很好,1+,但如果你不介意的话,请说几句... 1) 为什么终结者不像 ArrayList::new 那样简单 2) 它也应该报告SORTED 如果 Comparator.naturalOrder() 会被使用(我想知道这是否可用于检测,或者更糟糕的是 Comparator.naturalOrder().reserved().reversed() - 但可能超出范围)......
  • @Eugene 当然,没有问题。 1) ArrayList 复制构造函数将在提供的集合上迭代 以获取元素,并且 PriorityQueue 不按排序顺序迭代。 PQ 只能通过重复删除或轮询 PQ 的头部来进行破坏性排序。 2) SORTED 是 Spliterator 特征,而不是 Collector 特征。我指定了UNORDERED,因为中间存储(PQ)不能保证保留比较等于的元素的顺序。
【解决方案2】:

使用排序的Stream

List<HashMap<String, String>> newestImages = 
    imageList.stream()
             .sorted(new MapComparator(Function.KEY_TIMESTAMP, "dsc"))
             .limit(100)
             .collect(Collectors.toList());

但是,这需要处理List 中的所有元素。如果你想要排序输出,你就无法避免这种情况。

【讨论】:

  • 那么这是否仍然对整个集合进行排序,还是在找到最佳 100 个结果后“停止”?
  • @frizzle 它对整个集合进行排序,因为如果不处理整个集合,您将无法确定前 100 个最新元素。一旦找到最好的 100 个结果,它就不能“停止”,因为在遍历整个集合之前它不知道它们是否是最好的 100 个结果。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-02-22
  • 1970-01-01
  • 2013-11-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-21
相关资源
最近更新 更多