Стек, опашка и свързан списък
Проблем
„Отмени“ отменя не това
Текстовият редактор пази действията в масив. Ctrl+Z трябва да отмени последното, но отменя първото:
const actions = [];actions.push("Въведе текст");actions.push("Удебели заглавието");actions.push("Вмъкна снимка");
// Ctrl+Z: отмени последното действие.const undone = actions.shift();console.log(`Отменено: ${undone}`);console.log("Остава:", actions);Отменено: Въведе текстОстава: [ 'Удебели заглавието', 'Вмъкна снимка' ]Поправката е actions.pop(). Но всеки, който пипа този код, трябва да помни от кой край се маха. А при касата в магазина е обратното: обслужва се първият, който е дошъл. Има ли колекция, която сама знае от кой край?
Идеята
Стек: последното най-отгоре
Стекът е като купчина чинии. Слагаш отгоре и взимаш отгоре. Последното сложено излиза първо. На английски това е LIFO: last in, first out.
Бутонът „Назад“ в браузъра е стек:
const history = [];history.push("Начало");history.push("Лаптопи");history.push("Лаптоп 14 инча");console.log(`Сега: ${history.at(-1)}`);
history.pop();console.log(`Назад: ${history.at(-1)}`);console.log(history);Сега: Лаптоп 14 инчаНазад: Лаптопи[ 'Начало', 'Лаптопи' ]Отделен клас за стек в JavaScript няма: масивът е стек, ако слагаш и взимаш само от края.
pushслага отгоре,pop()маха и връща горния,at(-1)само поглежда горния.- Отгоре е краят на масива. И двете операции са бързи, защото в края нищо не се мести.
console.logпечата стека отдолу нагоре: горният е последен.
По-точно, за любопитните
pop()иat(-1)на празен масив не спират програмата, а даватundefined:
const history = [];console.log(history.pop());console.log(history.at(-1));undefinedundefinedЗатова провери history.length, преди да вземеш.
Пробвай
Какво остава за отмяна?
Какво ще отпечата програмата?
const undo = [];undo.push("шрифт");undo.push("цвят");undo.push("размер");undo.pop();undo.push("подчертаване");while (undo.length > 0) { console.log(undo.pop());}pop маха „размер“, защото е най-отгоре. После „подчертаване“ застава отгоре. Цикълът вади от горе надолу: подчертаване, цвят, шрифт.
Идеята
Опашка: първият дошъл
Опашката е като на касата. Нареждаш се отзад, обслужва се първият. На английски е FIFO: first in, first out.
Заявките към принтера чакат на опашка:
const printer = [];printer.push("фактура.pdf");printer.push("договор.docx");printer.push("снимка.jpg");console.log(`Следващ: ${printer[0]}`);
while (printer.length > 0) { console.log(`Печатам ${printer.shift()}`);}console.log("Опашка:", printer);Следващ: фактура.pdfПечатам фактура.pdfПечатам договор.docxПечатам снимка.jpgОпашка: []Отделен клас за опашка в JavaScript няма. Масивът става опашка, ако слагаш отзад и взимаш отпред:
pushслага отзад,shiftмаха и връща първия,printer[0]поглежда първия.shiftна празен масив даваundefined, а не спира програмата.
shift обаче има цена. Той мести всички останали с едно място напред, като вмъкването отпред в урока за масивите. При няколко заявки към принтера това не се усеща. При опашка с десетки хиляди елементи е много пъти по-бавно, отколкото само да четеш поред. Тогава се ползва свързаният списък от следващата карта, който пази и първия, и последния възел.
По-точно, за любопитните
shiftна празна опашка:
const clients = ["Клиент 1"];console.log(`Обслужен: ${clients.shift()}`);console.log(clients.shift());Обслужен: Клиент 1undefined- Двигателят V8 понякога прави
shiftбърз, като премества началото на масива, вместо да мести елементите. При големи масиви това не става. Не разчитай на него.
Пробвай
Следващият на касата
Касиерката вика следващия клиент. Кой метод го взима от опашката?
const checkout = [];
checkout.push("Иван");
checkout.push("Мария");
const client = checkout.();
console.log(`Заповядай, ${client}`);shift маха и връща първия в опашката, тук Иван. checkout[0] само би погледнал, без да го маха, и следващият път пак щеше да е Иван. А pop() щеше да вземе последната, Мария.
Виж го
Свързан списък отвътре
Масивът пази елементите един до друг в масив. Затова вмъкването отпред мести всички останали. Има и друг начин: всеки елемент е в отделен обект, който пази стойността и стрелка към следващия. Такъв обект се казва възел (node):
class Node { constructor(value, next) { this.value = value; this.next = next; }}Списък от възли се казва свързан списък. Достатъчно е да пазиш първия възел, head. Всеки следващ се намира по стрелката next. Последният сочи null.
let head = null;head = new Node("Кафе", head);head = new Node("Мляко", head);head = new Node("Хляб", head);
for (let n = head; n !== null; n = n.next) { console.log(n.value);}Стъпка по стъпка, с думи
- Списъкът е празен: head сочи към null.
- Нов възел: next е старият head (null), а head се премества на възела.
- Вторият възел сочи към първия. Нищо не се мести в паметта, сменят се само стрелките.
- Третият възел също влиза отпред: последното добавено е първо.
- n тръгва от head и печата всеки възел.
- n стига null: цикълът спира.
- Вмъкване отпред: нов възел, чийто
nextсочи стария първи, иheadсочи новия. Две стрелки, без да местиш нищо. - Обхождане: тръгваш от
headи минаваш поnext, докато не стигнешnull. - Цената: възлите нямат индекси. До петия елемент се стига, като минеш през първите четири.
Идеята
Масивът и кое кога
Готов свързан списък в JavaScript няма. Масивът може да добавя и маха в двата края: push и pop отзад, unshift и shift отпред:
const route = [];route.push("Пловдив");route.push("Стара Загора");route.unshift("София");route.push("Бургас");console.log(route);
route.splice(route.indexOf("Стара Загора"), 1);console.log("След промяната:", route);console.log(`От ${route[0]} до ${route.at(-1)}`);[ 'София', 'Пловдив', 'Стара Загора', 'Бургас' ]След промяната: [ 'София', 'Пловдив', 'Бургас' ]От София до БургасКое кога:
- Списък по номер: масив. Това е обичайният избор.
- Стек: масив с
pushиpop. - Опашка: масив с
pushиshift, докато е малка. Голяма опашка с хиляди елементи: свързан списък като този от предишната карта, който пази и последния възел.
По-точно, за любопитните
unshiftе катоshift: мести всички елементи с едно място, затова отпред е бавен при голям масив.- Защо масивът обикновено е по-бърз от свързан списък, дори при вмъкване? Всеки възел е отделен обект някъде в паметта, а процесорът чете най-бързо данни, които стоят една до друга. Масивът е точно такъв.
- Възелът от тази карта има само
next. Такъв списък се казва едносвързан.
Пробвай
Намери грешката
Функцията трябва да преброи възлите. За списъка Хляб → Мляко → Кафе връща 2 вместо 3. А за празен списък спира с TypeError. На кой ред е грешката?
Избери реда с грешката.
n.next !== null спира, когато n е на последния възел, преди да го преброи. А ако head е null, n.next няма откъде да се прочете. Условието трябва да е n !== null: продължи, докато има възел.
Съобщението при празен списък е TypeError: Cannot read properties of null (reading 'next'): null няма next.
Упражнение
Стекове и опашки в работа
# отпред.1. Влакче. Клас Train с методи addWagon(wagon) и removeWagon(). Влакът е стек: откача се последният закачен вагон. removeWagon() връща откачения вагон или null, ако няма. Добави count(). Закачи три вагона и откачи два.
Откачен: Вагон 3Откачен: Вагон 2Вагони: 12. Маршрут на автобус. Клас BusRoute с опашка от спирки и методи addStop(stop) и getNextStop(), който връща следващата спирка или null, ако маршрутът е свършил. Добави „Централна гара“, „НДК“, „Студентски град“ и обходи маршрута.
Следваща спирка: Централна гараСледваща спирка: НДКСледваща спирка: Студентски градКрай на маршрута.3. Борсов тикер. Клас StockTicker пази последните N цени на една акция. Конструкторът получава символа и N. updatePrice(price) добавя цена, а ако цените станат повече от N, маха най-старата. Добави getLastPrice() и average() за пазените цени. За SAP с N = 3 подай цените 150, 152, 149 и 155. Подсказка: at(-1) е последната цена. Цените печатай с два знака след точката, с toFixed(2).
SAP: последна цена 155.00 €Средно за последните 3: 152.00 €4. Скобите в баланс. Редакторът на код проверява дали скобите (), [] и {} са затворени в правилния ред. Напиши isBalanced(text) със стек: при отваряща скоба я слагаш в стека, при затваряща махаш горната и проверяваш дали е от същия вид. Накрая стекът трябва да е празен. Прочети три реда.
(1 + [2 * 3])(1 + 2]((5)(1 + [2 * 3]) -> да(1 + 2] -> не((5) -> не5. Свой свързан списък. Клас MyLinkedList с възлите от урока и методи add(value) (в края), remove(value), contains(value), size() и toString(). При remove намери възела преди търсения и пренасочи неговия next. Отделно провери случая, когато се маха първият.
[Фактура, Отчет, Среща]Има ли Отчет: trueСлед триене: [Фактура, Среща]Размер: 2Още задачи
- Отмени и Повтори. Пази два стека:
undoиredo. Ново действие отива вundoи изчистваredo. „Отмени“ мести горното отundoвredo, а „Повтори“ го връща обратно. - Думите наобратно. Прочети изречение и го отпечатай с думите в обратен ред, като минеш през стек: „Днес е петък“ става „петък е Днес“.
- Две каси. Клиентите идват един по един и отиват на касата с по-къса опашка. Отпечатай кой на коя каса е обслужен.
- Обърни свързания списък. Добави на
MyLinkedListметодreverse(), който обръща стрелките, без да прави нови възли.
- Бърза опашка. Добави на
MyLinkedListполе за последния възел, за да е бърз иaddв края. Сега той е опашка безshift:addотзад, махане на първия отпред.
Тест