【问题标题】:Sorting a list of objects in a specific sequence using comparator使用比较器按特定顺序对对象列表进行排序
【发布时间】:2021-11-06 23:50:21
【问题描述】:

我遇到了一个让我感到困惑的问题。我有一堂课:

class Order{
   private long customerId;
   private long uniqueId;
}

现在假设我们有 3 个客户的总共 9 个订单对象。每个客户三个订单。所有这些对象都在一个列表中。

我需要使用 Comparator 对该列表进行排序,以便列表首先包含每个客户 ID 的最大唯一 ID。然后是每个客户 ID 的第二大唯一 ID,依此类推。

最终列表示例:

  1. order(customerId:1, uniqueId:87)
  2. order(customerId:2, uniqueId:22)
  3. order(customerId:3, uniqueId:57)
  4. order(customerId:1, uniqueId:66)
  5. order(customerId:2, uniqueId:-4)
  6. order(customerId:3, uniqueId:41)
  7. order(customerId:1, uniqueId:10)
  8. order(customerId:2, uniqueId:-11)
  9. order(customerId:3, uniqueId:10)

如何在 'int compare(o1, o2)' 方法中实现此功能以根据要求对列表进行排序?

还有其他更好的方法来实现这一点吗?

【问题讨论】:

  • 不确定这是否适用于单个比较函数,至少不是没有显着开销(对于每个项目一次又一次地检查它的客户所在的位置)。相反,我建议 (a) 按客户拆分项目,(b) 按 ID 排序,降序,(c) 合并列表。

标签: java list sorting collections comparator


【解决方案1】:

单个compare 方法可能不适合在这里使用。单个Order 中的信息不足以确定其相对顺序。你必须在compare 中做的是:

  • 获取具有相同客户 ID 的所有订单,并按订单 ID 降序排列
  • 为另一个订单做同样的事情
  • 比较排名,如果相同,则比较客户 ID 本身

而且您必须对要比较的每个对订单重复此操作。

相反,您应该将其设为多阶段流程:

  • 首先,按客户 ID 拆分订单,例如使用Collector.groupingBy
  • 按订单 ID 分别对每个批次进行排序
  • 使用两个嵌套循环将它们合并在一起,方法是为第一个客户获取第一个订单,为第二个客户获取第一个订单,等等;确保处理不同客户的订单数量不同的情况

或者,您可以结合第一种和第二种方法,而不是最后的合并步骤,这可能是上述最棘手的部分:

  • 按客户对订单进行分组、排序、在地图中存储每个订单的“排名”
  • 使用 compare 函数对所有订单进行排序,比较这些排名(并将客户 ID 作为平局)

这是最后(混合)方法的一些 Java 代码。不是很漂亮;好久没做Java了,找不到第二步的好办法。

// group by CustomerId
Map<Long, List<Order>> byCustomer = orders.stream()
        .collect(Collectors.groupingBy(Order::getCustomerId));

// build Map of Ranks per CustomerId
Map<Order, Integer> ranks = new HashMap<>();
for (List<Order> group : byCustomer.values()) {
    group.sort(Comparator.comparing(Order::getUniqueId).reversed());
    for (int i = 0; i < group.size(); i++) {
        ranks.put(group.get(i), i);
    }
}

// sort by Rank, then by CustomerId
List<Order> orderedOrders = orders.stream()
        .sorted(Comparator.comparing((Order o) -> ranks.get(o))
                .thenComparing(Order::getCustomerId))
        .collect(Collectors.toList());

【讨论】:

    【解决方案2】:

    这将使用Guava 及其TreeMultimap 完成工作

        import com.google.common.collect.Ordering;
        import com.google.common.collect.TreeMultimap;
    
        import java.util.*;
        [...]
        List<Order> orders = new ArrayList<>();
        Order o1 = new Order(1, 87);
        Order o2 = new Order(2, 22);
        Order o3 = new Order(3, 57);
        Order o4 = new Order(1, 66);
        Order o5 = new Order(2, 4);
        Order o6 = new Order(3, 41);
        Order o7 = new Order(1, 10);
        Order o8 = new Order(2, 11);
        Order o9 = new Order(3, 10);
        orders.add(o1);
        orders.add(o2);
        orders.add(o3);
        orders.add(o4);
        orders.add(o5);
        orders.add(o6);
        orders.add(o7);
        orders.add(o8);
        orders.add(o9);
    
        // Use a treemap to order automatically uniqueId and customer
        // Map[customId => list of uniqueId ordered...]
        TreeMultimap<Long, Long> treeMap = TreeMultimap.create(Ordering.natural(), Ordering.natural());
        orders.forEach(order -> treeMap.put(order.getCustomerId(), order.getUniqueId()));
    
        System.out.println(treeMap); 
        // output {1=[10, 66, 87], 2=[4, 11, 22], 3=[10, 41, 57]}
    
        // Will be used to know when all Orders are sorted
        boolean allCleared = false;
    
        // The result list
        List<Order> ordersOrdered = new ArrayList<>();
    
        while (!allCleared) {
            for (Long customerId : treeMap.keySet()) {
                SortedSet<Long> uniqueIds = treeMap.get(customerId);
                // We retrieve the Order to sort and we add it to the ordered list
                orders.stream()
                        .filter(order -> order.getCustomerId() == customerId && order.getUniqueId() == uniqueIds.last())
                        .findFirst()
                        .ifPresent(ordersOrdered::add);
            }
    
            // Once an Order has been sorted, we remove it from the map
            ordersOrdered.forEach(order -> treeMap.remove(order.getCustomerId(), order.getUniqueId()));
    
            // As long as we have Order to sort, we continue
            allCleared = treeMap.asMap().size() == 0;
        }
    
        System.out.println(ordersOrdered);
        // [Order{customerId=1, uniqueId=87},
        // Order{customerId=2, uniqueId=22},
        // Order{customerId=3, uniqueId=57},
        // Order{customerId=1, uniqueId=66},
        // Order{customerId=2, uniqueId=11},
        // Order{customerId=3, uniqueId=41},
        // Order{customerId=1, uniqueId=10},
        // Order{customerId=2, uniqueId=4},
        // Order{customerId=3, uniqueId=10}]
    

    注意如果您不能使用Treemultimap,您可以将其替换为经典的Map&lt;Long, List&lt;Long&gt;&gt; sortedMap = new TreeMap&lt;&gt;(Long::compare),并使用list.sort(Long::compareTo); 对不同列表进行排序。

    【讨论】:

    • TreeMultimap 不包含在标准java API中,最好添加导入并声明依赖。
    【解决方案3】:

    嗯,这就是我想出的。我分两个阶段完成。

    • 首先创建一个TreeMap&lt;Long,List&lt;Order&gt;&gt;,其中列表根据uniqueID按降序排列。
    • 然后遍历映射值并提取每个 List 元素的相同位置。先是 1,然后是 2,...

    使用记录来简化示例。

    record Order(long getCustomerId, long getUniqueId) {
        @Override
        public String toString() {
            return String.format("Cid = %s, Uid = %s", getCustomerId,
                    getUniqueId);
        }
    }
    

    数据和随机播放以确保随机化。添加了额外的值来为每个CustomerId创建不同数量的订单

    List<Order> orders = new ArrayList<>(List.of(new Order(1, 87),
            new Order(2, 22), new Order(3, 57), new Order(1, 66),
            new Order(2, -11), new Order(3, 98), new Order(2, -4),
            new Order(3, 41), new Order(1, 10), new Order(2, -14),
            new Order(3, 10)));
    
    Collections.shuffle(orders);
    

    现在创建地图

    • 按相反顺序排序,唯一 ID
    • 在执行groupingBy 时,每个列表将按照遇到的顺序构建,保留已排序的列表。由于这是一个TreeMap,因此每个列表都将按照与键相关的正确顺序。
    Map<Long, List<Order>> map = orders.stream()
            .sorted(Comparator.comparing(Order::getUniqueId)
                    .reversed())
            .collect(Collectors.groupingBy(Order::getCustomerId,
                    TreeMap::new,
                    Collectors.toList()));
    

    现在是最终列表的创建

    • 首先迭代一个从 0 开始的索引。这是针对每个列表的
    • keys 不再重要,所以只需要值(即List&lt;Order&gt;
    • 随着每个索引的迭代,每个列表中的 Order 条目被映射到一个流 以及随后的新列表。
    • 由于每个CustomerId 的订单数量可能不同,因此会根据索引检查大小以过滤掉较短的列表。否则会抛出 IndexOutOfBounds 异常。
    • 然后通过takeWhile 检查新列表的大小。如果为空,则所有元素都已处理。然后,所有这些列表都按要求的顺序将flatMapped 转换为单个返回的List&lt;Order&gt;
    List<Order> result = Stream.iterate(0, i -> i + 1)
            .map(i -> map.values().stream()
                    .filter(lst -> lst.size() > i)
                    .map(lst -> lst.get(i)).toList())
            .takeWhile(lst -> lst.size() > 0)
            .flatMap(List::stream).toList();
    
    result.forEach(System.out::println);
    

    打印

    Cid = 1, Uid = 87
    Cid = 2, Uid = 22
    Cid = 3, Uid = 98
    Cid = 1, Uid = 66
    Cid = 2, Uid = -4
    Cid = 3, Uid = 57
    Cid = 1, Uid = 10
    Cid = 2, Uid = -11
    Cid = 3, Uid = 41
    Cid = 2, Uid = -14
    Cid = 3, Uid = 10
    

    【讨论】:

      猜你喜欢
      • 2017-04-08
      • 2013-05-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-07-11
      • 2018-09-10
      • 1970-01-01
      • 2018-02-11
      相关资源
      最近更新 更多