实现Undo / Redo的好收藏?

时间:2011-04-24 11:31:48

标签: c# collections undo-redo linked-list

我正在阅读撤消/重做技术,我明白应该如何实现(我发现它很直观)。

但是我正在考虑应该用作历史的集合,

很多人使用堆栈,但C#堆栈是作为一个数组实现的,这是一个问题: 如果我使用“有限”历史记录(例如,对于2000命令),当达到限制时,我没有办法从堆栈的末尾删除项目,如果我找到了一种方法,我必须移动数组的所有元素(每次命令完成时都这样)。

LinkedList看起来不错,但它浪费了大量内存。

我的最后一个选项是链表的自定义实现,SingleLinkedList。 这个列表的一个节点由Value属性和NextNode指针属性组成,所以我为每个项目使用双重内存(但除此之外,除非我使用的东西小于“sizeof(void *)”)。

我还存储了指向第一个元素的指针和指向集合中最后一个元素的指针。

我可以轻松地将命令添加到历史记录中并以这种方式将它们移动到重做历史,但是我无法创建“有限”历史记录,因为不允许使用RemoveLast(我必须通过整个集合来删除最后一个项)。

所以我的问题是: 我应该使用LinkedList还是我的自定义SingleLinkedList?

更新1:

感谢您的回答,在我的情况下,我没有内存问题,好吧,我不知道我的目标是谁,我正在创建一个实用程序,并且我自己的想法是“实用程序” “,他们应该浪费最少的CPU /内存(显然不要告诉我”用c ++写它“,因为它有很大的不同)。

在我看来,单链接列表效果很好,我真的不想限制历史,我在考虑你的历史是“无限制”的Photoshop。

我只担心撤消历史变得非常大时会发生什么,比如使用8小时。 这就是我考虑通过LinkedList限制它的原因。

正如其他人所说,如果我将链表限制为大尺寸,大约60000个命令(我认为它们应该足够了),我只会浪费少量内存(4字节* 60000) singlelinkedlist。

那就是说,我想我会使用LinkedList,但是,如果我使用了无限制的历史记录,那么肯定会没问题吗?

更新2:

@Akash Kava 嗯,你说的很重要,但你误解为什么我想使用LinkedList以及为什么我不想使用堆栈。 Stack的主要问题是必须限制它的大小,并且当达到这个限制时,没有一种快速的方法来删除旧的命令(它是一个数组,并且每当它不是我们想要的东西时它的尺寸加倍)。 / p>

单个链表(考虑它是如何构建的)作为堆栈是快速的(所有堆栈操作都是O(1))并且没有限制。但是在这种情况下,需要没有限制,否则我们遇到与堆栈相同的问题,我们没有快速的方法来删除单链接列表的最后一个元素(其行为类似于堆栈) ,因为我们不知道最后一个节点的前一个节点元素。

在这种情况下,考虑一个LinkedList,你可以很容易地使用Previous指针。 然而,我们为“Stack”的每个元素使用了2个额外的指针(这次是通过链表制作的),这就像使用3倍于存储命令所需的内存(带有正常内存的数组)用法,singlelinkedlist有2倍的内存使用量,链表有3倍的内存使用量。)

所以我基本上要问的是哪个是“最佳”集合来实现undo-redo模式的堆栈。

你的回答让我觉得,即使我在一个程序中创建60000命令,它在一个程序中大约有5MB的内存,这不是那么多。

基本上,如果你想限制你的撤销/重做历史,你需要一个LinkedList,否则SingleLinkedList会更好。

6 个答案:

答案 0 :(得分:3)

现在使用LinkedList或任何标准解决方案,但要小心如何实现它。将所有撤消/重做操作置于精心设计的抽象之后。然后,如果LinkedList正在消耗的额外内存确实证明是一个问题(不太可能),您可以用自己的实现替换它。

我们一直这样做;将现有功能包装在抽象中,以便在需要时我们可以修补它,因为有时特定于域的条件可能提供额外效率的机会。这是你的情况;链接列表将起作用,但您的问题域表明可能会以实现为代价提高效率。

答案 1 :(得分:2)

如果您想要链接列表,则应使用LinkedList。为什么要重写已经存在的代码?节省16MB的RAM?

答案 2 :(得分:2)

LinkedList是第一次尝试但是启用/禁用按钮并保持状态变得不复杂,所以我找到了更好的方法。

Stack<Action> undoStack;
Stack<Action> redoStack;

撤销/重做条件是否简单

undoStack.Count > 0 (We can undo)
redoStack.Count > 0 (We can redo)

修改时,我们必须清除redoStack,因为我们修改了活动文档中的任何内容,您可以在VS中清楚地注意到这一点,当您编辑任何内容时,重做会被禁用。

redoStack.Clear() <-- important step
undoStack.Push(action);

撤消时

Action action = undoStack.Pop();
redoStack.Push(action); <-- necessary...

重做时

Action action = redoStack.Pop();
undoStack.Push(action);

可以使用链表实现相同,但管理头部和尾部并维护当前指针变得复杂。

在阅读完你的观点后,我认为你可以使用单个链表来实现撤销和重做堆栈,因为你只需要一个方向指针。我只是给你一个不同的方式来看看如何使用堆栈(单链表)来解决这个问题,堆栈使用更少的内存并且没有状态相关的问题。

答案 3 :(得分:1)

正如我所说,并且正如其他人所说,每个节点而不是数组有一个成瘾指针不是一个大问题,我们假设我们的值是另一个指针,在60000命令的情况下,我们每个节点有8个字节。 480 KB,如果我们考虑它,它的价值非常低。

那就是说我觉得这个案例中最好的集合是SingleLinkedList,允许我们无限制的撤销/重做“堆栈”(围绕SingleLinkedList构建)。

如果必要来限制我们的堆栈大小,则需要LinkedList。

答案 4 :(得分:0)

您可以使用具有旋转startindex和endindex值的数组。摘要同步类方法中的元素检索和添加过程也保持startIndex和endIndex,这种方法只增加了o(2)内存而不是普通数组支持栈。

答案 5 :(得分:0)

我会得到一个奇怪的答案,只是说“你想要什么”,因为这通常不是一个性能关键的情况,特别是有限的历史记录,当达到2000个可撤销的操作时开始弹出最旧的条目

双向链表工作正常。双端队列工作正常。像ArrayList这样的连续结构需要你在线性时间从前面移除,如果每个操作只是一个存储在其中的对象引用,它仍然可以正常工作。如果对可记录的撤消操作的数量有固定限制,则圆形数组也可以正常工作(在这种情况下,您可以提前调整圆形数组的大小,当超过某个条目时,它会自动开始覆盖最旧的条目容量)。

由于您对此进行的唯一操作是对整个用户操作进行一次回退,因此对于整个撤消操作会弹回一次,如果用户记录操作并且历史记录开始,则可能从前面弹出一个元素充满或使用太多内存。它根本不是性能关键。除非你有一个非常不寻常的情况,用户可以每秒记录一万次操作(只是看到有人点击那么快就会给人留下深刻的印象),因为它受到用户输入的限制,所以不会发生太大的事情。

当然你在里面存储单个撤销操作可能对性能非常关键,你可能想要一个非常有效的数据表示来最小化内存使用(取决于你在里面存储多少状态)一个可撤销的条目)。但外部撤销堆栈/历史实际上并不是非常关键的性能,我认为在这种情况下所有选项都是合理的。这是来自一个喜欢减少内存使用以提高性能的人,但在这种情况下你的撤销堆栈内存是“冷”。很多都不经常访问(例如:不是每一帧访问) - 只是当用户点击撤销按钮或记录新操作时,除非您的目标是非常有限的硬件。

但是如果你想把我认为没有必要的东西压下来,那么展开的链表就可以很好地运作了。它基本上是一个链表,每个节点在其中存储多个元素。在这种情况下,链接的内存使用是微不足道的。你可以这样做:

struct Node
{
     Command commands[n];
     Node next;
     Node prev;
     int first;
     int last;
}

撤消时,可以递减尾节点的last计数器。如果last == first,则将其弹出并释放它并使前一个节点成为新尾部。录制操作时,可以递增last计数器。如果last == n,则分配一个新节点并使其成为新尾部。如果要开始减小历史记录的大小(即从前面删除撤消条目),请增加头节点的first计数器。如果是first == last,则取消分配节点并将下一个节点设置为新头。这将为您提供恒定时间回退,弹回和弹出前端,同时在每次撤销时使用非常少的内存,因为您不必逐个存储节点数据(如链接)(对于您存储的每个n撤消条目只需一次,您可以使n成为512这样的大数字,将链表开销减少到1/512或~1.95%)并改善引用的局部性。 / p>