@TomaZe

Как эффективно разбить множество строковых значений на непересекающиеся группы?

Строки имеют следующий вид:
A1;B1;C1
A2;B2;C2
Как найти множество уникальных строчек и разбить его на не пересекающиеся группы по следующему критерию: если две строчки имеют совпадения непустых значений в одной или более колонках, они принадлежат одной группе
Например, строки
1,2,3
4,5,6
1,5,7
Принадлежат одной группе.

Изначально думал сделать через совокупность хэшсетов (три хэшсета для каждой колонки) для быстрого просмотра, входит ли строка в список уникальных значений, с последующим добавлением либо в список уже сгруппированных строк, либо в список уникальных строк. Но алгоритм в таком случае имеет узкое место по производительности: при необходимости слияния групп необходимо проходить каждую группу в списке. Алгоритм на большом количестве данных (>1 млн записей), при большом количестве слияний работает медленно. Если слияний мало (порядка тысяч), работает быстро. Поймал затык в этом месте и не знаю, как оптимизировать это узкое место или же необходимо использовать другие структуры данных и алгоритмы.
Может кто подскажет, в каком направлении копать.
  • Вопрос задан
  • 2015 просмотров
Пригласить эксперта
Ответы на вопрос 3
@koronabora
Человек
Я вижу только один вариант - хранить множество уникальных значений для каждой колонки. Если строка имеет в одной из колонок совпадение - тогда добавить в это множество и обновить уникальные значения колонок.

ИМХО, быстрее уже не сделать. Только если не оптимизировать поиск по уникальным значениям колонки, переделав его в бинарный, например.
Ответ написан
@MaxLich
java developer
Нашёл одно решение. Алгоритм:
  1. храним результат в виде списка списков: [номер_группы -> [строки_группы]]
  2. используем вспомогательный список хэш-таблиц: [позиция_слова -> { слово -> номер_группы }] и вспомогательную хэш-таблицу для хранения какая группа в какую была влита
  3. каждое слово строки ищем в соответствующей (позиции слова в строке) хэш-таблице
    а) если слово есть, запоминаем номер группы (значение из хэш-таблицы), в которой оно найдено
    б) если слова нет, то добавляем его в список новых слов
  4. если строка (а точнее её слова) найдена в группах, то берём первую из "живых" (объяснение этого позже) групп, иначе создаём новую группу
  5. добавляем новые слова в соответствующие хэш-таблицы с номером найденной/созданной группы
  6. объединяем найденные группы в одну, выбранную ранее. Так как группы хранятся в виде списка строк, то просто объединяем списки строк в один у выбранной группы, а более ненужные группы отмечаем как "мёртвые" (присваиваем null, дабы не перемещать элементы внутри списка)
  7. добавляем строку в список строк группы


Код метода поиска групп:
code
private static List<List<String>> findLineGroups(List<String> lines) {
        class NewLineElement {
            private String lineElement;
            private int columnNum;

            private NewLineElement(String lineElement, int columnNum) {
                this.lineElement = lineElement;
                this.columnNum = columnNum;
            }
        }

        if (lines == null)
            return Collections.emptyList();

        List<List<String>> linesGroups = new ArrayList<>(); //список групп, каждый элемент вида "номер группы - список строк группы"
        if (lines.size() < 2) {
            linesGroups.add(lines);
            return linesGroups;
        }

        List<Map<String, Integer>> columns = new ArrayList<>(); // список стобцов, каждый столбец - мапа с парами "элемент строки/столбца-номер группы"
        Map<Integer, Integer> unitedGroups = new HashMap<>(); //мэп с парами "номер некоторой группы - номер группы, с которой надо объединить данную"
        for (String line : lines) {
            String[] lineElements = line.split(";");
            TreeSet<Integer> groupsWithSameElems = new TreeSet<>(); //список групп, имеющих совпадающие элементы
            List<NewLineElement> newElements = new ArrayList<>(); //список элементов, которых нет в мапах столбцов

            for (int elmIndex = 0; elmIndex < lineElements.length; elmIndex++) {
                String currLnElem = lineElements[elmIndex];
                if (columns.size() == elmIndex)
                    columns.add(new HashMap<>());
                if ("".equals(currLnElem.replaceAll("\"","").trim()))
                    continue;

                Map<String, Integer> currCol = columns.get(elmIndex);
                Integer elemGrNum = currCol.get(currLnElem);
                if (elemGrNum != null) {
                    while (unitedGroups.containsKey(elemGrNum)) // если группа с таким номером объединена с другой,
                        elemGrNum = unitedGroups.get(elemGrNum); //то сохраняем номер группы, с которой была объединена данная
                    groupsWithSameElems.add(elemGrNum);
                } else {
                    newElements.add(new NewLineElement(currLnElem, elmIndex));
                }
            }
            int groupNumber;
            if (groupsWithSameElems.isEmpty()) {
                linesGroups.add(new ArrayList<>());
                groupNumber = linesGroups.size() - 1;
            } else {
                groupNumber = groupsWithSameElems.first();
            }
            for (NewLineElement newLineElement : newElements) {
                columns.get(newLineElement.columnNum).put(newLineElement.lineElement, groupNumber);
            }
            for (int matchedGrNum : groupsWithSameElems) { //перебираем все группы с таким же элементом
                if (matchedGrNum != groupNumber) {
                    unitedGroups.put(matchedGrNum, groupNumber); //сохраняем инф-цию об объединённых группах
                    linesGroups.get(groupNumber).addAll(linesGroups.get(matchedGrNum)); //объединяем группы
                    linesGroups.set(matchedGrNum, null); //помечаем группу с текущим номер, как несуществующую
                }
            }
            linesGroups.get(groupNumber).add(line);
        }
        linesGroups.removeAll(Collections.singleton(null)); //удаляем несуществующие группы
        return linesGroups;
    }
Ответ написан
@Stepan524
попробуйте решить сами, а потом возвращайтесь сюда.
- храним результат в виде списка множеств для уникальности: [номер_группы -> [строки_группы]]
- используем вспомогательный список хэш-таблиц: [позиция_слова -> { слово -> номер_группы }]
1. считать строку, разбить на колонки
2. обойти колонки. для каждой колонки которая указывает на существующий к текущему моменту номер группы, если это не впервые для этой строки, пересасываем все строки найденной теперь группы в первую, переназначаем их колонки на нее и освобождаем найденную теперь группу.
3. если не нашли ни одной, добавляем строку как новую группу. иначе добавляем ее к той группе которая найдена первая.
4. когда считали все строки остается вывести список множеств строк - все группы
code

package org.example;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.*;

public class Main {
    public static void main(String[] args) {
        BufferedReader reader;
        try {
            reader = new BufferedReader(new FileReader(args[0]));
            List<Set<String>> groups = new ArrayList<>();
            List<Map<String, Integer>> parts = new ArrayList<>();

            String line = reader.readLine();
            while (line != null) {
                String[] columns = getColumnsOf(line);
                Integer groupNumber = null;
                for (int i = 0; i < Math.min(parts.size(), columns.length); i++) {
                    Integer groupNumber2 = parts.get(i).get(columns[i]);
                    if (groupNumber2 != null) {
                        if (groupNumber == null) {
                            groupNumber = groupNumber2;
                        } else if (!Objects.equals(groupNumber, groupNumber2)) {
                            for (String line2 : groups.get(groupNumber2)) {
                                groups.get(groupNumber).add(line2);
                                apply(getColumnsOf(line2), groupNumber, parts);
                            }
                            groups.set(groupNumber2, new HashSet<>());
                        }
                    }
                }
                if (groupNumber == null) {
                    if (Arrays.stream(columns).anyMatch(s -> !s.isEmpty())) {
                        groups.add(new HashSet<>(List.of(line)));
                        apply(columns, groups.size() - 1, parts);
                    }
                } else {
                    groups.get(groupNumber).add(line);
                    apply(columns, groupNumber, parts);
                }
                line = reader.readLine();
            }
            reader.close();

            System.out.println("Групп размера больше 1: " + groups.stream().filter(s -> s.size() > 1).count());
            groups.sort(Comparator.comparingInt(s -> -s.size()));
            int i = 0;
            for (Set<String> group : groups) {
                i++;
                System.out.println("\nГруппа " + i);
                for (String val : group) {
                    System.out.println(val);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static String[] getColumnsOf(String line) {
        for (int i = 1; i < line.length() - 1; i++) {
            if (line.charAt(i) == '"' && line.charAt(i - 1) != ';' && line.charAt(i + 1) != ';') {
                return new String[0];
            }
        }
        return line.replaceAll("\"", "").split(";");
    }

    private static void apply(String[] newValues, int groupNumber, List<Map<String, Integer>> parts) {
        for (int i = 0; i < newValues.length; i++) {
            if (newValues[i].isEmpty()) {
                continue;
            }
            if (i < parts.size()) {
                parts.get(i).put(newValues[i], groupNumber);
            } else {
                HashMap<String, Integer> map = new HashMap<>();
                map.put(newValues[i], groupNumber);
                parts.add(map);
            }
        }
    }
}


программа работает достаточно быстро и требует меньше гигабайта памяти.
следует дописать вывод не в sout а в файл, назвать переменные более соответствующе.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы