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

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

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

  1. порядок из кошек взбивается полностью
  2. кошки индивидуально двигаться вверх или вниз по списку
  3. новые кошки присоединиться, в определенный момент в автоколонну
  4. кошки покидают конвой

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

  • движение Fluffy в положение 12
  • вставить Snuggles на должность 37
  • удалить Mr. Chubbs
  • etc.

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

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

(этот пост и этот пост задайте аналогичные вопросы, но они оба имеют дело с отсортированный списки. Мои приказал, но несортированный.)

редактировать

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

5 ответов


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

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

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). Это было необходимо, потому что мы не хотели перерисовывать всю таблицу при изменении данных; различия между списками, вероятно, были небольшими и затрагивали только одну или две строки. Возможно, это не тот алгоритм, который вы ищете для своих данных. Если нет, дайте мне знать, и я удалите это.

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


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

http://www.levenshtein.net/


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

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


Я знаю, что спрашивающий искал решение 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;
    }
}

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

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

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;
}

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