实现数组的撤消和重做

时间:2014-03-14 18:03:37

标签: java arrays algorithm collections

我今天在Java采访中得到了这个问题。

我必须实现一个集合,它是一个数组,并且只能在数组的末尾执行adddelete方法。

除此之外,我还需要实现另外两种方法,即undoredo方法,只能执行一次。

例如:

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

同样对于删除。

在复杂性和记忆方面实现这一目标的最佳方法是什么?

我对面试官的解决方案:

  1. 创建一个存储最后一个操作的字符串字段,即“添加”/“删除”。
  2. 一个hashmap,它存储键作为数组索引,值作为数组的索引值。
  3. 根据采访者的说法,这是一个完全不正确的解决方案。

10 个答案:

答案 0 :(得分:12)

我会像这样解决它。基本上,将有三个列表。

  • 包含实际值的列表
  • 使用Command interface 的实例
  • 撤消列表 带有Command接口实例的
  • 重做列表(可选,如下所述)

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

将有两个Command实施:AddCommandRemoveCommand

  • add()上,AddCommand的新实例已创建并添加到撤消列表。然后我们调用此命令的do(),该命令修改实际值并存储添加项目的index
  • remove()上,RemoveCommand的新实例已创建并添加到撤消列表。然后我们调用此命令的do(),它修改实际值并存储index和已删除项的值。
  • undo()上,我们从撤消列表中提取最后一个命令,并执行此命令的undo()方法。该命令被推送到重做列表。撤消AddCommandRemoveCommand的方法可以还原更改。
  • redo()上,我们从重做列表中提取最后一个命令,然后再次执行do()方法。已拉出的命令将添加到撤消列表

另一个优化是删除重做列表并使用撤消索引。在这种情况下,当您undo()时,您不需要从列表中删除/添加值,而只需将撤消索引减少一个。同样,redo()会将其增加一个。

因此,最终解决方案将包含值列表撤消列表索引值

更新:

对于最简单的情况,只需要一个undo() / redo()操作,解决方案看起来会更简单。它没有撤消列表索引,而是足以存储最新的命令实例。因此,我们将有值列表最后撤消命令的实例。

答案 1 :(得分:7)

要么我误解了这个问题,要么是人们过分思考这个问题。这个问题有很多限制,所以:

  • 当前内容的数组
  • 最后删除项目的额外数组(需要撤消删除/重做添加)
  • 前一个数组长度的一个变量(需要撤消添加/重做删除)
  • 上次操作的一个枚举变量(添加/删除)
  • 一个布尔值,表示撤消是否刚刚完成

然后在每个不同的操作上更新这些。不需要堆栈或任何东西,因为不需要多次撤消/重做。

答案 2 :(得分:3)

我要说的一件事是面试官可能不赞同你的回答是因为它不是很优雅。不是因为它很慢或占用了很多空间。

basically two ways to do undo and redo,其中一个是每次都保存整个数组的副本。这种方式在这里可能是不可取的(并且很容易为数组实现)所以我将谈论另一种方式。

现在,这里的一个方面是重做实际上也只是一个撤销。如果undo是逆,则redo是undo的逆的倒数。这实际上意味着每个撤消操作应该能够做到这两点。撤消应该导致操作自行翻转,以便下次撤消时执行原始操作。

  

undo和redo方法,只能执行一次

如果你必须存储它们的长历史(它变得非常方便),Undos和redos是同一件事更重要但它仍然适用于此。

这样做意味着撤消是一个具有一点状态的对象。由于可能存在多个不同的操作,因此界面在此处很有用(策略模式,如注释中所述):

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。
  • 要重做,您必须保存索引和值,以便稍后在该索引处调用add。

删除:

  • 要撤消,您必须保存索引和值,以便稍后在该索引处调用add。
  • 要重做,您必须保存索引,以便稍后在该索引处调用remove。

这些是相反的,因此我们可以描述与add操作类似的简单removeset操作:

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)

现在,从具有数组的类的角度来看,它只需要适当地创建和存储这些对象。

如果您有多个undos的历史记录,您可以使用某种类型的数据结构(可能是堆栈或队列)来存储它们,尽管您需要一些特殊功能:

  • 元素的最大数量,每次按下新元素时都应开始丢弃旧编辑。
  • 当执行undos和redos时,可以在next和previous元素之间迭代的'当前元素指针'。 (这也可以通过两个堆叠来实现,一个用于撤销,一个用于重做,你可以从一个弹出,然后推到另一个。)
  • 如果'当前元素指针'位于堆栈的中间(已执行撤消并且现在有重做),并且推送了新的撤消,则应删除头部的重做。

您可以使用java.util.Stack并从外部管理这些内容(通过ListIterator),但我发现这是一个令人头痛的问题,而且为此专门编写单独的数据结构要好得多。< / p>

对于仅存储单个项目的撤消和重做,它更容易,您可以只有两个字段。

在数组保持类上调用undo时:

  • 应检查是否存在撤消对象。
  • 应该在撤消对象上调用undo
  • 它应该将撤消对象移动到重做字段。

在数组保持类上调用redo时:

  • 应检查是否有重做对象。
  • 应该在重做对象上调用undo
  • 它应该将重做对象移动到撤消字段。

调用可以撤消的数组保持类的方法应该在undo字段中添加一个新的UndoAction对象,并将重做字段设置为null。

这种设计对于多重撤销情况最有利,因为抽象的每一层只对自己负责:

  • 自我意识到撤消或重做的撤消对象。 (命令模式或抽象类可用于使这些编写更简单。)
  • 撤消管理器,一种管理历史记录中某个时间点的迭代器。及时向后移动导致撤消并及时向前移动会导致重做。
  • 了解自己的数据以及应如何更改数据的撤消客户端。

答案 3 :(得分:2)

我一直在做一些研究,正如我的建议,你可以注册&#34;事物的数量&#34;进入数组 - 然后存储在某个地方。因此,当您进行撤消时 - 它会删除最后插入的对象。

所以以这种方式思考:

容器保存输入数组的项目数= 1,2,4,2

数组= {[x] [x,x] [x,x,x,x] [x,x]}

如果你想删除最后一个动作,你从容器2中取出并删除数组中的最后一个条目,这样你就可以了:

容器中包含输入数组的项目数= 1,2,4

Array = {[x] [x,x] [x,x,x,x]}

依此类推。

此外,您可以查看this有趣的模式,该模式可以将对象恢复到以前的状态。

答案 4 :(得分:2)

我的解决方案是使用ArrayLists Map来保存Array的历史值。 这是代码:

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

}

答案 5 :(得分:1)

如果您想使用最小内存:

  1. 存储主阵列。它将包含数组中的所有条目。
  2. 存储地图(位图足以实现单一撤消/重做迭代。使用更复杂的地图类型实现多级撤消/重做)以标记元素的状态 - 活动/新建/删除。

答案 6 :(得分:1)

我能想到的一件事就是拥有一组数组。像这样:

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

在索引0处添加[1,2]

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

撤消三次应该→结束索引= 4 →使用位图历史记录[111111110]来展平

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

有一个名为flatten()的方法,它使用位图历史记录中的数据来获取数组的内容并从中创建一个数组。

现在,当您要撤消时,您所做的就是减少结束指数值。要重做,只需增加结束指数值即可。 flatten方法将负责显示Actual Data数组中的正确元素。

删除操作会在位图历史记录中插入一个新条目,并关闭该号码的标记。

答案 7 :(得分:1)

我会像这样实现它:

执行的操作可以作为字符串保存在数组/向量中。在阵列上的每个操作上,可以存储关于操作的信息。在数组上的每次撤销/重做时,将从操作数组中获取一个值,如果是撤消然后计数器操作,并且在重做的情况下将执行相同的操作,并且在执行操作数组或操作数组中的指针后将更新。 / p>

假设你有一个阵列 arr ,操作数组说 optArr ptr 将指向 optArr <中的最后一个元素/强>

将5添加到数组

arr {5} optArr {&#34;添加5&#34;} ptr = 0

将9添加到数组

arr {5,9} optArr {&#34;添加5&#34;,&#34;添加9&#34;} ptr = 1

将7添加到数组

arr {5,9,7} optArr {&#34;添加5&#34;,&#34;添加9&#34;,&#34;添加7& #34; } ptr = 2

从数组

中删除9

arr {5,7} optArr {&#34;添加5&#34;,&#34;添加9&#34;,&#34;添加7&#34 ;,&#34;删除9&#34; } ptr = 3

对于撤消命令

value = optArr[ptr--]

&#34;删除9&#34; 现在计数器操作(&#34;添加9&#34;)将执行此操作。

对于重做命令

value = optArr[++ptr]

值为&#34;将执行删除9&#34;

答案 8 :(得分:1)

使用:

// Create three lists 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();
}
  

<强>逻辑:

     

当列表添加到最后时,新列表将添加为最后一个   列表中的元素。因此可以从中提取最后一个列表   索引sizeBefore添加列表和sizeAfterAddding。所以保持   一个大小的轨道;我将更新的大小存储在sizeIndex上。

     

<强>运营

     

加(listToBeAppended); //将列表添加到当前列表中。

     

删除(); //将删除最后添加的列表。

     

撤消(); //将重做最后一次操作,即如果添加则删除,如果删除则添加。

     

重做(); //再次执行最后一次操作,即如果添加然后删除,则重做将再次添加。

答案 9 :(得分:1)

我不是Java人,但我认为这个解决方案可以很容易地转换为Java:

首先对问题进行分析:我们需要一个设计用于执行一组操作的类,此操作只能执行一次,并且每次都撤消和重做一次。所以过程是:

  1. 执行操作(adddelete)。
  2. (可选)撤消操作。
  3. (可选)重做动作。
  4. 所以它是一个设计有一个action / s来完成和撤消的系统,而可以认为do / redo命令会频繁执行并以这种方式设计系统。

    考虑到这一点,我建议实施add / delete操作,只需不以任何方式修改基础数据,只需提供不同的数据代理,具体取决于行动被取消或重做