Java: разница между двумя списками

Приложение моей компании по разведению кошек отслеживает колонну кошек. Периодически ему нужно сравнивать previousOrder с currentOrder (каждый из них - ArrayList<Cat>) и уведомлять обработчиков кошек о любых изменениях.

Каждая кошка уникальна и может появляться в каждом списке только один раз (или не появляться вовсе). В большинстве случаев списки previousOrder и currentOrder имеют одинаковое содержимое в одном и том же порядке, но может произойти любое из следующего (от более частого к менее частому):

  1. Порядок кошек зашифрован полностью
  2. Кошки по отдельности перемещаются вверх или вниз по списку
  3. Новые кошки присоединяются в определенном месте в колонне.
  4. Кошки покидают конвой

Мне это кажется проблемой редактирования расстояния. В идеале я ищу алгоритм, который определяет шаги, необходимые для совмещения previousOrder currentOrder:

  • ПЕРЕМЕСТИТЬ Fluffy в положение 12
  • ВСТАВИТЬ Snuggles в позицию 37
  • УДАЛИТЬ Mr. Chubbs
  • и Т. Д.

Алгоритм также должен распознавать сценарий № 1, и в этом случае новый заказ передается полностью.

Какой лучший подход для этого?

(Это сообщение и этот пост имеют похожие вопросы, но оба они имеют дело с отсортированными списками. Мои упорядочены, но не отсортированы.)

ИЗМЕНИТЬ

Алгоритм Левенштейна - отличное предложение, но меня беспокоит время / пространство, необходимое для создания матрицы. Моя главная цель - как можно быстрее определить и сообщить об изменениях. Что-то более быстрое, чем поиск дополнений и отправка сообщения типа «Вот новые кошки, а вот текущий порядок».


person Tony the Pony    schedule 01.06.2011    source источник
comment
Случайное интервью или домашнее задание?   -  person Justin Niessner    schedule 01.06.2011
comment
Нет, это реальная проблема, с которой я столкнулся. По крайней мере, это похоже на кошачье стадо!   -  person Tony the Pony    schedule 01.06.2011
comment
+1 просто за пример с кошачьим стадом   -  person Nicolas78    schedule 01.06.2011
comment
Вы просто пытаетесь найти набор команд и / или расстояние редактирования, чтобы изменить порядок списка, или вы действительно меняете порядок в списке? Недавно мы сделали нечто подобное для аналогичной ситуации, связанной с манипулированием упорядоченными <table> строками с помощью javascript. Мы придумали алгоритм, чтобы переместить их все, но он не самый эффективный и не дает списка команд; он просто выполняет их при просмотре списков.   -  person Rob Hruska    schedule 01.06.2011
comment
Аналогичная проблема - мы должны синхронизировать упорядоченные списки между серверным и клиентским процессом.   -  person Tony the Pony    schedule 01.06.2011
comment
Я выложу псевдокод того, что мы делаем. Если это не то, что вы ищете, дайте мне знать, и я удалю его. Опять же, это не самый элегантный и эффективный алгоритм, но он работает.   -  person Rob Hruska    schedule 01.06.2011


Ответы (5)


Вот алгоритм, который я составил для объединения двух списков, old и new. Он не самый элегантный или эффективный, но, похоже, он подходит для данных, для которых я его использую.

new - это самый обновляемый список данных, а old - устаревший список, который необходимо преобразовать в new. Алгоритм выполняет свои операции со списком old - соответственно удаляет, перемещает и вставляет элементы.

for(item in old)
    if (new does not contain item)
        remove item from old

for(item in new)
    if (item exists in old)
        if (position(item, old) == position(item, new))
            continue // next loop iteration
        else
            move old item to position(item, new)
    else
        insert new item into old at position(item, new)

Все удаления выполняются заранее, чтобы положение элементов во втором цикле было более предсказуемым.

Движущей силой этого была синхронизация списка данных с сервера с <table> строками в DOM браузера (с использованием javascript). Это было необходимо, потому что мы не хотели перерисовывать всю таблицу при изменении данных; различия между списками, вероятно, были небольшими и затрагивали только одну или две строки. Возможно, это не тот алгоритм, который вы ищете для своих данных. Если нет, дайте мне знать, и я удалю это.

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

person Rob Hruska    schedule 01.06.2011
comment
Основная оптимизация, о которой я могу думать, - это запуск сравнения списка с индексом сопоставляемого элемента, а не использование общего метода contains (возможно, вы уже делаете это) - person Tony the Pony; 01.06.2011
comment
Хорошее предложение. Я буду иметь это в виду, если цикл начнет работать медленнее, чем мне нужно. Спасибо за это. - person Rob Hruska; 01.06.2011
comment
@Jen - Я немного упростил алгоритм для ясности здесь. Код, который у меня есть, делает немного больше; position() вызывается только один раз за итерацию и сохраняется в области итерации. Я думаю, это то, о чем вы имели в виду? Тем не менее, хорошее предложение. - person Rob Hruska; 01.06.2011

Метрика расстояния Левенштейна.

http://www.levenshtein.net/

person bmargulies    schedule 01.06.2011

Эффективный способ решить эту проблему - использовать динамическое программирование. В Википедии есть псевдокод для тесно связанной проблемы: Расчет расстояния Левенштейна.

Отслеживание фактических операций и включение операции «скремблирования» не должно быть слишком сложной задачей.

person NPE    schedule 01.06.2011

Я знаю, что спрашивающий искал решение для Java, но я столкнулся с этим вопросом, когда искал алгоритм для реализации на C #.

Вот мое решение, которое генерирует перечисление простых значений IListDifference: либо ItemAddedDifference, ItemRemovedDifference, либо ItemMovedDifference.

Он использует рабочую копию исходного списка, чтобы установить, элемент за элементом, какие изменения необходимы, чтобы преобразовать его в соответствие с целевым списком.

public class ListComparer<T>
    {
        public IEnumerable<IListDifference> Compare(IEnumerable<T> source, IEnumerable<T> target)
        {
            var copy = new List<T>(source);

            for (var i = 0; i < target.Count(); i++)
            {
                var currentItemsMatch = false;

                while (!currentItemsMatch)
                {
                    if (i < copy.Count && copy[i].Equals(target.ElementAt(i)))
                    {
                        currentItemsMatch = true;
                    }
                    else if (i == copy.Count())
                    {
                        // the target item's index is at the end of the source list
                        copy.Add(target.ElementAt(i));
                        yield return new ItemAddedDifference { Index = i };
                    }
                    else if (!target.Skip(i).Contains(copy[i]))
                    {
                        // the source item cannot be found in the remainder of the target, therefore
                        // the item in the source has been removed 
                        copy.RemoveAt(i);
                        yield return new ItemRemovedDifference { Index = i };
                    }
                    else if (!copy.Skip(i).Contains(target.ElementAt(i)))
                    {
                        // the target item cannot be found in the remainder of the source, therefore
                        // the item in the source has been displaced by a new item
                        copy.Insert(i, target.ElementAt(i));
                        yield return new ItemAddedDifference { Index = i };
                    }
                    else
                    {
                        // the item in the source has been displaced by an existing item
                        var sourceIndex = i + copy.Skip(i).IndexOf(target.ElementAt(i));
                        copy.Insert(i, copy.ElementAt(sourceIndex));
                        copy.RemoveAt(sourceIndex + 1);
                        yield return new ItemMovedDifference { FromIndex = sourceIndex, ToIndex = i };
                    }
                }
            }

            // Remove anything remaining in the source list
            for (var i = target.Count(); i < copy.Count; i++)
            {
                copy.RemoveAt(i);
                yield return new ItemRemovedDifference { Index = i };
            }
        }
    }

Только что заметил, что в IEnumerable используется специальный метод расширения - 'IndexOf':

public static class EnumerableExtensions
{
    public static int IndexOf<T>(this IEnumerable<T> list, T item)
    {
        for (var i = 0; i < list.Count(); i++)
        {
            if (list.ElementAt(i).Equals(item))
            {
                return i;
            }
        }

        return -1;
    }
}
person sandy    schedule 07.03.2014

Мне недавно приходилось это делать, за исключением того, что предметы могли существовать несколько раз. Это сложные вещи, но я смог сделать это, используя счетчики с упреждением и какое-то другое безумие. Это очень похоже на решение Роба, поэтому спасибо ему за то, что заставил меня начать!

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

public interface Operation {
    /**
     * Apply the operation to the given list.
     */
    void apply(List<String> keys);
}

и у нас есть несколько вспомогательных методов для построения операций. На самом деле вам не нужна операция "перемещения", и вы могли бы даже иметь "своп" (или вместо этого), но вот что я выбрал:

Operation delete(int index) { ... }
Operation insert(int index, String key) { ... }
Operation move(int from, int to) { ... }

Теперь мы определим специальный класс для хранения наших предварительных подсчетов:

class Counter {
    private Map<String, Integer> counts;

    Counter(List<String> keys) {
        counts = new HashMap<>();

        for (String key : keys) {
            if (counts.containsKey(key)) {
                counts.put(key, counts.get(key) + 1);
            } else {
                counts.put(key, 1);
            }
        }
    }

    public int get(String key) {
        if (!counts.containsKey(key)) {
            return 0;
        }

        return counts.get(key);
    }

    public void dec(String key) {
        counts.put(key, counts.get(key) - 1);
    }
}

И вспомогательный метод для получения индекса следующего ключа в списке:

int next(List<String> list, int start, String key) {
    for (int i = start; i < list.size(); i++) {
        if (list.get(i).equals(key)) {
            return i;
        }
    }

    throw new RuntimeException("next index not found for " + key);
}

Теперь мы готовы выполнить преобразование:

List<Operation> transform(List<String> from, List<String> to) {
    List<Operation> operations = new ArrayList<>();

    // make our own copy of the first, that we can mutate
    from = new ArrayList<>(from);

    // maintain lookahead counts
    Counter fromCounts = new Counter(from);
    Counter toCounts = new Counter(to);

    // do all our deletes first
    for (int i = 0; i < from.size(); i++) {
        String current = from.get(i);

        if (fromCounts.get(current) > toCounts.get(current)) {
            Operation op = delete(i);
            operations.add(op);
            op.apply(from);
            fromCounts.dec(current);
            i--;
        }
    }

    // then one more iteration for the inserts and moves
    for (int i = 0; i < to.size(); i++) {
        String current = to.get(i);

        if (from.size() > i && from.get(i).equals(current)) {
            fromCounts.dec(current);
            continue;
        }

        if (fromCounts.get(current) > 0) {
            Operation op = move(next(from, i + 1, current), i);
            operations.add(op);
            op.apply(from);

            fromCounts.dec(current);
        } else {
            Operation op = insert(i, current);
            operations.add(op);
            op.apply(from);
        }
    }

    return operations;
}

Это немного сложно понять, но в основном вы выполняете удаление, чтобы знать для каждой клавиши, которую вы вставляете или перемещаете. Затем вы снова просматриваете список и, если его достаточно, перемещаете одну из части списка, которую еще не видели, в противном случае вставьте. К тому времени, как вы дойдете до конца, все выровняется.

person Phil Kulak    schedule 07.07.2017