Внедрение отмены и повтора для массива?
Я получил этот вопрос в интервью 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
только , не изменяя основные данные каким-либо образом, и просто предоставлять разные прокси-серверы для данных в зависимости от того, было ли действие отменено или переделано.