Java: разница между двумя списками - PullRequest
18 голосов
/ 01 июня 2011

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

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

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

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

  • MOVE Fluffy в положение 12
  • INSERT Snuggles в позиции 37
  • УДАЛИТЬ Mr. Chubbs
  • и т.д.

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

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

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

EDIT

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

Ответы [ 5 ]

10 голосов
/ 01 июня 2011

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

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

2 голосов
/ 01 июня 2011

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

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

2 голосов
/ 01 июня 2011

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

http://www.levenshtein.net/

1 голос
/ 08 июля 2017

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

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

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

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

1 голос
/ 08 марта 2014

Я знаю, что спрашивающий искал решение 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;
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...