Стек, опашка и свързан списък
Накратко: Стекът връща последния добавен елемент пръв (LIFO), опашката връща първия добавен пръв (FIFO), а свързаният списък пази всеки елемент във възел със стрелка към следващия.
Проблем
„Отмени“ отменя не това
Текстовият редактор пази действията в списък. Ctrl+Z трябва да отмени последното, но отменя първото:
List<string> actions = new List<string>();actions.Add("Въведе текст");actions.Add("Удебели заглавието");actions.Add("Вмъкна снимка");
// Ctrl+Z: отмени последното действие.string undone = actions[0];actions.RemoveAt(0);Console.WriteLine("Отменено: " + undone);Console.WriteLine("Остава: " + string.Join(", ", actions));Отменено: Въведе текстОстава: Удебели заглавието, Вмъкна снимкаПоправката е actions[^1] и actions.RemoveAt(actions.Count - 1): RemoveAt не връща махнатия елемент, затова той се чете преди това. Но всеки, който пипа този код, трябва да помни от кой край се маха. А при касата в магазина е обратното: обслужва се първият, който е дошъл. Има ли колекция, която сама знае от кой край?
Идеята
Стек: последното най-отгоре
Стекът е като купчина чинии. Слагаш отгоре и взимаш отгоре. Последното сложено излиза първо. На английски това е LIFO: last in, first out.
Бутонът „Назад“ в браузъра е стек:
Stack<string> history = new Stack<string>();history.Push("Начало");history.Push("Лаптопи");history.Push("Лаптоп 14 инча");Console.WriteLine("Сега: " + history.Peek());
history.Pop();Console.WriteLine("Назад: " + history.Peek());Console.WriteLine(string.Join(", ", history));Сега: Лаптоп 14 инчаНазад: ЛаптопиЛаптопи, НачалоPushслага отгоре,Popмаха и връща горния,Peekсамо поглежда горния.Stack<T>е вSystem.Collections.Generic, катоList<T>.string.Joinминава по стека отгоре надолу.
По-точно, за любопитните
PopиPeekна празен стек спират програмата.TryPeekне я спира: връщаfalse, ако стекът е празен, а горния елемент дава презout:
Stack<string> history = new Stack<string>();if (history.TryPeek(out string page)){ Console.WriteLine("Сега: " + page);}else{ Console.WriteLine("Няма предишна страница.");}history.Pop();Unhandled exception. System.InvalidOperationException: Stack empty.Програмата първо печата „Няма предишна страница.“, а спира на Pop. За Pop има и TryPop.
- C# има и по-стар клас
Stackбез ъгли, вSystem.Collections. Той пази всичко катоobject. Работи, но днес се ползваStack<T>:
Stack stack = new Stack();stack.Push("Google");stack.Push("Apple");stack.Push("Microsoft");Console.WriteLine("На върха: " + stack.Peek());while (stack.Count > 0){ Console.WriteLine("Изваден: " + stack.Pop());}На върха: MicrosoftИзваден: MicrosoftИзваден: AppleИзваден: GoogleПробвай
Какво остава за отмяна?
Какво ще отпечата програмата?
Stack<string> undo = new Stack<string>();undo.Push("шрифт");undo.Push("цвят");undo.Push("размер");undo.Pop();undo.Push("подчертаване");while (undo.Count > 0){ Console.WriteLine(undo.Pop());}Pop маха „размер“, защото е най-отгоре. После „подчертаване“ застава отгоре. Цикълът вади от горе надолу: подчертаване, цвят, шрифт.
Идеята
Опашка: първият дошъл
Опашката е като на касата. Нареждаш се отзад, обслужва се първият. На английски е FIFO: first in, first out.
Заявките към принтера чакат на опашка:
Queue<string> printer = new Queue<string>();printer.Enqueue("фактура.pdf");printer.Enqueue("договор.docx");printer.Enqueue("снимка.jpg");Console.WriteLine("Следващ: " + printer.Peek());
while (printer.Count > 0){ Console.WriteLine("Печатам " + printer.Dequeue());}Console.WriteLine("Опашка: " + printer.Count);Следващ: фактура.pdfПечатам фактура.pdfПечатам договор.docxПечатам снимка.jpgОпашка: 0Enqueueслага отзад,Dequeueмаха и връща първия,Peekпоглежда първия.Queue<T>е вSystem.Collections.Generic.Countказва колко чакат, затова цикълът питаCount > 0.DequeueиPeekна празна опашка спират програмата.TryDequeueиTryPeekвръщатfalse.
По-точно, за любопитните
- Празната опашка:
Queue<string> clients = new Queue<string>();clients.Enqueue("Клиент 1");Console.WriteLine("Обслужен: " + clients.Dequeue());if (!clients.TryDequeue(out string next)){ Console.WriteLine("Няма чакащи.");}clients.Dequeue();Unhandled exception. System.InvalidOperationException: Queue empty.TryDequeue казва „Няма чакащи.“, а последният Dequeue спира програмата.
- Опашка с два края като отделен клас в C# няма.
LinkedList<T>може да слага и маха и в двата края, затова в по-стари примери го ползват за опашка:
LinkedList<string> clients = new LinkedList<string>();clients.AddLast("Клиент 1");clients.AddLast("Клиент 2");clients.AddFirst("Клиент с час");Console.WriteLine("Обслужен: " + clients.First.Value);clients.RemoveFirst();Console.WriteLine("Последен: " + clients.Last.Value);Console.WriteLine("Чакат: " + string.Join(", ", clients));Обслужен: Клиент с часПоследен: Клиент 2Чакат: Клиент 1, Клиент 2Queue<string>приема иnullкато елемент. Празната опашка се познава поCountили поTryDequeue, не поnull:
Queue<string> printer = new Queue<string>();printer.Enqueue(null);Console.WriteLine(printer.Count);1Пробвай
Следващият на касата
Касиерката вика следващия клиент. Кой метод го взима от опашката?
Queue<string> checkout = new Queue<string>();
checkout.Enqueue("Иван");
checkout.Enqueue("Мария");
string next = checkout.();
Console.WriteLine("Заповядай, " + next);Dequeue маха и връща първия в опашката, тук Иван. Peek само би погледнал, без да го маха, и следващият път пак щеше да е Иван.
Виж го
Свързан списък отвътре
List<T> пази елементите един до друг в масив. Затова вмъкването отпред мести всички останали. Има и друг начин: всеки елемент е в отделен обект, който пази стойността и стрелка към следващия. Такъв обект се казва възел (node):
class Node{ public string Value; public Node Next;
public Node(string value, Node next) { Value = value; Next = next; }}Списък от възли се казва свързан списък. Достатъчно е да пазиш първия възел, head. Всеки следващ се намира по стрелката Next. Последният сочи null.
Node head = null;head = new Node("Кафе", head);head = new Node("Мляко", head);head = new Node("Хляб", head);
for (Node n = head; n != null; n = n.Next){ Console.WriteLine(n.Value);}Стъпка по стъпка, с думи
- Списъкът е празен: head сочи към null.
- Нов възел: Next е старият head (null), а head се премества на възела.
- Вторият възел сочи към първия. Нищо не се мести в паметта, сменят се само стрелките.
- Третият възел също влиза отпред: последното добавено е първо.
- n тръгва от head и печата всеки възел.
- n стига null: цикълът спира.
- Вмъкване отпред: нов възел, чийто
Nextсочи стария първи, иheadсочи новия. Две стрелки, без да местиш нищо. - Обхождане: тръгваш от
headи минаваш поNext, докато не стигнешnull. - Цената: възлите нямат индекси. До петия елемент се стига, като минеш през първите четири.
Идеята
LinkedList<T> и кое кога
C# има готов свързан списък: LinkedList<T>. Той слага в двата края с AddFirst и AddLast, маха по стойност с Remove, а първия и последния възел дава през First и Last:
LinkedList<string> route = new LinkedList<string>();route.AddLast("Пловдив");route.AddLast("Стара Загора");route.AddFirst("София");route.AddLast("Бургас");Console.WriteLine(string.Join(", ", route));
route.Remove("Стара Загора");Console.WriteLine("След промяната: " + string.Join(", ", route));Console.WriteLine("От " + route.First.Value + " до " + route.Last.Value);София, Пловдив, Стара Загора, БургасСлед промяната: София, Пловдив, БургасОт София до БургасИндекси LinkedList<T> няма изобщо. До елемент по номер се стига само през възлите, затова компилаторът не дава [ ]:
Console.WriteLine(route[1]);error CS0021: Cannot apply indexing with [] to an expression of type 'LinkedList<string>'Кое кога:
- Списък по номер:
List<T>. Това е обичайният избор. - Стек:
Stack<T>. Опашка:Queue<T>. LinkedList<T>: рядко. Когато трябват и двата края, или често вмъкваш и махаш до възел, който вече имаш.
По-точно, за любопитните
- Защо
LinkedList<T>обикновено е по-бавен, дори при вмъкване? Всеки възел е отделен обект някъде в паметта, а процесорът чете най-бързо данни, които стоят една до друга. Масивът задList<T>,Stack<T>иQueue<T>е точно такъв. - Възлите на
LinkedList<T>се виждат:FirstеLinkedListNode<string>сValue,NextиPrevious. Стрелките са в двете посоки, затоваLastиRemoveLastсъщо са бързи:
LinkedList<string> route = new LinkedList<string>();route.AddLast("София");route.AddLast("Пловдив");route.AddLast("Бургас");LinkedListNode<string> second = route.First.Next;Console.WriteLine(second.Value);Console.WriteLine(second.Previous.Value + " <- " + second.Value + " -> " + second.Next.Value);Console.WriteLine(route.Last.Next == null);ПловдивСофия <- Пловдив -> БургасTrue- Възелът от предишната карта има само
Next. Такъв списък се казва едносвързан.
Пробвай
Намери грешката
Методът трябва да преброи възлите. За списъка Хляб → Мляко → Кафе връща 2 вместо 3. А за празен списък спира с NullReferenceException. На кой ред е грешката?
Избери реда с грешката.
n.Next != null спира, когато n е на последния възел, преди да го преброи. А ако head е null, n.Next няма откъде да се прочете. Условието трябва да е n != null: продължи, докато има възел.
Съобщението при празен списък е NullReferenceException: Object reference not set to an instance of an object.: null няма Next.
Упражнение
Стекове и опашки в работа
private поле на класа.1. Влакче. Клас Train с методи AddWagon(string wagon) и RemoveWagon(). Влакът е стек: откача се последният закачен вагон. RemoveWagon() връща откачения вагон или null, ако няма. Добави свойство Count. Закачи три вагона и откачи два.
Откачен: Вагон 3Откачен: Вагон 2Вагони: 12. Маршрут на автобус. Клас BusRoute с опашка от спирки и методи AddStop(string stop) и GetNextStop(), който връща следващата спирка или null, ако маршрутът е свършил. Добави „Централна гара“, „НДК“, „Студентски град“ и обходи маршрута.
Следваща спирка: Централна гараСледваща спирка: НДКСледваща спирка: Студентски градКрай на маршрута.3. Борсов тикер. Клас StockTicker пази последните N цени на една акция. Конструкторът получава символа и N. UpdatePrice(double price) добавя цена, а ако цените станат повече от N, маха най-старата. Добави GetLastPrice() и Average() за пазените цени. За SAP с N = 3 подай цените 150, 152, 149 и 155. Подсказка: Queue<T> вижда само първия елемент, затова пази последната цена в отделно поле. Цените печатай с F2.
SAP: последна цена 155.00 €Средно за последните 3: 152.00 €4. Скобите в баланс. Редакторът на код проверява дали скобите (), [] и {} са затворени в правилния ред. Напиши IsBalanced(string text) със стек: при отваряща скоба я слагаш в стека, при затваряща махаш горната и проверяваш дали е от същия вид. Накрая стекът трябва да е празен. Прочети три реда.
(1 + [2 * 3])(1 + 2]((5)(1 + [2 * 3]) -> да(1 + 2] -> не((5) -> не5. Свой свързан списък. Клас MyLinkedList с възлите от урока и методи Add(string value) (в края), Remove(string value), Contains(string value), свойство Count и ToString(). При Remove намери възела преди търсения и пренасочи неговия Next. Отделно провери случая, когато се маха първият.
[Фактура, Отчет, Среща]Има ли Отчет: TrueСлед триене: [Фактура, Среща]Размер: 2Още задачи
- Отмени и Повтори. Пази два стека:
undoиredo. Ново действие отива вundoи изчистваredo. „Отмени“ мести горното отundoвredo, а „Повтори“ го връща обратно. - Думите наобратно. Прочети изречение и го отпечатай с думите в обратен ред, като минеш през стек: „Днес е петък“ става „петък е Днес“.
- Две каси. Клиентите идват един по един и отиват на касата с по-къса опашка. Отпечатай кой на коя каса е обслужен.
- Обърни свързания списък. Добави на
MyLinkedListметодReverse(), който обръща стрелките, без да прави нови възли.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Полиморфизъм · Интерфейси · Изключения · Списъци
- Продължава в: Хеш код и хеш функции · Вложени класове · Command · Сложност по памет
- Задачи по темата: Колекции