Реализация очереди при помощи массива на языке C#
Совсем недавно я рассказывал про базовую структуру данных очередь. И, как обещал, от теории переходим к практике. Сегодня хочу показать как реализовать алгоритм очереди нативными средствами языка C#.
Замечу, что собственная реализация подобных базовых структур, как правило, избыточный овер инжиниринг и не нужный велосипед. Всё уже давно реализовано, наверное, для всех платформ и на всех языках программирования. Конечно, .NET C# не исключение. В пространстве имён System.Collections.Generic
есть класс Queue<T>
, который отвечает за данный функционал. Но цель данной статьи показать как раз таки нативную и простую реализацию алгоритма. Такой подход к организации очереди — самая простая и служит скорее для демонстрации, чем для использования в реальных проектах. Это базис, служащий исключительно для понимания основных принципов очереди.
Итак, давайте создадим класс и назовём его Queue
. Объявим внутри локальные целочисленные переменные _Size
— размер массива, _Front
— позиция головы (инициализируем со значением по-умолчанию -1), _Rear
— позиция хвоста (также -1), _Count
— кол-во элементов. Также локальный массив типа T
.
public class Queue<T> { private int _Front = -1; private int _Rear = -1; private int _Count = 0; private readonly int _Size; private readonly T[] _Array; }
Создадим конструктор, в котором определим размер очереди и создадим массив нужной длины:
public Queue(int Size) { this._Size = Size; this._Array = new T[Size]; }
Согласно алгоритму, описанному в предыдущей статье, нам для работы понадобится два метода: IsFull
, проверяющий заполнена ли очередь (условие, когда значение _Rear
подобралось к размеру всего массива) и IsEmpty
, возвращающий true
если очередь пуста:
public bool IsFull() { return _Rear == _Size - 1; } public bool IsEmpty() { return _Count == 0; }
Непосредственно переходим к методу добавления элементов в очередь: Enqueue
. Мы проверяем заполнен ли полностью массив и если да — выбрасываем ошибку. В случае, когда есть ещё место — мы устанавливаем наш новый элемент в позицию _Rear+1
и инкрементим на единицу переменную _Rear
:
public void Enqueue(T Item) { if (this.IsFull()) throw new Exception("Очередь полностью заполнена."); _Array[++_Rear] = Item; _Count++; }
Следующий метод Dequeue
— проверяет не пустая ли очередь и если нет, то считывает элемент: возвращает его нам и удаляет из очереди. В случае, когда _Rear
и _Front
равны — это означает, что из очереди были вычитаны все элементы и мы заново присваиваем этим двум переменным значение -1
.
public T Dequeue() { if (this.IsEmpty()) throw new Exception("Очередь не заполнена."); T value = _Array[++_Front]; _Count--; if(_Front == _Rear) { _Front = -1; _Rear = -1; } return value; }
В целом, описанное выше уже полностью удовлетворяет алгоритму: можно добавлять элементы, можно считывать. Всё, что пойдёт ниже — это больше удобства и расширение базового функционала. К примеру, мы можем инкапсулировать некоторые локальные переменные в открытые public
свойства:
public int Size { get { return _Size; } } public int Count { get { return _Count; } }
Метод Peek
служит для считывания верхнего элемента, но без его удаления. В целом, он очень похож на Dequeue
с небольшими изменениями:
public T Peek() { if (this.IsEmpty()) throw new Exception("Очередь не заполнена."); T value = _Array[_Front + 1]; return value; }
И, конечно, было бы неплохо добавить GetEnumerator
для того, чтобы иметь возможность получать данные при помощи foreach
. Для этого необходимо нашему классу реализовать интерфейс IEnumerable<T>
и добавить следующий код:
public IEnumerator GetEnumerator() { if (this.IsEmpty()) throw new Exception("Очередь не заполнена."); for (int i = _Front + 1; i <= _Rear; i++) yield return _Array[i]; } IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); }
Теперь можно написать несколько примеров использования. Допустим вы приоритизировали свои утренние дела следующим образом: «Встать с кровати«, «Почистить зубы«, «Позавтракать» и «Пойти на работу«. Для этого мы пишем следующий код:
class Program { static void Main(string[] args) { Queue<string> q = new Queue<string>(5); q.Enqueue("Встать с кровати"); q.Enqueue("Почистить зубы"); q.Enqueue("Позавтракать"); q.Enqueue("Пойти на работу"); Console.WriteLine("Сделано: " + q.Dequeue()); Console.WriteLine("Сделано: " + q.Dequeue()); foreach (string c in q) { Console.WriteLine("Осталось: " + c); } Console.WriteLine("Всего осталось: " + q.Count.ToString()); Console.Read(); } }
Мы добавляем в очередь наши задачи в том порядке, в каком они должны быть выполнены. Две из них «исполняем» сразу, т.е. вычитываем и в очереди остаётся последние 2 задачи. В конце работы консольного приложения выводим счётчик оставшихся задач и ждём реакцию пользователя:

Вот и всё. В подобной имплементации есть, конечно, минусы. К примеру, нет возможности вмещать в очередь больше элементов, чем было изначально проинциализировано через конструктор. Но цель была показать самый простой способ реализации алгоритма. Если вас интересует более серьёзный подход, то можете ознакомиться с одноимённым классом Queue<T>
в репозитории dotnet/corefx.
P.S. По-хорошему, конечно, нужны тесты, но это лишь ознакомительный проект и покрывать его тестированием, а тем более описывать их здесь — считаю лишним. Ну или пусть это будет вашим домашним заданием 🙂