Стек(stack) - это абстрактная структура данных, в которой элементы организованы по принципу LIFO(Last In First Out - “пришел последним, вышел первым”).
Принцип работы стека можно сравнить со стопкой книг - для того, чтобы взять вторую сверху книгу, нужно для начала отложить первую.
Алгоритм работы стека, в самой простой реализации, следующий:
- каждый новый элемент добавляется в вершину стека при этом все последующие элементы смещаются на одну позицию вниз;
- мы можем получить из стека только элемент расположенный в вершине(его также называют головным элементом), при этом он удаляется из стека, а последующие - смещаются на позицию вверх;
Таким образом, если стек не пустой, в его вершине всегда располагается последний добавленный элемент, с которым мы и можем взаимодействовать.
В библиотеке System.Collections.Generic языка C#, уже есть реализация .Net класса Stack, однако для понимания устройства этой структуры данных, напишем свою реализацию стека.
Стандартно для стека реализуют три операции:
- Push - метод добавления нового элемента в вершину;
- Pop - чтение элемента с его удалением;
- Peek - чтение головного элемента без удаления из стека.
Мы добавим к этим операциям следующие:
- Size - получение размера структуры stack;
- Clear - метод для очистки стека.
Стек на основе массива
Для хранения данных используется массив фиксированного размера, если мы добавляем новый элемент, а массив уже заполнен, то вызывается метод RebuildData который смещает элементы стека на одну позицию, при этом первый элемент удаляется. По скольку привязка к конкретному типу данных ограничит использование стека, сделаем реализацию на языке C#, с применением универсальных типов данных или обобщений.
using System;
public class StackFixed<T>
{
private int _currentSize; // текущий размер стека
private T[] _dataArray; // данные стека
private int _stackMaxSize; //максимальное количество элементов в стеке
// конструктор
public StackFixed(int stackMaxSize)
{
_dataArray = new T[stackMaxSize];
_currentSize = 0;
_stackMaxSize = stackMaxSize;
}
// метод для получения размера стека
public int Size()
{
return _currentSize;
}
// добавление нового элемента
public void Push(T item)
{
// если текущий размер равен максимальному, то смещаем стек на одну позицию
// при этом первый элемент удаляется
if (_currentSize == _stackMaxSize)
{
RebuildData();
}
_dataArray[_currentSize] = item;
_currentSize++;
}
// чтение главного элемента стека без удаления
public T Peek()
{
// если данных нет, выбрасываем исключение
if (Size() == 0)
{
throw new Exception("Stack is empty");
}
return _dataArray[_currentSize - 1];
}
// извлечение элемента
public T Pop()
{
var item = Peek();
_currentSize--;
return item;
}
// очистка стека
public void Clear()
{
_dataArray = new T[_stackMaxSize];
_currentSize = 0;
}
private void RebuildData()
{
var newData = new T[_stackMaxSize];
for (var i = 1; i < _dataArray.Length; i++)
{
newData[i - 1] = _dataArray[i];
}
_dataArray = newData;
_currentSize = _stackMaxSize - 1;
}
}
Такой же алгоритм выполнен в буфере клавиатуры. Он может содержать фиксированное количество информации о нажатии клавиш, при переполнении буфера, старые данные затираются новыми.
При необходимости можно переписать метод RebuildData таким образом, чтобы он изменял размер массива если значения не помещаются:
private void RebuildData()
{
_stackMaxSize += 10;
var d = new T[_stackMaxSize];
Array.Copy(_dataArray, d, _dataArray.Length);
_dataArray = d;
}
Пример использования фиксированного стека
private static void Main(string[] args)
{
var stackFixed = new StackFixed<string>(5);
stackFixed.Push("Lora");
stackFixed.Push("Dale");
stackFixed.Push("Jack");
stackFixed.Push("Andrew");
stackFixed.Push("Linda");
stackFixed.Push("Lisa");
while (stackFixed.Size() > 0)
{
Console.WriteLine(stackFixed.Pop());
}
Console.ReadLine();
}
В результате выполнения программа выведет на экран 5 последних добавленных имен:
Стек на основе списка(List)
Для хранения данных стека, будем использовать коллекцию List из стандартной библиотеки платформы .Net System.Collections.Generic.
using System;
using System.Collections.Generic;
public class Stack<T>
{
private int _size; // текущий размер стека
private readonly List<T> _data; // данные стека
// конструктор
public Stack()
{
_data = new List<T>();
_size = 0;
}
// метод для получения размера стека
public int Size()
{
return _size;
}
// добавление нового элемента
public void Push(T item)
{
_data.Add(item);
_size++;
}
// чтение главного элемента стека без удаления
public T Peek()
{
// если данных нет, выбрасываем исключение
if (Size() == 0)
{
throw new Exception("Stack is empty");
}
return _data[_size - 1];
}
// извлечение элемента
public T Pop()
{
var item = Peek();
_data.Remove(item);
_size--;
return item;
}
// очистка стека
public void Clear()
{
_data.Clear();
_size = 0;
}
}
Используя List для хранения данных, у нас нет необходимости следить за количеством объектов помещенных в стек, как это было в классе с массивом. Список автоматически расширяется при добавлении новых значений.