Внедрение отмены и повтора для массива?

Я получил этот вопрос в интервью JAVA сегодня.

Мне нужно реализовать коллекцию, которая является массивом и имеет методы add и delete, которые могут выполняться только в конце массива.

В дополнение к этому я должен реализовать еще два метода, которые являются undo и redo, которые могут выполняться только один раз.

Например:

Разрешает x массив, содержащий {1,5,7,9}.

Теперь я добавляю {44,66,77} в него и делает его {1,5,7,9,44,66,77}.

Теперь, когда я отменю его, массив должен удалить {44,66,77}.. И если я сделаю повтор после этого, он должен вернуться к {1,5,7,9,44,66,77}.

И аналогично для удаления.

Каков наилучший способ достичь этого с точки зрения сложности и памяти?

Мое решение для интервьюера:

  • Создайте одно поле String, которое сохранит последнюю операцию, то есть "Добавить" / "Удалить".
  • И Hashmap, который хранит ключ как индекс массива и значение как значение индекса массива.

что является абсолютно неправильным решением в отношении интервьюера.

Спасибо заранее.

Ответы

Ответ 1

Я бы решил это так. В принципе, будет три списка.

  • Список с фактическими значениями
  • Отменить список с экземплярами интерфейса Command
  • Повторить список с экземплярами интерфейса Command (необязательно, поясняется ниже)

    public interface Command {
        void do();
        void undo();
    }
    

Будут две реализации Command: AddCommand и RemoveCommand.

  • В add() новый экземпляр AddCommand создан и добавлен в список отмены. Затем мы вызываем do() этой команды, которая изменяет фактические значения и сохраняет index добавленного элемента.
  • В remove() новый экземпляр RemoveCommand создан и добавлен в список отмены. Затем мы вызываем do() этой команды, которая изменяет фактические значения и сохраняет index и значение удаляемого элемента.
  • В undo() мы вытаскиваем последнюю команду из списка отмены и выполняем метод undo() этой команды. Команда переместилась в список повтора. Отменить методы AddCommand и RemoveCommand вернуть изменения назад.
  • В redo() мы вытаскиваем последнюю команду из списка повтора и снова выполняем метод do(). Команда "Пульпинг" добавляется в список отмены.

Другая оптимизация - удалить список повтора и вместо этого использовать индекс отмены. В этом случае, когда вы undo(), вам не нужно удалять/добавлять значение из списка, а просто уменьшать индекс отмены на единицу. Аналогично, redo() будет увеличивать его на единицу.

Таким образом, окончательное решение будет иметь список значений, отменить список и значение индекса.

Обновление:

Для простейшего случая, когда требуется только одна операция undo()/redo(), решение будет выглядеть еще проще. Вместо того, чтобы отменить список и индекс, достаточно хранить последний экземпляр команды. Таким образом, у вас будет список значений и экземпляр последней команды отмены.

Ответ 2

Либо я неправильно понимаю вопрос, либо люди переусердствуют. Вопрос имеет довольно много ограничений, поэтому:

  • Массив для текущего содержимого
  • Дополнительный массив для удаленных ранее элементов (необходимо отменить удаление/повторное добавление)
  • Одна переменная для предыдущей длины массива (требуется для отмены добавления/повтора удаления)
  • Одна переменная enum для последней операции (добавить/удалить)
  • Один логический, чтобы сказать, было ли отменено или нет

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

Ответ 3

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

Есть в основном два способа сделать отмену и повторение, одним из которых является сохранение копии всего массива каждый раз. Этот способ здесь, вероятно, не является желательным (и его очень легко реализовать для массива), поэтому я расскажу о другом.

Теперь, один аспект здесь состоит в том, что повтор - это просто отмена. Если undo является инверсным, то redo является инверсией инверсии undo. На самом деле это означает, что каждое действие отмены должно быть в состоянии сделать то и другое. Отмена должна привести к тому, что действие перевернется, поэтому в следующий раз, когда он отменяет, он выполнит первоначальное действие.

отменить и повторить метод, который можно выполнить только один раз

Undos и redos, являющиеся одним и тем же, более важны, если вы должны хранить их длинную историю (где это становится очень удобно), но он по-прежнему применяется здесь.

Выполнение этого способа означает, что undo - это объект с небольшим количеством состояний. Поскольку здесь возможно несколько разных действий, здесь полезен интерфейс (шаблон стратегии, как отмечено в комментариях):

interface UndoAction {
    public void undo();
}

Для операции set очень простой объект отмены будет выглядеть следующим образом:

class SetUndoAction implements UndoAction {
    int index;
    Object oldValue;

    SetUndoAction(int index, Object oldValue) {
        this.index = index;
        this.oldValue = oldValue;
    }

    public void undo() {
        Object newValue = get(index);
        set(index, oldValue);
        oldValue = newValue;
    }
}

Если вы вызываете undo на этом, он выполняет отмену, а при следующем вызове undo он выполняет повтор.

Добавить и удалить немного отличается, потому что они не просто меняют значение. Состояние для undo, которое отменяет добавление или удаление, также будет включать вызов метода.

Для добавления:

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

Для удаления:

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

Они снова обратные, поэтому мы можем описать простое действие add или remove, подобное действию set:

class AddRemoveUndoAction implements UndoAction {
    int index;
    Object theValue;
    boolean wasAdd;

    AddRemoveUndoAction(int index, Object theValue, boolean wasAdd) {
        this.index = index;
        this.theValue = theValue;
        this.wasAdd = wasAdd;
    }

    public void undo() {
        if(wasAdd) {
            remove(index);
        } else {
            add(index, theValue);
        }

        wasAdd = !wasAdd;
    }
}

add создает new AddRemoveUndoAction(..., ..., true) и remove создает new AddRemoveUndoAction(..., ..., false).

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

Если у вас есть история множественных отменить, у вас будет какая-то структура данных (возможно, стек или очередь), чтобы сохранить их, хотя вам нужна специальная функциональность:

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

Вы можете использовать java.util.Stack и управлять этими вещами извне (через ListIterator), но я обнаружил, что головная боль и гораздо приятнее написать отдельную структуру данных специально для этого.

Для отмены и повтора, которые хранят только один элемент, это намного проще, вы можете просто иметь два поля.

Когда undo вызывается в классе хранения массива:

  • Он должен проверить, что есть объект отмены.
  • Он должен называть undo для объекта отмены.
  • Он должен переместить объект отмены в поле повтора.

Когда redo вызывается в классе хранения массива:

  • Он должен проверить, что есть объект redo.
  • Он должен вызвать undo для объекта redo.
  • Он должен переместить объект redo в поле отмены.

Методы, вызываемые классом удерживания массива, который можно отменить, должны добавить новый объект UndoAction в поле отмены и установить для поля повтора значение null.

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

  • Объекты отмены, которые самосознательны, должны ли они отменять или повторять. (Шаблон команды или абстрактный класс можно использовать, чтобы сделать их немного проще.)
  • Менеджер отмены, итератор, который управляет точкой в ​​истории. Перемещение назад во времени вызывает отмена и перемещение вперед во времени вызывает повтор.
  • Отменить клиентов, которые знают свои собственные данные и как их следует изменить.

Ответ 4

Я занимаюсь некоторыми исследованиями, и, как я уже сказал, вы можете либо зарегистрировать количество "вещей", идущих в массив, и затем сохранить их где-нибудь. Поэтому, когда вы делаете отмену - удаляет последние вставленные объекты.

Итак, так оно и есть.

Контейнер, содержащий количество элементов, введенных в массив = 1,2,4,2
Array = {[x] [x, x] [x, x, x, x] [x, x]}

ЕСЛИ вы хотите удалить последнее действие, которое вы берете из контейнера 2, и удалить последнюю запись из массива, чтобы вы остались с

Контейнер, содержащий количество элементов, введенных в массив = 1,2,4
Array = {[x] [x, x] [x, x, x, x]}

И так далее и так далее. Также вы можете взглянуть на this Интересный шаблон, который обеспечивает возможность восстановления объекта до его предыдущего состояния

Ответ 5

Если вы хотите использовать минимальную память:
1) сохранить основной массив, он будет содержать все записи в вашем массиве
2) хранить карту (битмап будет достаточным для реализации одиночной итерации отмены/повтора, более сложного типа карты для реализации многоуровневого отмены/повтора) для статуса элемента элемента - active/new/delete.

Ответ 6

Одна вещь, о которой я могу думать, - это иметь массив массивов. Что-то вроде этого.

Actual Data = []
Bitmap History = [[]]
End Index = 0

Добавить [4, 5]

Actual Data = [[4, 5]]
Bitmap History = [ [00], [11] ]
End Index= 1

Добавить [6, 7]

Actual Data = [[4, 5], [6, 7]]
Bitmap History = [ [0000], [1100], [1111] ]
End Index = 2

Добавить [8, 9]

Actual Data = [[4, 5], [6, 7], [8, 9]]
Bitmap History = [ [000000], [110000], [111100], [111111] ]
End Index = 3

Добавьте [1,2] в индекс 0

Actual Data = [[1,2], [4,5], [6,7], [8,9]]
Bitmap History = [ [00000000], [00110000], [00111100], [00111111], [11111111] ]
End Index = 4

Удалить 9

Actual Data = [[1,2], [4,5], [6,7], [8,9]]
Bitmap History = [ [00000000], [00110000], [00111100], [00111111], [11111111], [11111110] ]

Удалить 8

Actual Data = [[1,2], [4,5], [6,7], [8,9]]
Bitmap History = [ [00000000], [00110000], [00111100], [00111111], [11111111], [11111110], [11111100] ]

Добавить 10 в конце

Actual Data = [[1,2], [4,5], [6,7], [8,9], [10]]
Bitmap History = [ [000000000], [001100000], [001111000], [001111110], [111111110], [111111100], [111111000], [111111001] ]
End Index = 7

Отменить 3 раза → End Index = 4 → Использовать историю битмапов [111111110] для выравнивания

Flattened Data = [1,2,4,5,6,7,8,9]

У вас есть метод, называемый flatten(), который использует данные в Истории Bitmap, чтобы получить содержимое массива и создать из него один массив.

Теперь, когда вы хотите отменить, все, что вы делаете, это уменьшение значения конечного индекса. И чтобы повторить, просто увеличьте значение End Index. Метод flatten позаботится о отображении правильных элементов из массива Actual Data.

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

Ответ 7

Я реализую его так: Выполненные операции могут поддерживаться в виде массива/вектора в виде строк. При каждой операции над массивом можно сохранить информацию об операции. При каждом отмене/повторе значение массива будет извлекаться из массива операций, а если отменяется, то выполняется операция счетчика, и в случае повтора выполняется такая же операция и после выполнения операции значение или указатель в массиве операций будет обновляться. Допустим, у вас есть массив arr, а массив операций говорит optArr и ptr, который укажет на последний элемент в optArr

Добавить 5 в массив

arr {5} и optArr { "Добавить 5" } и ptr = 0

Добавить 9 в массив

arr {5, 9} и optArr { "Добавить 5", "Добавить 9" } и ptr = 1

Добавить 7 в массив

arr {5, 9, 7} и optArr { "Добавить 5", "Добавить 9", "Добавить 7" } и ptr = 2

Удалить 9 из массива

arr {5, 7} и optArr { "Добавить 5", "Добавить 9", "Добавить 7", "Удалить 9" } и ptr = 3

для команды отмены

value = optArr[ptr--]

значение "Удалить 9" теперь работает счетчик ( "Добавить 9" ).

для команды redo

value = optArr[++ptr]

значение "Удалить 9" будет выполнено

Ответ 8

Мой ответ...!

//create 3 list and a string variable that stores the last operation name.
List<Integer> sizeIndex= new ArrayList<Integer>();
List<Integer> finalArray= new ArrayList<Integer>();
List<Integer> lastDelete= new ArrayList<Integer>();
String lastOperation;
// add the first size of the finalArray. 
sizeIndex.add(finalArray.size());


public void add(List<Integer> someArray){
lastOperation="add";
finalArray.addAll(someArray);
sizeIndex.add(finalArray.size());
}

public void delete(){
lastOperation="delete";
lastDelete=finalArray.subList(sizeIndex.size()-1,sizeIndex.size());
finalArray=finalArray.subList(0,sizeIndex.size()-1);
sizeIndex.remove(sizeIndex.size() - 1);
}

public undo(){
if("add".equals(lastOperation))
delete();
else
add(lastDelete);
}

public redo(){
if("add".equals(lastOperation))
add(lastDelete);
else
delete();
}

Логика:

Когда список добавляется в конец, новый список добавляется как последний элемент в списке. Таким образом, последний список можно извлечь из индекс sizeBeforeAdding List и sizeAfterAddding. Так держать трек размером, я сохраняю размер при обновлении на sizeIndex.

Операции

добавить (listToBeAppended);//Добавить список в текущий список.

удалить();//Удалит последний добавленный список.

отменить();//Будет выполнена повторная операция, т.е. удалить, если добавлено и добавить, если оно удалено.

повтор();//Делаем последнюю операцию снова, т.е. если он добавлен, а затем удален, то повтор снова добавится.

Ответ 9

Мое решение - использовать Map of ArrayLists для хранения исторических значений массива. Вот код:

public class UndoRedoArray {
    private List<Integer> currentValues;
    private int version = 0;
    private int highestVersion = 0;
    private Map<Integer, List<Integer>> history = new HashMap<Integer,List<Integer>>();

    public Integer[] getValues() {
        return (Integer[]) getCurrentValues().toArray();
    }

    private List<Integer> getCurrentValues() {
        if (currentValues == null) {
            currentValues = new ArrayList<Integer>();
        }
        return currentValues;
    }

    private void add(Integer[] newValues) {
        incrementHistory();
        getCurrentValues().addAll(Arrays.asList(newValues));
    }

    private void incrementHistory() {
        if (history.get(version) != null)  {
            throw new IllegalArgumentException("Cannot change history");
        }
        history.put(version,getCurrentValues());
        if (version > 2) {
            history.remove(version - 2);
        }
        version++;
        if (version > highestVersion) {
            highestVersion = version;
        }
    }

    private void delete(Integer[] endValues) {
        incrementHistory();
        int currentLength = getCurrentValues().size();
        int i = endValues.length-1;
        for (int deleteIndex = currentLength - 1; deleteIndex > currentLength - endValues.length; deleteIndex--) {
            if (!endValues[i].equals(getCurrentValues().get(deleteIndex))) {
                throw new IllegalArgumentException("Cannot delete element(" + endValues[i] + ") that isn't there");                
            }
            getCurrentValues().remove(deleteIndex);
        }
    }

    public void undo() {
       version--;
       if (history.get(version) == null) {
           throw new RuntimeException("Undo operation only supports 2 undos");
       }
       this.currentValues = history.get(version);
    }

    public void redo() {
       version++;
       if (history.get(version) == null) {
           throw new RuntimeException("Redo operation only supported after undo");
       }
       this.currentValues = history.get(version);
    }

}

Ответ 10

Я не парень Java, но я думаю, что это решение может быть легко переведено на Java:

Сначала сделайте анализ проблемы: нам нужен класс, назначенный для выполнения набора операций, и эти операции могут выполняться только один раз и отменять и переделывать один раз каждый. Таким образом, процесс:

  • Выполните действие (add или delete).
  • (необязательно) отменить действие.
  • (необязательно) повторить действие.

Таким образом, его система, разработанная с действием /s, должна быть выполнена и отменена, а приемлемо считать, что команды do/redo будут выполняться frecuently и dessign система таким образом.

Имея это в виду, я предлагаю реализовать действия add/delete только , не изменяя основные данные каким-либо образом, и просто предоставлять разные прокси-серверы для данных в зависимости от того, было ли действие отменено или переделано.