我如何使用随机断开关系的比较器进行排序?

How can I sort using a comparator with ties broken randomly?

我正在构建一个模拟体育联赛的程序,而且我经常想按积分、净胜球等对球队进行排序,以确定哪支球队获得晋级或哪支球队获得优先种子比赛。但是,有时,团队会在我想使用的所有标准上打成平手,我希望这些平局可以随机打破。我以前使用团队的 id(这是唯一的整数)来打破平局,但我想这样做,以便没有团队在这种排序中得到一致的优惠待遇。我知道如果我只是让 Java 决定哪支并列球队将排在另一支球队的前面,那将是非常不可预测的,但我不相信它实际上是随机的,而且我担心它会无意中给一些团队带来优势。

我一直在使用 Collections.sort() 和比较器来进行排序,我最近才这样做,并将我的比较器切换为使用随机数作为最终决胜局而不是团队 ID .这是有问题的比较器:

public static final Comparator<Team> pointsGDRand = new Comparator<Team>() {
    public int compare(Team a, Team b) {
        int result = b.getStats().getPts() - a.getStats().getPts();
        if (result == 0) {
            result = b.getStats().getGD() - a.getStats().getGD();
            if (result == 0) {
                result = R.boolNum();
            }
        }
        return result;
    }
};

我认为 getter 函数是不言自明的,但这是随机函数:

public static int boolNum() {
    return r.nextBoolean() ? 1 : -1;
}

但是,就在不久前(这个错误弹出似乎很久了),我得到了这个错误:

Exception in thread "main" java.lang.IllegalArgumentException: Comparison method violates its general contract!

我读了一点书,我认为问题很可能是我在某些情况下发现 A > B 和 B > C,还有 A < C。这说明了一些问题为什么要花这么长时间才弹出这个错误(在弹出这个错误之前可能做了 20,000 次排序),因为 2 个团队在 all 标准上获得完全相同的分数似乎是对我来说很少见,更不用说 3 个或更多了。

我宁愿不写我自己的自定义排序,我也宁愿在每次排序之前不打乱有问题的列表(我觉得那样效率很低,但我有信心会工作)。

那么,我怎样才能制作一个随机打破平局的比较器(这样就不会有团队不成比例地赢得平局的风险)而不会再次出现同样的错误?

我们需要使用一个额外的属性来比较并列球队。此属性需要在比较器的生命周期内保持不变,但从一个比较器到下一个比较器是随机的。

每次需要排序时创建一个新的比较器,并确保它以可预测但随机的方式运行:

class PointsGDRandComparator implements Comparator<Team> {
    // each comparator has a different seed
    private final long seed = new Random().nextLong();

    public int compare(Team a, Team b) {
        int result = b.getStats().getPts() - a.getStats().getPts();
        if (result == 0) {
            // every time we compare a particular team, the same random number will be produced
            long aValue = new Random(seed * a.getTeamId()).nextLong();
            long bValue = new Random(seed * b.getTeamId()).nextLong();
            return Long.compare(aValue, bValue);
        }
        return result;
    }
}

所以每个比较器都会有不同的随机数 seed,但每个随机决策都是相同的,因为我们使用基于比较器 seed 的种子创建新的 Random 实例以及两队的id。

我们仍然基于团队 ID 的函数进行排序(因为我们需要在比较团队之间建立对称和​​传递关系)。

使用相同种子创建的两个 Random 实例将产生相同的随机数序列。因此,每当我们将 ID 为 45 的团队与另一个团队进行比较时,我们将创建一个带有种子 seed * 45Random 实例(其中 seed 在整个生命周期内保持不变比较器),因此我们第一次调用 nextLong() 任何使用该种子创建的 Random 实例时,我们将始终获得相同的数字。

这里有一个不那么棘手的方法来做同样的事情。它假定 Team 具有正确的 hashCodeequals 实现,因此我们可以将其用作 Map:

中的键
class PointsGDRandComparator implements Comparator<Team> {
    private final Map<Team,Long> tiebreakers = new HashMap<>();
    
    public PointsGDRandComparator(List<Team> teams) {
        Random r = new Random();
        for (Team t : teams) {
            tiebreakers.put(t, r.nextLong());
        }
    }

    public int compare(Team a, Team b) {
        int result = b.getStats().getPts() - a.getStats().getPts();
        if (result == 0) {
            return Long.compare(tiebreakers.get(a), tiebreakers.get(b));
        }
        return result;
    }
}

上面的解决方案很昂贵,因为它创建了一个大小为团队列表的地图,并为每个团队创建了一个随机数。除非你有成千上万的团队,这无关紧要,但我们可以通过延迟填充地图来降低成本:

class PointsGDRandComparator2 implements Comparator<Team> {
    private final Map<Team,Long> tiebreakers = new HashMap<>();
    private final Random r = new Random();

    public int compare(Team a, Team b) {
        int result = b.getStats().getPts() - a.getStats().getPts();
        if (result == 0) {
            return Long.compare(
                    tiebreakers.computeIfAbsent(a, t -> r.nextLong()), 
                    tiebreakers.computeIfAbsent(b, t -> r.nextLong())
            );
        }
        return result;
    }
}

这是随机选择并列球队的另一种方法

使用地图。

  • 存储地图中并列的球队,以他们的分数作为键值。地图可以是树形图,这样可以排序。
  • 然后遍历地图并从每个列表中随机选择一个团队。

以下是任何处理前的团队。输出是比分和队名。

[3, A]
[1, B]
[2, C]
[0, D]
[0, E]
[1, F]
[1, G]
[2, H]
[2, I]
[2, J]

现在构建团队地图。

Map<Integer, List<Team>> team = teams.stream().collect(
   Collectors.groupingBy(t->t.getStats().getPts(),
        ()->new TreeMap<Integer,List<Team>>(Comparator.reverseOrder()),
            Collectors.toList()));

这是球队的地图,按得分降序排列。键是分数,值是具有该分数的球队列表。

3=[[3, A]]
2=[[2, C], [2, H], [2, I], [2, J]]
1=[[1, B], [1, F], [1, G]]
0=[[0, D], [0, E]]

现在,只需从每个列表中为给定分数选择一个随机值。这会在 0 和列表中的值数 (i.e. size())

之间选择一个值
Random r = new Random();
for (Entry<Integer, List<Team>> e : team.entrySet()) {
    int pick = r.nextInt(e.getValue().size());
    System.out.println(e.getKey() + " " + e.getValue().get(pick));

为此 运行 印刷

3 [3, A]
2 [2, C]
1 [1, B]
0 [0, E]