sort and group a java collection

前端 未结 5 437
无人及你
无人及你 2021-02-01 21:17

I have an object which has a name and a score. I would like to sort a collection of such objects so that they are grouped by name and sorted by maximum score in each group (and

相关标签:
5条回答
  • 2021-02-01 21:59

    Yes Go for Comparator

    Give first preference to name in comparison and then to score. it will be grouped up with sorted score also

        List<Score> scores = new ArrayList<Score>();
        scores.add(new Score("a", 58));
        scores.add(new Score("a", 10));
        scores.add(new Score("b", 165));
        scores.add(new Score("a", 1));
        scores.add(new Score("b", 1658));
        scores.add(new Score("c", 1));
        scores.add(new Score("c", 10));
        scores.add(new Score("c", 0));
    
        Collections.sort(scores, new Comparator<Score>() {
    
            public int compare(Score o1, Score o2) {
                if (o1.getName().compareTo(o2.getName()) == 0) {
                    return o2.getScore() - o1.getScore();
                } else {
                    return o1.getName().compareTo(o2.getName());
                }
            }
        });
        System.out.println(scores);
    

    Update

    As Chris pointed out.

    import java.util.*;
    
    /**
     *
     * @author Jigar
     */
    class Score {
    
        private String name;
        private List<Integer> scores;
    
        public Score() {
        }
    
        public Score(String name, List<Integer> scores) {
            this.name = name;
            this.scores = scores;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public List<Integer> getScores() {
            return scores;
        }
    
        public void setScores(List<Integer> scores) {
            this.scores = scores;
        }
    
        @Override
        public String toString() {
            return name + " , " + scores + "\n";
        }
    }
    
    public class ScoreDemo { 
    
        public static void main(String[] args) {
            List<Score> scores = new ArrayList<Score>();
    
    
            List<Integer> lstA = new ArrayList<Integer>();
            lstA.add(3);
            lstA.add(9);
            lstA.add(7);
            Collections.sort(lstA);
            Collections.reverse(lstA);
    
            List<Integer> lstB = new ArrayList<Integer>();
            lstB.add(10);
            lstB.add(8);
            lstB.add(3);
            Collections.sort(lstB);
            Collections.reverse(lstB);
    
            List<Integer> lstC = new ArrayList<Integer>();
            lstC.add(8);
            lstC.add(3);
            Collections.sort(lstC);
            Collections.reverse(lstC);
    
    
            scores.add(new Score("a", lstA));
            scores.add(new Score("b", lstB));
            scores.add(new Score("c", lstC));
    
    
    
    
    
            Collections.sort(scores, new Comparator<Score>() {
    
                public int compare(Score o1, Score o2) {
                    return o2.getScores().get(0).compareTo(o1.getScores().get(0));
                }
            });
            System.out.println(scores);
    
        }
    }
    
    • Here is working ideone demo

    • UPDATE: Here is working ideone demo

    0 讨论(0)
  • 2021-02-01 22:00
    public class ScoreComparator implements Comparator<Item>
    {
    
      public int compare(Item a, Item b){
    
        if (a.name.equals(b.name){
          return a.score.compareTo(b.score);
        }
    
        return a.name.compareTo(b.Name);    
    
      }
    
    }
    
    0 讨论(0)
  • 2021-02-01 22:07

    No, you can't do it with a single sort with a single Comparator.

    You have to:

    1. group by name
    2. sort the groups, by highest score in group
    3. Then you need to flatten the groups back to a list.

    With Java 8

    Edit: Since i wrote this answer, Java 8 has come out, which simplifies the problem a lot:

    import java.util.*;
    import static java.util.Comparator.*;
    import static java.util.stream.Collectors.*;
    

    List<Record> result = records.stream()
        .sorted(comparingInt(Record::getScore).reversed())
        .collect(groupingBy(Record::getName, LinkedHashMap::new, toList()))
        .values().stream()
        .flatMap(Collection::stream)
        .collect(toList());
    

    First we sort by score reversed, and then we group using a LinkedHashMap, which will preserve the insertion order for the keys, so keys with higher score will come first.

    Sorting first is OK if the groups are small, so the redundant compares between objects in different groups don't hurt so much.

    Also, with this method, duplicates are preserved.


    Alternatively, if you don't care about preserving duplicates, you can:

    Comparator<Record> highestScoreFirst = comparingInt(Record::getScore).reversed();
    
    List<Record> result = records.stream()
            .collect(groupingBy(Record::getName,
                    toCollection(() -> new TreeSet<>(highestScoreFirst))))
            .values().stream()
            .sorted(comparing(SortedSet::first, highestScoreFirst))
            .flatMap(Collection::stream)
            .collect(toList());
    

    Where the records are grouped into sorted TreeSets, instead of sorting the values as the first operation of the stream, and then the sets are sorted by their first, highest value.

    Grouping before sorting is appropriate if the groups are big, to cut down on redundant compares.


    Implementing Comparable:

    And you can make it shorter by having your record implement Comparable

    public class Record implements Comparable<Record> {
        @Override
        public int compareTo(Record other) {
            // Highest first
            return -Integer.compare(getScore(), other.getScore());
    
            /* Or equivalently:
               return Integer.compare(other.getScore(), getScore());
            */
        }
        ...
    }
    

    List<Record> result = records.stream()
        .collect(groupingBy(Record::getName, toCollection(TreeSet::new)))
        .values().stream()
        .sorted(comparing(SortedSet::first))
        .flatMap(Collection::stream)
        .collect(toList());
    

    Before Java 8

    Edit: Here is a really rough unit test that demonstrates one way to do it. I haven't cleaned it up as much as i would have liked.

    Stuff like this is painful in Java, and i would normally use Google Guava for this.

    import org.junit.Test;
    
    import java.util.*;
    
    import static java.util.Arrays.asList;
    import static org.junit.Assert.assertEquals;
    
    public class GroupSortTest {
    
        @Test
        public void testGroupSort() {
            List<Record> records = asList(
                    new Record("a", 3),
                    new Record("a", 9),
                    new Record("b", 7),
                    new Record("b", 10),
                    new Record("c", 8),
                    new Record("c", 3));
    
            List<SortedMap<Integer, Record>> recordsGroupedByName = groupRecordsByNameAndSortedByScoreDescending(records);
            Collections.sort(recordsGroupedByName, byHighestScoreInGroupDescending());
            List<Record> result = flattenGroups(recordsGroupedByName);
    
            List<Record> expected = asList(
                    new Record("b", 10),
                    new Record("b", 7),
                    new Record("a", 9),
                    new Record("a", 3),
                    new Record("c", 8),
                    new Record("c", 3));
    
            assertEquals(expected, result);
        }
    
        private List<Record> flattenGroups(List<SortedMap<Integer, Record>> recordGroups) {
            List<Record> result = new ArrayList<Record>();
            for (SortedMap<Integer, Record> group : recordGroups) {
                result.addAll(group.values());
            }
            return result;
        }
    
        private List<SortedMap<Integer, Record>> groupRecordsByNameAndSortedByScoreDescending(List<Record> records) {
            Map<String, SortedMap<Integer, Record>> groupsByName = new HashMap<String, SortedMap<Integer, Record>>();
            for (Record record : records) {
                SortedMap<Integer, Record> group = groupsByName.get(record.getName());
                if (null == group) {
                    group = new TreeMap<Integer, Record>(descending());
                    groupsByName.put(record.getName(), group);
                }
                group.put(record.getScore(), record);
            }
            return new ArrayList<SortedMap<Integer, Record>>(groupsByName.values());
        }
    
        private DescendingSortComparator descending() {
            return new DescendingSortComparator();
        }
    
        private ByFirstKeyDescending byHighestScoreInGroupDescending() {
            return new ByFirstKeyDescending();
        }
    
        private static class ByFirstKeyDescending implements Comparator<SortedMap<Integer, Record>> {
            public int compare(SortedMap<Integer, Record> o1, SortedMap<Integer, Record> o2) {
                return o2.firstKey().compareTo(o1.firstKey());
            }
        }
    
        private static class DescendingSortComparator implements Comparator<Comparable> {
            public int compare(Comparable o1, Comparable o2) {
                return o2.compareTo(o1);
            }
        }
    }
    
    0 讨论(0)
  • 2021-02-01 22:08

    I think you can do this. First check to see if the group is equal. If it is then compare on score. Otherwise return which group you want to be more on top. Let me code it up.

        class Item{
          String name;
          int score;
        }
    
       new Comparator<Item>(){
    
           @Override
           public int compare(Item o1, Item o2) {
                if (o1.name.equals(o2.name)) {
                    return o1.score > o2.score ? 1 : -1; // might have to flip this. I didn't test
                }else {
                    return o1.name.compareTo(o2.name);
                }
           }
        };
    
    0 讨论(0)
  • 2021-02-01 22:14

    Foreach over the collection, and put the objects into a Map<String, SortedSet<YourObject>>, keyed by name, where the SortedSet is a TreeSet with a custom comparator that compares by score.

    Then foreach over the map's values() collection, and put the groups into a SortedSet<SortedSet<YourObject>>, with a second custom comparator that compares SortedSets according to their largest element. Actually, instead of foreaching, you can simply use addAll().

    Here's the code:

    public class SortThings {
    
        static class Thing {
            public final String name;
            public final int score;
            public Thing(String name, int score) {
                this.name = name;
                this.score = score;
            }
            @Override
            public String toString() {
                return "(" + name + ", " + score + ")";
            }
        }
    
        public static void main(String[] args) {
            Collection<Thing> things = Arrays.asList(
                new Thing("a", 3),
                new Thing("a", 9),
                new Thing("b", 7),
                new Thing("b", 10),
                new Thing("c", 8),
                new Thing("c", 3)
            );
    
            SortedSet<SortedSet<Thing>> sortedGroups = sortThings(things);
    
            System.out.println(sortedGroups);
        }
    
        private static SortedSet<SortedSet<Thing>> sortThings(Collection<Thing> things) {
            final Comparator<Thing> compareThings = new Comparator<Thing>() {
                public int compare(Thing a, Thing b) {
                    Integer aScore = a.score;
                    Integer bScore = b.score;
                    return aScore.compareTo(bScore);
                }
            };
    
            // first pass
            Map<String, SortedSet<Thing>> groups = new HashMap<String, SortedSet<Thing>>();
            for (Thing obj: things) {
                SortedSet<Thing> group = groups.get(obj.name);
                if (group == null) {
                    group = new TreeSet<Thing>(compareThings);
                    groups.put(obj.name, group);
                }
                group.add(obj);
            }
    
            // second pass
            SortedSet<SortedSet<Thing>> sortedGroups = new TreeSet<SortedSet<Thing>>(new Comparator<SortedSet<Thing>>() {
                public int compare(SortedSet<Thing> a, SortedSet<Thing> b) {
                    return compareThings.compare(a.last(), b.last());
                }
            });
            sortedGroups.addAll(groups.values());
            return sortedGroups;
        }
    
    }
    

    Note that the output is in smallest-to-largest order. That's the natural order with Java's collections; it would be trivial to modify this to sort the other way if that's what you need.

    0 讨论(0)
提交回复
热议问题