【问题标题】:Implementing undo and redo for an array为数组实现撤消和重做
【发布时间】:2014-04-20 04:35:10
【问题描述】:

我今天在 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,它把 key 存储为数组的索引,将 value 存储为数组的索引值。

根据面试官的说法,这是一个完全错误的解决方案。

【问题讨论】:

    标签: java arrays algorithm collections


    【解决方案1】:

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

    所以这样想:

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

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

    如果要删除最后一个操作,请从容器 2 中取出并从数组中删除最后一个条目,这样就剩下:

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

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

    等等等等。

    此外,您还可以查看this 有趣的模式,该模式提供了将对象恢复到其先前状态的能力。

    【讨论】:

    • Memento 就是为此而生的,你也指出了链接,但如果提到它或澄清模式 w.r.t 可能会更好。这个问题
    【解决方案2】:

    如果你想使用最少的内存:

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

    【讨论】:

      【解决方案3】:

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

      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
      

      撤销3次应该→结束索引=4→使用位图历史[111111110]来展平

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

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

      现在,当您想要撤消时,您所做的就是减少 End Index 的值。要重做,只需增加 End Index 值。 flatten 方法将负责显示实际数据数组中的正确元素。

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

      【讨论】:

      • getArray() 应该称为flatten(),但这仅处理撤消添加。它不处理撤消删除或重做添加或删除。
      • 对于撤消删除,您只需增加结束索引计数器。您绝不会从实际数据数组中删除内容。
      • 啊,我明白了。如果您在中间添加或删除内容而不是从末尾添加或删除内容,它将不起作用。
      • 我不明白,如果我添加[1, 2],然后添加[3, 4],然后删除4,删除3,然后添加[8, 9],然后撤消会发生什么三次?它应该给我 [1, 2, 3, 4]。会吗?
      • 更新了一个例子
      【解决方案4】:

      要么我误解了这个问题,要么人们想多了。这个问题有很多限制,所以:

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

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

      【讨论】:

        【解决方案5】:

        我会这样实现:

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

        假设您有一个数组 arr 和操作数组说 optArrptr 将指向 optArr。

        将 5 加到数组中

        arr{5}optArr{"Add 5"}ptr = 0

        将 9 加到数组中

        arr{5, 9}optArr{"Add 5", "Add 9"}ptr = 1

        将 7 加到数组中

        arr{5, 9, 7}optArr{"Add 5", "Add 9", "Add 7" }ptr = 2

        从数组中删除 9

        arr{5, 7}optArr{“添加 5”、“添加 9”、“添加 7”、“删除 9”} ptr = 3

        对于撤销命令

        value = optArr[ptr--]
        

        值为"Delete 9" 现在将执行计数器操作("Add 9")

        对于重做命令

        value = optArr[++ptr]
        

        值为“删除9”将被执行。

        【讨论】:

          【解决方案6】:

          我会这样解决。基本上会有三个列表。

          • 包含实际值的列表
          • 撤消列表带有Command接口的实例
          • 重做列表Command 接口的实例(可选,解释如下)

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

          将有两个Command 实现:AddCommandRemoveCommand

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

          另一个优化是删除 redo list 并改用 undo index。在这种情况下,当您undo() 时,您不需要从列表中删除/添加值,只需将 撤消索引 减一即可。同样,redo() 会加一。

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

          更新:

          对于最简单的情况,只需要一个undo()/redo() 操作,解决方案看起来会更简单。不用undo listindex,存储最新的命令实例就足够了。因此,我们将拥有一个值列表和一个last undo command实例。

          【讨论】:

          • 我认为这是使用命令设计模式 +1 的优雅解决方案
          • 我已经开始根据这个想法编写一个示例答案,但坦率地说,这比我的答案更简洁且表述清楚。太棒了。
          • 我认为您的两个列表将始终包含零个或一个对象,所以也许改用Optional
          • @NiklasB。在建议的实现中,命令的数量总是会增加,在每个新的 add()、remove() 和 undo() 之后。第二个列表确实是可选的。这就是我在下面写的。
          • 但我认为你只能连续撤消一次。无需过度设计,一个简单的堆栈就可以很好地解决这个问题,恕我直言。但是哦,好吧,为设计模式 +1 ;)
          【解决方案7】:

          用途:

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

          逻辑:

          当列表添加到末尾时,新列表添加为最后一个 列表中的元素。所以最后一个列表可以从 索引 sizeBeforeAdding 列表和 sizeAfterAddding。所以保持 尺寸上的跟踪;我将更新时的大小存储在sizeIndex

          运营

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

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

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

          重做(); // 再做上一次操作,即如果添加再删除,redo 会再次添加。

          【讨论】:

            【解决方案8】:

            我的解决方案是使用 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);
                }
            
            }
            

            【讨论】:

              【解决方案9】:

              我要说的一件事是面试官可能不赞成你的回答的原因是它不是很优雅。不是因为它速度慢或占用大量空间。

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

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

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

              如果您必须存储它们的长期历史记录(这变得非常方便),撤消和重做是同一件事更为重要,但它仍然适用于此。

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

              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。

              这些又是相反的,所以我们可以描述一个简单的addremove 操作,类似于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在数组持有类上被调用时:

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

              在可撤消的数组持有类上调用的方法应将新的UndoAction 对象添加到撤消字段并将重做字段设置为空。

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

              • 撤消对象可以自行感知是否应该撤消或重做。 (可以使用命令模式或抽象类来简化编写这些内容。)
              • 撤消管理器,一种管理历史记录中某个时间点的迭代器。在时间上向后移动会导致撤消,而在时间上向前移动会导致重做。
              • 了解自己的数据以及应如何更改数据的撤消客户端。

              【讨论】:

                【解决方案10】:

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

                首先分析问题:我们需要一个类来执行一组操作,而这些操作只能执行一次,并且每次都可以撤消和重做一次。所以流程是:

                1. 执行操作(adddelete)。
                2. (可选)撤消操作。
                3. (可选)重做操作。

                所以它是一个设计有一个动作/s要完成和撤消的系统,认为do/redo命令将被频繁执行并以这种方式设计系统是可以接受的。

                考虑到这一点,我建议实施add/delete 操作,只是不以任何方式修改基础数据,而只是根据操作是否撤消或为数据提供不同的代理重做

                【讨论】:

                  猜你喜欢
                  • 2011-11-14
                  • 2012-06-02
                  • 2012-02-08
                  • 2016-02-18
                  • 2011-03-10
                  • 1970-01-01
                  • 2020-08-08
                  • 2021-06-04
                  • 2012-07-02
                  相关资源
                  最近更新 更多