Вопрос: Как эффективно сгруппировать строки?

Лучший ответ:
Как ни странно, итерировать группы совсем не обязательно!
1) группа у вас состоит из одного элемента. В вашем примере F и X - две группы, в которые нужно положить номера строк.
2) за один проход бежим по строкам и добавляем их в соответствующие группы термов, которые держим в hashtable, где ключом у нас сам терм, а значением - массив из номеров строк.
3) после того, как заполнили хеш, пробегаемся по нему один раз и смотрим, у кого длина массива больше единицы, это и будут исходные группы.
Если нам нужно дополнительно сформировать группы из двух-трех термов, то делает все тоже самое, но ключом ставим treeset из этих элементов.import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.TreeSet;

public class Groups {

	public static void main(String[] args) {
		String[] myData = {
				"F;I;J", 
				"F;X;A",
				"X;D;P",

				"A;B;C",
				"X;Y;Z",
				"J;A;Z",
				"U;V;W",
				"E;E;E",
				"D;F;G",
		};
		
		HashMap<String, TreeSet<Integer>> groups = new HashMap<String, TreeSet<Integer>>();
		
		for(int line=0; line< myData.length; line++ ) { // бежим по строкам
			
			List<String> terms = Arrays.asList(myData[line].split(";")); // разбиваем на термы
			
			for(String term: terms) { // пробегаем по термам
				TreeSet<Integer> group = groups.get(term); // выдергиваем группу
				
				if(group == null) { // если группы нет
					group = new TreeSet<Integer>();
					groups.put(term, group);
				}
				group.add(line); // добваляем строку
			}
		}
		
		// выводим результат
		for(Entry<String, TreeSet<Integer>> group: groups.entrySet()) {
			if(group.getValue().size() >1)
				System.out.printf("%s - %s\n", group.getKey().toString(), group.getValue().toString());
		}
	}
}
И результатA - [1, 3, 5]
D - [2, 8]
F - [0, 1, 8]
J - [0, 5]
X - [1, 2, 4]
Z - [4, 5]

Худший ответ:
Нашёл одно решение. Алгоритм:храним результат в виде списка списков: [номер_группы -> [строки_группы]] используем вспомогательный список хэш-таблиц: [позиция_слова -> { слово -> номер_группы }]  и вспомогательную хэш-таблицу для хранения какая группа в какую была влитакаждое слово строки ищем в соответствующей (позиции слова в строке) хэш-таблице
        а) если слово есть, запоминаем номер группы (значение из хэш-таблицы), в которой оно найдено
        б) если слова нет, то добавляем его в список новых слов если строка (а точнее её слова) найдена в группах, то берём первую из "живых" (объяснение этого позже) групп,  иначе создаём новую группудобавляем новые слова в соответствующие хэш-таблицы с номером найденной/созданной группы объединяем найденные группы в одну, выбранную ранее. Так как группы хранятся в виде списка строк, то просто объединяем списки строк в один у выбранной группы, а более ненужные группы отмечаем как "мёртвые" (присваиваем null, дабы не перемещать элементы внутри списка)добавляем строку в список строк группы
Код метода поиска групп:codeprivate 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;
    }