Здесь могут быть более сложные варианты, например { a, b }, { b, c }, { c ,d }, { d ,e }, должен ли такой набор объектов схлопнуться в один { a, b, c, d, e }?
Опишу идею алгоритма:
0. на входе имеем
[ { a, b }, { b, c }, { c ,d }, { d ,e } ]
1. строим словарь из ключей и списка содержащих их объектов, т. е. должно получиться такое:
a: [ { a, b } ],
b: [ { a, b }, { b, c } ]
c: [ { b, c }, { c, d } ]
d: [ { c, d }, { d, e } ]
e: [ { d, e } ]
и строим второй словарь статусов
{ a, b }: 0
{ b, c }: 0
{ c ,d }: 0
{ d ,e }: 0
2. пробегаем в цикле по словарю и объединяем все объекты для каждого ключа, берем любой из объектов в каждом массиве и расширяем его, остальные удаляем из массива
a: [ { a, b } ], при изменении объекта при проходе b, он будет расширен до { a, b, c }
b: [ { a, b, c } ], например берем за основу { a, b } и расширяем до { a, b, c }, второй удаляем
c: [ { b, c, d } ], например берем за основу { b, c } и расширяем до { b, c, d }, второй удаляем
d: [ { c, d, e } ], например берем за основу { c, d } и расширяем до { c, d, e }, второй удаляем
e: [ { d, e } ], останется без изменений
в словаре статусов проснавляем для объединенных объектов признак объединения (1), для удаленных признак удаления (2), при этом правило выставления статуса должно быть 0->1, 0->2, 2->1
3. берем полученный набор значений как первоначальный массив объектов, при этом фильтруем по статусу, он не должен быть 2
[ { a, b, c }, { b, c, d }, { c, d, e } ]
4. в случае если на 2-м шаге объединять было нечего, то выходим, иначе переходим к шагу 1
Для оптимизации выполнения можно на 3-м шаге не просто фильтровать по статусу 2, но еще и отделять объекты со статусом 0, они в ходе прохода ни с чем не объединились, а значит больше их можно не трогать, перекладывать их в результирующий массив, а в следующий цикл входить только с объектами со статусом 1, и если таковых нет, то все