37. Объединить два отсортированных итератора
Условие задачи:
Даны два итератора a и b. Каждый из них возвращает элементы в порядке возрастания согласно заданному Comparator.
Необходимо реализовать CollatingIterator<E>, который объединяет оба источника и возвращает все их элементы в общем отсортированном порядке.
Требования:
реализовать методы
hasNext()иnext();сохранить все элементы, включая одинаковые;
не считывать исходные итераторы целиком в коллекции;
использовать
O(1)дополнительной памяти;предполагается, что элементы не равны
null.
Код:
public class CollatingIterator<E> implements Iterator<E> {
private final Iterator<E> a;
private final Iterator<E> b;
private final Comparator<E> comparator;
public CollatingIterator(
Iterator<E> a,
Iterator<E> b,
Comparator<E> comparator
) {
this.a = a;
this.b = b;
this.comparator = comparator;
}
@Override
public boolean hasNext() {
return false;
}
@Override
public E next() {
return null;
}
}
Пример:
a: 1, 3
b: 2, 3, 5, 6
Результат:
1 → 2 → 3 → 3 → 5 → 6
Спойлеры к решению
Подсказки
💡 Достаточно хранить по одному заранее считанному элементу из каждого итератора.
💡 Если элементы есть в обоих источниках, сравни их и верни меньший.
💡 После возврата элемента продвигай только тот итератор, из которого он был взят.
💡 Если один источник закончился, продолжай возвращать элементы второго.
Решение
public class CollatingIterator<E> implements Iterator<E> {
private final Iterator<E> a;
private final Iterator<E> b;
private final Comparator<E> comparator;
private E nextA;
private E nextB;
private boolean hasNextA;
private boolean hasNextB;
public CollatingIterator(
Iterator<E> a,
Iterator<E> b,
Comparator<E> comparator
) {
this.a = Objects.requireNonNull(a);
this.b = Objects.requireNonNull(b);
this.comparator = Objects.requireNonNull(comparator);
advanceA();
advanceB();
}
@Override
public boolean hasNext() {
return hasNextA || hasNextB;
}
@Override
public E next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
if (!hasNextA) {
E result = nextB;
advanceB();
return result;
}
if (!hasNextB) {
E result = nextA;
advanceA();
return result;
}
if (comparator.compare(nextA, nextB) <= 0) {
E result = nextA;
advanceA();
return result;
}
E result = nextB;
advanceB();
return result;
}
private void advanceA() {
if (a.hasNext()) {
nextA = a.next();
hasNextA = true;
} else {
nextA = null;
hasNextA = false;
}
}
private void advanceB() {
if (b.hasNext()) {
nextB = b.next();
hasNextB = true;
} else {
nextB = null;
hasNextB = false;
}
}
}
Алгоритм работает как слияние двух отсортированных последовательностей.
В памяти постоянно находятся максимум два элемента:
private E nextA;
private E nextB;
Например, если:
nextA = 1
nextB = 2
то возвращается 1, после чего продвигается только итератор a.
Получаем:
nextA = 3
nextB = 2
Теперь возвращается 2 и продвигается только b.
Если значения равны:
if (comparator.compare(nextA, nextB) <= 0)
сначала возвращается элемент из a, но элемент из b остаётся сохранённым и будет возвращён позже. Поэтому дубликаты не теряются.
Когда один итератор заканчивается:
if (!hasNextA) {
E result = nextB;
advanceB();
return result;
}
элементы продолжают последовательно считываться из второго.
Если закончились оба источника, next() обязан согласно контракту Iterator выбросить:
throw new NoSuchElementException();
Если первый итератор содержит n элементов, а второй — m, полный обход занимает O(n + m) времени.
Каждый отдельный вызов next() выполняется за O(1), а дополнительная память — O(1), поскольку независимо от размера входных последовательностей хранится не более двух предварительно считанных элементов.