Реализация очереди при помощи массива на языке C#

26 октября, 2017DEV

Совсем недавно я рассказывал про базовую структуру данных очередь. И, как обещал, от теории переходим к практике. Сегодня хочу показать как реализовать алгоритм очереди нативными средствами языка 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. По-хорошему, конечно, нужны тесты, но это лишь ознакомительный проект и покрывать его тестированием, а тем более описывать их здесь — считаю лишним. Ну или пусть это будет вашим домашним заданием 🙂

Leave a comment

Ваш адрес email не будет опубликован.

Prev Post Next Post