11

Is there a built in way to limit the depth of a System.Collection.Generics.Stack? So that if you are at max capacity, pushing a new element would remove the bottom of the stack?

I know I can do it by converting to an array and rebuilding the stack, but I figured there's probably a method on it already.

EDIT: I wrote an extension method:

    public static void Trim<T> (this Stack<T> stack, int trimCount) 
    {
        if (stack.Count <= trimCount)
            return;

       stack = new 
            Stack<T>
            (
                stack
                    .ToArray()
                    .Take(trimCount)
            );           
    }

So, it returns a new stack upon trimming, but isn't immutability the functional way =)

The reason for this is that I'm storing undo steps for an app in a stack, and I only want to store a finite amount of steps.

FlySwat
  • 172,459
  • 74
  • 246
  • 311
  • This isn't a true Stack if you implement your behaviour. Arguably more correct behaviour is to either throw an exception or (in concurrent systems) block until space is available (assuming the current thread isn't the only thread pushing onto the stack). – cletus Dec 21 '08 at 03:18
  • My first thought is that you are not doing your users a favor this way. – Jay Bazuzi Dec 21 '08 at 03:51
  • 4
    Jay, Elaborate? How would you store depth limited undo information? – FlySwat Dec 21 '08 at 04:04

6 Answers6

28

What you are looking for is called a dropout stack. AFAIK, the BCL does not contain one, although they are trivial to implement. Typically Undo and Redo functionality relies on such data structures.

They are basically an array, and when you push onto the stack the 'top' of the stack moves around the array. Eventually the top will wrap back to the beginning when the stack is full and replace the 'bottom' of the stack.

Google didn't provide much info on it. This is the best i could find:

(Warning PDF) http://courses.cs.vt.edu/~cs2704/spring04/projects/DropOutStack.pdf

Here's some boiler plate code, to get you started. I'll let you fill in the rest (sanity checking, count, indexer, etc.)

class DropOutStack<T>
{
    private T[] items;
    private int top = 0;
    public DropOutStack(int capacity)
    { 
        items = new T[capacity];
    }

    public void Push(T item)
    {
        items[top] = item;
        top = (top + 1) % items.Length;
    }
    public T Pop()
    {
        top = (items.Length + top - 1) % items.Length;
        return items[top];
    }
}
Greg Dean
  • 29,221
  • 14
  • 67
  • 78
  • A great use of space and time but it doesn't take into account the case when if the user pops too many elements the top make a full circle and reach the starting point – Jai dewani Feb 13 '23 at 05:52
4

You're actually looking at something similar to circular list implementation. There's a LimitedQueue implementation done by PIEBALDconsult at CodeProject. It's similar to your requirement. You just need to wrap Stack instead of Queue as done by the author. Plus the author also implemented indexer, which is handy if you need to access anything else other than the top stack (to show undo list maybe).

EDIT: The author's implementation also raise an event when the last(first, depends on whether it's a queue or stack) is being removed, so that you can know when something is being thrown away.

faulty
  • 8,117
  • 12
  • 44
  • 61
2

I can't see a way. You can inherit from Stack<T>, but there doesn't appear to be anything useful to override.

The easy (if a bit tedious) way would be to wrap the Stack<T> in your own, say, LimitedStack<T>. Then implement the methods you want and pass through to an internal Stack<T>, while including your limiting logic in the Push method and wherever else you need it.

It's a pain to write all those pass-through members, especially if you're implementing all the same interfaces as Stack<T>...but on the other hand, you only have to do it once and then it's done.

Ryan Lundy
  • 204,559
  • 37
  • 180
  • 211
2

I believe you're looking for a (possibly-modified) dequeue - the data structure that allows access from either end.

warren
  • 32,620
  • 21
  • 85
  • 124
2

the solution provided By Greg Dean => dropout stack is very good, but I think the main question, is about removing the bottom of the stack when the stack overflowed But the provided solution just replace the last item of the stack once the stack was filled, so you do not get a real history,

But to get a real history, you need to shift the list once the list reaches your specific capacity, but this is an expansive operation,

So I think the best solution for this is a linked list

This is my solution to the problem


public class HistoryStack<T>
{
    private LinkedList<T> items = new LinkedList<T>();
    public List<T> Items => items.ToList();
    public int Capacity { get;}
    public HistoryStack(int capacity)
    {
        Capacity = capacity;
    }

    public void Push(T item)
    {
        // full
        if (items.Count == Capacity)
        {
            // we should remove first, because some times, if we exceeded the size of the internal array
            // the system will allocate new array.
            items.RemoveFirst();
            items.AddLast(item);
        }
        else
        {
            items.AddLast(new LinkedListNode<T>(item));
        }
    }

    public T Pop()
    {
        if (items.Count == 0)
        {
            return default;
        }
        var ls = items.Last;
        items.RemoveLast();
        return ls == null ? default : ls.Value;
    }
}


Test it


var hs = new HistoryStack<int>(5);
hs.Push(1);
hs.Push(2);
hs.Push(3);
hs.Push(4);
hs.Push(5);
hs.Push(6);
hs.Push(7);
hs.Push(8);

var ls = hs.Items;
Console.WriteLine(String.Join(",", ls));

Console.WriteLine(hs.Pop());
Console.WriteLine(hs.Pop());

hs.Push(9);


Console.WriteLine(hs.Pop());
Console.WriteLine(hs.Pop());
Console.WriteLine(hs.Pop());
Console.WriteLine(hs.Pop());
Console.WriteLine(hs.Pop()); // empty
Console.WriteLine(hs.Pop()); // empty

The Result

4,5,6,7,8
8
7
9
6
5
4
0
0

1

So, expanding on Greg Deans answer, I created a Limited Depth Stack with Count and Index accessor. Like his one it will just overwrite the first elements added to the array once it has reached capacity in a loop. But it will always track how many are in the array according to the capacity rule and throw an exception if more than what was added are tried to be popped (can use TryPop for safe operation). It will also track how many it has overwritten. With the Index accessor and Count property, it can be looped in a for loop. (can implement IEnumerable for foreach, but for loop is fine and fast) Will Throw an exception if an index not in the stack is used. The last added item will always be an index of 0 with the items added before going up to the last item. Edit: Added ToArray() and IEnumerable implementation.

class WriteOverStack<T> : IEnumerable<T>
{
    private T[] _items;
    private int _topIndex = -1;
    private int _count = 0;
    private int _totalOverWritten;
    private readonly int _capacity;

    public WriteOverStack(int capacity)
    {
        _capacity = capacity;
        _items = new T[capacity];
    }

    public int Count => _count;
    public int WriteOverCount => _totalOverWritten;

    public T this[int index]
    {
        get => GetItem(index);
        set => SetItem(index, value);
    }

    private T GetItem(int index)
    {
        CheckIndex(index);
        return _items[CalculateIndex(index)];
    }

    private void SetItem(int index, T item)
    {
        CheckIndex(index);
        _items[CalculateIndex(index)] = item;
    }

    private void CheckIndex(int index)
    {
        if (index < 0 && index >= _count)
            throw new IndexOutOfRangeException(
                $"WriteOverStack Index Out Of Range: {index}");
    }

    private int CalculateIndex(int num) => 
        (_capacity + _topIndex - num) % _capacity;

    public void Push(T item)
    {
        _topIndex = CalculateIndex(-1);
        _items[_topIndex] = item;
        if (_count < _capacity) _count++;
        else _totalOverWritten++;
    }

    public T Pop()
    {
        if (_count <= 0)
            throw new InvalidOperationException(
                "WriteOverStack Empty");
        _topIndex = CalculateIndex(1);
        _count--;
        return _items[CalculateIndex(-1)];
    }

    public bool TryPop(out T? item)
    {
        item = default;
        if (_count <= 0)
            return false;
        _topIndex = CalculateIndex(1);
        item = _items[CalculateIndex(-1)];
        _count--;
        return true;
    }

    public T Peek()
    {
        if (_count <= 0)
            throw new InvalidOperationException(
                "WriteOverStack Empty");
        return _items[CalculateIndex(0)];
    }

    public bool TryPeek(out T? item)
    {
        item = default;
        if (_count <= 0)
            return false;
        item = _items[CalculateIndex(0)];
        return true;
    }

    public void Clear()
    {
        _topIndex = -1;
        _count = 0;
        _totalOverWritten = 0;
        _items = new T[_capacity];
    }

    public T[] ToArray()
    {
        var tmp = new T[_count];
        for (int i = 0; i < _count; i++)
        {
            tmp[i] = this[i];
        }
        return tmp;
    }

    public IEnumerator<T> GetEnumerator()
    {
        return ((IEnumerable<T>)ToArray()).GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return ToArray().GetEnumerator();
    }
}

Example Usage:

var writeOverStack = new WriteOverStack<int>(10);
    
    writeOverStack.Push(1);
    writeOverStack.Push(2);
    writeOverStack.Push(3);
    writeOverStack.Push(4);
    writeOverStack.Push(5);
    writeOverStack.Push(6);
    writeOverStack.Push(7);
    Console.WriteLine(writeOverStack[0]);
    Console.WriteLine("");
    Console.WriteLine("");
    for(int i = 0; i < writeOverStack.Count; i++)
        Console.WriteLine(writeOverStack[i]);
    Console.WriteLine("");
    Console.WriteLine("");
    writeOverStack.TryPop(out _);
    writeOverStack.TryPop(out _);
    writeOverStack.TryPop(out _);
    
    for(int i = 0; i < writeOverStack.Count; i++)
        Console.WriteLine(writeOverStack[i]);
    Console.WriteLine("");
    Console.WriteLine("");
    writeOverStack.Push(8);
    writeOverStack.Push(9);
    writeOverStack.Push(10);
    writeOverStack.Push(11);
    writeOverStack.Push(12);
    writeOverStack.Push(13);
    writeOverStack.Push(14);
    writeOverStack.Push(777);
    for(int i = 0; i < writeOverStack.Count; i++)
        Console.WriteLine(writeOverStack[i]);
    Console.WriteLine("");
    Console.WriteLine(writeOverStack.WriteOverCount);

Output:

7

7
6
5
4
3
2
1


4
3
2
1


777
14
13
12
11
10
9
8
4
3

2
GammaSoul
  • 11
  • 1
  • 3
  • IMHO the `public int WriteOverCount` and the `public T this[int index]` are superfluous, and the `public T[] ToArray()`/`IEnumerable` implementation are missing. – Theodor Zoulias Feb 28 '23 at 21:13
  • Hi yes you are right about the missing `public T[] ToArray() / IEnumerable` implementation as i was just coming back to add them as i thought it really wasn't a complete implementation like yourself. This could live without the WriteOverCount quite easily and happily. But im curious why you think the `Public T this [int index]` is superfluous? – GammaSoul Mar 01 '23 at 13:03
  • Because the native [`Stack`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1) doesn't have it either. Is there any reason for the indexer being more useful in the `WriteOverStack`, than it would be in the `Stack`? The question asks for a `Stack` with a limited size, not for a `Stack` with a limited size and a bunch of [other features](https://en.wikiquote.org/wiki/Featuritis) that someone someday might find useful! – Theodor Zoulias Mar 01 '23 at 13:11
  • 1
    Ok sorry you are right.. There is no Indexer on the normal Stack. I created this class because i needed it for my own use now. I need to peek items at specific depths, and the indexer is integral to that use. – GammaSoul Mar 01 '23 at 13:41