实现撤消/重做的好集合?

发布于 2024-11-02 23:29:57 字数 1741 浏览 1 评论 0原文

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

然而我正在考虑应该用作历史的集合,

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

LinkedList 看起来不错,但它浪费了很多内存。

我的最后一个选择是链表的自定义实现,即 SingleLinkedList。 该列表的节点由 Value 属性和 NextNode 指针属性组成,因此我为每个项目使用双倍内存(但仅此而已,除非我使用小于“sizeof(void*)”的东西)。

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

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

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

更新1:

感谢您目前的回答,在我的情况下,我没有记忆问题,嗯,我不知道我的目标是谁,我正在创建一个实用程序并在我自己对“实用程序”的想法,他们应该尽可能浪费最少的CPU/内存(显然不要告诉我“用C++编写它”,因为有很大的区别)。

在我看来,singlelinkedlist效果很好,我不太喜欢限制历史记录,我在考虑Photoshop,其中你的历史记录是“无限的”。

我只担心撤消历史记录变得非常大(例如使用 8 小时)时会发生什么。 这就是为什么我考虑通过 LinkedList 来限制它。

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

也就是说,我想我会使用 LinkedList,但是为了确定,如果我无限制地使用历史记录就可以了吗?

更新2:

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

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

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

所以我基本上问的是哪个是实现撤消重做模式堆栈的“最佳”集合。

你的回答让我觉得即使我在 1 个程序中创建 60000 个命令,程序中的内存也只有 5MB 左右,这并不算多。

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

I was reading around for undo/redo techniques, I understand how should it be implemented (I found it intuitive).

However I'm thinking about the collection that should be used as history,

A lot of people use stack, but C# stack is implemented as an array, and this is a problem:
If I use a "limited" history (for example, for 2000 commands), when the limit is reached I don't have a way to remove items from the end of the stack, and if I find a way to do it, I have to move all elements of the array (and this for each time a command is done).

A LinkedList looks good, but it wastes a lot of memory.

My last option is a custom implementation of a linkedlist, a SingleLinkedList.
A node of this list consist of Value property and a NextNode pointer property so I'm using double memory for each item (but nothing more,except if I'm using things smaller than "sizeof(void*)").

I'm also storing a pointer to the first element and a pointer to the last element in the collection.

I can easily add commands to the history and move them to the redohistory in this way, however I can't create a "limited" history because RemoveLast is not allowed (I have to go through the whole collection to remove the last item).

So my question is:
Should I use a LinkedList or my custom SingleLinkedList?

Update 1:

Thanks for answers at the moment, in my situation I don't have a memory problem, well, I don't know who I am targeting, I'm creating an utility program and in my own idea of "utility program", they should waste least cpu/memory possible (obviusly don't tell me "write it in c++ so", because there is a big difference).

In my opinion, the singlelinkedlist works well, I don't really like to limit the History, I'm thinking about Photoshop where your history is "unlimited".

I'm only feared from what could happen when the undo history becomes really big, like 8 hours of use.
That's why I was thinking about limiting it through a LinkedList.

As someone else stated however, if I limit the linkedlist to a big size, around 60000 commands (I think they should be enough), I'm only wasting a small amount of memory, (4 bytes * 60000) compared to singlelinkedlist.

That said, I think I'll use the LinkedList, however just to be sure, would have been ok if I've used an history without limit?

Update 2:

@Akash Kava Well, what you say it's important but you misunderstood WHY I want use a LinkedList and why I don't want use a stack. The main problem with Stack is that it's necessary to limit it's size, and there isn't a fast way to remove older commands when this limit is reached (it's an array and doubling it's dimension every time it's not something that we want).

A single linked list (think about how it's built) is fast as a stack (all stack operations are O(1)) and doesn't have a limit. However in this case it's needed not to have a limit, otherwise we have the same problem as stack, we don't have a fast way to remove last element of our singlelinkedlist (which behaves like a stack),because we don't know our previous node element from the last node.

In this case it's quite easy, so, to think about a LinkedList, where you have the Previous pointer.
However we are using 2 additional pointers for each element of our "Stack" (which is built through a linkedlist this time), which is like using 3 times more memory than what it's necessary to store a Command (with an array we have normal memory usage, singlelinkedlist has 2 times memory usage and linkedlist has 3 times memory usage).

So what I was basically asking is which is the "best" collection to implement a stack for undo-redo pattern.

Your answer made me think that even if I create 60000 command in 1 program it's around 5MB of memory in a program, which is not so much.

Basically if you want to limit your undo/redo history you need a LinkedList, otherwise a SingleLinkedList is better.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

落花浅忆 2024-11-09 23:29:57

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

While undoing

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

While redoing

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

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

读完你的观点后,我认为你可以使用单链表来实现撤消和重做堆栈,因为你只需要一个方向指针。我只是为您提供了一种不同的方法来了解如何使用堆栈(单链表)来解决此问题,该堆栈使用较少的内存并且不存在与状态相关的问题。

LinkedList is first attempt however it becomes little complicated to enable/disable buttons and maintain state so I found better approach.

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

Can Undo/Redo conditions are simple

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

While modifying, we have to clear redoStack as we modify anything in active document, you can notice this clearly in VS, when you edit anything, redo goes disabled.

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

While undoing

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

While redoing

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

Same can be implemented with linked list but it becomes complicated to manage head and tails and maintain the current pointer.

After reading your point I think you can use single linked list to implement undo and redo stack as you will only need one directional pointer. I just gave you a different way to look at how to solve this using stack (single linked list) that uses less memory and does not have state related issues.

晒暮凉 2024-11-09 23:29:57

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

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

Use a LinkedList, or any standard solution, for now, but be careful how you implement it. Put all your undo/redo actions behind a well designed abstraction. Then, if the extra memory that LinkedList is consuming really proves to be a problem (unlikely), you can replace it with your own implementation.

We do this all the time; wrap existing functionality in an abstraction so we can tinker with it if needs be because sometimes domain-specific conditions may offer a chance for extra efficiency. This is your case here; the linked list will work, but your problem domain suggests efficiencies may be possible at the cost of implementation.

遥远的绿洲 2024-11-09 23:29:57

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

If you want a linked list, you should use a LinkedList. Why rewrite code that already exists? to save 16MB of RAM?

情感失落者 2024-11-09 23:29:57

正如我所说和其他人建议的那样,每个节点有 1 个上瘾的指针而不是数组并不是一个大问题,同时我们假设我们的值是另一个指针,如果有 60000 个命令,每个节点有 8 个字节。 480 KB,仔细想想,这个值确实很低。

也就是说,我认为这种情况下最好的集合是 SingleLinkedList,它允许我们无限地撤消/重做“堆栈”(围绕 SingleLinkedList 构建)。

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

As I stated and as someone else suggested, having 1 addictional pointer per Node instead of an array is not a big problem, also let's suppose our value is another pointer, in case of 60000 commands we have 8 byte per node. 480 KB, a really low value if we think about it.

That said I think the best collection in this case is SingleLinkedList, allowing us unlimited undo/redo "stacks" (built around SingleLinkedList).

If is necessary to limit our stack size, a LinkedList is required.

苹果你个爱泡泡 2024-11-09 23:29:57

我会给出一个奇怪的答案,只是说“无论你想要什么”,因为这通常不是性能关键的情况,特别是在历史记录有限的情况下,当达到 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%)并且改善局部性 参考。

I'll come in with a weird answer and just say "whatever you want", because this is generally not a performance-critical case, especially with a limited history that starts popping off the oldest entries when reaching just 2000 undoable operations.

A doubly-linked list works fine. A double-ended queue works fine. A contiguous structure like ArrayList which requires you to remove from the front in linear-time like might still work just fine if each operation is just an object reference stored in it. A circular array can also work well if you have a fixed limit on the number of undo operations which can be recorded (in which case you can size the circular array in advance and it'll automatically start overwriting the oldest entries when it exceeds a certain capacity).

Since the only things you do with this are push back one time for an entire user operation, pop back one time for an entire undo operation, and maybe pop an element from the front if the user records an operation and the history starts getting full or using too much memory. It's not performance-critical at all. Unless you have a very unusual case where the user can record like ten thousand operations per second (would be pretty impressed just to see someone click that fast), there's just not that much that can happen since it's very bound by user input.

Of course what you store inside a single undo operation could be quite performance-critical, and there you might want a very efficient data representation that minimizes memory use (depending on how much state you store inside an undoable entry). But the outer undo stack/history really isn't very performance-critical at all, and I think just about all options are reasonable in such a case. This is coming from a guy who likes to reduce memory use to improve performance, but in this case your undo stack memory is "cold". A lot of it isn't accessed frequently (ex: not accessed every single frame) -- just when the user hits the undo button or records a new operation, unless you're targeting very limited hardware.

But if you want to squash things down which I don't think is really necessary, then an unrolled linked list sort of thing can work quite well. It's basically a linked list where each node stores more than one element inside. In that case the memory use of the links is trivialized. You can do something like this:

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

When you undo, you can decrement the last counter of the tail node. If last == first, then pop it off and free it and make the previous node the new tail. When you record an operation, you can increment the last counter. If last == n, then allocate a new node and make that the new tail. When you want to start reducing the size of the history (i.e., remove an undo entry from the front), increment the first counter of the head node. If first == last, then deallocate the node and set the next node as the new head. That'll give you constant-time push backs, pop backs, and pop fronts while using very little memory on on a per-undo basis since you don't have to store the node data like the links on a per-undo basis (just once for every n undo entries you store, and you can make n a big number like 512, reducing the linked list overhead to 1/512 or ~1.95%) and improving locality of reference.

花海 2024-11-09 23:29:57

您可以使用具有旋转起始索引和结束索引值的数组。在同步类方法中抽象元素检索和添加过程,这些方法还维护 startIndex 和 endIndex,此方法只会增加 ao(2) 内存而不是普通数组支持的堆栈。

You coud use a array with rotating startindex, and endindex values. Abstract the element retrieval and addition process in the synchronized class methods which also maintain startIndex, and endIndex, This method would have just a o(2) increase in memory instead of plain array backed stack.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文