Решение небольшой, но интересной задачи на java, с которой я столкнулся, сподвигло в очередной раз возвратиться к теме выбора реализации List-та.

Известно, что реализация Linked- или ArrayList-а в разных ситуациях (вставка, удаление.. добавление элемента в середину/начало/конец списка) ведет себя по разному. Что делает выбор текущей реализации не всегда очевидным. Proof?..

Например, существует задача выделения сообществ (подграфов). Необходимо сделать следующее:

а) считать файл со строками вида

A1;C2;F4
C2;Z4;N2
N2;H1;L8
T7;O7
...

б) найти множество уникальных строк и разбить его на непересекающиеся группы по следующему критерию:

- если строчки имеют совпадения непустых значений в одной или более колонках,

они принадлежат одной группе. Например, строчки:

111;123;222
200;123;100
300;;100

все принадлежат одной группе, так как первые две - имеют одинаковое значение 123 во второй колонке, а две последние одинаковое значение 100 в третьей колонке.

На вход консольного приложения подаем файл (csv) c более чем млн. строк. Рассмотрим решение задачи с помощью библиотеки JGraphT:

@Component
public class Processing {

    @Autowired
    FiosUtil fiosUtil;

    public Graph<String, DefaultEdge> graph = new SimpleGraph<>(DefaultEdge.class);     DefaultDirectedWeightedGraph
    public Map<String, List<List<String>>> groupMap = new HashMap<>();
    public List<List<List<String>>> groupList = new LinkedList<>();
    private Integer groupCounter = 1;

    public void process() throws IOException {
        BufferedReader csvReader = new BufferedReader(new InputStreamReader(fiosUtil.getResourceFileStream()));
        String row;
        while ((row = csvReader.readLine()) != null) {
            List<String> rowSet = fiosUtil.getRowSet(row);
            createGraph(rowSet);
        }
        csvReader.close();
        System.out.println(">> Created graph. Graph vertexes are: " + graph.vertexSet().size() + "\t\t(unique digits in all row's)");
        boolean isEndOfGroups = graphIterate(graph);
        while (!isEndOfGroups) {
            isEndOfGroups = graphIterate(graph);
        }
        outOfGroups();
    }

    public void createGraph(List<String> row) {
        List<List<String>> list = new ArrayList<>();
        list.add(row);
        String initItem = "";
        if (!row.isEmpty()) initItem = row.get(0);
        for (String item : row) {
            if (!item.isEmpty()) {
                graph.addVertex(item);
                groupMap.merge(item, list, (x, y) -> {
                    x.addAll(y);
                    return x.stream().distinct().collect(Collectors.toList());
                });
                if (!initItem.isEmpty() && !initItem.equals(item)) {
                    graph.addEdge(initItem, item);
                    initItem = item;
                }
            }
        }
        if(row.size() == 3) graph.addEdge(row.get(0), row.get(row.size()-1));
    }

    public boolean graphIterate(Graph<String, DefaultEdge> graph) {
        List<List<String>> groupRows = new LinkedList<>();
        List<String> groupVertex = new ArrayList<>();
        Iterator<String> graphIterator = graph.vertexSet().iterator();
        if (graphIterator.hasNext()) {
            DepthFirstIterator<String, DefaultEdge> depthFirstIterator = new DepthFirstIterator<>(graph, graphIterator.next());
            while (depthFirstIterator.hasNext()) {
                String vertex = depthFirstIterator.next();
                groupVertex.add(vertex);
            }
            for (String vertex : groupVertex) {
                groupRows.addAll(new ArrayList<>(groupMap.get(vertex)));
            }
        }
        removeGraphEntrySaveRows(groupRows);
        return graph.vertexSet().size() == 0;
    }

    private void removeGraphEntrySaveRows(List<List<String>> group){
        List<List<String>> rowsGroup = group.stream()
                .peek(row -> row.forEach(vertex -> graph.removeVertex(vertex)))
                .distinct()
                .sorted(Comparator.comparingInt(List::size))
                .collect(Collectors.toList());
        groupList.add(rowsGroup);
    }

    public void outOfGroups(){
        Iterator<List<List<String>>> groupsIterator = groupList.iterator();
        while (groupsIterator.hasNext()){
            List<List<String>> currentGroup = groupsIterator.next();
            groupList.stream()
                    .filter(iterateGroup -> iterateGroup.equals(currentGroup))
                    .peek(iterateGroup -> {
                        iterateGroup.addAll(currentGroup);
                        groupList.remove(currentGroup);
                    });
        }

        for (List<List<String>> rowsGroup : groupList){
            System.out.println("\nGroup<" + groupCounter++ + ">:" +
                    "\n----------------------------------------------------");
            rowsGroup.forEach(System.out::println);
        }
    }
}

Для этого, в каждой входящей строке создаем vertex для каждого значения, создаем между ними edges. Затем просто обходим созданный граф DepthFirstIterator-итератором, исключая пройденные вершины и находим т.о., очередной подграф (или подгруппу, согласно условию задачи).

При правильной реализации на среднем ноутбуке мы вполне укладываемся в 30 sec. Как думаете, на сколько увеличится время решения, если поменять реализацию groupList на  ArrayList<>()?

Ссылка на GitHub

Вариант решения через алгоритм

Комментарии (13)


  1. grigorym
    18.06.2023 06:58
    +5

    Если ничего не сказано про размер входного файла, то оценка в 30 сек - ни о чём.


    1. ris58h
      18.06.2023 06:58

      На вход консольного приложения подаем файл (csv) c более чем млн. строк.


      1. dopusteam
        18.06.2023 06:58
        +1

        Более чем 1млн - это сколько? 1000001? 2 миллиона? Миллиард? Гугол?


        1. ris58h
          18.06.2023 06:58
          -3

          Да.


          1. grigorym
            18.06.2023 06:58
            +4

            Рекомендую разобраться с тем, что такое метрики и зачем они нужны. Неплохо ещё в целом освоить научный подход и правила постановки и проведения экспериментов.


  1. mayorovp
    18.06.2023 06:58
    +7

    Что-то как-то всё сложно. Я так и не понял что за вершины у вас в графе, но кажется что вы просто используете значения в качестве вершин. Это совершенно неправильно, вот на таких исходных данных у вас будет ошибка:


    1;2;3
    4;5;1

    Здесь у строк нет совпадений ни в одной колонке, но вы ведь всё равно их отнесёте в одну группу?




    Для нормального решения задачи надо не списки выбирать (хотя чего их выбирать, ArrayList всегда лучше если вам достаточно интерфейса List), а построить нормальный граф, который отражает исходную задачу.


    Я бы строил его как двудольный, левая доля — строки, правая доля — пары из номера колонки и значения в ней. Никаких groupMap и groupList тут быть не должно.




    PS в вашем коде невозможно разбираться. Вас что, штрафуют за созданные классы? Как вы вообще помните какой из List в вашем List<List<List<String>>> за что отвечает?


    Вот так же гораздо понятнее:


    class Row {
        public List<String> Values = new ArrayList<>;
    }
    
    class RowGroup {
        public List<Row> Rows = new ArrayList<>();
    }
    
    private Graph<String, DefaultEdge> graph = new SimpleGraph<>(DefaultEdge.class);
    private Map<String, RowGroup> groupMap = new HashMap<>();
    private List<RowGroup> groupList = new LinkedList<>();

    Или вот, для моего варианта графа:


    sealed interface Vertex {}
    
    class Row implements Vertex {
        public List<String> Values = new ArrayList<>();
    }
    
    record RowValue(int column, String value) implements Vertex {}
    
    private Graph<Vertex, DefaultEdge> graph = new SimpleGraph<>(DefaultEdge.class);


  1. Alexandroppolus
    18.06.2023 06:58
    +1

    Навскидку, тут прям напрашивается DSU

    Согласен с мнением @mayorovp, что вершинами должны быть строки. Плюс понадобится хэшмэп ((колонка+значение) -> вершина), ну или отдельный хэшмэп (значение -> вершина) для каждой колонки. Для очередной строки проходим по всем её значениям, и если нашли смежную вершину по хэшмэпу, то присоединяем к её группе. Это позволит быстро обрабатывать кейсы, когда строка соединяет две большие группы (собственно, для чего DSU и придумана).


  1. vasyakolobok77
    18.06.2023 06:58
    +6

    Это точно код для современной java? Напишу несколько замечаний, чтобы новички знали как делать не надо.

    Открыли I/O stream / reader – не забудьте его закрыть, try with resources в этом поможет.

    Для работы с локальными файлами есть Files, для работы с classpath ресурсами в экосистеме spring – Resource.

    Ручная итерация через iterator моветон уже лет 20 как, если не больше.

    Нет инкапсуляции, поля должны быть скрыты, доступ через getter-ы.

    Дикая сложность алгоритма, цикл циклом погоняет (стримы внутри циклов). Стоило вычитать все строки, построить таблицу соответствия, а уже потом устанавливать связи для графа.

    Чисто для визуальной чистоты в современной java для ссылок можно указывать var.

    В подавляющем большинстве случаев ArrayList наше все.

    Как уже упоминули выше, что это за 30 сек и для какого объема данные? Как проводили замеры? Для создания бенчмарков используем JMH.


    1. mayorovp
      18.06.2023 06:58

      Нет инкапсуляции, поля должны быть скрыты, доступ через getter-ы.

      Там вообще доступа к полям снаружи не требуется.


  1. sergey-gornostaev
    18.06.2023 06:58
    +9

    В постах про выбор реализации списка невозможно удержаться от цитаты:


  1. binakot
    18.06.2023 06:58

    Так замеры не делаются. Нужно срочно все перевести на JMH (https://github.com/openjdk/jmh) с прогревом JVM и прогоном в минимум 10 раз с выведением средних результатов. Нельзя утверждать о каких-то сравнениях, не предоставив информацию о входных данных (размер входного файла, характеристики железа, сборка и версия JDK и так далее).

    Ну и в дополнение к цитате Joshua Bloch, Шипилёв говорил, что знает только пару кейсов, когда можно использовать LinkedList вместо ArrayList, но даже тогда выигрыш настолько несущественный, что проще всегда использовать ArrayList ;)


  1. tmk826
    18.06.2023 06:58

    Плоскогубцы vs. Кусачки


  1. alexandr179 Автор
    18.06.2023 06:58

    Друзья, на столь простой задаче мы побили все рейтинги..) Статья, конечно, носит обзорный характер. Тема "LinkedList & ArrayList" не подлежит какому-то серьезному обсуждению.. Тем не менее, приятно, что (не сильно подготовленная к публикации) заметка, вызвала столько комментариев. Основной интерес, полагаю, вызван применением, рассмотренном на простом примере, библиотеки jgrapht.

    grigorym - размер входного файла - в репозитории.

    mayorovp - у приведенных строк есть совпадения, в первой и последней колонках.

    tmk826 - полностью согласен) "Плоскогубцы vs. Кусачки" Метрики и научный подход здесь, пожалуй, будет лишними. Joshua Bloch, Шипилёв... ) Да, знаем. Авторы не безизвестные 

    vasyakolobok77 ..бэнчмарки для этой задачи, нет, правда - я пас.

    Alexandroppolus ..а вот это - полезный комментарий, по сути задачи. Спасибо. Принято к сведению