Stack និង Queue
មាតិកា
- វិធីពីរយ៉ាងនៃការរង់ចាំ
- Stack៖ ចូលក្រោយ ចេញមុន
- កន្លែងដែលអ្នកជួប Stack នៅកន្លែងធ្វើការ
- Undo និង Redo
- បញ្ហាបុរាណ៖ តង្កៀបដែលតុល្យភាព
- បញ្ហា Stack បន្ថែម៖ Postfix និងថ្ងៃកក់ក្តៅជាង
- Queue៖ ចូលមុន ចេញមុន
- កន្លែងដែលអ្នកជួប Queue នៅកន្លែងធ្វើការ
- សាងសង់ Queue ឲ្យបានល្អ
- Circular Buffer
- Queue ពី Stack ពីរ
- រូបមន្ត៖ stack.ts និង queue.ts សម្រាប់ algolab
- សាកល្បងដោយខ្លួនឯង
1. វិធីពីរយ៉ាងនៃការរង់ចាំ
ស្រមៃមើលជង់ចានមួយនៅ cafeteria។ អ្នកដាក់ចានស្អាតនៅលើកំពូល ហើយអ្នកយកចានពីលើកំពូល។ ចានដែលដាក់ចុងក្រោយ គឺជាចានដែលចេញមុនគេ។ ឥឡូវស្រមៃមើលជួរមនុស្សនៅបញ្ជរលក់សំបុត្រ។ មនុស្សថ្មីចូលនៅខាងក្រោយ ហើយអ្នកនៅខាងមុខត្រូវបានបម្រើមុន។ អ្នកដែលមកដល់មុនគេ ចេញមុនគេ។
រូបភាពទាំងពីរនោះ គឺជារចនាសម្ព័ន្ធពីរនៃជំពូកនេះ៖
- Stack គឺ ចូលក្រោយ ចេញមុន (last in, first out ឬ LIFO)។
- Queue គឺ ចូលមុន ចេញមុន (first in, first out ឬ FIFO)។
ទាំងពីរគ្រាន់តែជា list ដែលមានច្បាប់មួយកំណត់កន្លែងដែលអ្នកអាចបន្ថែម និងដកចេញ។ ការកំណត់នោះហើយជាចំណុចសំខាន់។ Stack ប៉ះតែចុងម្ខាង។ Queue បន្ថែមនៅចុងម្ខាង ហើយដកចេញនៅចុងម្ខាងទៀត។ ដោយសារវាមានដែនកំណត់ខ្លាំង វាអាចត្រូវបានធ្វើឲ្យលឿនខ្លាំង ហើយច្បាប់របស់វាត្រូវនឹងបញ្ហាពិតជាច្រើនយ៉ាងត្រឹមត្រូវ។ នៅពេលអ្នកអានកូដ ហើយឃើញ "stack" ឬ "queue" អ្នកដឹងភ្លាមៗអំពីលំដាប់ដែលវត្ថុចេញមក។
ក្នុងជំពូកនេះ អ្នកនឹងសាងសង់ទាំងពីរ ដោះស្រាយបញ្ហាបុរាណ "តង្កៀបដែលតុល្យភាព" និងឃើញរបៀបដែល call stack និង background job queue ដំណើរការ។ Stack និង queue ក៏នឹងលេចឡើងម្តងទៀតនៅពេលក្រោយ ជាម៉ាស៊ីនដែលនៅពីក្រោយការស្វែងរក depth-first និង breadth-first។
2. Stack៖ ចូលក្រោយ ចេញមុន
Stack មានប្រតិបត្តិការស្នូលបី៖
| ប្រតិបត្តិការ | អ្វីដែលវាធ្វើ |
|---|---|
push(x) | ដាក់ x នៅលើកំពូល |
pop() | ដកចេញ ហើយ return ធាតុនៅលើកំពូល |
peek() | មើលធាតុនៅលើកំពូលដោយមិនដកចេញ |
បូកជាមួយ size និង isEmpty()។ រាល់ប្រតិបត្តិការទាំងអស់គឺ O(1)។
push(1) push(2) push(3) pop() -> 3 pop() -> 2
[3]
[2] [2] [2]
[1] [1] [1] [1] [1]
ក្នុង JavaScript array ធ្វើបែបនេះរួចហើយជាមួយ push និង pop ដែលទាំងពីរជា O(1) នៅចុង (អ្នកបានឃើញក្នុងជំពូក array)។ ដូច្នេះ stack គឺជា wrapper ស្តើងមួយដែលលាក់អ្វីៗផ្សេងទៀតដែល array អាចធ្វើបាន៖
class Stack<T> {
private items: T[] = [];
get size(): number {
return this.items.length;
}
isEmpty(): boolean {
return this.items.length === 0;
}
push(value: T): void {
this.items.push(value);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
}
ហេតុអ្វីបានជាត្រូវ wrap array? ព្រោះប្រភេទ Stack សន្យា អំពីរបៀបប្រើទិន្នន័យ។ គ្មាននរណាអាចហៅ shift() ឬអាន items[0] ដោយចៃដន្យបានទេ។ អ្នកអានឃើញ Stack<Page> ហើយដឹងថាមានតែកំពូលប៉ុណ្ណោះដែលសំខាន់។
ការប្រើប្រាស់៖
const s = new Stack<string>();
s.push("a");
s.push("b");
s.push("c");
console.log(s.peek(), s.pop(), s.size); // c c 2
peek() បង្ហាញ c, pop() ដកចេញ ហើយ return c ហើយនៅសល់ធាតុពីរ។
3. កន្លែងដែលអ្នកជួប Stack នៅកន្លែងធ្វើការ
- Call stack។ រាល់ពេលដែល function មួយហៅ function មួយទៀត engine push "frame" មួយដែលផ្ទុក variable របស់វា និងកន្លែងត្រូវត្រឡប់ទៅ។ នៅពេល function បញ្ចប់ frame ត្រូវបាន pop។ នេះជាមូលហេតុដែល function ដែលត្រូវបានហៅចុងក្រោយ គឺជាអ្នកបញ្ចប់មុនគេ។ វាក៏ជាមូលហេតុដែល recursion គ្មានទីបញ្ចប់បញ្ចប់ដោយ "Maximum call stack size exceeded" ដែលអ្នកនឹងឃើញក្នុងជំពូក recursion។ នៅពេលអ្នកអាន stack trace ក្នុងកំហុសមួយ អ្នកកំពុងអាន stack នេះពីលើចុះក្រោម។
- Undo និង redo។ រាល់ editor រក្សា stack នៃស្ថានភាពពីអតីតកាល។
- ប៊ូតុង Back របស់ browser។ ទំព័រដែលអ្នកបានចូលមើលត្រូវបាន push។ Back ធ្វើ pop។
- Parsing។ Compiler, linter និងការផ្គូផ្គងតង្កៀបរបស់ editor ប្រើ stack ដើម្បីតាមដានអ្វីដែលនៅបើកនៅឡើយ។
- Depth-first search។ ការស្វែងរកផ្លូវក្នុងព្រៃល្អក់ ឬ file tree ដោយតែងតែដើរតាមផ្លូវថ្មីបំផុតមុន គឺជា stack (ជា stack ច្បាស់លាស់ ឬ call stack)។
- ការគណនា expression។ ម៉ាស៊ីនគិតលេខ និង spreadsheet បំប្លែង
(3 + 4) * 2ទៅជាជំហានៗដោយប្រើ stack។
4. Undo និង Redo
តោះសាងសង់ប្រវត្តិ undo របស់ text editor ដោយប្រើ stack ពីរ។ មួយផ្ទុកអតីតកាល (សម្រាប់ undo)។ មួយទៀតផ្ទុកស្ថានភាពដែលបាន undo (សម្រាប់ redo)។
state: "Hi there" undo stack: ["", "Hi"] redo stack: []
undo -> "Hi" undo stack: [""] redo stack: ["Hi there"]
undo -> "" undo stack: [] redo stack: ["Hi there", "Hi"]
redo -> "Hi" undo stack: [""] redo stack: ["Hi there"]
ច្បាប់៖
- ការកែថ្មី push ស្ថានភាពបច្ចុប្បន្នទៅលើ undo stack ហើយ សម្អាត redo stack។ នៅពេលអ្នកវាយអ្វីថ្មី អនាគតចាស់លែងមានទៀតហើយ។ (សាកល្បងវាក្នុង editor ណាមួយ។)
undoផ្លាស់ទីស្ថានភាពបច្ចុប្បន្នទៅ redo stack ហើយ pop undo stack ចូលជាស្ថានភាពបច្ចុប្បន្ន។redoធ្វើបញ្ច្រាស។
class History<T> {
private undoStack = new Stack<T>();
private redoStack = new Stack<T>();
constructor(private current: T) {}
get value(): T {
return this.current;
}
set(next: T): void {
this.undoStack.push(this.current);
this.current = next;
this.redoStack = new Stack<T>(); // a new edit clears redo
}
undo(): T {
if (!this.undoStack.isEmpty()) {
this.redoStack.push(this.current);
this.current = this.undoStack.pop()!;
}
return this.current;
}
redo(): T {
if (!this.redoStack.isEmpty()) {
this.undoStack.push(this.current);
this.current = this.redoStack.pop()!;
}
return this.current;
}
}
undo និង redo ឆ្លុះគ្នា ហើយទាំងពីរមិនធ្វើអ្វីទេ នៅពេលគ្មានអ្វីត្រូវផ្លាស់ទី។ ប្រើវាដូចនេះ៖
const h = new History("");
h.set("Hi");
h.set("Hi there");
console.log(h.undo(), h.undo(), h.redo()); // Hi (empty) Hi
តម្លៃកណ្តាលគឺ string ទទេ ដូច្នេះបន្ទាត់ output អានថា Hi Hi ជាមួយចន្លោះពីរ។ Editor ពិតរក្សាទុក diff ឬ command ជំនួសឲ្យច្បាប់ចម្លងពេញនៃអត្ថបទសម្រាប់រាល់ជំហាន ប៉ុន្តែគំនិត stack ពីរគឺដូចគ្នា។
5. បញ្ហាបុរាណ៖ តង្កៀបដែលតុល្យភាព
បញ្ហា៖ ផ្តល់ string ដែលមាន (), [] និង {} ចូរសម្រេចថាតើរាល់តង្កៀបត្រូវបានបិទតាមលំដាប់ត្រឹមត្រូវឬអត់។
"{[()]}"គឺតុល្យភាព។"([)]"មិនតុល្យភាពទេ៖)មកដល់ ខណៈដែល[គឺជាតង្កៀបបើកថ្មីបំផុត។"(("មិនតុល្យភាពទេ៖ តង្កៀបពីរមិនដែលត្រូវបានបិទ។
តង្កៀបបើកដែលមិនទាន់បិទថ្មីបំផុត ត្រូវតែជាតង្កៀបបន្ទាប់ដែលបិទ។ "ថ្មីបំផុតមុន" គឺជា stack យ៉ាងពិតប្រាកដ។ Algorithm៖
- ដើរកាត់តួអក្សរ។
- ពេលជួបតង្កៀបបើក ត្រូវ push វា។
- ពេលជួបតង្កៀបបិទ ត្រូវ pop stack។ ធាតុដែលបាន pop ត្រូវតែជាតង្កៀបបើកដែលត្រូវគ្នា បើមិនដូច្នោះទេ វាមិនតុល្យភាពទេ។
- នៅចុងបញ្ចប់ stack ត្រូវតែទទេ បើមិនដូច្នោះទេ មានតង្កៀបខ្លះនៅបើកនៅឡើយ។
const PAIRS: Record<string, string> = { ")": "(", "]": "[", "}": "{" };
function isBalanced(text: string): boolean {
const stack = new Stack<string>();
for (const ch of text) {
if (ch === "(" || ch === "[" || ch === "{") {
stack.push(ch);
} else if (ch in PAIRS) {
if (stack.pop() !== PAIRS[ch]) return false;
}
}
return stack.isEmpty();
}
តាមដាន "([)]"៖
| តួអក្សរ | សកម្មភាព | Stack |
|---|---|---|
( | push | ( |
[ | push | ( [ |
) | pop ផ្តល់ [ ប៉ុន្តែយើងត្រូវការ ( | មិនត្រូវគ្នា, return false |
Edge case ដែលកូដដោះស្រាយរួចហើយ៖ តង្កៀបបិទដែលគ្មានអ្វីត្រូវ pop (")" pop បាន undefined ដែលមិនដែលស្មើតង្កៀបណាមួយ ដូច្នេះវា return false), តង្កៀបបើកដែលនៅសល់ ("((" បញ្ចប់ដោយ stack មិនទទេ) និងតួអក្សរផ្សេងទៀតដូចជាអក្សរ (វាត្រូវបានព្រងើយកន្តើយ ដូច្នេះ "a(b[c]d)e" គឺតុល្យភាព)។
Time គឺ O(n) ព្រោះតួអក្សរនីមួយៗត្រូវបាន push និង pop យ៉ាងច្រើនម្តង។ Space គឺ O(n) ក្នុងករណីអាក្រក់បំផុត ដូចជា string ដែលមានតែតង្កៀបបើក។
បញ្ហាតែមួយនេះគឺជាសំណួរបើកការសម្ភាសន៍ដ៏ពេញនិយម និងជាមុខងារពិតរបស់ឧបករណ៍ដូចជា JSON validator និង code formatter។
6. បញ្ហា Stack បន្ថែម៖ Postfix និងថ្ងៃកក់ក្តៅជាង
ការគណនា Postfix (reverse Polish)
ក្នុងសញ្ញាណ postfix operator មកក្រោយ operand របស់វា៖ 3 4 + មានន័យថា 3 + 4 ហើយ 3 4 + 2 * មានន័យថា (3 + 4) * 2។ វាមិនត្រូវការវង់ក្រចកទេ ហើយ stack គណនាវាក្នុងការឆ្លងកាត់តែមួយ៖ push លេខ ហើយពេលជួប operator ត្រូវ pop លេខពីរ អនុវត្ត operator រួច push លទ្ធផល។
function evalRpn(tokens: string[]): number {
const stack = new Stack<number>();
for (const token of tokens) {
if (["+", "-", "*", "/"].includes(token)) {
const b = stack.pop()!; // the right operand comes off first
const a = stack.pop()!;
if (token === "+") stack.push(a + b);
else if (token === "-") stack.push(a - b);
else if (token === "*") stack.push(a * b);
else stack.push(Math.trunc(a / b));
} else {
stack.push(Number(token));
}
}
return stack.pop()!;
}
console.log(evalRpn(["3", "4", "+", "2", "*"])); // 14
ចាប់អារម្មណ៍លើលំដាប់៖ pop ដំបូង គឺជា operand ខាងស្តាំ b។ សម្រាប់ប្រមាណវិធីដកនិងចែក លំដាប់សំខាន់៖ 7 2 - ត្រូវតែជា 7 - 2 មិនមែន 2 - 7 ទេ។
Monotonic stack៖ ចំនួនថ្ងៃរហូតដល់សីតុណ្ហភាពកក់ក្តៅជាង
បញ្ហា៖ សម្រាប់ថ្ងៃនីមួយៗ តើប៉ុន្មានថ្ងៃទៀតទើបមានថ្ងៃកក់ក្តៅជាង? សម្រាប់ [73, 74, 75, 71, 69, 72, 76, 73] ចម្លើយគឺ [1, 1, 4, 2, 1, 1, 0, 0] (0 មានន័យថាវាមិនដែលកក់ក្តៅជាងទេ)។
វិធីយឺតពិនិត្យរាល់ថ្ងៃក្រោយៗសម្រាប់រាល់ថ្ងៃ៖ O(n ការេ)។ Stack ធ្វើបានក្នុង O(n)។ រក្សា stack នៃ index របស់ថ្ងៃដែលនៅរង់ចាំថ្ងៃកក់ក្តៅជាង។ នៅពេលថ្ងៃនេះកក់ក្តៅជាងថ្ងៃនៅលើកំពូល ការរង់ចាំរបស់ថ្ងៃនោះបានចប់ ដូច្នេះ pop វា ហើយកត់ត្រាគម្លាត។ បន្ទាប់មក push ថ្ងៃនេះ។
function daysUntilWarmer(temps: number[]): number[] {
const result = new Array<number>(temps.length).fill(0);
const waiting = new Stack<number>(); // indexes, temps decreasing
for (let i = 0; i < temps.length; i++) {
while (!waiting.isEmpty() && temps[waiting.peek()!] < temps[i]) {
const j = waiting.pop()!;
result[j] = i - j;
}
waiting.push(i);
}
return result;
}
សីតុណ្ហភាពនៃថ្ងៃក្នុង stack តែងតែថយចុះពីក្រោមទៅលើ ដែលជាមូលហេតុដែលវាត្រូវបានហៅថា monotonic stack។ មាន while នៅក្នុង for ប៉ុន្តែ index នីមួយៗត្រូវបាន push ម្តង និង pop ម្តង ដូច្នេះការងារសរុបនៅតែ O(n)។ នោះគឺជាការរំលឹកដ៏ល្អ៖ loop ជាន់គ្នាមិនមែនមានន័យថា O(n ការេ) ជានិច្ចទេ។ ចូររាប់ថាធាតុនីមួយៗត្រូវបានប៉ះប៉ុន្មានដង។
7. Queue៖ ចូលមុន ចេញមុន
Queue មានប្រតិបត្តិការទាំងនេះ៖
| ប្រតិបត្តិការ | អ្វីដែលវាធ្វើ |
|---|---|
enqueue(x) | បន្ថែម x នៅខាងក្រោយ |
dequeue() | ដកចេញ ហើយ return ធាតុខាងមុខ |
peek() | មើលខាងមុខដោយមិនដកចេញ |
បូកជាមួយ size និង isEmpty()។ ទាំងអស់គួរតែជា O(1)។
enqueue(a) enqueue(b) enqueue(c) dequeue() -> a
front [a] [a b] [a b c] [b c] front
តើយើងអាចប្រើ array ជាមួយ push និង shift បានទេ? វាដំណើរការ ហើយសម្រាប់ queue តូចៗវាមិនអីទេ។ ប៉ុន្តែពីជំពូក array អ្នកដឹងថា shift() ត្រូវផ្លាស់ទីធាតុដែលនៅសល់ ដែលជាគោលការណ៍គឺ O(n)។ Queue នៃ job មួយលានដែលកំពុងរង់ចាំ មិនគួរបង់តម្លៃនោះរាល់ពេល dequeue ទេ។
8. កន្លែងដែលអ្នកជួប Queue នៅកន្លែងធ្វើការ
- Job queue។ ការផ្ញើអ៊ីមែល ការ resize រូបភាព និងការដំណើរការការបង់ប្រាក់ ត្រូវបានដាក់ក្នុង queue ហើយ worker ដោះស្រាយតាមលំដាប់។ ឧបករណ៍ដូចជា RabbitMQ, SQS និង queue ផ្អែកលើ Redis គឺជា queue ក្នុងមាត្រដ្ឋានធំ។
- Event loop។ JavaScript ខ្លួនឯងប្រើ queue៖ callback និង promise ដែលបាន resolve រង់ចាំជាជួរ ឲ្យ call stack ទំនេរ។
- ការបោះពុម្ព ticket គាំទ្រ និងសំណើជិះ។ មកមុនបម្រើមុន។
- Breadth-first search។ ការស្វែងរកម្តងមួយកម្រិត (ជំពូក tree និង graph) ប្រើ queue។
- Buffer និង streaming។ ទិន្នន័យដែលមកដល់លឿនជាងការដំណើរការ រង់ចាំក្នុង queue (network packet, video frame, log line)។
- Rate limiting និង throttling។ Sliding window នៃសំណើថ្មីៗ ជារឿយៗត្រូវបានរក្សាទុកក្នុង queue។
9. សាងសង់ Queue ឲ្យបានល្អ
អ្នកមានឧបករណ៍ល្អឥតខ្ចោះរួចហើយ។ Linked list ដែលមាន tail pointer បន្ថែមនៅចុងក្នុង O(1) ហើយដកចេញពីខាងមុខក្នុង O(1) ហើយទាំងនោះគឺជាប្រតិបត្តិការពីរដែល queue ត្រូវការយ៉ាងពិតប្រាកដ៖
import { LinkedList } from "./linked-list.ts";
class Queue<T> {
private list = new LinkedList<T>();
get size(): number {
return this.list.size;
}
isEmpty(): boolean {
return this.list.size === 0;
}
enqueue(value: T): void {
this.list.append(value);
}
dequeue(): T | undefined {
return this.list.removeFirst();
}
peek(): T | undefined {
return this.list.head?.value;
}
}
ការប្រើប្រាស់៖
const q = new Queue<string>();
q.enqueue("a");
q.enqueue("b");
q.enqueue("c");
console.log(q.dequeue(), q.peek(), q.size); // a b 2
នោះជាមូលហេតុដែលយើងបានសាងសង់ LinkedList ជាមួយ tail នៅក្នុងជំពូកមុន។ ចំណាំភាពផ្ទុយគ្នា៖ stack សប្បាយបំផុតលើ array (ចុងម្ខាង គ្មាន pointer) ចំណែក queue សប្បាយបំផុតលើ linked list (ចុងពីរ គ្មានការ shift)។
| រចនាសម្ព័ន្ធ | ផ្អែកលើ | បន្ថែម | ដកចេញ | Peek |
|---|---|---|---|---|
| Stack | array | O(1) | O(1) | O(1) |
| Queue | linked list | O(1) | O(1) | O(1) |
| Queue | array ជាមួយ shift() | O(1) | O(n) | O(1) |
10. Circular Buffer
ពេលខ្លះអ្នកចង់បាន queue ដែលមាន ទំហំថេរ៖ រក្សា log line 100 ចុងក្រោយ metric 60 វិនាទីចុងក្រោយ ឬការស្វែងរក 10 ចុងក្រោយ។ នៅពេលវាពេញ ធាតុចាស់បំផុតត្រូវបានបោះចោលដើម្បីបង្កើតកន្លែង។ Linked list ដំណើរការបាន ប៉ុន្តែ array ទំហំថេរមានទំហំតូចជាង ហើយមិនបែងចែកអង្គចងចាំអ្វីបន្ថែមក្រោយពេលចាប់ផ្តើមទេ។
ល្បិចគឺ circular buffer (ឬ ring buffer)។ វាប្រើ array ធម្មតាទំហំថេរ ហើយចាត់ទុកថាចុងភ្ជាប់នឹងដើម ដូច carousel ដែលអ្នកបានសាងសង់ជាមួយនព្វន្ត modular។ ក្នុង នព្វន្ត Modular អ្នកបានឃើញថា (i + 1) % length ឲ្យ index វិលត្រឡប់ទៅ 0។ ទីនេះ operator % ដដែលនោះជំរុញរចនាសម្ព័ន្ធទាំងមូល។
យើងរក្សាលេខពីរ៖ start (index នៃធាតុចាស់បំផុត) និង count (ចំនួនធាតុដែលបានរក្សាទុក)។ Slot ទំនេរបន្ទាប់គឺ (start + count) % capacity។
capacity 3, after pushing 1, 2, 3, then 4 and 5:
push 1,2,3: [1, 2, 3] start=0 count=3
push 4: [4, 2, 3] start=1 count=3 (overwrote 1)
push 5: [4, 5, 3] start=2 count=3 (overwrote 2)
Reading in order from start: 3, 4, 5
class CircularBuffer<T> {
private items: (T | undefined)[];
private start = 0;
private count = 0;
constructor(readonly capacity: number) {
if (capacity < 1) throw new RangeError("capacity must be at least 1");
this.items = new Array(capacity);
}
push(value: T): T | undefined {
const end = (this.start + this.count) % this.capacity;
if (this.count < this.capacity) {
this.items[end] = value;
this.count++;
return undefined;
}
const dropped = this.items[this.start]; // full: replace the oldest
this.items[this.start] = value;
this.start = (this.start + 1) % this.capacity;
return dropped;
}
shift(): T | undefined {
if (this.count === 0) return undefined;
const value = this.items[this.start];
this.items[this.start] = undefined;
this.start = (this.start + 1) % this.capacity;
this.count--;
return value;
}
}
push return ធាតុដែលវាបានបោះចោល (ឬ undefined បើមានកន្លែង) ដូច្នេះអ្នកហៅអាច log ឬឆ្លើយតបនឹងការបាត់បង់។ ប្រតិបត្តិការទាំងពីរគឺ O(1) ហើយមិនដែលផ្លាស់ទីធាតុផ្សេងទៀតទេ។
const b = new CircularBuffer<number>(3);
for (let i = 1; i <= 5; i++) b.push(i);
console.log(b.toArray().join(",")); // 3,4,5
(toArray() ស្ថិតក្នុងរូបមន្តខាងក្រោម។) ធាតុចាស់បំផុត 1 និង 2 ត្រូវបានសរសេរជាន់ ហើយអ្វីដែលនៅសល់គឺតម្លៃថ្មីបំផុតទាំង 3 តាមលំដាប់។
11. Queue ពី Stack ពីរ
សំណួរសម្ភាសន៍ដ៏ល្បី៖ អនុវត្ត queue ដោយប្រើតែ stack។ Stack ត្រឡប់លំដាប់ ចំណែក queue រក្សាវា ដូច្នេះគំនិតគឺត្រឡប់ពីរដង៖
inboxទទួលរាល់enqueue(push)។outboxបម្រើរាល់dequeue(pop)។- នៅពេល
outboxទទេ ហើយមានសំណើ dequeue ត្រូវចាកinboxទាំងមូលចូល outbox ម្តងមួយធាតុ។ លំដាប់ pop បញ្ច្រាស ដូច្នេះធាតុចាស់បំផុតទៅនៅលើកំពូល។
enqueue 1, 2, 3 -> inbox (bottom to top): 1 2 3
dequeue: outbox is empty, so pour:
pop 3 -> push to outbox, pop 2 -> push, pop 1 -> push
outbox (bottom to top): 3 2 1 -> pop gives 1. Correct.
class TwoStackQueue<T> {
private inbox = new Stack<T>();
private outbox = new Stack<T>();
enqueue(value: T): void {
this.inbox.push(value);
}
dequeue(): T | undefined {
if (this.outbox.isEmpty()) {
while (!this.inbox.isEmpty()) this.outbox.push(this.inbox.pop()!);
}
return this.outbox.pop();
}
}
dequeue តែមួយអាចចំណាយ O(n) នៅពេលវាចាក។ ប៉ុន្តែរាល់ធាតុត្រូវបានចាកយ៉ាងច្រើនម្តងក្នុងជីវិតរបស់វា ដូច្នេះលើប្រតិបត្តិការច្រើន តម្លៃជាមធ្យមគឺ O(1) ក្នុងមួយប្រតិបត្តិការ។ នេះត្រូវបានហៅថា O(1) amortized ដែលជាពាក្យដូចគ្នាដែលប្រើសម្រាប់ Array.push ក្នុងជំពូក array។ កុំចាកខណៈដែល outbox នៅមានធាតុ បើមិនដូច្នោះទេលំដាប់នឹងខូច។
12. រូបមន្ត៖ stack.ts និង queue.ts សម្រាប់ algolab
បង្កើត src/stack.ts។ វាផ្ទុក Stack បញ្ហាពីជំពូកនេះដែលប្រើវា MinStack (មើលលំហាត់ទី 3) និង History ពេញលេញ៖
export class Stack<T> {
private items: T[] = [];
get size(): number {
return this.items.length;
}
isEmpty(): boolean {
return this.items.length === 0;
}
// O(1) amortized
push(value: T): void {
this.items.push(value);
}
// O(1)
pop(): T | undefined {
return this.items.pop();
}
// O(1)
peek(): T | undefined {
return this.items[this.items.length - 1];
}
toArray(): T[] {
return [...this.items];
}
}
const PAIRS: Record<string, string> = { ")": "(", "]": "[", "}": "{" };
export function isBalanced(text: string): boolean {
const stack = new Stack<string>();
for (const ch of text) {
if (ch === "(" || ch === "[" || ch === "{") {
stack.push(ch);
} else if (ch in PAIRS) {
if (stack.pop() !== PAIRS[ch]) return false;
}
}
return stack.isEmpty();
}
// Reverse Polish notation: "3 4 + 2 *" -> 14
export function evalRpn(tokens: string[]): number {
const stack = new Stack<number>();
for (const token of tokens) {
if (["+", "-", "*", "/"].includes(token)) {
const b = stack.pop()!;
const a = stack.pop()!;
if (token === "+") stack.push(a + b);
else if (token === "-") stack.push(a - b);
else if (token === "*") stack.push(a * b);
else stack.push(Math.trunc(a / b));
} else {
stack.push(Number(token));
}
}
return stack.pop()!;
}
// Monotonic stack: days until a warmer temperature
export function daysUntilWarmer(temps: number[]): number[] {
const result = new Array<number>(temps.length).fill(0);
const waiting = new Stack<number>(); // indexes, temps decreasing
for (let i = 0; i < temps.length; i++) {
while (!waiting.isEmpty() && temps[waiting.peek()!] < temps[i]) {
const j = waiting.pop()!;
result[j] = i - j;
}
waiting.push(i);
}
return result;
}
// Stack that also reports its minimum in O(1)
export class MinStack {
private items: { value: number; min: number }[] = [];
push(value: number): void {
const min = this.items.length
? Math.min(value, this.items[this.items.length - 1].min)
: value;
this.items.push({ value, min });
}
pop(): number | undefined {
return this.items.pop()?.value;
}
min(): number | undefined {
return this.items[this.items.length - 1]?.min;
}
}
// Undo / redo history
export class History<T> {
private undoStack = new Stack<T>();
private redoStack = new Stack<T>();
constructor(private current: T) {}
get value(): T {
return this.current;
}
set(next: T): void {
this.undoStack.push(this.current);
this.current = next;
this.redoStack = new Stack<T>(); // a new edit clears redo
}
undo(): T {
if (!this.undoStack.isEmpty()) {
this.redoStack.push(this.current);
this.current = this.undoStack.pop()!;
}
return this.current;
}
redo(): T {
if (!this.redoStack.isEmpty()) {
this.undoStack.push(this.current);
this.current = this.redoStack.pop()!;
}
return this.current;
}
}
ឥឡូវ src/queue.ts ដែលសាងសង់លើ LinkedList របស់អ្នក៖
import { LinkedList } from "./linked-list.ts";
import { Stack } from "./stack.ts";
// Queue on top of the linked list: both ends are O(1).
export class Queue<T> {
private list = new LinkedList<T>();
get size(): number {
return this.list.size;
}
isEmpty(): boolean {
return this.list.size === 0;
}
enqueue(value: T): void {
this.list.append(value);
}
dequeue(): T | undefined {
return this.list.removeFirst();
}
peek(): T | undefined {
return this.list.head?.value;
}
toArray(): T[] {
return this.list.toArray();
}
}
// Fixed-size ring: when full, the oldest item is overwritten.
export class CircularBuffer<T> {
private items: (T | undefined)[];
private start = 0;
private count = 0;
constructor(readonly capacity: number) {
if (capacity < 1) throw new RangeError("capacity must be at least 1");
this.items = new Array(capacity);
}
get size(): number {
return this.count;
}
isFull(): boolean {
return this.count === this.capacity;
}
// O(1). Returns the overwritten item, if any.
push(value: T): T | undefined {
const end = (this.start + this.count) % this.capacity;
if (this.count < this.capacity) {
this.items[end] = value;
this.count++;
return undefined;
}
const dropped = this.items[this.start];
this.items[this.start] = value;
this.start = (this.start + 1) % this.capacity;
return dropped;
}
// O(1)
shift(): T | undefined {
if (this.count === 0) return undefined;
const value = this.items[this.start];
this.items[this.start] = undefined;
this.start = (this.start + 1) % this.capacity;
this.count--;
return value;
}
toArray(): T[] {
const out: T[] = [];
for (let i = 0; i < this.count; i++) {
out.push(this.items[(this.start + i) % this.capacity] as T);
}
return out;
}
}
// Interview classic: a queue built from two stacks.
export class TwoStackQueue<T> {
private inbox = new Stack<T>();
private outbox = new Stack<T>();
enqueue(value: T): void {
this.inbox.push(value);
}
dequeue(): T | undefined {
if (this.outbox.isEmpty()) {
while (!this.inbox.isEmpty()) this.outbox.push(this.inbox.pop()!);
}
return this.outbox.pop();
}
get size(): number {
return this.inbox.size + this.outbox.size;
}
}
Test សម្រាប់ stack ក្នុង src/stack.test.ts៖
import { test } from "node:test";
import assert from "node:assert/strict";
import { Stack, isBalanced, evalRpn, daysUntilWarmer, MinStack, History } from "./stack.ts";
test("Stack is last in, first out", () => {
const s = new Stack<number>();
s.push(1);
s.push(2);
assert.equal(s.peek(), 2);
assert.equal(s.pop(), 2);
assert.equal(s.pop(), 1);
assert.equal(s.pop(), undefined);
assert.equal(s.isEmpty(), true);
});
test("isBalanced", () => {
assert.equal(isBalanced("{[()]}"), true);
assert.equal(isBalanced("a(b[c]d)e"), true);
assert.equal(isBalanced("([)]"), false);
assert.equal(isBalanced("(("), false);
assert.equal(isBalanced(")"), false);
assert.equal(isBalanced(""), true);
});
test("evalRpn", () => {
assert.equal(evalRpn(["3", "4", "+", "2", "*"]), 14);
assert.equal(evalRpn(["10", "2", "8", "*", "+", "3", "-"]), 23);
assert.equal(evalRpn(["7", "2", "/"]), 3);
});
test("daysUntilWarmer", () => {
assert.deepEqual(daysUntilWarmer([73, 74, 75, 71, 69, 72, 76, 73]), [1, 1, 4, 2, 1, 1, 0, 0]);
});
test("MinStack", () => {
const s = new MinStack();
s.push(5);
s.push(2);
s.push(7);
assert.equal(s.min(), 2);
s.pop();
s.pop();
assert.equal(s.min(), 5);
});
test("History undo and redo", () => {
const h = new History("a");
h.set("ab");
h.set("abc");
assert.equal(h.undo(), "ab");
assert.equal(h.undo(), "a");
assert.equal(h.undo(), "a");
assert.equal(h.redo(), "ab");
h.set("abx");
assert.equal(h.redo(), "abx");
});
ហើយសម្រាប់ queue ក្នុង src/queue.test.ts៖
import { test } from "node:test";
import assert from "node:assert/strict";
import { Queue, CircularBuffer, TwoStackQueue } from "./queue.ts";
test("Queue is first in, first out", () => {
const q = new Queue<string>();
q.enqueue("a");
q.enqueue("b");
assert.equal(q.peek(), "a");
assert.equal(q.dequeue(), "a");
assert.equal(q.dequeue(), "b");
assert.equal(q.dequeue(), undefined);
assert.equal(q.isEmpty(), true);
});
test("CircularBuffer overwrites the oldest", () => {
const b = new CircularBuffer<number>(3);
[1, 2, 3].forEach((n) => b.push(n));
assert.equal(b.push(4), 1);
assert.equal(b.push(5), 2);
assert.deepEqual(b.toArray(), [3, 4, 5]);
assert.equal(b.shift(), 3);
assert.deepEqual(b.toArray(), [4, 5]);
b.push(6);
b.push(7);
assert.deepEqual(b.toArray(), [5, 6, 7]);
});
test("TwoStackQueue keeps FIFO order", () => {
const q = new TwoStackQueue<number>();
q.enqueue(1);
q.enqueue(2);
assert.equal(q.dequeue(), 1);
q.enqueue(3);
assert.equal(q.dequeue(), 2);
assert.equal(q.dequeue(), 3);
assert.equal(q.dequeue(), undefined);
});
រត់អ្វីៗទាំងអស់៖
npx tsx --test
Test ទាំងអស់ពីជំពូកនេះ និងជំពូកមុនៗគួរតែជាប់។
demo.ts តូចមួយដើម្បីមើលវាដំណើរការ៖
import { Stack, isBalanced, evalRpn, daysUntilWarmer, History } from "./src/stack";
import { Queue, CircularBuffer } from "./src/queue";
const s = new Stack<string>();
s.push("a");
s.push("b");
s.push("c");
console.log(s.peek(), s.pop(), s.size);
console.log(isBalanced("{[()]}"), isBalanced("([)]"), isBalanced("(("));
console.log(evalRpn(["3", "4", "+", "2", "*"]));
console.log(daysUntilWarmer([73, 74, 75, 71, 69, 72, 76, 73]).join(" "));
const h = new History("");
h.set("Hi");
h.set("Hi there");
console.log(h.undo(), h.undo(), h.redo());
const q = new Queue<string>();
q.enqueue("a");
q.enqueue("b");
q.enqueue("c");
console.log(q.dequeue(), q.peek(), q.size);
const ring = new CircularBuffer<number>(3);
for (let i = 1; i <= 5; i++) ring.push(i);
console.log(ring.toArray().join(","));
npx tsx demo.ts
c c 2
true false false
14
1 1 4 2 1 1 0 0
Hi Hi
a b 2
3,4,5
13. សាកល្បងដោយខ្លួនឯង
- ត្រឡប់ string។ ប្រើ
Stack<string>ដើម្បីត្រឡប់"stack"។ ហេតុអ្វីវាដំណើរការ? បន្ទាប់មកពន្យល់ថាហេតុអ្វី[...text].reverse().join("")ខ្លីជាង និងពេលណាវិធីផ្អែកលើ stack នៅតែជាជម្រើសល្អជាង (គន្លឹះ៖ គិតអំពីការ stream តួអក្សរម្តងមួយ)។ - រកកំហុស (សម្ភាសន៍)។ ពង្រីក
isBalancedឲ្យ return index នៃកំហុសដំបូង ឬ-1នៅពេលអត្ថបទគ្មានបញ្ហា។ សម្រាប់"(a]"តើអ្នករាយការណ៍ index ណា? - Min stack (សម្ភាសន៍)។ អាន
MinStackក្នុងរូបមន្ត។ ហេតុអ្វីបានជារាល់ធាតុរក្សាទុក minimum នៅពេលដែលវាត្រូវបាន push? Push5, 2, 7pop ពីរដង ហើយតាមដានថាmin()return អ្វីរាល់ជំហាន។ បន្ទាប់មកព្យាយាមសរសេរ stack ដែល return maximum ក្នុង O(1) ផងដែរ។ - ធាតុធំជាងបន្ទាប់។ ផ្តល់
[4, 5, 2, 10, 8]ចូរបង្កើត សម្រាប់ធាតុនីមួយៗ ធាតុបន្ទាប់នៅខាងស្តាំដែលធំជាង (ឬ-1)។ ប្រើគំនិត monotonic stack ពីdaysUntilWarmer។ ចម្លើយនៅទីនេះគឺ[5, 10, 10, -1, -1]។ - Hot potato។ អ្នកលេងឈរជារង្វង់។ ចាប់ផ្តើមពីអ្នកទីមួយ រាប់អ្នកលេង
kនាក់ ហើយដកអ្នកនោះចេញ បន្ទាប់មករាប់បន្តពីអ្នកបន្ទាប់។ ក្លែងធ្វើវាដោយប្រើQueue៖ dequeue និង enqueue អ្នកលេងk - 1នាក់ដើម្បីបង្វិល បន្ទាប់មក dequeue (កម្ចាត់) អ្នកបន្ទាប់។ តើអ្នកណានៅសល់ជាមួយអ្នកលេង["A", "B", "C", "D", "E"]និងk = 3? - មធ្យមភាគរំកិល (សម្ភាសន៍)។ រចនា class
MovingAverage(windowSize)ដែលមាន methodnext(value)ដែល return មធ្យមភាគនៃតម្លៃwindowSizeចុងក្រោយ។ ប្រើCircularBufferរបស់អ្នកបូកផលបូកដែលកំពុងដំណើរការ ដូច្នេះរាល់ការហៅគឺ O(1)។ ប្រៀបធៀបវាជាមួយបច្ចេកទេស sliding window ពីជំពូក array។
Stack និង queue មានទំហំតូច ប៉ុន្តែវាលេចឡើងក្នុង algorithm ធំៗជានិច្ច។ មិនយូរទេ អ្នកនឹងប្រើ stack ដើម្បីស្វែងរករាល់មែកធាងនៃបញ្ហាមួយ ហើយ call stack ធ្វើឲ្យវាអាចទៅរួច។ ក្នុងជំពូកបន្ទាប់ Recursion function មួយហៅខ្លួនវាផ្ទាល់ call stack កើនឡើង ហើយអ្នកនឹងរៀនគ្រប់គ្រងវាមុនពេលវាហូរលើស។
ទិញកាហ្វេជូនខ្ញុំ
ចូលចិត្តមេរៀននេះមែនទេ? សូមពិចារណាទិញកាហ្វេជូនខ្ញុំមួយកែវ ដើម្បីគាំទ្រការបង្កើតមាតិកាឥតគិតថ្លៃបន្ថែមទៀត!